diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 59a3e3c..9df6bd4 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -3,12 +3,36 @@ module JSONAPI # [ActiveModel::Errors] serializer class ActiveModelErrorSerializer < ErrorSerializer - attribute :status do - '422' + class << self + ## + # Get the status code to render for the serializer + # + # This considers an optional status provided through the serializer + # parameters, as either a symbol or a number. + # + # @param params [Hash] + # The serializer parameters + # + # @return [Integer] + # The status code to use + def status_code(params) + case params[:status] + when Symbol + Rack::Utils::SYMBOL_TO_STATUS_CODE[params[:status]] + when Integer + params[:status] + else + 422 + end + end + end + + attribute :status do |_, params| + status_code(params).to_s end - attribute :title do - Rack::Utils::HTTP_STATUS_CODES[422] + attribute :title do |_, params| + Rack::Utils::HTTP_STATUS_CODES[status_code(params)] end attribute :code do |object| @@ -28,12 +52,12 @@ class ActiveModelErrorSerializer < ErrorSerializer message = errors_object.generate_message( error_key, nil, error_hash[:error] ) - elsif error_hash[:error].present? + elsif error_hash[:error].present? && error_hash[:error].is_a?(Symbol) message = errors_object.generate_message( error_key, error_hash[:error], error_hash ) else - message = error_hash[:message] + message = error_hash[:message] || error_hash[:error] end errors_object.full_message(error_key, message) @@ -49,8 +73,10 @@ class ActiveModelErrorSerializer < ErrorSerializer { pointer: "/data/attributes/#{error_key}" } elsif rels.include?(error_key) { pointer: "/data/relationships/#{error_key}" } + elsif error_key == :base + { pointer: '/data' } else - { pointer: '' } + { pointer: nil } end end end diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 8d29d17..75a4790 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -62,8 +62,22 @@ def self.add_errors_renderer! details[attr] ||= [] details[attr] << error.detail.merge(message: error.message) end - elsif resource.respond_to?(:details) - details = resource.details + elsif resource.respond_to?(:details) && resource.respond_to?(:messages) + resource.details.each do |attr, problems| + problems.each_with_index do |error, index| + details[attr] ||= [] + + if error[:error].is_a?(Hash) + current = error[:error].dup + current[:error] ||= :invalid + + details[attr] << current + else + message = resource.messages[attr][index] + details[attr] << error.merge(message: message) + end + end + end else details = resource.messages end @@ -79,7 +93,11 @@ def self.add_errors_renderer! JSONAPI::Rails.serializer_to_json( JSONAPI::ActiveModelErrorSerializer.new( - errors, params: { model: model, model_serializer: model_serializer } + errors, params: { + model: model, + model_serializer: model_serializer, + status: options[:status] + } ) ) end diff --git a/spec/dummy.rb b/spec/dummy.rb index 6f0c434..076e5c8 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -36,7 +36,25 @@ class User < ActiveRecord::Base class Note < ActiveRecord::Base validates_format_of :title, without: /BAD_TITLE/ validates_numericality_of :quantity, less_than: 100, if: :quantity? + validate :title_check belongs_to :user, required: true + + before_destroy :deletable? + + # Provide a validation adding an error to the model's base + def title_check + return unless title == 'n/a' + + message = 'The record has an unacceptable title.' + errors.add(:base, :model_invalid, errors: message) + end + + def deletable? + return true unless title == 'Lovely' + + errors.add(:base, "Can't delete lovely notes") + throw :abort + end end class CustomNoteSerializer @@ -69,7 +87,7 @@ class Dummy < Rails::Application routes.draw do scope defaults: { format: :jsonapi } do resources :users, only: [:index] - resources :notes, only: [:update] + resources :notes, only: [:update, :destroy] end end end @@ -136,6 +154,15 @@ def update end end + def destroy + note = Note.find(params[:id]) + if note.destroy + head :no_content + else + render jsonapi_errors: note.errors, status: :conflict + end + end + private def render_jsonapi_internal_server_error(exception) Rails.logger.error(exception) diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index da3d0f5..49ce6f0 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -56,13 +56,8 @@ .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) expect(response_json['errors'][0]['source']) .to eq('pointer' => '/data/relationships/user') - if Rails::VERSION::MAJOR >= 6 && Rails::VERSION::MINOR >= 1 - expect(response_json['errors'][0]['detail']) - .to eq('User must exist') - else - expect(response_json['errors'][0]['detail']) - .to eq('User can\'t be blank') - end + expect(response_json['errors'][0]['detail']) + .to eq('User must exist') end context 'required by validations' do @@ -133,6 +128,27 @@ .to eq('pointer' => '/data/attributes/title') end end + + context 'with a validation error on the class' do + let(:params) do + payload = note_params.dup + payload[:data][:attributes][:title] = 'n/a' + payload + end + + it do + expect(response).to have_http_status(:unprocessable_entity) + expect(response_json['errors'].size).to eq(1) + expect(response_json['errors'][0]['status']).to eq('422') + expect(response_json['errors'][0]['code']).to include('invalid') + expect(response_json['errors'][0]['title']) + .to eq(Rack::Utils::HTTP_STATUS_CODES[422]) + expect(response_json['errors'][0]['source']) + .to eq('pointer' => '/data') + expect(response_json['errors'][0]['detail']) + .to eq('Validation failed: The record has an unacceptable title.') + end + end end context 'with a bad note ID' do @@ -165,4 +181,41 @@ end end end + + describe 'DELETE /nodes/:id' do + let(:note) { create_note } + let(:note_id) { note.id } + let(:user) { note.user } + let(:user_id) { user.id } + + context 'with a random note' do + before { delete(note_path(note_id), headers: jsonapi_headers) } + + it { expect(response).to have_http_status(:no_content) } + end + + context 'with a lovely note' do + let(:errors) do + { + 'errors' => [ + { + 'code' => 'cant_delete_lovely_notes', + 'detail' => "Can't delete lovely notes", + 'source' => { 'pointer' => '/data' }, + 'status' => '409', + 'title' => 'Conflict' + } + ] + } + end + + before do + note.update(title: 'Lovely') + delete(note_path(note_id), headers: jsonapi_headers) + end + + it { expect(response).to have_http_status(:conflict) } + it { expect(response_json).to match(errors) } + end + end end