Skip to content
2 changes: 1 addition & 1 deletion .travis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,4 @@ env:
rvm:
- 2.2
before_install: gem install bundler -v 1.10.4
script: bundle exec rspec spec/controllers/{user,post}s_controller_spec.rb
script: bundle exec rspec spec/controllers
14 changes: 6 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -68,13 +68,15 @@ jsonapi_render json: { data: { id: 1, first_name: 'Tiago' } }, options: { model:
jsonapi_render json: { data: [{ id: 1, first_name: 'Tiago' }, { id: 2, first_name: 'Doug' }] }, options: { model: User }
```

### jsonapi_serialize
### jsonapi_format

In the backstage this is the method that actually parses ActiveRecord/Hash objects and builds a new Hash compliant with JSON API. It can be called anywhere in your controllers being very useful whenever you need to work with a JSON API "serialized" version of your object before rendering it.

Note: because of semantic reasons `JSONAPI::Utils#jsonapi_serialize` was renamed being now just an alias to `JSONAPI::Utils#jsonapi_format`.

```ruby
def index
result = do_some_magic(jsonapi_serialize(User.all))
result = do_some_magic(jsonapi_format(User.all))
render json: result
end
```
Expand Down Expand Up @@ -228,12 +230,8 @@ Content-Type: application/vnd.api+json
{
"title": "Record not found",
"detail": "The record identified by 3 could not be found.",
"id": null,
"href": null,
"code": 404,
"source": null,
"links": null,
"status": "not_found"
"code": "404",
"status": "404"
}
]
}
Expand Down
61 changes: 44 additions & 17 deletions lib/jsonapi/utils/exceptions.rb
Original file line number Diff line number Diff line change
@@ -1,25 +1,52 @@
require 'jsonapi/utils/version'

module JSONAPI::Utils::Exceptions
class BadRequest < ::JSONAPI::Exceptions::Error
def code; 400 end
module JSONAPI
module Utils
module Exceptions
class ActiveRecord < ::JSONAPI::Exceptions::Error
attr_accessor :object

def errors
[JSONAPI::Error.new(code: 400,
status: :bad_request,
title: 'Bad Request',
detail: 'This request is not supported.')]
end
end
def initialize(object)
@object = object
end

def errors
object.errors.keys.map do |key|
JSONAPI::Error.new(
code: JSONAPI::VALIDATION_ERROR,
status: :unprocessable_entity,
id: key,
title: object.errors.full_messages_for(key).first
)
end
end
end

class BadRequest < ::JSONAPI::Exceptions::Error
def code; '400' end

def errors
[JSONAPI::Error.new(
code: code,
status: :bad_request,
title: 'Bad Request',
detail: 'This request is not supported.'
)]
end
end

class InternalServerError < ::JSONAPI::Exceptions::Error
def code; 500 end
class InternalServerError < ::JSONAPI::Exceptions::Error
def code; '500' end

def errors
[JSONAPI::Error.new(code: 500,
status: :internal_server_error,
title: 'Internal Server Error',
detail: 'An internal error ocurred while processing the request.')]
def errors
[JSONAPI::Error.new(
code: code,
status: :internal_server_error,
title: 'Internal Server Error',
detail: 'An internal error ocurred while processing the request.'
)]
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/jsonapi/utils/request.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ def setup_request
end

def check_request
@request.errors.blank? || render_errors(@request.errors)
@request.errors.blank? || jsonapi_render_errors(json: @request)
end
end
end
Expand Down
16 changes: 11 additions & 5 deletions lib/jsonapi/utils/response/formatters.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,7 @@ module JSONAPI
module Utils
module Response
module Formatters
def jsonapi_format_errors(exception)
JSONAPI::ErrorsOperationResult.new(exception.errors[0].code, exception.errors)
end

def jsonapi_serialize(records, options = {})
def jsonapi_format(records, options = {})
if records.is_a?(Hash)
hash = records.with_indifferent_access
records = hash_to_active_record(hash[:data], options[:model])
Expand All @@ -15,6 +11,16 @@ def jsonapi_serialize(records, options = {})
build_response_document(records, options).contents
end

alias_method :jsonapi_serialize, :jsonapi_format

def jsonapi_format_errors(data)
data = JSONAPI::Utils::Exceptions::ActiveRecord.new(data) if data.is_a?(ActiveRecord::Base)
errors = data.respond_to?(:errors) ? data.errors : data
JSONAPI::Utils::Support::Error.sanitize(errors).uniq
end

protected

def build_response_document(records, options)
results = JSONAPI::OperationResults.new

Expand Down
16 changes: 8 additions & 8 deletions lib/jsonapi/utils/response/renders.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@ module Utils
module Response
module Renders
def jsonapi_render(json:, status: nil, options: {})
body = jsonapi_serialize(json, options)
body = jsonapi_format(json, options)
render json: body, status: status || @_response_document.status
rescue => e
handle_exceptions(e)
ensure
if response.body.size > 0
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
end
correct_media_type
end

def jsonapi_render_errors(exception)
result = jsonapi_format_errors(exception)
errors = result.errors
render json: { errors: errors }, status: errors.first.status
def jsonapi_render_errors(exception = nil, json: nil, status: nil)
body = jsonapi_format_errors(exception || json)
status = status || body.try(:first).try(:[], :status)
render json: { errors: body }, status: status
ensure
correct_media_type
end

