mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-23 06:16:50 +00:00
RFC: Json Api Errors (WIP)
- ActiveModelSerializers::JsonPointer - ActiveModel::Serializer::Adapter::JsonApi::Error - ActiveModel::Serializer::Adapter::JsonApi::Error.attributes - Fix rubocop config
This commit is contained in:
parent
df815c48ba
commit
0ba944dabf
@ -16,6 +16,19 @@ module ActiveModel
|
|||||||
@resource = resource
|
@resource = resource
|
||||||
@adapter_opts, @serializer_opts =
|
@adapter_opts, @serializer_opts =
|
||||||
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
|
options.partition { |k, _| ADAPTER_OPTION_KEYS.include? k }.map { |h| Hash[h] }
|
||||||
|
|
||||||
|
# TECHDEBT: clean up single vs. collection of resources
|
||||||
|
if resource.respond_to?(:each)
|
||||||
|
if resource.any? { |elem| elem.respond_to?(:errors) && !elem.errors.empty? }
|
||||||
|
@serializer_opts[:serializer] = ActiveModel::Serializer::ErrorSerializer
|
||||||
|
@adapter_opts[:adapter] = :'json_api/error'
|
||||||
|
end
|
||||||
|
else
|
||||||
|
if resource.respond_to?(:errors) && !resource.errors.empty?
|
||||||
|
@serializer_opts[:serializer] = ActiveModel::Serializer::ErrorSerializer
|
||||||
|
@adapter_opts[:adapter] = :'json_api/error'
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
def serialization_scope=(scope)
|
def serialization_scope=(scope)
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
require 'thread_safe'
|
require 'thread_safe'
|
||||||
require 'active_model/serializer/collection_serializer'
|
require 'active_model/serializer/collection_serializer'
|
||||||
require 'active_model/serializer/array_serializer'
|
require 'active_model/serializer/array_serializer'
|
||||||
|
require 'active_model/serializer/error_serializer'
|
||||||
require 'active_model/serializer/include_tree'
|
require 'active_model/serializer/include_tree'
|
||||||
require 'active_model/serializer/associations'
|
require 'active_model/serializer/associations'
|
||||||
require 'active_model/serializer/attributes'
|
require 'active_model/serializer/attributes'
|
||||||
|
|||||||
2
lib/active_model/serializer/error_serializer.rb
Normal file
2
lib/active_model/serializer/error_serializer.rb
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
class ActiveModel::Serializer::ErrorSerializer < ActiveModel::Serializer
|
||||||
|
end
|
||||||
@ -129,6 +129,20 @@ module ActiveModel::Serializer::Lint
|
|||||||
assert_instance_of resource_class.model_name, ActiveModel::Name
|
assert_instance_of resource_class.model_name, ActiveModel::Name
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def test_active_model_errors
|
||||||
|
assert_respond_to resource, :errors
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_active_model_errors_human_attribute_name
|
||||||
|
assert_respond_to resource.class, :human_attribute_name
|
||||||
|
assert_equal(-2, resource.class.method(:human_attribute_name).arity)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_active_model_errors_lookup_ancestors
|
||||||
|
assert_respond_to resource.class, :lookup_ancestors
|
||||||
|
assert_equal 0, resource.class.method(:lookup_ancestors).arity
|
||||||
|
end
|
||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
def resource
|
def resource
|
||||||
|
|||||||
@ -10,6 +10,7 @@ module ActiveModelSerializers
|
|||||||
autoload :Logging
|
autoload :Logging
|
||||||
autoload :Test
|
autoload :Test
|
||||||
autoload :Adapter
|
autoload :Adapter
|
||||||
|
autoload :JsonPointer
|
||||||
|
|
||||||
class << self; attr_accessor :logger; end
|
class << self; attr_accessor :logger; end
|
||||||
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
|
||||||
|
|||||||
@ -8,6 +8,7 @@ module ActiveModelSerializers
|
|||||||
require 'active_model/serializer/adapter/json_api/meta'
|
require 'active_model/serializer/adapter/json_api/meta'
|
||||||
autoload :Deserialization
|
autoload :Deserialization
|
||||||
require 'active_model/serializer/adapter/json_api/api_objects'
|
require 'active_model/serializer/adapter/json_api/api_objects'
|
||||||
|
autoload :Error
|
||||||
|
|
||||||
# TODO: if we like this abstraction and other API objects to it,
|
# TODO: if we like this abstraction and other API objects to it,
|
||||||
# then extract to its own file and require it.
|
# then extract to its own file and require it.
|
||||||
|
|||||||
92
lib/active_model_serializers/adapter/json_api/error.rb
Normal file
92
lib/active_model_serializers/adapter/json_api/error.rb
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
module ActiveModelSerializers
|
||||||
|
module Adapter
|
||||||
|
class JsonApi < Base
|
||||||
|
class Error < Base
|
||||||
|
=begin
|
||||||
|
## http://jsonapi.org/format/#document-top-level
|
||||||
|
|
||||||
|
A document MUST contain at least one of the following top-level members:
|
||||||
|
|
||||||
|
- data: the document's "primary data"
|
||||||
|
- errors: an array of error objects
|
||||||
|
- meta: a meta object that contains non-standard meta-information.
|
||||||
|
|
||||||
|
The members data and errors MUST NOT coexist in the same document.
|
||||||
|
|
||||||
|
## http://jsonapi.org/format/#error-objects
|
||||||
|
|
||||||
|
Error objects provide additional information about problems encountered while performing an operation. Error objects MUST be returned as an array keyed by errors in the top level of a JSON API document.
|
||||||
|
|
||||||
|
An error object MAY have the following members:
|
||||||
|
|
||||||
|
- id: a unique identifier for this particular occurrence of the problem.
|
||||||
|
- links: a links object containing the following members:
|
||||||
|
- about: a link that leads to further details about this particular occurrence of the problem.
|
||||||
|
- status: the HTTP status code applicable to this problem, expressed as a string value.
|
||||||
|
- code: an application-specific error code, expressed as a string value.
|
||||||
|
- title: a short, human-readable summary of the problem that SHOULD NOT change from occurrence to occurrence of the problem, except for purposes of localization.
|
||||||
|
- detail: a human-readable explanation specific to this occurrence of the problem.
|
||||||
|
- source: an object containing references to the source of the error, optionally including any of the following members:
|
||||||
|
- pointer: a JSON Pointer [RFC6901] to the associated entity in the request document [e.g. "/data" for a primary data object, or "/data/attributes/title" for a specific attribute].
|
||||||
|
- parameter: a string indicating which query parameter caused the error.
|
||||||
|
- meta: a meta object containing non-standard meta-information about the error.
|
||||||
|
|
||||||
|
=end
|
||||||
|
def self.attributes(attribute_name, attribute_errors)
|
||||||
|
attribute_errors.map do |attribute_error|
|
||||||
|
{
|
||||||
|
source: { pointer: ActiveModelSerializers::JsonPointer.new(:attribute, attribute_name) },
|
||||||
|
detail: attribute_error
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def serializable_hash(*)
|
||||||
|
@result = []
|
||||||
|
# TECHDEBT: clean up single vs. collection of resources
|
||||||
|
if serializer.object.respond_to?(:each)
|
||||||
|
@result = collection_errors.flat_map do |collection_error|
|
||||||
|
collection_error.flat_map do |attribute_name, attribute_errors|
|
||||||
|
attribute_error_objects(attribute_name, attribute_errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
else
|
||||||
|
@result = object_errors.flat_map do |attribute_name, attribute_errors|
|
||||||
|
attribute_error_objects(attribute_name, attribute_errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
{ root => @result }
|
||||||
|
end
|
||||||
|
|
||||||
|
def fragment_cache(cached_hash, non_cached_hash)
|
||||||
|
JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash)
|
||||||
|
end
|
||||||
|
|
||||||
|
def root
|
||||||
|
'errors'.freeze
|
||||||
|
end
|
||||||
|
|
||||||
|
private
|
||||||
|
|
||||||
|
# @return [Array<symbol, Array<String>] i.e. attribute_name, [attribute_errors]
|
||||||
|
def object_errors
|
||||||
|
cache_check(serializer) do
|
||||||
|
serializer.object.errors.messages
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def collection_errors
|
||||||
|
cache_check(serializer) do
|
||||||
|
serializer.object.flat_map do |elem|
|
||||||
|
elem.errors.messages
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def attribute_error_objects(attribute_name, attribute_errors)
|
||||||
|
Error.attributes(attribute_name, attribute_errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
14
lib/active_model_serializers/json_pointer.rb
Normal file
14
lib/active_model_serializers/json_pointer.rb
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
module ActiveModelSerializers
|
||||||
|
module JsonPointer
|
||||||
|
module_function
|
||||||
|
|
||||||
|
POINTERS = {
|
||||||
|
attribute: '/data/attributes/%s'.freeze,
|
||||||
|
primary_data: '/data'.freeze
|
||||||
|
}.freeze
|
||||||
|
|
||||||
|
def new(pointer_type, value = nil)
|
||||||
|
format(POINTERS[pointer_type], value)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -6,10 +6,11 @@ module ActiveModelSerializers
|
|||||||
include ActiveModel::Model
|
include ActiveModel::Model
|
||||||
include ActiveModel::Serializers::JSON
|
include ActiveModel::Serializers::JSON
|
||||||
|
|
||||||
attr_reader :attributes
|
attr_reader :attributes, :errors
|
||||||
|
|
||||||
def initialize(attributes = {})
|
def initialize(attributes = {})
|
||||||
@attributes = attributes
|
@attributes = attributes
|
||||||
|
@errors = ActiveModel::Errors.new(self)
|
||||||
super
|
super
|
||||||
end
|
end
|
||||||
|
|
||||||
@ -35,5 +36,14 @@ module ActiveModelSerializers
|
|||||||
attributes[key]
|
attributes[key]
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# The following methods are needed to be minimally implemented for ActiveModel::Errors
|
||||||
|
def self.human_attribute_name(attr, _options = {})
|
||||||
|
attr
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.lookup_ancestors
|
||||||
|
[self]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
41
test/action_controller/json_api/errors_test.rb
Normal file
41
test/action_controller/json_api/errors_test.rb
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
module ActionController
|
||||||
|
module Serialization
|
||||||
|
class JsonApi
|
||||||
|
class ErrorsTest < ActionController::TestCase
|
||||||
|
def test_active_model_with_multiple_errors
|
||||||
|
get :render_resource_with_errors
|
||||||
|
|
||||||
|
expected_errors_object =
|
||||||
|
{ 'errors'.freeze =>
|
||||||
|
[
|
||||||
|
{ :source => { :pointer => '/data/attributes/name' }, :detail => 'cannot be nil' },
|
||||||
|
{ :source => { :pointer => '/data/attributes/name' }, :detail => 'must be longer' },
|
||||||
|
{ :source => { :pointer => '/data/attributes/id' }, :detail => 'must be a uuid' }
|
||||||
|
]
|
||||||
|
}.to_json
|
||||||
|
assert_equal json_reponse_body.to_json, expected_errors_object
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_reponse_body
|
||||||
|
JSON.load(@response.body)
|
||||||
|
end
|
||||||
|
|
||||||
|
class ErrorsTestController < ActionController::Base
|
||||||
|
def render_resource_with_errors
|
||||||
|
resource = Profile.new(name: 'Name 1',
|
||||||
|
description: 'Description 1',
|
||||||
|
comments: 'Comments 1')
|
||||||
|
resource.errors.add(:name, 'cannot be nil')
|
||||||
|
resource.errors.add(:name, 'must be longer')
|
||||||
|
resource.errors.add(:id, 'must be a uuid')
|
||||||
|
render json: resource, adapter: :json_api
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests ErrorsTestController
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
@ -102,7 +102,8 @@ module ActiveModelSerializers
|
|||||||
'null'.freeze => ActiveModelSerializers::Adapter::Null,
|
'null'.freeze => ActiveModelSerializers::Adapter::Null,
|
||||||
'json'.freeze => ActiveModelSerializers::Adapter::Json,
|
'json'.freeze => ActiveModelSerializers::Adapter::Json,
|
||||||
'attributes'.freeze => ActiveModelSerializers::Adapter::Attributes,
|
'attributes'.freeze => ActiveModelSerializers::Adapter::Attributes,
|
||||||
'json_api'.freeze => ActiveModelSerializers::Adapter::JsonApi
|
'json_api'.freeze => ActiveModelSerializers::Adapter::JsonApi,
|
||||||
|
'json_api/error'.freeze => ActiveModelSerializers::Adapter::JsonApi::Error
|
||||||
}
|
}
|
||||||
actual = ActiveModelSerializers::Adapter.adapter_map
|
actual = ActiveModelSerializers::Adapter.adapter_map
|
||||||
assert_equal actual, expected_adapter_map
|
assert_equal actual, expected_adapter_map
|
||||||
@ -113,6 +114,7 @@ module ActiveModelSerializers
|
|||||||
'attributes'.freeze,
|
'attributes'.freeze,
|
||||||
'json'.freeze,
|
'json'.freeze,
|
||||||
'json_api'.freeze,
|
'json_api'.freeze,
|
||||||
|
'json_api/error'.freeze,
|
||||||
'null'.freeze
|
'null'.freeze
|
||||||
]
|
]
|
||||||
end
|
end
|
||||||
|
|||||||
20
test/active_model_serializers/json_pointer_test.rb
Normal file
20
test/active_model_serializers/json_pointer_test.rb
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
class ActiveModelSerializers::JsonPointerTest < ActiveSupport::TestCase
|
||||||
|
def test_attribute_pointer
|
||||||
|
attribute_name = 'title'
|
||||||
|
pointer = ActiveModelSerializers::JsonPointer.new(:attribute, attribute_name)
|
||||||
|
assert_equal '/data/attributes/title', pointer
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_primary_data_pointer
|
||||||
|
pointer = ActiveModelSerializers::JsonPointer.new(:primary_data)
|
||||||
|
assert_equal '/data', pointer
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unkown_data_pointer
|
||||||
|
assert_raises(TypeError) do
|
||||||
|
ActiveModelSerializers::JsonPointer.new(:unknown)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
64
test/adapter/json_api/errors_test.rb
Normal file
64
test/adapter/json_api/errors_test.rb
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
require 'test_helper'
|
||||||
|
|
||||||
|
module ActiveModelSerializers
|
||||||
|
module Adapter
|
||||||
|
class JsonApi < Base
|
||||||
|
class ErrorsTest < Minitest::Test
|
||||||
|
include ActiveModel::Serializer::Lint::Tests
|
||||||
|
|
||||||
|
def setup
|
||||||
|
@resource = ModelWithErrors.new
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_active_model_with_error
|
||||||
|
options = {
|
||||||
|
serializer: ActiveModel::Serializer::ErrorSerializer,
|
||||||
|
adapter: :'json_api/error'
|
||||||
|
}
|
||||||
|
|
||||||
|
@resource.errors.add(:name, 'cannot be nil')
|
||||||
|
|
||||||
|
serializable_resource = ActiveModel::SerializableResource.new(@resource, options)
|
||||||
|
assert_equal serializable_resource.serializer_instance.attributes, {}
|
||||||
|
assert_equal serializable_resource.serializer_instance.object, @resource
|
||||||
|
|
||||||
|
expected_errors_object =
|
||||||
|
{ 'errors'.freeze =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
source: { pointer: '/data/attributes/name' },
|
||||||
|
detail: 'cannot be nil'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert_equal serializable_resource.as_json, expected_errors_object
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_active_model_with_multiple_errors
|
||||||
|
options = {
|
||||||
|
serializer: ActiveModel::Serializer::ErrorSerializer,
|
||||||
|
adapter: :'json_api/error'
|
||||||
|
}
|
||||||
|
|
||||||
|
@resource.errors.add(:name, 'cannot be nil')
|
||||||
|
@resource.errors.add(:name, 'must be longer')
|
||||||
|
@resource.errors.add(:id, 'must be a uuid')
|
||||||
|
|
||||||
|
serializable_resource = ActiveModel::SerializableResource.new(@resource, options)
|
||||||
|
assert_equal serializable_resource.serializer_instance.attributes, {}
|
||||||
|
assert_equal serializable_resource.serializer_instance.object, @resource
|
||||||
|
|
||||||
|
expected_errors_object =
|
||||||
|
{ 'errors'.freeze =>
|
||||||
|
[
|
||||||
|
{ :source => { :pointer => '/data/attributes/name' }, :detail => 'cannot be nil' },
|
||||||
|
{ :source => { :pointer => '/data/attributes/name' }, :detail => 'must be longer' },
|
||||||
|
{ :source => { :pointer => '/data/attributes/id' }, :detail => 'must be a uuid' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert_equal serializable_resource.as_json, expected_errors_object
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
11
test/fixtures/poro.rb
vendored
11
test/fixtures/poro.rb
vendored
@ -27,6 +27,17 @@ class Model < ActiveModelSerializers::Model
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# see
|
||||||
|
# https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/errors.rb
|
||||||
|
# The below allows you to do:
|
||||||
|
#
|
||||||
|
# model = ModelWithErrors.new
|
||||||
|
# model.validate! # => ["cannot be nil"]
|
||||||
|
# model.errors.full_messages # => ["name cannot be nil"]
|
||||||
|
class ModelWithErrors < ::ActiveModelSerializers::Model
|
||||||
|
attr_accessor :name
|
||||||
|
end
|
||||||
|
|
||||||
class Profile < Model
|
class Profile < Model
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
@ -27,6 +27,15 @@ module ActiveModel
|
|||||||
def updated_at
|
def updated_at
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def errors
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.human_attribute_name(attr, options = {})
|
||||||
|
end
|
||||||
|
|
||||||
|
def self.lookup_ancestors
|
||||||
|
end
|
||||||
|
|
||||||
def self.model_name
|
def self.model_name
|
||||||
@_model_name ||= ActiveModel::Name.new(self)
|
@_model_name ||= ActiveModel::Name.new(self)
|
||||||
end
|
end
|
||||||
|
|||||||
@ -31,5 +31,37 @@ module ActiveModel
|
|||||||
def test_use_adapter_with_adapter_option_as_false
|
def test_use_adapter_with_adapter_option_as_false
|
||||||
refute ActiveModel::SerializableResource.new(@resource, { adapter: false }).use_adapter?
|
refute ActiveModel::SerializableResource.new(@resource, { adapter: false }).use_adapter?
|
||||||
end
|
end
|
||||||
|
|
||||||
|
class SerializableResourceErrorsTest < Minitest::Test
|
||||||
|
def test_serializable_resource_with_errors
|
||||||
|
options = nil
|
||||||
|
resource = ModelWithErrors.new
|
||||||
|
resource.errors.add(:name, 'must be awesome')
|
||||||
|
serializable_resource = ActiveModel::SerializableResource.new(resource)
|
||||||
|
expected_response_document =
|
||||||
|
{ 'errors'.freeze =>
|
||||||
|
[
|
||||||
|
{ :source => { :pointer => '/data/attributes/name' }, :detail => 'must be awesome' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert_equal serializable_resource.as_json(options), expected_response_document
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_serializable_resource_with_collection_containing_errors
|
||||||
|
options = nil
|
||||||
|
resources = []
|
||||||
|
resources << resource = ModelWithErrors.new
|
||||||
|
resource.errors.add(:title, 'must be amazing')
|
||||||
|
resources << ModelWithErrors.new
|
||||||
|
serializable_resource = ActiveModel::SerializableResource.new(resources)
|
||||||
|
expected_response_document =
|
||||||
|
{ 'errors'.freeze =>
|
||||||
|
[
|
||||||
|
{ :source => { :pointer => '/data/attributes/title' }, :detail => 'must be amazing' }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
assert_equal serializable_resource.as_json(options), expected_response_document
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user