From 884b5ce26ab6a0f32d6ac41940e142bb7d277fd6 Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Sun, 9 Jul 2017 00:36:01 -0300 Subject: [PATCH 1/8] Add "detail" to AR's default error JSON --- lib/jsonapi/utils/exceptions.rb | 17 ++++++++--------- spec/controllers/posts_controller_spec.rb | 20 +++++++++++++------- 2 files changed, 21 insertions(+), 16 deletions(-) diff --git a/lib/jsonapi/utils/exceptions.rb b/lib/jsonapi/utils/exceptions.rb index fb5faa1..b9ba070 100644 --- a/lib/jsonapi/utils/exceptions.rb +++ b/lib/jsonapi/utils/exceptions.rb @@ -1,5 +1,3 @@ -require 'jsonapi/utils/version' - module JSONAPI module Utils module Exceptions @@ -19,12 +17,13 @@ def initialize(object, resource_klass, context) def errors object.errors.messages.flat_map do |key, messages| messages.map do |message| - error_meta = error_base - .merge(title: title_member(key, message)) - .merge(id: id_member(key)) - .merge(source_member(key)) - - JSONAPI::Error.new(error_meta) + error = error_base + .merge( + id: id_member(key), + title: message, + detail: detail_member(key, message) + ).merge(source_member(key)) + JSONAPI::Error.new(error) end end end @@ -71,7 +70,7 @@ def source_member(key) end end - def title_member(key, message) + def detail_member(key, message) if key == :base message else diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 3ee566a..463f9c9 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -237,7 +237,8 @@ expect { subject }.to change(Post, :count).by(0) expect(response).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('title') - expect(errors.dig(0, 'title')).to eq('Title can\'t be blank') + expect(errors.dig(0, 'title')).to eq('can\'t be blank') + expect(errors.dig(0, 'detail')).to eq('Title can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data/attributes/title') end @@ -255,7 +256,8 @@ expect(subject).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('author') - expect(errors.dig(0, 'title')).to eq('Author can\'t be blank') + expect(errors.dig(0, 'title')).to eq('can\'t be blank') + expect(errors.dig(0, 'detail')).to eq('Author can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data/relationships/author') end @@ -273,7 +275,8 @@ expect(subject).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('category') - expect(errors.dig(0, 'title')).to eq('Category can\'t be blank') + expect(errors.dig(0, 'title')).to eq('can\'t be blank') + expect(errors.dig(0, 'detail')).to eq('Category can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data/relationships/category') end @@ -283,7 +286,7 @@ subject { post :create, params: params.merge(invalid_body) } let(:invalid_body) do - body.tap { |b| b[:data][:attributes][:title] = 'Fail Hidden' } + body.tap { |body| body[:data][:attributes][:title] = 'Fail Hidden' } end it 'renders a 422 response' do @@ -291,7 +294,8 @@ expect(subject).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('hidden_field') - expect(errors.dig(0, 'title')).to eq('Hidden field error was tripped') + expect(errors.dig(0, 'title')).to eq('error was tripped') + expect(errors.dig(0, 'detail')).to eq('Hidden field error was tripped') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to be_nil end @@ -318,7 +322,8 @@ expect(subject).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('content-type') - expect(errors.dig(0, 'title')).to eq('Content type can\'t be blank') + expect(errors.dig(0, 'title')).to eq('can\'t be blank') + expect(errors.dig(0, 'detail')).to eq('Content type can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data/attributes/content-type') end @@ -338,7 +343,8 @@ expect { subject }.to change(Post, :count).by(0) expect(response).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('title') - expect(errors.dig(0, 'title')).to eq('Заголовок не может быть пустым') + expect(errors.dig(0, 'title')).to eq('не может быть пустым') + expect(errors.dig(0, 'detail')).to eq('Заголовок не может быть пустым') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data/attributes/title') end From d5861c1d8b115513981bf14b9b4bd29dda8d5e7e Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Sun, 9 Jul 2017 00:47:38 -0300 Subject: [PATCH 2/8] Fix more test cases --- .gitignore | 2 +- spec/controllers/profile_controller_spec.rb | 3 ++- spec/controllers/users_controller_spec.rb | 6 ++++-- spec/support/controllers.rb | 11 +++++------ 4 files changed, 12 insertions(+), 10 deletions(-) diff --git a/.gitignore b/.gitignore index b0fb6ac..39b02a7 100644 --- a/.gitignore +++ b/.gitignore @@ -18,6 +18,6 @@ *.a *.gem .DS_Store +.byebug_history test_db Gemfile.lock - diff --git a/spec/controllers/profile_controller_spec.rb b/spec/controllers/profile_controller_spec.rb index 7974b7f..844bbf9 100644 --- a/spec/controllers/profile_controller_spec.rb +++ b/spec/controllers/profile_controller_spec.rb @@ -31,7 +31,8 @@ patch :update, params: body expect(response).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('nickname') - expect(errors.dig(0, 'title')).to eq("Nickname can't be blank") + expect(errors.dig(0, 'title')).to eq("can't be blank") + expect(errors.dig(0, 'detail')).to eq("Nickname can't be blank") expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data/attributes/nickname') end diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 6f0a5bd..6d1331e 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -419,12 +419,14 @@ expect(response).to have_http_status :unprocessable_entity expect(errors.dig(0, 'id')).to eq('first_name') - expect(errors.dig(0, 'title')).to eq('First name can\'t be blank') + expect(errors.dig(0, 'title')).to eq('can\'t be blank') + expect(errors.dig(0, 'detail')).to eq('First name can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source')).to be_nil expect(errors.dig(1, 'id')).to eq('last_name') - expect(errors.dig(1, 'title')).to eq('Last name can\'t be blank') + expect(errors.dig(1, 'title')).to eq('can\'t be blank') + expect(errors.dig(1, 'detail')).to eq('Last name can\'t be blank') expect(errors.dig(1, 'code')).to eq('100') expect(errors.dig(1, 'source')).to be_nil end diff --git a/spec/support/controllers.rb b/spec/support/controllers.rb index b67c269..22a9ec1 100644 --- a/spec/support/controllers.rb +++ b/spec/support/controllers.rb @@ -45,7 +45,7 @@ def create if post.save jsonapi_render json: post, status: :created else - jsonapi_render_errors json: post, status: :unprocessable_entity + jsonapi_render_errors json: post end end @@ -54,7 +54,7 @@ def update_with_error_on_base post = Post.find(params[:id]) # Example of response rendering with error on base post.errors.add(:base, 'This is an error on the base') - jsonapi_render_errors json: post, status: :unprocessable_entity + jsonapi_render_errors json: post end private @@ -96,10 +96,9 @@ def create else # Example of error rendering for Array of Hashes: errors = [ - { id: 'first_name', title: 'First name can\'t be blank', code: '100' }, - { id: 'last_name', title: 'Last name can\'t be blank', code: '100' } + { id: 'first_name', code: '100', title: 'can\'t be blank', detail: 'First name can\'t be blank' }, + { id: 'last_name', code: '100', title: 'can\'t be blank', detail: 'Last name can\'t be blank' } ] - jsonapi_render_errors json: errors, status: :unprocessable_entity end end @@ -139,6 +138,6 @@ def show def update profile = Profile.new profile.valid? - jsonapi_render_errors json: profile, status: :unprocessable_entity + jsonapi_render_errors json: profile end end From e34ed396ebc1f8ceda2b9fef2cedfaa88127e023 Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Sun, 9 Jul 2017 03:15:53 -0300 Subject: [PATCH 3/8] Take exceptions into separate files --- lib/jsonapi/utils/exceptions.rb | 127 +----------------- lib/jsonapi/utils/exceptions/active_record.rb | 95 +++++++++++++ .../utils/exceptions/internal_server_error.rb | 20 +++ 3 files changed, 117 insertions(+), 125 deletions(-) create mode 100644 lib/jsonapi/utils/exceptions/active_record.rb create mode 100644 lib/jsonapi/utils/exceptions/internal_server_error.rb diff --git a/lib/jsonapi/utils/exceptions.rb b/lib/jsonapi/utils/exceptions.rb index b9ba070..b243c76 100644 --- a/lib/jsonapi/utils/exceptions.rb +++ b/lib/jsonapi/utils/exceptions.rb @@ -1,125 +1,2 @@ -module JSONAPI - module Utils - module Exceptions - class ActiveRecord < ::JSONAPI::Exceptions::Error - attr_reader :object, :resource, :relationships, :relationship_keys, :foreign_keys - - def initialize(object, resource_klass, context) - @object = object - @resource = resource_klass.new(object, context) - - # Need to reflect on resource's relationships for error reporting. - @relationships = resource_klass._relationships.values - @relationship_keys = @relationships.map(&:name).map(&:to_sym) - @foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym) - end - - def errors - object.errors.messages.flat_map do |key, messages| - messages.map do |message| - error = error_base - .merge( - id: id_member(key), - title: message, - detail: detail_member(key, message) - ).merge(source_member(key)) - JSONAPI::Error.new(error) - end - end - end - - private - - def id_member(key) - id = resource_key_for(key) - key_formatter = JSONAPI.configuration.key_formatter - key_formatter.format(id).to_sym - end - - # Determine if this is a foreign key, which will need to look up its - # matching association name. - def resource_key_for(key) - if foreign_keys.include?(key) - relationships.find { |r| r.foreign_key == key }.name.to_sym - else - key - end - end - - def source_member(key) - Hash.new.tap do |hash| - resource_key = resource_key_for(key) - - # Pointer should only be created for whitelisted attributes. - return hash unless resource.fetchable_fields.include?(resource_key) || key == :base - - id = id_member(key) - - hash[:source] = {} - hash[:source][:pointer] = - # Relationship - if relationship_keys.include?(resource_key) - "/data/relationships/#{id}" - # Base - elsif key == :base - '/data' - # Attribute - else - "/data/attributes/#{id}" - end - end - end - - def detail_member(key, message) - if key == :base - message - else - resource_key = resource_key_for(key) - [translation_for(resource_key), message].join(' ') - end - end - - def translation_for(key) - object.class.human_attribute_name(key) - end - - def error_base - { - code: JSONAPI::VALIDATION_ERROR, - status: :unprocessable_entity - } - 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 - - 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 +require 'jsonapi/utils/exceptions/active_record' +require 'jsonapi/utils/exceptions/internal_server_error' diff --git a/lib/jsonapi/utils/exceptions/active_record.rb b/lib/jsonapi/utils/exceptions/active_record.rb new file mode 100644 index 0000000..12546ce --- /dev/null +++ b/lib/jsonapi/utils/exceptions/active_record.rb @@ -0,0 +1,95 @@ +module JSONAPI + module Utils + module Exceptions + class ActiveRecord < ::JSONAPI::Exceptions::Error + attr_reader :object, :resource, :relationships, :relationship_types, :foreign_keys + + def initialize(object, resource_klass, context) + @object = object + @resource = resource_klass.new(object, context) + + # Need to reflect on resource's relationships for error reporting. + @relationships = resource_klass._relationships.values + @relationship_types = @relationships.map(&:name).map(&:to_sym) + @foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym) + end + + def errors + object.errors.messages.flat_map do |key, messages| + messages.map { |message| build_error(key, message) } + end + end + + private + + def build_error(key, message) + error = error_base + .merge( + id: id_member(key), + title: message, + detail: detail_member(key, message) + ).merge(source_member(key)) + JSONAPI::Error.new(error) + end + + def id_member(key) + @id_member ||= JSONAPI.configuration + .key_formatter + .format(resource_key_for(key)) + .to_sym + end + + # Determine if this is a foreign key, which will need to look up its + # matching association name. + def resource_key_for(key) + return key unless foreign_keys.include?(key) + relationships.find { |r| r.foreign_key == key }.name.to_sym + end + + def source_member(key) + Hash.new.tap do |hash| + resource_key = resource_key_for(key) + + # Pointer should only be created for whitelisted attributes. + return hash unless resource.fetchable_fields.include?(resource_key) || key == :base + + id = id_member(key) + + hash[:source] = {} + hash[:source][:pointer] = + # Relationship + if relationship_types.include?(resource_key) + "/data/relationships/#{id}" + # Base + elsif key == :base + '/data' + # Attribute + else + "/data/attributes/#{id}" + end + end + end + + def detail_member(key, message) + if key == :base + message + else + resource_key = resource_key_for(key) + [translation_for(resource_key), message].join(' ') + end + end + + def translation_for(key) + object.class.human_attribute_name(key) + end + + def error_base + { + code: JSONAPI::VALIDATION_ERROR, + status: :unprocessable_entity + } + end + end + end + end +end diff --git a/lib/jsonapi/utils/exceptions/internal_server_error.rb b/lib/jsonapi/utils/exceptions/internal_server_error.rb new file mode 100644 index 0000000..efcc2d0 --- /dev/null +++ b/lib/jsonapi/utils/exceptions/internal_server_error.rb @@ -0,0 +1,20 @@ +module JSONAPI + module Utils + module Exceptions + class InternalServerError < ::JSONAPI::Exceptions::Error + def code + '500' + end + + 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 From 8f4eace1411a7d80b8375b5b889d57c61ad2558f Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Sun, 9 Jul 2017 15:55:38 -0300 Subject: [PATCH 4/8] Refactor and document JSONAPI::Utils::Exceptions::ActiveRecord --- lib/jsonapi/utils/exceptions/active_record.rb | 158 +++++++++++++----- lib/jsonapi/utils/response/formatters.rb | 15 +- 2 files changed, 124 insertions(+), 49 deletions(-) diff --git a/lib/jsonapi/utils/exceptions/active_record.rb b/lib/jsonapi/utils/exceptions/active_record.rb index 12546ce..51124b9 100644 --- a/lib/jsonapi/utils/exceptions/active_record.rb +++ b/lib/jsonapi/utils/exceptions/active_record.rb @@ -2,87 +2,157 @@ module JSONAPI module Utils module Exceptions class ActiveRecord < ::JSONAPI::Exceptions::Error - attr_reader :object, :resource, :relationships, :relationship_types, :foreign_keys + attr_reader :object, :resource, :relationships, :relationship_names, :foreign_keys + # Construct an error decorator over ActiveRecord objects. + # + # @param object [ActiveRecord::Base] Invalid ActiveRecord object. + # e.g.: User.new(name: nil).tap(&:save) + # + # @param resource_klass [JSONAPI::Resource] Resource class to be used for reflection. + # e.g.: UserResuource + # + # @return [JSONAPI::Utils::Exceptions::ActiveRecord] + # + # @api public def initialize(object, resource_klass, context) @object = object @resource = resource_klass.new(object, context) # Need to reflect on resource's relationships for error reporting. @relationships = resource_klass._relationships.values - @relationship_types = @relationships.map(&:name).map(&:to_sym) + @relationship_names = @relationships.map(&:name).map(&:to_sym) @foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym) + @resource_key_for = {} end + # Decorate errors from AR invalid objects. + # + # @note That's the method used by formatters to build the response's error body. + # + # @return [Array] + # + # @api public def errors - object.errors.messages.flat_map do |key, messages| - messages.map { |message| build_error(key, message) } + object.errors.messages.flat_map do |field, messages| + messages.map { |message| build_error(field, message) } end end private - def build_error(key, message) + # Turn AR error into JSONAPI::Error. + # + # @param field [Symbol] Name of the invalid field + # e.g.: :title + # + # @param message [String] Error message + # e.g.: "can't be blank" + # + # @return [JSONAPI::Error] + # + # @api private + def build_error(field, message) error = error_base .merge( - id: id_member(key), + id: id_member(field), title: message, - detail: detail_member(key, message) - ).merge(source_member(key)) + detail: detail_member(field, message) + ).merge(source_member(field)) JSONAPI::Error.new(error) end - def id_member(key) + # Build the "id" member value for the JSON API error object. + # e.g.: for :first_name => :"first-name" + # + # @note The returned value depends on the key formatter type defined + # via configuration: config.json_key_format = :dasherized_key + # + # @param field [Symbol] Name of the invalid field + # e.g.: :title + # + # @return [Symbol] + # + # @api private + def id_member(field) @id_member ||= JSONAPI.configuration .key_formatter - .format(resource_key_for(key)) + .format(resource_key_for(field)) .to_sym end - # Determine if this is a foreign key, which will need to look up its - # matching association name. - def resource_key_for(key) - return key unless foreign_keys.include?(key) - relationships.find { |r| r.foreign_key == key }.name.to_sym - end - - def source_member(key) - Hash.new.tap do |hash| - resource_key = resource_key_for(key) + # Build the "source" member value for the JSON API error object. + # e.g.: :title => "/data/attributes/title" + # + # @param field [Symbol] Name of the invalid field + # e.g.: :title + # + # @return [Hash] + # + # @api private + def source_member(field) + resource_key = resource_key_for(field) + return {} unless field == :base || resource.fetchable_fields.include?(resource_key) + id = id_member(field) - # Pointer should only be created for whitelisted attributes. - return hash unless resource.fetchable_fields.include?(resource_key) || key == :base + pointer = + if field == :base then '/data' + elsif relationship_names.include?(resource_key) then "/data/relationships/#{id}" + else "/data/attributes/#{id}" + end - id = id_member(key) + { source: { pointer: pointer } } + end - hash[:source] = {} - hash[:source][:pointer] = - # Relationship - if relationship_types.include?(resource_key) - "/data/relationships/#{id}" - # Base - elsif key == :base - '/data' - # Attribute - else - "/data/attributes/#{id}" - end - end + # Build the "detail" member value for the JSON API error object. + # e.g.: :first_name, "can't be blank" => "First name can't be blank" + # + # @param field [Symbol] Name of the invalid field + # e.g.: :first_name + # + # @return [String] + # + # @api private + def detail_member(field, message) + return message if field == :base + resource_key = resource_key_for(field) + [translation_for(resource_key), message].join(' ') end - def detail_member(key, message) - if key == :base - message - else - resource_key = resource_key_for(key) - [translation_for(resource_key), message].join(' ') + # Return the resource's attribute or relationship key name for a given field name. + # e.g.: :title => :title, :user_id => :author + # + # @param field [Symbol] Name of the invalid field + # e.g.: :title + # + # @return [Symbol] + # + # @api private + def resource_key_for(field) + @resource_key_for[field] ||= begin + return field unless foreign_keys.include?(field) + relationships.find { |r| r.foreign_key == field }.name.to_sym end end - def translation_for(key) - object.class.human_attribute_name(key) + # Turn the field name into human-friendly one. + # e.g.: :first_name => "First name" + # + # @param field [Symbol] Name of the invalid field + # e.g.: :first_name + # + # @return [String] + # + # @api private + def translation_for(field) + object.class.human_attribute_name(field) end + # Return the base data used for all errors of this kind. + # + # @return [Hash] + # + # @api private def error_base { code: JSONAPI::VALIDATION_ERROR, diff --git a/lib/jsonapi/utils/response/formatters.rb b/lib/jsonapi/utils/response/formatters.rb index f8d3dde..52cc0d5 100644 --- a/lib/jsonapi/utils/response/formatters.rb +++ b/lib/jsonapi/utils/response/formatters.rb @@ -13,16 +13,21 @@ def jsonapi_format(records, options = {}) alias_method :jsonapi_serialize, :jsonapi_format - def jsonapi_format_errors(data) - data = JSONAPI::Utils::Exceptions::ActiveRecord.new(data, @request.resource_klass, context) if active_record_obj?(data) - errors = data.respond_to?(:errors) ? data.errors : data + def jsonapi_format_errors(object) + if active_record_obj?(object) + object = JSONAPI::Utils::Exceptions::ActiveRecord.new(object, @request.resource_klass, context) + end + + errors = object.respond_to?(:errors) ? object.errors : object JSONAPI::Utils::Support::Error.sanitize(errors).uniq end private - def active_record_obj?(data) - defined?(ActiveRecord::Base) && (data.is_a?(ActiveRecord::Base) || data.singleton_class.include?(ActiveModel::Model)) + def active_record_obj?(object) + defined?(ActiveRecord::Base) && + (object.is_a?(ActiveRecord::Base) || + object.singleton_class.include?(ActiveModel::Model)) end def build_response_document(records, options) From 7d12a1bb3573967428fcc076d4f0123ddc6f8bf3 Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Mon, 10 Jul 2017 01:40:45 -0300 Subject: [PATCH 5/8] Fix issue with unique ids + general refactoring --- README.md | 14 ++--- lib/jsonapi/utils/exceptions/active_record.rb | 58 +++++++++++++---- lib/jsonapi/utils/response/renders.rb | 63 +++++++++++++++++-- lib/jsonapi/utils/version.rb | 2 +- spec/controllers/posts_controller_spec.rb | 14 ++--- spec/controllers/profile_controller_spec.rb | 2 +- spec/controllers/users_controller_spec.rb | 9 ++- spec/support/controllers.rb | 2 +- 8 files changed, 123 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 98e4e05..e39d817 100644 --- a/README.md +++ b/README.md @@ -65,7 +65,7 @@ gem 'jsonapi-utils', '~> 0.4.9' For Rails 5: ```ruby -gem 'jsonapi-utils', '~> 0.6.0.beta' +gem 'jsonapi-utils', '~> 0.7.0' ``` And then execute: @@ -110,8 +110,8 @@ end Arguments: - - `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array of Hashes; - - `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered; + - `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array; + - `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered; - `options`: - `resource`: explicitly points the resource to be used in the serialization. By default, JU will select resources by inferencing from controller's name. - `count`: explicitly points the total count of records for the request in order to build a proper pagination. By default, JU will count the total number of records. @@ -155,8 +155,8 @@ It renders a JSON API-compliant error response. Arguments: - Exception - - `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array of Hashes or any object which implements the `errors` method; - - `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered from the error body. + - `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array or any object which implements the `errors` method; + - `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered from the error body. Other examples: @@ -164,7 +164,7 @@ Other examples: # Render errors from a custom exception: jsonapi_render_errors Exceptions::MyCustomError.new(user) -# Render errors from an Array of Hashes: +# Render errors from an Array: errors = [{ id: 'validation', title: 'Something went wrong', code: '100' }] jsonapi_render_errors json: errors, status: :unprocessable_entity ``` @@ -188,7 +188,7 @@ end ``` Arguments: - - First: ActiveRecord object, Hash or Array of Hashes; + - First: ActiveRecord object, Hash or Array; - Last: Hash of options (same as `JSONAPI::Utils#jsonapi_render`). #### Paginators diff --git a/lib/jsonapi/utils/exceptions/active_record.rb b/lib/jsonapi/utils/exceptions/active_record.rb index 51124b9..ede6eba 100644 --- a/lib/jsonapi/utils/exceptions/active_record.rb +++ b/lib/jsonapi/utils/exceptions/active_record.rb @@ -23,7 +23,8 @@ def initialize(object, resource_klass, context) @relationships = resource_klass._relationships.values @relationship_names = @relationships.map(&:name).map(&:to_sym) @foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym) - @resource_key_for = {} + @resource_key_for = {} + @formatted_key = {} end # Decorate errors from AR invalid objects. @@ -35,7 +36,9 @@ def initialize(object, resource_klass, context) # @api public def errors object.errors.messages.flat_map do |field, messages| - messages.map { |message| build_error(field, message) } + messages.map.with_index do |message, index| + build_error(field, message, index) + end end end @@ -49,13 +52,15 @@ def errors # @param message [String] Error message # e.g.: "can't be blank" # + # @param index [Integer] Index of the error detail + # # @return [JSONAPI::Error] # # @api private - def build_error(field, message) + def build_error(field, message, index = 0) error = error_base .merge( - id: id_member(field), + id: id_member(field, index), title: message, detail: detail_member(field, message) ).merge(source_member(field)) @@ -63,10 +68,37 @@ def build_error(field, message) end # Build the "id" member value for the JSON API error object. - # e.g.: for :first_name => :"first-name" + # e.g.: for :first_name, :too_short => "first-name#too-short" + # + # @note The returned value depends on the key formatter type defined + # via configuration, e.g.: config.json_key_format = :dasherized_key + # + # @param field [Symbol] Name of the invalid field + # e.g.: :first_name + # + # @param index [Integer] Index of the error detail + # + # @return [String] + # + # @api private + def id_member(field, index) + [ + key_format(field), + key_format( + object.errors.details + .dig(field, index, :error) + .to_s.downcase + .split + .join('_') + ) + ].join('#') + end + + # Bring the formatted resource key for a given field. + # e.g.: for :first_name => :"first-name" # # @note The returned value depends on the key formatter type defined - # via configuration: config.json_key_format = :dasherized_key + # via configuration, e.g.: config.json_key_format = :dasherized_key # # @param field [Symbol] Name of the invalid field # e.g.: :title @@ -74,15 +106,15 @@ def build_error(field, message) # @return [Symbol] # # @api private - def id_member(field) - @id_member ||= JSONAPI.configuration + def key_format(field) + @formatted_key[field] ||= JSONAPI.configuration .key_formatter .format(resource_key_for(field)) .to_sym end # Build the "source" member value for the JSON API error object. - # e.g.: :title => "/data/attributes/title" + # e.g.: :title => "/data/attributes/title" # # @param field [Symbol] Name of the invalid field # e.g.: :title @@ -93,7 +125,7 @@ def id_member(field) def source_member(field) resource_key = resource_key_for(field) return {} unless field == :base || resource.fetchable_fields.include?(resource_key) - id = id_member(field) + id = key_format(field) pointer = if field == :base then '/data' @@ -105,7 +137,7 @@ def source_member(field) end # Build the "detail" member value for the JSON API error object. - # e.g.: :first_name, "can't be blank" => "First name can't be blank" + # e.g.: :first_name, "can't be blank" => "First name can't be blank" # # @param field [Symbol] Name of the invalid field # e.g.: :first_name @@ -120,7 +152,7 @@ def detail_member(field, message) end # Return the resource's attribute or relationship key name for a given field name. - # e.g.: :title => :title, :user_id => :author + # e.g.: :title => :title, :user_id => :author # # @param field [Symbol] Name of the invalid field # e.g.: :title @@ -136,7 +168,7 @@ def resource_key_for(field) end # Turn the field name into human-friendly one. - # e.g.: :first_name => "First name" + # e.g.: :first_name => "First name" # # @param field [Symbol] Name of the invalid field # e.g.: :first_name diff --git a/lib/jsonapi/utils/response/renders.rb b/lib/jsonapi/utils/response/renders.rb index b7d6e92..49079d7 100644 --- a/lib/jsonapi/utils/response/renders.rb +++ b/lib/jsonapi/utils/response/renders.rb @@ -2,36 +2,87 @@ module JSONAPI module Utils module Response module Renders + # Helper method to render JSON API-compliant responses. + # + # @param json [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array] + # Object to be serialized into JSON + # e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } }, + # [{ data: { id: 1, first_name: 'Tiago' } }] + # + # @param status [Integer, String, Symbol] HTTP status code + # e.g.: 201, '201', :created + # + # @option options [JSONAPI::Resource] resource: it tells the render the resource + # class that should be used rather than infered (default) + # + # @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated + # when a Hash or Array of Hashes is passed to the "json" key argument + # + # @option options [Integer] count: if it's rendering a collection of resource, the default + # gem's counting method could be bypassed by the use of this options. It's used to show + # the total records a request would retrieve and also to calculate the pagination. + # + # @return [String] + # + # @api public def jsonapi_render(json:, status: nil, options: {}) body = jsonapi_format(json, options) - render json: body, status: status || @_response_document.status + render json: body, status: (status || @_response_document.status) rescue => e - handle_exceptions(e) + handle_exceptions(e) # http://bit.ly/2sEEGTN ensure correct_media_type end - def jsonapi_render_errors(exception = nil, json: nil, status: nil) - body = jsonapi_format_errors(exception || json) - status = status || body.try(:first).try(:[], :status) + # Helper method to render JSON API-compliant error responses. + # + # @param error [ActiveRecord::Base or any object that responds to #errors] + # Error object to be serialized into JSON + # e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object) + # + # @param json [ActiveRecord::Base or any object that responds to #errors] + # Error object to be serialized into JSON + # e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object) + # + # @param status [Integer, String, Symbol] HTTP status code + # e.g.: 422, '422', :unprocessable_entity + # + # @return [String] + # + # @api public + def jsonapi_render_errors(error = nil, json: nil, status: nil) + body = jsonapi_format_errors(error || json) + status = status || body.try(:first).try(:[], :status) || :bad_request render json: { errors: body }, status: status ensure correct_media_type end + # Helper method to render HTTP 500 Interval Server Error. + # + # @api public def jsonapi_render_internal_server_error jsonapi_render_errors(::JSONAPI::Utils::Exceptions::InternalServerError.new) end + # Helper method to render HTTP 400 Bad Request. + # + # @api public def jsonapi_render_bad_request jsonapi_render_errors(::JSONAPI::Utils::Exceptions::BadRequest.new) end + # Helper method to render HTTP 404 Bad Request. + # + # @api public def jsonapi_render_not_found(exception) - id = exception.message.match(/=([\w-]+)/).try(:[], 1) || '(no identifier)' + id = exception.message =~ /=([\w-]+)/ && $1 || '(no identifier)' jsonapi_render_errors(JSONAPI::Exceptions::RecordNotFound.new(id)) end + # Helper method to render HTTP 404 Bad Request with null "data". + # + # @api public def jsonapi_render_not_found_with_null render json: { data: nil }, status: 200 end diff --git a/lib/jsonapi/utils/version.rb b/lib/jsonapi/utils/version.rb index 6a9159f..436b181 100644 --- a/lib/jsonapi/utils/version.rb +++ b/lib/jsonapi/utils/version.rb @@ -1,5 +1,5 @@ module JSONAPI module Utils - VERSION = '0.6.0'.freeze + VERSION = '0.7.0'.freeze end end diff --git a/spec/controllers/posts_controller_spec.rb b/spec/controllers/posts_controller_spec.rb index 463f9c9..aeef1f4 100644 --- a/spec/controllers/posts_controller_spec.rb +++ b/spec/controllers/posts_controller_spec.rb @@ -236,7 +236,7 @@ it 'renders a 422 response' do expect { subject }.to change(Post, :count).by(0) expect(response).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('title') + expect(errors.dig(0, 'id')).to eq('title#blank') expect(errors.dig(0, 'title')).to eq('can\'t be blank') expect(errors.dig(0, 'detail')).to eq('Title can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') @@ -255,7 +255,7 @@ expect { subject }.to change(Post, :count).by(0) expect(subject).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('author') + expect(errors.dig(0, 'id')).to eq('author#blank') expect(errors.dig(0, 'title')).to eq('can\'t be blank') expect(errors.dig(0, 'detail')).to eq('Author can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') @@ -274,7 +274,7 @@ expect { subject }.to change(Post, :count).by(0) expect(subject).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('category') + expect(errors.dig(0, 'id')).to eq('category#blank') expect(errors.dig(0, 'title')).to eq('can\'t be blank') expect(errors.dig(0, 'detail')).to eq('Category can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') @@ -293,7 +293,7 @@ expect { subject }.to change(Post, :count).by(0) expect(subject).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('hidden_field') + expect(errors.dig(0, 'id')).to eq('hidden_field#error_was_tripped') expect(errors.dig(0, 'title')).to eq('error was tripped') expect(errors.dig(0, 'detail')).to eq('Hidden field error was tripped') expect(errors.dig(0, 'code')).to eq('100') @@ -321,7 +321,7 @@ expect { subject }.to change(Post, :count).by(0) expect(subject).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('content-type') + expect(errors.dig(0, 'id')).to eq('content-type#blank') expect(errors.dig(0, 'title')).to eq('can\'t be blank') expect(errors.dig(0, 'detail')).to eq('Content type can\'t be blank') expect(errors.dig(0, 'code')).to eq('100') @@ -342,7 +342,7 @@ it 'renders a 422 response' do expect { subject }.to change(Post, :count).by(0) expect(response).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('title') + expect(errors.dig(0, 'id')).to eq('title#blank') expect(errors.dig(0, 'title')).to eq('не может быть пустым') expect(errors.dig(0, 'detail')).to eq('Заголовок не может быть пустым') expect(errors.dig(0, 'code')).to eq('100') @@ -371,7 +371,7 @@ expect { subject }.to change(Post, :count).by(0) expect(response).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('base') + expect(errors.dig(0, 'id')).to eq('base#this_is_an_error_on_the_base') expect(errors.dig(0, 'title')).to eq('This is an error on the base') expect(errors.dig(0, 'code')).to eq('100') expect(errors.dig(0, 'source', 'pointer')).to eq('/data') diff --git a/spec/controllers/profile_controller_spec.rb b/spec/controllers/profile_controller_spec.rb index 844bbf9..b32cba1 100644 --- a/spec/controllers/profile_controller_spec.rb +++ b/spec/controllers/profile_controller_spec.rb @@ -30,7 +30,7 @@ it 'renders a 422 response' do patch :update, params: body expect(response).to have_http_status :unprocessable_entity - expect(errors.dig(0, 'id')).to eq('nickname') + expect(errors.dig(0, 'id')).to eq('nickname#blank') expect(errors.dig(0, 'title')).to eq("can't be blank") expect(errors.dig(0, 'detail')).to eq("Nickname can't be blank") expect(errors.dig(0, 'code')).to eq('100') diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb index 6d1331e..3b0248b 100644 --- a/spec/controllers/users_controller_spec.rb +++ b/spec/controllers/users_controller_spec.rb @@ -7,12 +7,11 @@ @user = FactoryGirl.create_list(:user, 3, :with_posts).first end - let(:user) { @user } - before(:each) do JSONAPI.configuration.json_key_format = :underscored_key end + let(:user) { @user } let(:relationships) { UserResource._relationships.keys.map(&:to_s) } let(:fields) { UserResource.fields.reject { |e| e == :id }.map(&:to_s) - relationships } let(:attributes) { { first_name: 'Yehuda', last_name: 'Katz' } } @@ -411,12 +410,12 @@ it_behaves_like '400 response', admin: true end - context 'when validation fails' do + context 'with validation error and no status code set' do before { user_params.dig(:data, :attributes).merge!(first_name: nil, last_name: nil) } - it 'renders a 422 response' do + it 'renders a 400 response by default' do expect { post :create, params: user_params }.to change(User, :count).by(0) - expect(response).to have_http_status :unprocessable_entity + expect(response).to have_http_status :bad_request expect(errors.dig(0, 'id')).to eq('first_name') expect(errors.dig(0, 'title')).to eq('can\'t be blank') diff --git a/spec/support/controllers.rb b/spec/support/controllers.rb index 22a9ec1..9f556d7 100644 --- a/spec/support/controllers.rb +++ b/spec/support/controllers.rb @@ -99,7 +99,7 @@ def create { id: 'first_name', code: '100', title: 'can\'t be blank', detail: 'First name can\'t be blank' }, { id: 'last_name', code: '100', title: 'can\'t be blank', detail: 'Last name can\'t be blank' } ] - jsonapi_render_errors json: errors, status: :unprocessable_entity + jsonapi_render_errors json: errors end end From 6b734cad23452eca283c477bdc8f2f3b933cdbb1 Mon Sep 17 00:00:00 2001 From: Marcel Pursche Date: Mon, 10 Jul 2017 13:06:35 +0200 Subject: [PATCH 6/8] Apply sort before pagination to ensure right response for arrays --- lib/jsonapi/utils/response/formatters.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/utils/response/formatters.rb b/lib/jsonapi/utils/response/formatters.rb index f8d3dde..6f3a376 100644 --- a/lib/jsonapi/utils/response/formatters.rb +++ b/lib/jsonapi/utils/response/formatters.rb @@ -62,8 +62,8 @@ def result_options(records, options) def build_collection(records, options = {}) records = apply_filter(records, options) - records = apply_pagination(records, options) records = apply_sort(records) + records = apply_pagination(records, options) records.respond_to?(:to_ary) ? records.map { |record| turn_into_resource(record, options) } : [] end From fab1885d86cff92e3f2ec44f6b0d1a5b70ed1f33 Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Sun, 16 Jul 2017 13:14:55 -0300 Subject: [PATCH 7/8] Refactor on JSONAPI::Utils::Response::Formatters --- lib/jsonapi/utils/response/formatters.rb | 193 +++++++++++++++++++---- lib/jsonapi/utils/response/renders.rb | 10 +- 2 files changed, 166 insertions(+), 37 deletions(-) diff --git a/lib/jsonapi/utils/response/formatters.rb b/lib/jsonapi/utils/response/formatters.rb index a0721bc..460e3a0 100644 --- a/lib/jsonapi/utils/response/formatters.rb +++ b/lib/jsonapi/utils/response/formatters.rb @@ -2,77 +2,168 @@ module JSONAPI module Utils module Response module Formatters - def jsonapi_format(records, options = {}) - if records.is_a?(Hash) - hash = records.with_indifferent_access - records = hash_to_active_record(hash[:data], options[:model]) + # Helper method to format ActiveRecord or Hash objects into JSON API-compliant ones. + # + # @note The return of this method represents what will actually be displayed in the response body. + # @note It can also be called as #jsonapi_serialize due to backward compatibility issues. + # + # @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array] + # Object to be formatted into JSON + # e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } }, + # [{ data: { id: 1, first_name: 'Tiago' } }] + # + # @option options [JSONAPI::Resource] resource: it tells the formatter which resource + # class to be used rather than use an infered one (default behaviour) + # + # @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated + # when a Hash or Array of Hashes is passed as the "object" argument + # + # @option options [Integer] count: if it's rendering a collection of resources, the default + # gem's counting method can be bypassed by the use of this options. It's shows then the total + # records resulting from that request and also calculates the pagination. + # + # @return [Hash] + # + # @api public + def jsonapi_format(object, options = {}) + if object.is_a?(Hash) + hash = object.with_indifferent_access + object = hash_to_active_record(hash[:data], options[:model]) end - fix_request_options(params, records) - build_response_document(records, options).contents + fix_custom_request_options(object) + build_response_document(object, options).contents end alias_method :jsonapi_serialize, :jsonapi_format + # Helper method to format ActiveRecord or any object that responds to #errors + # into JSON API-compliant error response bodies. + # + # @note The return of this method represents what will actually be displayed in the response body. + # @note It can also be called as #jsonapi_serialize_errors due to backward compatibility issues. + # + # @param object [ActiveRecord::Base or any object that responds to #errors] + # Error object to be serialized into JSON + # e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object) + # + # @return [Array] + # + # @api public def jsonapi_format_errors(object) if active_record_obj?(object) object = JSONAPI::Utils::Exceptions::ActiveRecord.new(object, @request.resource_klass, context) end - errors = object.respond_to?(:errors) ? object.errors : object JSONAPI::Utils::Support::Error.sanitize(errors).uniq end + alias_method :jsonapi_serialize_errors, :jsonapi_format_errors + private + # Check whether the given object is an ActiveRecord-like one. + # + # @param object [Object] Object to be checked + # + # @return [TrueClass, FalseClass] + # + # @api private def active_record_obj?(object) defined?(ActiveRecord::Base) && (object.is_a?(ActiveRecord::Base) || object.singleton_class.include?(ActiveModel::Model)) end - def build_response_document(records, options) + # Build the full response document. + # + # @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array] + # Object to be formatted into JSON + # e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } }, + # [{ data: { id: 1, first_name: 'Tiago' } }] + # + # @option options [JSONAPI::Resource] resource: it tells the builder which resource + # class to be used rather than use an infered one (default behaviour) + # + # @option options [Integer] count: if it's rendering a collection of resources, the default + # gem's counting method can be bypassed by the use of this options. It's shows then the total + # records resulting from that request and also calculates the pagination. + # + # @return [JSONAPI::ResponseDocument] + # + # @api private + def build_response_document(object, options) results = JSONAPI::OperationResults.new - if records.respond_to?(:to_ary) - @_records = build_collection(records, options) - results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, @_records, result_options(records, options))) + if object.respond_to?(:to_ary) + records = build_collection(object, options) + results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, records, result_options(object, options))) else - @_record = turn_into_resource(records, options) - results.add_result(JSONAPI::ResourceOperationResult.new(:ok, @_record)) + record = turn_into_resource(object, options) + results.add_result(JSONAPI::ResourceOperationResult.new(:ok, record)) end @_response_document = create_response_document(results) end - def fix_request_options(params, records) - return if request.method !~ /get/i || - params.nil? || - %w(index show create update destroy).include?(params[:action]) - action = records.respond_to?(:to_ary) ? 'index' : 'show' + # Apply a proper action setup for custom requests/actions. + # + # @note The setup_(index|show)_action comes from JSONAPI::Resources' API. + # + # @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array] + # It's checked whether this object refers to a collection or not. + # + # @api private + def fix_custom_request_options(object) + return unless custom_get_request_with_params? + action = object.respond_to?(:to_ary) ? 'index' : 'show' @request.send("setup_#{action}_action", params) end - def result_options(records, options) - {}.tap do |data| - if JSONAPI.configuration.default_paginator != :none && - JSONAPI.configuration.top_level_links_include_pagination - data[:pagination_params] = pagination_params(records, options) - end - - if JSONAPI.configuration.top_level_meta_include_record_count - data[:record_count] = count_records(records, options) - end - end + # Check whether it's a custom GET request with params. + # + # @return [TrueClass, FalseClass] + # + # @api private + def custom_get_request_with_params? + request.method =~ /get/i && !%w(index show).include?(params[:action]) && !params.nil? end - def build_collection(records, options = {}) + # Turn a collection of AR or Hash objects into a collection of JSONAPI::Resource ones. + # + # @param records [ActiveRecord::Relation, Hash, Array] + # Objects to be instantiated as JSONAPI::Resource ones. + # e.g.: User.all, [{ data: { id: 1, first_name: 'Tiago' } }] + # + # @option options [JSONAPI::Resource] resource: it tells the buider which resource + # class to be used rather than use an infered one (default behaviour) + # + # @option options [Integer] count: if it's rendering a collection of resources, the default + # gem's counting method can be bypassed by the use of this options. It's shows then the total + # records resulting from that request and also calculates the pagination. + # + # @return [Array] + # + # @api private + def build_collection(records, options) records = apply_filter(records, options) records = apply_sort(records) - records = apply_pagination(records, options) + records = apply_pagination(records, options) records.respond_to?(:to_ary) ? records.map { |record| turn_into_resource(record, options) } : [] end - def turn_into_resource(record, options = {}) + # Turn an AR or Hash object into a JSONAPI::Resource one. + # + # @param records [ActiveRecord::Relation, Hash, Array] + # Object to be instantiated as a JSONAPI::Resource one. + # e.g.: User.first, { data: { id: 1, first_name: 'Tiago' } } + # + # @option options [JSONAPI::Resource] resource: it tells which resource + # class to be used rather than use an infered one (default behaviour) + # + # @return [JSONAPI::Resource] + # + # @api private + def turn_into_resource(record, options) if options[:resource] options[:resource].to_s.constantize.new(record, context) else @@ -80,6 +171,44 @@ def turn_into_resource(record, options = {}) end end + # Apply some result options like pagination params and count to a collection response. + # + # @param records [ActiveRecord::Relation, Hash, Array] + # Object to be formatted into JSON + # e.g.: User.all, [{ data: { id: 1, first_name: 'Tiago' } }] + # + # @option options [Integer] count: if it's rendering a collection of resources, the default + # gem's counting method can be bypassed by the use of this options. It's shows then the total + # records resulting from that request and also calculates the pagination. + # + # @return [Hash] + # + # @api private + def result_options(records, options) + {}.tap do |data| + if JSONAPI.configuration.default_paginator != :none && + JSONAPI.configuration.top_level_links_include_pagination + data[:pagination_params] = pagination_params(records, options) + end + + if JSONAPI.configuration.top_level_meta_include_record_count + data[:record_count] = count_records(records, options) + end + end + end + + # Convert Hash or collection of Hashes into AR objects. + # + # @param data [Hash, Array] Hash or collection to be converted + # e.g.: { data: { id: 1, first_name: 'Tiago' } }, + # [{ data: { id: 1, first_name: 'Tiago' } }], + # + # @option options [ActiveRecord::Base] model: ActiveRecord model class to be + # used as base for the objects' intantialization. + # + # @return [ActiveRecord::Base, ActiveRecord::Relation] + # + # @api private def hash_to_active_record(data, model) return data if model.nil? coerced = [data].flatten.map { |hash| model.new(hash) } diff --git a/lib/jsonapi/utils/response/renders.rb b/lib/jsonapi/utils/response/renders.rb index 49079d7..7777edf 100644 --- a/lib/jsonapi/utils/response/renders.rb +++ b/lib/jsonapi/utils/response/renders.rb @@ -12,15 +12,15 @@ module Renders # @param status [Integer, String, Symbol] HTTP status code # e.g.: 201, '201', :created # - # @option options [JSONAPI::Resource] resource: it tells the render the resource - # class that should be used rather than infered (default) + # @option options [JSONAPI::Resource] resource: it tells the render which resource + # class to be used rather than use an infered one (default behaviour) # # @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated # when a Hash or Array of Hashes is passed to the "json" key argument # - # @option options [Integer] count: if it's rendering a collection of resource, the default - # gem's counting method could be bypassed by the use of this options. It's used to show - # the total records a request would retrieve and also to calculate the pagination. + # @option options [Integer] count: if it's rendering a collection of resources, the default + # gem's counting method can be bypassed by the use of this options. It's shows then the total + # records resulting from that request and also calculates the pagination. # # @return [String] # From 63c53d15802bac860fbc531f633c537ecd23580e Mon Sep 17 00:00:00 2001 From: Tiago Guedes Date: Sun, 16 Jul 2017 13:28:13 -0300 Subject: [PATCH 8/8] Document JSONAPI::Utils::Exceptions::InternalServerError --- lib/jsonapi/utils/exceptions/active_record.rb | 2 +- lib/jsonapi/utils/exceptions/internal_server_error.rb | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/lib/jsonapi/utils/exceptions/active_record.rb b/lib/jsonapi/utils/exceptions/active_record.rb index ede6eba..2c1b036 100644 --- a/lib/jsonapi/utils/exceptions/active_record.rb +++ b/lib/jsonapi/utils/exceptions/active_record.rb @@ -27,7 +27,7 @@ def initialize(object, resource_klass, context) @formatted_key = {} end - # Decorate errors from AR invalid objects. + # Decorate errors for AR invalid objects. # # @note That's the method used by formatters to build the response's error body. # diff --git a/lib/jsonapi/utils/exceptions/internal_server_error.rb b/lib/jsonapi/utils/exceptions/internal_server_error.rb index efcc2d0..87d44d1 100644 --- a/lib/jsonapi/utils/exceptions/internal_server_error.rb +++ b/lib/jsonapi/utils/exceptions/internal_server_error.rb @@ -2,10 +2,20 @@ module JSONAPI module Utils module Exceptions class InternalServerError < ::JSONAPI::Exceptions::Error + # HTTP status code + # + # @return [String] + # + # @api public def code '500' end + # Decorate errors for 500 responses. + # + # @return [Array] + # + # @api public def errors [JSONAPI::Error.new( code: code,