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
Author

Choose a reason for hiding this comment

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

Previous discussion: #45 (comment)

pointer: '' references the document root, which strikes me as the wrong fallback behavior here?

Original commit: 43cbaee

Copy link
Owner

Choose a reason for hiding this comment

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

This should return an empty object, since there's no source to point to.

end
end
end
Expand Down
6 changes: 5 additions & 1 deletion lib/jsonapi/rails.rb
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,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
28 changes: 27 additions & 1 deletion spec/dummy.rb
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,9 @@ def self.ransackable_associations(auth_object = nil)
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?

def self.ransackable_associations(auth_object = nil)
%w(user)
Expand All @@ -52,6 +54,21 @@ def self.ransackable_associations(auth_object = nil)
def self.ransackable_attributes(auth_object = nil)
%w(created_at id quantity title updated_at user_id)
end

# 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 @@ -84,7 +101,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 @@ -151,6 +168,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
58 changes: 58 additions & 0 deletions spec/errors_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -138,6 +138,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 @@ -170,4 +191,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