From 0ad9e6ae7a1ee52835793ea02d452b0d1b1b3903 Mon Sep 17 00:00:00 2001 From: Johannes Vetter Date: Sun, 14 Mar 2021 13:32:04 -0700 Subject: [PATCH 1/6] Validation errors on the model instance should return the correct pointer value --- lib/jsonapi/active_model_error_serializer.rb | 2 ++ spec/dummy.rb | 6 ++++++ spec/errors_spec.rb | 21 ++++++++++++++++++++ 3 files changed, 29 insertions(+) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 59a3e3c..0789bb9 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -49,6 +49,8 @@ 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: '' } end diff --git a/spec/dummy.rb b/spec/dummy.rb index 6f0c434..5070baf 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -36,7 +36,13 @@ 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 + + # Provide a validation adding an error to the model's base + def title_check + errors.add(:base, :invalid) if title == 'n/a' + end end class CustomNoteSerializer diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index da3d0f5..e47062e 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -133,6 +133,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('is invalid') + end + end end context 'with a bad note ID' do From 39aadc965f80715b3ce940a2146e76c36d24782d Mon Sep 17 00:00:00 2001 From: Johannes Vetter Date: Sun, 14 Mar 2021 13:32:48 -0700 Subject: [PATCH 2/6] Return a nil pointer rather than a blank --- lib/jsonapi/active_model_error_serializer.rb | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 0789bb9..31c6347 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -52,7 +52,7 @@ class ActiveModelErrorSerializer < ErrorSerializer elsif error_key == :base { pointer: "/data" } else - { pointer: '' } + { pointer: nil } end end end From e383a59fbecc72d596ab1144a646791d35157ea7 Mon Sep 17 00:00:00 2001 From: Johannes Vetter Date: Wed, 24 Mar 2021 18:39:58 -0700 Subject: [PATCH 3/6] Add support for status to the error serializer --- lib/jsonapi/active_model_error_serializer.rb | 30 +++++++++++++--- lib/jsonapi/rails.rb | 2 +- spec/dummy.rb | 20 ++++++++++- spec/errors_spec.rb | 37 ++++++++++++++++++++ 4 files changed, 83 insertions(+), 6 deletions(-) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 31c6347..4198839 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -3,12 +3,34 @@ module JSONAPI # [ActiveModel::Errors] serializer class ActiveModelErrorSerializer < ErrorSerializer - attribute :status do - '422' + class << self + ## + # Get the status code to render for the serializer, considering an eventual + # status provided through the serializer parameters + # + # @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| diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 8d29d17..6e812f9 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -79,7 +79,7 @@ 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 5070baf..7cc0eb8 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -39,10 +39,19 @@ class Note < ActiveRecord::Base 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 errors.add(:base, :invalid) if title == 'n/a' end + + def deletable? + return true unless title == 'Lovely' + + errors.add(:base, "Can't delete lovely notes") + throw :abort + end end class CustomNoteSerializer @@ -75,7 +84,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 @@ -142,6 +151,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 e47062e..1ced51f 100644 --- a/spec/errors_spec.rb +++ b/spec/errors_spec.rb @@ -186,4 +186,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 From 6a81a5a17ecdadcd25cedcb12fbe6f3b849965e7 Mon Sep 17 00:00:00 2001 From: Johannes Vetter Date: Mon, 29 Mar 2021 12:28:13 -0700 Subject: [PATCH 4/6] Make error handling consistent for Rails 5+ and 6+ --- lib/jsonapi/active_model_error_serializer.rb | 4 ++-- lib/jsonapi/rails.rb | 17 +++++++++++++++-- spec/dummy.rb | 2 +- spec/errors_spec.rb | 11 +++-------- 4 files changed, 21 insertions(+), 13 deletions(-) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 4198839..9e9dade 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -50,12 +50,12 @@ def status_code(params) 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) diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index 6e812f9..fe4b664 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -62,8 +62,21 @@ 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 + details[attr] << error.merge(message: resource.messages[attr][index]) + end + end + end else details = resource.messages end diff --git a/spec/dummy.rb b/spec/dummy.rb index 7cc0eb8..01e018c 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -43,7 +43,7 @@ class Note < ActiveRecord::Base # Provide a validation adding an error to the model's base def title_check - errors.add(:base, :invalid) if title == 'n/a' + errors.add(:base, :model_invalid, errors: 'The record has an unacceptable title.') if title == 'n/a' end def deletable? diff --git a/spec/errors_spec.rb b/spec/errors_spec.rb index 1ced51f..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 @@ -151,7 +146,7 @@ expect(response_json['errors'][0]['source']) .to eq('pointer' => '/data') expect(response_json['errors'][0]['detail']) - .to eq('is invalid') + .to eq('Validation failed: The record has an unacceptable title.') end end end From 0f67ee52039e4309ee8eab258b7241977f0505fa Mon Sep 17 00:00:00 2001 From: Johannes Vetter Date: Mon, 29 Mar 2021 16:26:26 -0700 Subject: [PATCH 5/6] Make the comment lines shorter --- lib/jsonapi/active_model_error_serializer.rb | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 9e9dade..3b797d2 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -5,8 +5,8 @@ module JSONAPI class ActiveModelErrorSerializer < ErrorSerializer class << self ## - # Get the status code to render for the serializer, considering an eventual - # status provided through the serializer parameters + # Get the status code to render for the serializer, considering an + # eventual status provided through the serializer parameters # # @param params [Hash] # The serializer parameters From 74f3ac29f98fb575f09b2e238f13c21b82cea0ac Mon Sep 17 00:00:00 2001 From: Johannes Vetter Date: Mon, 29 Mar 2021 16:40:39 -0700 Subject: [PATCH 6/6] Try to make Rubocop happy --- lib/jsonapi/active_model_error_serializer.rb | 8 +++++--- lib/jsonapi/rails.rb | 9 +++++++-- spec/dummy.rb | 5 ++++- 3 files changed, 16 insertions(+), 6 deletions(-) diff --git a/lib/jsonapi/active_model_error_serializer.rb b/lib/jsonapi/active_model_error_serializer.rb index 3b797d2..9df6bd4 100644 --- a/lib/jsonapi/active_model_error_serializer.rb +++ b/lib/jsonapi/active_model_error_serializer.rb @@ -5,8 +5,10 @@ module JSONAPI class ActiveModelErrorSerializer < ErrorSerializer class << self ## - # Get the status code to render for the serializer, considering an - # eventual status provided through the serializer parameters + # 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 @@ -72,7 +74,7 @@ def status_code(params) elsif rels.include?(error_key) { pointer: "/data/relationships/#{error_key}" } elsif error_key == :base - { pointer: "/data" } + { pointer: '/data' } else { pointer: nil } end diff --git a/lib/jsonapi/rails.rb b/lib/jsonapi/rails.rb index fe4b664..75a4790 100644 --- a/lib/jsonapi/rails.rb +++ b/lib/jsonapi/rails.rb @@ -73,7 +73,8 @@ def self.add_errors_renderer! details[attr] << current else - details[attr] << error.merge(message: resource.messages[attr][index]) + message = resource.messages[attr][index] + details[attr] << error.merge(message: message) end end end @@ -92,7 +93,11 @@ def self.add_errors_renderer! JSONAPI::Rails.serializer_to_json( JSONAPI::ActiveModelErrorSerializer.new( - errors, params: { model: model, model_serializer: model_serializer, status: options[:status] } + errors, params: { + model: model, + model_serializer: model_serializer, + status: options[:status] + } ) ) end diff --git a/spec/dummy.rb b/spec/dummy.rb index 01e018c..076e5c8 100644 --- a/spec/dummy.rb +++ b/spec/dummy.rb @@ -43,7 +43,10 @@ class Note < ActiveRecord::Base # Provide a validation adding an error to the model's base def title_check - errors.add(:base, :model_invalid, errors: 'The record has an unacceptable title.') if title == 'n/a' + return unless title == 'n/a' + + message = 'The record has an unacceptable title.' + errors.add(:base, :model_invalid, errors: message) end def deletable?