Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 33 additions & 7 deletions lib/jsonapi/active_model_error_serializer.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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|
Expand All @@ -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)
Expand All @@ -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 }
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johvet let's double check if this is a valid JSONAPI response, more context here:
https://jsonapi.org/examples/#error-objects-source-usage

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I do not remember the reasons for me to change this to nil, but after reading the JSONAPI spec above, using '' seems to be the correct way.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think the reason for nil is because the pointer is unknown/unmapped in this situation but '' means "the root of the document", which is incorrect here

end
end
end
Expand Down
24 changes: 21 additions & 3 deletions lib/jsonapi/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@johvet would you be kind to explain or provide a test for this changeset please. As far as I know, the error handling is supported for rails 4-6...

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am terribly sorry for my late reply. I don't recall the very details, but we have noticed that the current solution in master does not work consistently between Rails 5.x and 6.x -- It also makes a difference on whether symbols are used or text messages.

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

IIRC it's because the previous implementation didn't work for something like:

errors.add(:foo, :blank)
errors.add(:foo, :some_custom_key)

but did work with

errors.add(:foo, 'some string')

IIRC you'd end up with message: 'some_custom_key' instead of the I18n'ed string that exists in resource.messages. I think this is a Rails bug though.

But looking above, the current[:error] ||= :invalid is redundant/impossible, so I think this needs some comments explaining the different ways the data might be available

else
details = resource.messages
end
Expand All @@ -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
Expand Down
29 changes: 28 additions & 1 deletion spec/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
67 changes: 60 additions & 7 deletions spec/errors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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