mirror of
https://github.com/ditkrg/active_model_serializers.git
synced 2026-01-24 14:56:50 +00:00
Merge pull request #1004 from bf4/json_api_errors
JSON API Errors (Initial implementation and roadmap for full feature-set)
This commit is contained in:
commit
79e6acbc4a
@ -3,6 +3,9 @@
|
|||||||
Breaking changes:
|
Breaking changes:
|
||||||
|
|
||||||
Features:
|
Features:
|
||||||
|
- [#1004](https://github.com/rails-api/active_model_serializers/pull/1004) JSON API errors object implementation.
|
||||||
|
- Only implements `detail` and `source` as derived from `ActiveModel::Error`
|
||||||
|
- Provides checklist of remaining questions and remaining parts of the spec.
|
||||||
- [#1515](https://github.com/rails-api/active_model_serializers/pull/1515) Adds support for symbols to the
|
- [#1515](https://github.com/rails-api/active_model_serializers/pull/1515) Adds support for symbols to the
|
||||||
`ActiveModel::Serializer.type` method. (@groyoh)
|
`ActiveModel::Serializer.type` method. (@groyoh)
|
||||||
- [#1504](https://github.com/rails-api/active_model_serializers/pull/1504) Adds the changes missing from #1454
|
- [#1504](https://github.com/rails-api/active_model_serializers/pull/1504) Adds the changes missing from #1454
|
||||||
|
|||||||
@ -14,7 +14,9 @@ This is the documentation of ActiveModelSerializers, it's focused on the **0.10.
|
|||||||
- [Caching](general/caching.md)
|
- [Caching](general/caching.md)
|
||||||
- [Logging](general/logging.md)
|
- [Logging](general/logging.md)
|
||||||
- [Instrumentation](general/instrumentation.md)
|
- [Instrumentation](general/instrumentation.md)
|
||||||
- [JSON API Schema](jsonapi/schema.md)
|
- JSON API
|
||||||
|
- [Schema](jsonapi/schema.md)
|
||||||
|
- [Errors](jsonapi/errors.md)
|
||||||
- [ARCHITECTURE](ARCHITECTURE.md)
|
- [ARCHITECTURE](ARCHITECTURE.md)
|
||||||
|
|
||||||
## How to
|
## How to
|
||||||
|
|||||||
56
docs/jsonapi/errors.md
Normal file
56
docs/jsonapi/errors.md
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
[Back to Guides](../README.md)
|
||||||
|
|
||||||
|
# [JSON API Errors](http://jsonapi.org/format/#errors)
|
||||||
|
|
||||||
|
Rendering error documents requires specifying the error serializer(s):
|
||||||
|
|
||||||
|
- Serializer:
|
||||||
|
- For a single resource: `serializer: ActiveModel::Serializer::ErrorSerializer`.
|
||||||
|
- For a collection: `serializer: ActiveModel::Serializer::ErrorsSerializer`, `each_serializer: ActiveModel::Serializer::ErrorSerializer`.
|
||||||
|
|
||||||
|
The resource **MUST** have a non-empty associated `#errors` object.
|
||||||
|
The `errors` object must have a `#messages` method that returns a hash of error name to array of
|
||||||
|
descriptions.
|
||||||
|
|
||||||
|
## Use in controllers
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
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, status: 422, adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer
|
||||||
|
# #=>
|
||||||
|
# { :errors =>
|
||||||
|
# [
|
||||||
|
# { :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
|
||||||
|
```
|
||||||
|
|
||||||
|
## Direct error document generation
|
||||||
|
|
||||||
|
```ruby
|
||||||
|
options = nil
|
||||||
|
resource = ModelWithErrors.new
|
||||||
|
resource.errors.add(:name, 'must be awesome')
|
||||||
|
|
||||||
|
serializable_resource = ActiveModel::SerializableResource.new(
|
||||||
|
resource, {
|
||||||
|
serializer: ActiveModel::Serializer::ErrorSerializer,
|
||||||
|
adapter: :json_api
|
||||||
|
})
|
||||||
|
serializable_resource.as_json(options)
|
||||||
|
# #=>
|
||||||
|
# {
|
||||||
|
# :errors =>
|
||||||
|
# [
|
||||||
|
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'must be awesome' }
|
||||||
|
# ]
|
||||||
|
# }
|
||||||
|
```
|
||||||
@ -58,33 +58,33 @@ Example supported requests
|
|||||||
|-----------------------|----------------------------------------------------------------------------------------------------|----------|---------------------------------------|
|
|-----------------------|----------------------------------------------------------------------------------------------------|----------|---------------------------------------|
|
||||||
| schema | oneOf (success, failure, info) | |
|
| schema | oneOf (success, failure, info) | |
|
||||||
| success | data, included, meta, links, jsonapi | | AM::SerializableResource
|
| success | data, included, meta, links, jsonapi | | AM::SerializableResource
|
||||||
| success.meta | meta | | AM::S::Adapter::Base#meta
|
| success.meta | meta | | AMS::Adapter::Base#meta
|
||||||
| success.included | UniqueArray(resource) | | AM::S::Adapter::JsonApi#serializable_hash_for_collection
|
| success.included | UniqueArray(resource) | | AMS::Adapter::JsonApi#serializable_hash_for_collection
|
||||||
| success.data | data | |
|
| success.data | data | |
|
||||||
| success.links | allOf (links, pagination) | | AM::S::Adapter::JsonApi#links_for
|
| success.links | allOf (links, pagination) | | AMS::Adapter::JsonApi#links_for
|
||||||
| success.jsonapi | jsonapi | |
|
| success.jsonapi | jsonapi | |
|
||||||
| failure | errors, meta, jsonapi | errors |
|
| failure | errors, meta, jsonapi | errors | AMS::Adapter::JsonApi#failure_document, #1004
|
||||||
| failure.errors | UniqueArray(error) | | #1004
|
| failure.errors | UniqueArray(error) | | AM::S::ErrorSerializer, #1004
|
||||||
| meta | Object | |
|
| meta | Object | |
|
||||||
| data | oneOf (resource, UniqueArray(resource)) | | AM::S::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
|
| data | oneOf (resource, UniqueArray(resource)) | | AMS::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
|
||||||
| resource | String(type), String(id),<br>attributes, relationships,<br>links, meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
|
| resource | String(type), String(id),<br>attributes, relationships,<br>links, meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
|
||||||
| links | Uri(self), Link(related) | | #1028, #1246, #1282
|
| links | Uri(self), Link(related) | | #1028, #1246, #1282
|
||||||
| link | oneOf (linkString, linkObject) | |
|
| link | oneOf (linkString, linkObject) | |
|
||||||
| link.linkString | Uri | |
|
| link.linkString | Uri | |
|
||||||
| link.linkObject | Uri(href), meta | href |
|
| link.linkObject | Uri(href), meta | href |
|
||||||
| attributes | patternProperties(<br>`"^(?!relationships$|links$)\\w[-\\w_]*$"`),<br>any valid JSON | | AM::Serializer#attributes, AM::S::Adapter::JsonApi#resource_object_for
|
| attributes | patternProperties(<br>`"^(?!relationships$|links$)\\w[-\\w_]*$"`),<br>any valid JSON | | AM::Serializer#attributes, AMS::Adapter::JsonApi#resource_object_for
|
||||||
| relationships | patternProperties(<br>`"^\\w[-\\w_]*$"`);<br>links, relationships.data, meta | | AM::S::Adapter::JsonApi#relationships_for
|
| relationships | patternProperties(<br>`"^\\w[-\\w_]*$"`);<br>links, relationships.data, meta | | AMS::Adapter::JsonApi#relationships_for
|
||||||
| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AM::S::Adapter::JsonApi#resource_identifier_for
|
| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AMS::Adapter::JsonApi#resource_identifier_for
|
||||||
| relationshipToOne | anyOf(empty, linkage) | |
|
| relationshipToOne | anyOf(empty, linkage) | |
|
||||||
| relationshipToMany | UniqueArray(linkage) | |
|
| relationshipToMany | UniqueArray(linkage) | |
|
||||||
| empty | null | |
|
| empty | null | |
|
||||||
| linkage | String(type), String(id), meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
|
| linkage | String(type), String(id), meta | type, id | AMS::Adapter::JsonApi#primary_data_for
|
||||||
| pagination | pageObject(first), pageObject(last),<br>pageObject(prev), pageObject(next) | | AM::S::Adapter::JsonApi::PaginationLinks#serializable_hash
|
| pagination | pageObject(first), pageObject(last),<br>pageObject(prev), pageObject(next) | | AMS::Adapter::JsonApi::PaginationLinks#serializable_hash
|
||||||
| pagination.pageObject | oneOf(Uri, null) | |
|
| pagination.pageObject | oneOf(Uri, null) | |
|
||||||
| jsonapi | String(version), meta | | AM::S::Adapter::JsonApi::ApiObjects::JsonApi
|
| jsonapi | String(version), meta | | AMS::Adapter::JsonApi::ApiObjects::JsonApi#as_json
|
||||||
| error | String(id), links, String(status),<br>String(code), String(title),<br>String(detail), error.source, meta | |
|
| error | String(id), links, String(status),<br>String(code), String(title),<br>String(detail), error.source, meta | | AM::S::ErrorSerializer, AMS::Adapter::JsonApi::Error.resource_errors
|
||||||
| error.source | String(pointer), String(parameter) | |
|
| error.source | String(pointer), String(parameter) | | AMS::Adapter::JsonApi::Error.error_source
|
||||||
| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | |
|
| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | | AMS::JsonPointer
|
||||||
|
|
||||||
|
|
||||||
The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
|
The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
|
||||||
@ -102,7 +102,7 @@ The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
|
|||||||
### Failure Document
|
### Failure Document
|
||||||
|
|
||||||
- [ ] failure
|
- [ ] failure
|
||||||
- [ ] errors: array of unique items of type ` "$ref": "#/definitions/error"`
|
- [x] errors: array of unique items of type ` "$ref": "#/definitions/error"`
|
||||||
- [ ] meta: `"$ref": "#/definitions/meta"`
|
- [ ] meta: `"$ref": "#/definitions/meta"`
|
||||||
- [ ] jsonapi: `"$ref": "#/definitions/jsonapi"`
|
- [ ] jsonapi: `"$ref": "#/definitions/jsonapi"`
|
||||||
|
|
||||||
@ -137,4 +137,15 @@ The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
|
|||||||
- [ ] pagination
|
- [ ] pagination
|
||||||
- [ ] jsonapi
|
- [ ] jsonapi
|
||||||
- [ ] meta
|
- [ ] meta
|
||||||
- [ ] error: id, links, status, code, title: detail: source [{pointer, type}, {parameter: {description, type}], meta
|
- [ ] error
|
||||||
|
- [ ] 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.
|
||||||
|
- [x] detail: a human-readable explanation specific to this occurrence of the problem.
|
||||||
|
- [x] source: an object containing references to the source of the error, optionally including any of the following members:
|
||||||
|
- [x] pointer: a JSON Pointer [RFC6901](https://tools.ietf.org/html/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].
|
||||||
|
- [x] parameter: a string indicating which query parameter caused the error.
|
||||||
|
- [ ] meta: a meta object containing non-standard meta-information about the error.
|
||||||
|
|||||||
@ -1,6 +1,8 @@
|
|||||||
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/errors_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'
|
||||||
@ -116,6 +118,10 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
# Used by adapter as resource root.
|
# Used by adapter as resource root.
|
||||||
def json_key
|
def json_key
|
||||||
root || object.class.model_name.to_s.underscore
|
root || object.class.model_name.to_s.underscore
|
||||||
|
|||||||
@ -4,6 +4,10 @@ module ActiveModel
|
|||||||
class JsonApi
|
class JsonApi
|
||||||
module ApiObjects
|
module ApiObjects
|
||||||
class Relationship
|
class Relationship
|
||||||
|
# {http://jsonapi.org/format/#document-resource-object-related-resource-links Document Resource Object Related Resource Links}
|
||||||
|
# {http://jsonapi.org/format/#document-links Document Links}
|
||||||
|
# {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage}
|
||||||
|
# {http://jsonapi.org/format/#document-meta Docment Meta}
|
||||||
def initialize(parent_serializer, serializer, options = {}, links = {}, meta = nil)
|
def initialize(parent_serializer, serializer, options = {}, links = {}, meta = nil)
|
||||||
@object = parent_serializer.object
|
@object = parent_serializer.object
|
||||||
@scope = parent_serializer.scope
|
@scope = parent_serializer.scope
|
||||||
|
|||||||
@ -4,6 +4,7 @@ module ActiveModel
|
|||||||
class JsonApi
|
class JsonApi
|
||||||
module ApiObjects
|
module ApiObjects
|
||||||
class ResourceIdentifier
|
class ResourceIdentifier
|
||||||
|
# {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
|
||||||
def initialize(serializer)
|
def initialize(serializer)
|
||||||
@id = id_for(serializer)
|
@id = id_for(serializer)
|
||||||
@type = type_for(serializer)
|
@type = type_for(serializer)
|
||||||
|
|||||||
@ -22,6 +22,10 @@ module ActiveModel
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
true
|
||||||
|
end
|
||||||
|
|
||||||
def json_key
|
def json_key
|
||||||
root || derived_root
|
root || derived_root
|
||||||
end
|
end
|
||||||
|
|||||||
10
lib/active_model/serializer/error_serializer.rb
Normal file
10
lib/active_model/serializer/error_serializer.rb
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
class ActiveModel::Serializer::ErrorSerializer < ActiveModel::Serializer
|
||||||
|
# @return [Hash<field_name,Array<error_message>>]
|
||||||
|
def as_json
|
||||||
|
object.errors.messages
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
end
|
||||||
27
lib/active_model/serializer/errors_serializer.rb
Normal file
27
lib/active_model/serializer/errors_serializer.rb
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
require 'active_model/serializer/error_serializer'
|
||||||
|
class ActiveModel::Serializer::ErrorsSerializer
|
||||||
|
include Enumerable
|
||||||
|
delegate :each, to: :@serializers
|
||||||
|
attr_reader :object, :root
|
||||||
|
|
||||||
|
def initialize(resources, options = {})
|
||||||
|
@root = options[:root]
|
||||||
|
@object = resources
|
||||||
|
@serializers = resources.map do |resource|
|
||||||
|
serializer_class = options.fetch(:serializer) { ActiveModel::Serializer::ErrorSerializer }
|
||||||
|
serializer_class.new(resource, options.except(:serializer))
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
def success?
|
||||||
|
false
|
||||||
|
end
|
||||||
|
|
||||||
|
def json_key
|
||||||
|
nil
|
||||||
|
end
|
||||||
|
|
||||||
|
protected
|
||||||
|
|
||||||
|
attr_reader :serializers
|
||||||
|
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,11 +8,13 @@ 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.
|
||||||
module ApiObjects
|
module ApiObjects
|
||||||
module JsonApi
|
# {http://jsonapi.org/format/#document-jsonapi-object Jsonapi Object}
|
||||||
|
module Jsonapi
|
||||||
ActiveModelSerializers.config.jsonapi_version = '1.0'
|
ActiveModelSerializers.config.jsonapi_version = '1.0'
|
||||||
ActiveModelSerializers.config.jsonapi_toplevel_meta = {}
|
ActiveModelSerializers.config.jsonapi_toplevel_meta = {}
|
||||||
# Make JSON API top-level jsonapi member opt-in
|
# Make JSON API top-level jsonapi member opt-in
|
||||||
@ -50,9 +52,19 @@ module ActiveModelSerializers
|
|||||||
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
|
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#crud Requests are transactional, i.e. success or failure}
|
||||||
|
# {http://jsonapi.org/format/#document-top-level data and errors MUST NOT coexist in the same document.}
|
||||||
def serializable_hash(options = nil)
|
def serializable_hash(options = nil)
|
||||||
options ||= {}
|
options ||= {}
|
||||||
|
if serializer.success?
|
||||||
|
success_document(options)
|
||||||
|
else
|
||||||
|
failure_document
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-top-level Primary data}
|
||||||
|
def success_document(options)
|
||||||
is_collection = serializer.respond_to?(:each)
|
is_collection = serializer.respond_to?(:each)
|
||||||
serializers = is_collection ? serializer : [serializer]
|
serializers = is_collection ? serializer : [serializer]
|
||||||
primary_data, included = resource_objects_for(serializers)
|
primary_data, included = resource_objects_for(serializers)
|
||||||
@ -61,7 +73,7 @@ module ActiveModelSerializers
|
|||||||
hash[:data] = is_collection ? primary_data : primary_data[0]
|
hash[:data] = is_collection ? primary_data : primary_data[0]
|
||||||
hash[:included] = included if included.any?
|
hash[:included] = included if included.any?
|
||||||
|
|
||||||
ApiObjects::JsonApi.add!(hash)
|
ApiObjects::Jsonapi.add!(hash)
|
||||||
|
|
||||||
if instance_options[:links]
|
if instance_options[:links]
|
||||||
hash[:links] ||= {}
|
hash[:links] ||= {}
|
||||||
@ -76,6 +88,29 @@ module ActiveModelSerializers
|
|||||||
hash
|
hash
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#errors JSON API Errors}
|
||||||
|
# TODO: look into caching
|
||||||
|
# rubocop:disable Style/AsciiComments
|
||||||
|
# definition:
|
||||||
|
# ☑ toplevel_errors array (required)
|
||||||
|
# ☐ toplevel_meta
|
||||||
|
# ☐ toplevel_jsonapi
|
||||||
|
# rubocop:enable Style/AsciiComments
|
||||||
|
def failure_document
|
||||||
|
hash = {}
|
||||||
|
# PR Please :)
|
||||||
|
# ApiObjects::Jsonapi.add!(hash)
|
||||||
|
|
||||||
|
if serializer.respond_to?(:each)
|
||||||
|
hash[:errors] = serializer.flat_map do |error_serializer|
|
||||||
|
Error.resource_errors(error_serializer)
|
||||||
|
end
|
||||||
|
else
|
||||||
|
hash[:errors] = Error.resource_errors(serializer)
|
||||||
|
end
|
||||||
|
hash
|
||||||
|
end
|
||||||
|
|
||||||
def fragment_cache(cached_hash, non_cached_hash)
|
def fragment_cache(cached_hash, non_cached_hash)
|
||||||
root = false if instance_options.include?(:include)
|
root = false if instance_options.include?(:include)
|
||||||
ActiveModelSerializers::Adapter::JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash)
|
ActiveModelSerializers::Adapter::JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash)
|
||||||
@ -87,6 +122,7 @@ module ActiveModelSerializers
|
|||||||
|
|
||||||
private
|
private
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-resource-objects Primary data}
|
||||||
def resource_objects_for(serializers)
|
def resource_objects_for(serializers)
|
||||||
@primary = []
|
@primary = []
|
||||||
@included = []
|
@included = []
|
||||||
@ -128,10 +164,12 @@ module ActiveModelSerializers
|
|||||||
process_relationships(serializer, include_tree)
|
process_relationships(serializer, include_tree)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
|
||||||
def attributes_for(serializer, fields)
|
def attributes_for(serializer, fields)
|
||||||
serializer.attributes(fields).except(:id)
|
serializer.attributes(fields).except(:id)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
|
||||||
def resource_object_for(serializer)
|
def resource_object_for(serializer)
|
||||||
resource_object = cache_check(serializer) do
|
resource_object = cache_check(serializer) do
|
||||||
resource_object = ActiveModel::Serializer::Adapter::JsonApi::ApiObjects::ResourceIdentifier.new(serializer).as_json
|
resource_object = ActiveModel::Serializer::Adapter::JsonApi::ApiObjects::ResourceIdentifier.new(serializer).as_json
|
||||||
@ -155,6 +193,7 @@ module ActiveModelSerializers
|
|||||||
resource_object
|
resource_object
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
|
||||||
def relationships_for(serializer, requested_associations)
|
def relationships_for(serializer, requested_associations)
|
||||||
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations)
|
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations)
|
||||||
serializer.associations(include_tree).each_with_object({}) do |association, hash|
|
serializer.associations(include_tree).each_with_object({}) do |association, hash|
|
||||||
@ -168,16 +207,19 @@ module ActiveModelSerializers
|
|||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-links Document Links}
|
||||||
def links_for(serializer)
|
def links_for(serializer)
|
||||||
serializer._links.each_with_object({}) do |(name, value), hash|
|
serializer._links.each_with_object({}) do |(name, value), hash|
|
||||||
hash[name] = Link.new(serializer, value).as_json
|
hash[name] = Link.new(serializer, value).as_json
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#fetching-pagination Pagination Links}
|
||||||
def pagination_links_for(serializer, options)
|
def pagination_links_for(serializer, options)
|
||||||
JsonApi::PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options)
|
JsonApi::PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options)
|
||||||
end
|
end
|
||||||
|
|
||||||
|
# {http://jsonapi.org/format/#document-meta Docment Meta}
|
||||||
def meta_for(serializer)
|
def meta_for(serializer)
|
||||||
ActiveModel::Serializer::Adapter::JsonApi::Meta.new(serializer).as_json
|
ActiveModel::Serializer::Adapter::JsonApi::Meta.new(serializer).as_json
|
||||||
end
|
end
|
||||||
|
|||||||
77
lib/active_model_serializers/adapter/json_api/error.rb
Normal file
77
lib/active_model_serializers/adapter/json_api/error.rb
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
module ActiveModelSerializers
|
||||||
|
module Adapter
|
||||||
|
class JsonApi < Base
|
||||||
|
module Error
|
||||||
|
# rubocop:disable Style/AsciiComments
|
||||||
|
UnknownSourceTypeError = Class.new(ArgumentError)
|
||||||
|
|
||||||
|
# Builds a JSON API Errors Object
|
||||||
|
# {http://jsonapi.org/format/#errors JSON API Errors}
|
||||||
|
#
|
||||||
|
# @param [ActiveModel::Serializer::ErrorSerializer]
|
||||||
|
# @return [Array<Symbol, Array<String>] i.e. attribute_name, [attribute_errors]
|
||||||
|
def self.resource_errors(error_serializer)
|
||||||
|
error_serializer.as_json.flat_map do |attribute_name, attribute_errors|
|
||||||
|
attribute_error_objects(attribute_name, attribute_errors)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# definition:
|
||||||
|
# JSON Object
|
||||||
|
#
|
||||||
|
# properties:
|
||||||
|
# ☐ id : String
|
||||||
|
# ☐ status : String
|
||||||
|
# ☐ code : String
|
||||||
|
# ☐ title : String
|
||||||
|
# ☑ detail : String
|
||||||
|
# ☐ links
|
||||||
|
# ☐ meta
|
||||||
|
# ☑ error_source
|
||||||
|
#
|
||||||
|
# description:
|
||||||
|
# id : A unique identifier for 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. It **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.
|
||||||
|
def self.attribute_error_objects(attribute_name, attribute_errors)
|
||||||
|
attribute_errors.map do |attribute_error|
|
||||||
|
{
|
||||||
|
source: error_source(:pointer, attribute_name),
|
||||||
|
detail: attribute_error
|
||||||
|
}
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
# description:
|
||||||
|
# oneOf
|
||||||
|
# ☑ pointer : String
|
||||||
|
# ☑ parameter : String
|
||||||
|
#
|
||||||
|
# description:
|
||||||
|
# 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.
|
||||||
|
# https://tools.ietf.org/html/rfc6901
|
||||||
|
#
|
||||||
|
# parameter: A string indicating which query parameter caused the error
|
||||||
|
def self.error_source(source_type, attribute_name)
|
||||||
|
case source_type
|
||||||
|
when :pointer
|
||||||
|
{
|
||||||
|
pointer: ActiveModelSerializers::JsonPointer.new(:attribute, attribute_name)
|
||||||
|
}
|
||||||
|
when :parameter
|
||||||
|
{
|
||||||
|
parameter: attribute_name
|
||||||
|
}
|
||||||
|
else
|
||||||
|
fail UnknownSourceTypeError, "Unknown source type '#{source_type}' for attribute_name '#{attribute_name}'"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
# rubocop:enable Style/AsciiComments
|
||||||
|
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%s'.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 =>
|
||||||
|
[
|
||||||
|
{ :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, serializer: ActiveModel::Serializer::ErrorSerializer
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
tests ErrorsTestController
|
||||||
|
end
|
||||||
|
end
|
||||||
|
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
|
||||||
78
test/adapter/json_api/errors_test.rb
Normal file
78
test/adapter/json_api/errors_test.rb
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 =>
|
||||||
|
[
|
||||||
|
{
|
||||||
|
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
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 =>
|
||||||
|
[
|
||||||
|
{ :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
|
||||||
|
|
||||||
|
# see http://jsonapi.org/examples/
|
||||||
|
def test_parameter_source_type_error
|
||||||
|
parameter = 'auther'
|
||||||
|
error_source = ActiveModelSerializers::Adapter::JsonApi::Error.error_source(:parameter, parameter)
|
||||||
|
assert_equal({ parameter: parameter }, error_source)
|
||||||
|
end
|
||||||
|
|
||||||
|
def test_unknown_source_type_error
|
||||||
|
value = 'auther'
|
||||||
|
assert_raises(ActiveModelSerializers::Adapter::JsonApi::Error::UnknownSourceTypeError) do
|
||||||
|
ActiveModelSerializers::Adapter::JsonApi::Error.error_source(:hyper, value)
|
||||||
|
end
|
||||||
|
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,46 @@ 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, {
|
||||||
|
serializer: ActiveModel::Serializer::ErrorSerializer,
|
||||||
|
adapter: :json_api
|
||||||
|
})
|
||||||
|
expected_response_document =
|
||||||
|
{ :errors =>
|
||||||
|
[
|
||||||
|
{ :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, {
|
||||||
|
serializer: ActiveModel::Serializer::ErrorsSerializer,
|
||||||
|
each_serializer: ActiveModel::Serializer::ErrorSerializer,
|
||||||
|
adapter: :json_api
|
||||||
|
})
|
||||||
|
expected_response_document =
|
||||||
|
{ :errors =>
|
||||||
|
[
|
||||||
|
{ :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