def jsonapi_render_internal_server_error
Expand Down
10 changes: 10 additions & 0 deletions lib/jsonapi/utils/response/support.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,24 @@
require 'jsonapi/utils/support/filter'
require 'jsonapi/utils/support/pagination'
require 'jsonapi/utils/support/sort'
require 'jsonapi/utils/support/error'

module JSONAPI
module Utils
module Response
module Support
include ::JSONAPI::Utils::Support::Error
include ::JSONAPI::Utils::Support::Filter
include ::JSONAPI::Utils::Support::Pagination
include ::JSONAPI::Utils::Support::Sort

protected

def correct_media_type
if response.body.size > 0
response.headers['Content-Type'] = JSONAPI::MEDIA_TYPE
end
end
end
end
end
Expand Down
25 changes: 25 additions & 0 deletions lib/jsonapi/utils/support/error.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
module JSONAPI
module Utils
module Support
module Error
MEMBERS = %i(title detail id code source links status meta)

module_function

def sanitize(errors)
Array(errors).map do |error|
MEMBERS.reduce({}) do |sum, key|
value = error.try(key) || error.try(:[], key)
if value.nil?
sum
else
value = value.to_s if key == :code
sum.merge(key => value)
end
end
end
end
end
end
end
end
2 changes: 1 addition & 1 deletion lib/jsonapi/utils/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
module JSONAPI
module Utils
VERSION = '0.4.4'
VERSION = '0.4.5'
end
end
60 changes: 49 additions & 11 deletions spec/controllers/posts_controller_spec.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
require 'spec_helper'
require 'rspec/expectations'

describe PostsController, type: :controller do
include_context 'JSON API headers'
Expand All @@ -8,12 +7,28 @@

let(:fields) { (PostResource.fetchable_fields - %i(id author)).map(&:to_s) }
let(:relationships) { %w(author) }
let(:post) { Post.first }
let(:first_post) { Post.first }
let(:user_id) { first_post.user_id }

let(:attributes) do
{ title: 'Lorem ipsum', body: 'Lorem ipsum dolor sit amet.' }
end

let(:author_params) do
{ data: { type: 'users', id: user_id } }
end

let(:post_params) do
{
data: { type: 'posts', attributes: attributes },
relationships: { author: author_params }
}
end

describe '#index' do
context 'with ActiveRecord::Relation' do
it 'renders a collection of users' do
get :index, user_id: post.user_id
get :index, user_id: user_id
expect(response).to have_http_status :ok
expect(response).to have_primary_data('posts')
expect(response).to have_data_attributes(fields)
Expand All @@ -36,12 +51,12 @@
describe '#show' do
context 'with ActiveRecord' do
it 'renders a single post' do
get :show, user_id: post.user_id, id: post.id
get :show, user_id: user_id, id: first_post.id
expect(response).to have_http_status :ok
expect(response).to have_primary_data('posts')
expect(response).to have_data_attributes(fields)
expect(response).to have_relationships(relationships)
expect(data['attributes']['title']).to eq("Title for Post #{post.id}")
expect(data['attributes']['title']).to eq("Title for Post #{first_post.id}")
end
end

Expand All @@ -59,37 +74,60 @@
context 'when resource was not found' do
context 'with conventional id' do
it 'renders a 404 response' do
get :show, user_id: post.user_id, id: 999
get :show, user_id: user_id, id: 999
expect(response).to have_http_status :not_found
expect(error['title']).to eq('Record not found')
expect(error['detail']).to include('999')
expect(error['code']).to eq(404)
expect(error['code']).to eq('404')
end
end

context 'with uuid' do
let(:uuid) { SecureRandom.uuid }

it 'renders a 404 response' do
get :show, user_id: post.user_id, id: uuid
get :show, user_id: user_id, id: uuid
expect(response).to have_http_status :not_found
expect(error['title']).to eq('Record not found')
expect(error['detail']).to include(uuid)
expect(error['code']).to eq(404)
expect(error['code']).to eq('404')
end
end

context 'with slug' do
let(:slug) { 'some-awesome-slug' }

it 'renders a 404 response' do
get :show, user_id: post.user_id, id: slug
get :show, user_id: user_id, id: slug
expect(response).to have_http_status :not_found
expect(error['title']).to eq('Record not found')
expect(error['detail']).to include(slug)
expect(error['code']).to eq(404)
expect(error['code']).to eq('404')
end
end
end
end

describe '#create' do
it 'creates a new post' do
expect { post :create, post_params }.to change(Post, :count).by(1)
expect(response).to have_http_status :created
expect(response).to have_primary_data('posts')
expect(response).to have_data_attributes(fields)
expect(data['attributes']['title']).to eq(post_params[:data][:attributes][:title])
end

context 'when validation fails' do
it 'render a 422 response' do
post_params[:data][:attributes].merge!(title: nil)

expect { post :create, post_params }.to change(Post, :count).by(0)
expect(response).to have_http_status :unprocessable_entity

expect(errors[0]['id']).to eq('title')
expect(errors[0]['title']).to eq('Title can\'t be blank')
expect(errors[0]['code']).to eq('100')
end
end
end
end
Loading