diff --git a/CHANGELOG.md b/CHANGELOG.md
index 5bd18c868..dbd227579 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,9 @@
Breaking changes:
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
`ActiveModel::Serializer.type` method. (@groyoh)
- [#1504](https://github.com/rails-api/active_model_serializers/pull/1504) Adds the changes missing from #1454
diff --git a/docs/README.md b/docs/README.md
index 7f0a8ac02..b1db25da2 100644
--- a/docs/README.md
+++ b/docs/README.md
@@ -14,7 +14,9 @@ This is the documentation of ActiveModelSerializers, it's focused on the **0.10.
- [Caching](general/caching.md)
- [Logging](general/logging.md)
- [Instrumentation](general/instrumentation.md)
-- [JSON API Schema](jsonapi/schema.md)
+- JSON API
+ - [Schema](jsonapi/schema.md)
+ - [Errors](jsonapi/errors.md)
- [ARCHITECTURE](ARCHITECTURE.md)
## How to
diff --git a/docs/jsonapi/errors.md b/docs/jsonapi/errors.md
new file mode 100644
index 000000000..1d15dde01
--- /dev/null
+++ b/docs/jsonapi/errors.md
@@ -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' }
+# ]
+# }
+```
diff --git a/docs/jsonapi/schema.md b/docs/jsonapi/schema.md
index 72b149484..ba718ec0c 100644
--- a/docs/jsonapi/schema.md
+++ b/docs/jsonapi/schema.md
@@ -58,33 +58,33 @@ Example supported requests
|-----------------------|----------------------------------------------------------------------------------------------------|----------|---------------------------------------|
| schema | oneOf (success, failure, info) | |
| success | data, included, meta, links, jsonapi | | AM::SerializableResource
-| success.meta | meta | | AM::S::Adapter::Base#meta
-| success.included | UniqueArray(resource) | | AM::S::Adapter::JsonApi#serializable_hash_for_collection
+| success.meta | meta | | AMS::Adapter::Base#meta
+| success.included | UniqueArray(resource) | | AMS::Adapter::JsonApi#serializable_hash_for_collection
| 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 | |
-| failure | errors, meta, jsonapi | errors |
-| failure.errors | UniqueArray(error) | | #1004
-| meta | Object | |
-| data | oneOf (resource, UniqueArray(resource)) | | AM::S::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
+| failure | errors, meta, jsonapi | errors | AMS::Adapter::JsonApi#failure_document, #1004
+| failure.errors | UniqueArray(error) | | AM::S::ErrorSerializer, #1004
+| meta | Object | |
+| data | oneOf (resource, UniqueArray(resource)) | | AMS::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
| resource | String(type), String(id),
attributes, relationships,
links, meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
| links | Uri(self), Link(related) | | #1028, #1246, #1282
| link | oneOf (linkString, linkObject) | |
| link.linkString | Uri | |
| link.linkObject | Uri(href), meta | href |
-| attributes | patternProperties(
`"^(?!relationships$|links$)\\w[-\\w_]*$"`),
any valid JSON | | AM::Serializer#attributes, AM::S::Adapter::JsonApi#resource_object_for
-| relationships | patternProperties(
`"^\\w[-\\w_]*$"`);
links, relationships.data, meta | | AM::S::Adapter::JsonApi#relationships_for
-| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AM::S::Adapter::JsonApi#resource_identifier_for
+| attributes | patternProperties(
`"^(?!relationships$|links$)\\w[-\\w_]*$"`),
any valid JSON | | AM::Serializer#attributes, AMS::Adapter::JsonApi#resource_object_for
+| relationships | patternProperties(
`"^\\w[-\\w_]*$"`);
links, relationships.data, meta | | AMS::Adapter::JsonApi#relationships_for
+| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AMS::Adapter::JsonApi#resource_identifier_for
| relationshipToOne | anyOf(empty, linkage) | |
| relationshipToMany | UniqueArray(linkage) | |
| empty | null | |
-| linkage | String(type), String(id), meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
-| pagination | pageObject(first), pageObject(last),
pageObject(prev), pageObject(next) | | AM::S::Adapter::JsonApi::PaginationLinks#serializable_hash
+| linkage | String(type), String(id), meta | type, id | AMS::Adapter::JsonApi#primary_data_for
+| pagination | pageObject(first), pageObject(last),
pageObject(prev), pageObject(next) | | AMS::Adapter::JsonApi::PaginationLinks#serializable_hash
| pagination.pageObject | oneOf(Uri, null) | |
-| jsonapi | String(version), meta | | AM::S::Adapter::JsonApi::ApiObjects::JsonApi
-| error | String(id), links, String(status),
String(code), String(title),
String(detail), error.source, meta | |
-| error.source | String(pointer), String(parameter) | |
-| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | |
+| jsonapi | String(version), meta | | AMS::Adapter::JsonApi::ApiObjects::JsonApi#as_json
+| error | String(id), links, String(status),
String(code), String(title),
String(detail), error.source, meta | | AM::S::ErrorSerializer, AMS::Adapter::JsonApi::Error.resource_errors
+| error.source | String(pointer), String(parameter) | | AMS::Adapter::JsonApi::Error.error_source
+| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | | AMS::JsonPointer
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
- - [ ] 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"`
- [ ] jsonapi: `"$ref": "#/definitions/jsonapi"`
@@ -137,4 +137,15 @@ The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
- [ ] pagination
- [ ] jsonapi
- [ ] 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.
diff --git a/lib/active_model/serializer.rb b/lib/active_model/serializer.rb
index 64bff0eca..b0ca3fa6c 100644
--- a/lib/active_model/serializer.rb
+++ b/lib/active_model/serializer.rb
@@ -1,6 +1,8 @@
require 'thread_safe'
require 'active_model/serializer/collection_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/associations'
require 'active_model/serializer/attributes'
@@ -116,6 +118,10 @@ def initialize(object, options = {})
end
end
+ def success?
+ true
+ end
+
# Used by adapter as resource root.
def json_key
root || object.class.model_name.to_s.underscore
diff --git a/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb b/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb
index dfaabc39b..154b71fdb 100644
--- a/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb
+++ b/lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb
@@ -4,6 +4,10 @@ module Adapter
class JsonApi
module ApiObjects
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)
@object = parent_serializer.object
@scope = parent_serializer.scope
diff --git a/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb b/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb
index 058f06031..0336e0b50 100644
--- a/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb
+++ b/lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb
@@ -4,6 +4,7 @@ module Adapter
class JsonApi
module ApiObjects
class ResourceIdentifier
+ # {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
def initialize(serializer)
@id = id_for(serializer)
@type = type_for(serializer)
diff --git a/lib/active_model/serializer/collection_serializer.rb b/lib/active_model/serializer/collection_serializer.rb
index c1edfeafc..022588381 100644
--- a/lib/active_model/serializer/collection_serializer.rb
+++ b/lib/active_model/serializer/collection_serializer.rb
@@ -22,6 +22,10 @@ def initialize(resources, options = {})
end
end
+ def success?
+ true
+ end
+
def json_key
root || derived_root
end
diff --git a/lib/active_model/serializer/error_serializer.rb b/lib/active_model/serializer/error_serializer.rb
new file mode 100644
index 000000000..c12dfd370
--- /dev/null
+++ b/lib/active_model/serializer/error_serializer.rb
@@ -0,0 +1,10 @@
+class ActiveModel::Serializer::ErrorSerializer < ActiveModel::Serializer
+ # @return [Hash>]
+ def as_json
+ object.errors.messages
+ end
+
+ def success?
+ false
+ end
+end
diff --git a/lib/active_model/serializer/errors_serializer.rb b/lib/active_model/serializer/errors_serializer.rb
new file mode 100644
index 000000000..4b67bae83
--- /dev/null
+++ b/lib/active_model/serializer/errors_serializer.rb
@@ -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
diff --git a/lib/active_model/serializer/lint.rb b/lib/active_model/serializer/lint.rb
index b2bc48ff9..b791d40da 100644
--- a/lib/active_model/serializer/lint.rb
+++ b/lib/active_model/serializer/lint.rb
@@ -129,6 +129,20 @@ def test_model_name
assert_instance_of resource_class.model_name, ActiveModel::Name
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
def resource
diff --git a/lib/active_model_serializers.rb b/lib/active_model_serializers.rb
index e75e30a0d..bf2dac136 100644
--- a/lib/active_model_serializers.rb
+++ b/lib/active_model_serializers.rb
@@ -10,6 +10,7 @@ module ActiveModelSerializers
autoload :Logging
autoload :Test
autoload :Adapter
+ autoload :JsonPointer
class << self; attr_accessor :logger; end
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))
diff --git a/lib/active_model_serializers/adapter/json_api.rb b/lib/active_model_serializers/adapter/json_api.rb
index 841185f00..0407ef095 100644
--- a/lib/active_model_serializers/adapter/json_api.rb
+++ b/lib/active_model_serializers/adapter/json_api.rb
@@ -8,11 +8,13 @@ class JsonApi < Base
require 'active_model/serializer/adapter/json_api/meta'
autoload :Deserialization
require 'active_model/serializer/adapter/json_api/api_objects'
+ autoload :Error
# TODO: if we like this abstraction and other API objects to it,
# then extract to its own file and require it.
module ApiObjects
- module JsonApi
+ # {http://jsonapi.org/format/#document-jsonapi-object Jsonapi Object}
+ module Jsonapi
ActiveModelSerializers.config.jsonapi_version = '1.0'
ActiveModelSerializers.config.jsonapi_toplevel_meta = {}
# Make JSON API top-level jsonapi member opt-in
@@ -50,9 +52,19 @@ def initialize(serializer, options = {})
@fieldset = options[:fieldset] || ActiveModel::Serializer::Fieldset.new(options.delete(:fields))
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)
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)
serializers = is_collection ? serializer : [serializer]
primary_data, included = resource_objects_for(serializers)
@@ -61,7 +73,7 @@ def serializable_hash(options = nil)
hash[:data] = is_collection ? primary_data : primary_data[0]
hash[:included] = included if included.any?
- ApiObjects::JsonApi.add!(hash)
+ ApiObjects::Jsonapi.add!(hash)
if instance_options[:links]
hash[:links] ||= {}
@@ -76,6 +88,29 @@ def serializable_hash(options = nil)
hash
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)
root = false if instance_options.include?(:include)
ActiveModelSerializers::Adapter::JsonApi::FragmentCache.new.fragment_cache(root, cached_hash, non_cached_hash)
@@ -87,6 +122,7 @@ def fragment_cache(cached_hash, non_cached_hash)
private
+ # {http://jsonapi.org/format/#document-resource-objects Primary data}
def resource_objects_for(serializers)
@primary = []
@included = []
@@ -128,10 +164,12 @@ def process_relationship(serializer, include_tree)
process_relationships(serializer, include_tree)
end
+ # {http://jsonapi.org/format/#document-resource-object-attributes Document Resource Object Attributes}
def attributes_for(serializer, fields)
serializer.attributes(fields).except(:id)
end
+ # {http://jsonapi.org/format/#document-resource-objects Document Resource Objects}
def resource_object_for(serializer)
resource_object = cache_check(serializer) do
resource_object = ActiveModel::Serializer::Adapter::JsonApi::ApiObjects::ResourceIdentifier.new(serializer).as_json
@@ -155,6 +193,7 @@ def resource_object_for(serializer)
resource_object
end
+ # {http://jsonapi.org/format/#document-resource-object-relationships Document Resource Object Relationship}
def relationships_for(serializer, requested_associations)
include_tree = ActiveModel::Serializer::IncludeTree.from_include_args(requested_associations)
serializer.associations(include_tree).each_with_object({}) do |association, hash|
@@ -168,16 +207,19 @@ def relationships_for(serializer, requested_associations)
end
end
+ # {http://jsonapi.org/format/#document-links Document Links}
def links_for(serializer)
serializer._links.each_with_object({}) do |(name, value), hash|
hash[name] = Link.new(serializer, value).as_json
end
end
+ # {http://jsonapi.org/format/#fetching-pagination Pagination Links}
def pagination_links_for(serializer, options)
JsonApi::PaginationLinks.new(serializer.object, options[:serialization_context]).serializable_hash(options)
end
+ # {http://jsonapi.org/format/#document-meta Docment Meta}
def meta_for(serializer)
ActiveModel::Serializer::Adapter::JsonApi::Meta.new(serializer).as_json
end
diff --git a/lib/active_model_serializers/adapter/json_api/error.rb b/lib/active_model_serializers/adapter/json_api/error.rb
new file mode 100644
index 000000000..c8383d216
--- /dev/null
+++ b/lib/active_model_serializers/adapter/json_api/error.rb
@@ -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] 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
diff --git a/lib/active_model_serializers/json_pointer.rb b/lib/active_model_serializers/json_pointer.rb
new file mode 100644
index 000000000..a262f3b28
--- /dev/null
+++ b/lib/active_model_serializers/json_pointer.rb
@@ -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
diff --git a/lib/active_model_serializers/model.rb b/lib/active_model_serializers/model.rb
index 3043c389e..629713929 100644
--- a/lib/active_model_serializers/model.rb
+++ b/lib/active_model_serializers/model.rb
@@ -6,10 +6,11 @@ class Model
include ActiveModel::Model
include ActiveModel::Serializers::JSON
- attr_reader :attributes
+ attr_reader :attributes, :errors
def initialize(attributes = {})
@attributes = attributes
+ @errors = ActiveModel::Errors.new(self)
super
end
@@ -35,5 +36,14 @@ def read_attribute_for_serialization(key)
attributes[key]
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
diff --git a/test/action_controller/json_api/errors_test.rb b/test/action_controller/json_api/errors_test.rb
new file mode 100644
index 000000000..d58901103
--- /dev/null
+++ b/test/action_controller/json_api/errors_test.rb
@@ -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
diff --git a/test/active_model_serializers/json_pointer_test.rb b/test/active_model_serializers/json_pointer_test.rb
new file mode 100644
index 000000000..64acc7eb7
--- /dev/null
+++ b/test/active_model_serializers/json_pointer_test.rb
@@ -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
diff --git a/test/adapter/json_api/errors_test.rb b/test/adapter/json_api/errors_test.rb
new file mode 100644
index 000000000..da7eff9be
--- /dev/null
+++ b/test/adapter/json_api/errors_test.rb
@@ -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
diff --git a/test/fixtures/poro.rb b/test/fixtures/poro.rb
index 445530e40..c40b1ca61 100644
--- a/test/fixtures/poro.rb
+++ b/test/fixtures/poro.rb
@@ -27,6 +27,17 @@ def cache_key_with_digest
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
end
diff --git a/test/lint_test.rb b/test/lint_test.rb
index 0ebdda647..9d0f2bc87 100644
--- a/test/lint_test.rb
+++ b/test/lint_test.rb
@@ -27,6 +27,15 @@ def id
def updated_at
end
+ def errors
+ end
+
+ def self.human_attribute_name(attr, options = {})
+ end
+
+ def self.lookup_ancestors
+ end
+
def self.model_name
@_model_name ||= ActiveModel::Name.new(self)
end
diff --git a/test/serializable_resource_test.rb b/test/serializable_resource_test.rb
index 698795040..4c683f9b7 100644
--- a/test/serializable_resource_test.rb
+++ b/test/serializable_resource_test.rb
@@ -31,5 +31,46 @@ def test_use_adapter_with_adapter_option
def test_use_adapter_with_adapter_option_as_false
refute ActiveModel::SerializableResource.new(@resource, { adapter: false }).use_adapter?
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