Skip to content

Commit 79e6acb

Browse files
committed
Merge pull request #1004 from bf4/json_api_errors
JSON API Errors (Initial implementation and roadmap for full feature-set)
2 parents df815c4 + d03db81 commit 79e6acb

File tree

22 files changed

+504
-22
lines changed

22 files changed

+504
-22
lines changed

CHANGELOG.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,9 @@
33
Breaking changes:
44

55
Features:
6+
- [#1004](https://github.com/rails-api/active_model_serializers/pull/1004) JSON API errors object implementation.
7+
- Only implements `detail` and `source` as derived from `ActiveModel::Error`
8+
- Provides checklist of remaining questions and remaining parts of the spec.
69
- [#1515](https://github.com/rails-api/active_model_serializers/pull/1515) Adds support for symbols to the
710
`ActiveModel::Serializer.type` method. (@groyoh)
811
- [#1504](https://github.com/rails-api/active_model_serializers/pull/1504) Adds the changes missing from #1454

docs/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,9 @@ This is the documentation of ActiveModelSerializers, it's focused on the **0.10.
1414
- [Caching](general/caching.md)
1515
- [Logging](general/logging.md)
1616
- [Instrumentation](general/instrumentation.md)
17-
- [JSON API Schema](jsonapi/schema.md)
17+
- JSON API
18+
- [Schema](jsonapi/schema.md)
19+
- [Errors](jsonapi/errors.md)
1820
- [ARCHITECTURE](ARCHITECTURE.md)
1921

2022
## How to

docs/jsonapi/errors.md

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[Back to Guides](../README.md)
2+
3+
# [JSON API Errors](http://jsonapi.org/format/#errors)
4+
5+
Rendering error documents requires specifying the error serializer(s):
6+
7+
- Serializer:
8+
- For a single resource: `serializer: ActiveModel::Serializer::ErrorSerializer`.
9+
- For a collection: `serializer: ActiveModel::Serializer::ErrorsSerializer`, `each_serializer: ActiveModel::Serializer::ErrorSerializer`.
10+
11+
The resource **MUST** have a non-empty associated `#errors` object.
12+
The `errors` object must have a `#messages` method that returns a hash of error name to array of
13+
descriptions.
14+
15+
## Use in controllers
16+
17+
```ruby
18+
resource = Profile.new(name: 'Name 1',
19+
description: 'Description 1',
20+
comments: 'Comments 1')
21+
resource.errors.add(:name, 'cannot be nil')
22+
resource.errors.add(:name, 'must be longer')
23+
resource.errors.add(:id, 'must be a uuid')
24+
25+
render json: resource, status: 422, adapter: :json_api, serializer: ActiveModel::Serializer::ErrorSerializer
26+
# #=>
27+
# { :errors =>
28+
# [
29+
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'cannot be nil' },
30+
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'must be longer' },
31+
# { :source => { :pointer => '/data/attributes/id' }, :detail => 'must be a uuid' }
32+
# ]
33+
# }.to_json
34+
```
35+
36+
## Direct error document generation
37+
38+
```ruby
39+
options = nil
40+
resource = ModelWithErrors.new
41+
resource.errors.add(:name, 'must be awesome')
42+
43+
serializable_resource = ActiveModel::SerializableResource.new(
44+
resource, {
45+
serializer: ActiveModel::Serializer::ErrorSerializer,
46+
adapter: :json_api
47+
})
48+
serializable_resource.as_json(options)
49+
# #=>
50+
# {
51+
# :errors =>
52+
# [
53+
# { :source => { :pointer => '/data/attributes/name' }, :detail => 'must be awesome' }
54+
# ]
55+
# }
56+
```

docs/jsonapi/schema.md

Lines changed: 29 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -58,33 +58,33 @@ Example supported requests
5858
|-----------------------|----------------------------------------------------------------------------------------------------|----------|---------------------------------------|
5959
| schema | oneOf (success, failure, info) | |
6060
| success | data, included, meta, links, jsonapi | | AM::SerializableResource
61-
| success.meta | meta | | AM::S::Adapter::Base#meta
62-
| success.included | UniqueArray(resource) | | AM::S::Adapter::JsonApi#serializable_hash_for_collection
61+
| success.meta | meta | | AMS::Adapter::Base#meta
62+
| success.included | UniqueArray(resource) | | AMS::Adapter::JsonApi#serializable_hash_for_collection
6363
| success.data | data | |
64-
| success.links | allOf (links, pagination) | | AM::S::Adapter::JsonApi#links_for
64+
| success.links | allOf (links, pagination) | | AMS::Adapter::JsonApi#links_for
6565
| success.jsonapi | jsonapi | |
66-
| failure | errors, meta, jsonapi | errors |
67-
| failure.errors | UniqueArray(error) | | #1004
68-
| meta | Object | |
69-
| data | oneOf (resource, UniqueArray(resource)) | | AM::S::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
66+
| failure | errors, meta, jsonapi | errors | AMS::Adapter::JsonApi#failure_document, #1004
67+
| failure.errors | UniqueArray(error) | | AM::S::ErrorSerializer, #1004
68+
| meta | Object | |
69+
| data | oneOf (resource, UniqueArray(resource)) | | AMS::Adapter::JsonApi#serializable_hash_for_collection,#serializable_hash_for_single_resource
7070
| resource | String(type), String(id),<br>attributes, relationships,<br>links, meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
7171
| links | Uri(self), Link(related) | | #1028, #1246, #1282
7272
| link | oneOf (linkString, linkObject) | |
7373
| link.linkString | Uri | |
7474
| link.linkObject | Uri(href), meta | href |
75-
| attributes | patternProperties(<br>`"^(?!relationships$|links$)\\w[-\\w_]*$"`),<br>any valid JSON | | AM::Serializer#attributes, AM::S::Adapter::JsonApi#resource_object_for
76-
| relationships | patternProperties(<br>`"^\\w[-\\w_]*$"`);<br>links, relationships.data, meta | | AM::S::Adapter::JsonApi#relationships_for
77-
| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AM::S::Adapter::JsonApi#resource_identifier_for
75+
| attributes | patternProperties(<br>`"^(?!relationships$|links$)\\w[-\\w_]*$"`),<br>any valid JSON | | AM::Serializer#attributes, AMS::Adapter::JsonApi#resource_object_for
76+
| relationships | patternProperties(<br>`"^\\w[-\\w_]*$"`);<br>links, relationships.data, meta | | AMS::Adapter::JsonApi#relationships_for
77+
| relationships.data | oneOf (relationshipToOne, relationshipToMany) | | AMS::Adapter::JsonApi#resource_identifier_for
7878
| relationshipToOne | anyOf(empty, linkage) | |
7979
| relationshipToMany | UniqueArray(linkage) | |
8080
| empty | null | |
81-
| linkage | String(type), String(id), meta | type, id | AM::S::Adapter::JsonApi#primary_data_for
82-
| pagination | pageObject(first), pageObject(last),<br>pageObject(prev), pageObject(next) | | AM::S::Adapter::JsonApi::PaginationLinks#serializable_hash
81+
| linkage | String(type), String(id), meta | type, id | AMS::Adapter::JsonApi#primary_data_for
82+
| pagination | pageObject(first), pageObject(last),<br>pageObject(prev), pageObject(next) | | AMS::Adapter::JsonApi::PaginationLinks#serializable_hash
8383
| pagination.pageObject | oneOf(Uri, null) | |
84-
| jsonapi | String(version), meta | | AM::S::Adapter::JsonApi::ApiObjects::JsonApi
85-
| error | String(id), links, String(status),<br>String(code), String(title),<br>String(detail), error.source, meta | |
86-
| error.source | String(pointer), String(parameter) | |
87-
| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | |
84+
| jsonapi | String(version), meta | | AMS::Adapter::JsonApi::ApiObjects::JsonApi#as_json
85+
| 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
86+
| error.source | String(pointer), String(parameter) | | AMS::Adapter::JsonApi::Error.error_source
87+
| pointer | [JSON Pointer RFC6901](https://tools.ietf.org/html/rfc6901) | | AMS::JsonPointer
8888

8989

9090
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.
102102
### Failure Document
103103

104104
- [ ] failure
105-
- [ ] errors: array of unique items of type ` "$ref": "#/definitions/error"`
105+
- [x] errors: array of unique items of type ` "$ref": "#/definitions/error"`
106106
- [ ] meta: `"$ref": "#/definitions/meta"`
107107
- [ ] jsonapi: `"$ref": "#/definitions/jsonapi"`
108108

@@ -137,4 +137,15 @@ The [http://jsonapi.org/schema](schema/schema.json) makes a nice roadmap.
137137
- [ ] pagination
138138
- [ ] jsonapi
139139
- [ ] meta
140-
- [ ] error: id, links, status, code, title: detail: source [{pointer, type}, {parameter: {description, type}], meta
140+
- [ ] error
141+
- [ ] id: a unique identifier for this particular occurrence of the problem.
142+
- [ ] links: a links object containing the following members:
143+
- [ ] about: a link that leads to further details about this particular occurrence of the problem.
144+
- [ ] status: the HTTP status code applicable to this problem, expressed as a string value.
145+
- [ ] code: an application-specific error code, expressed as a string value.
146+
- [ ] 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.
147+
- [x] detail: a human-readable explanation specific to this occurrence of the problem.
148+
- [x] source: an object containing references to the source of the error, optionally including any of the following members:
149+
- [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].
150+
- [x] parameter: a string indicating which query parameter caused the error.
151+
- [ ] meta: a meta object containing non-standard meta-information about the error.

lib/active_model/serializer.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
require 'thread_safe'
22
require 'active_model/serializer/collection_serializer'
33
require 'active_model/serializer/array_serializer'
4+
require 'active_model/serializer/error_serializer'
5+
require 'active_model/serializer/errors_serializer'
46
require 'active_model/serializer/include_tree'
57
require 'active_model/serializer/associations'
68
require 'active_model/serializer/attributes'
@@ -116,6 +118,10 @@ def initialize(object, options = {})
116118
end
117119
end
118120

121+
def success?
122+
true
123+
end
124+
119125
# Used by adapter as resource root.
120126
def json_key
121127
root || object.class.model_name.to_s.underscore

lib/active_model/serializer/adapter/json_api/api_objects/relationship.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,10 @@ module Adapter
44
class JsonApi
55
module ApiObjects
66
class Relationship
7+
# {http://jsonapi.org/format/#document-resource-object-related-resource-links Document Resource Object Related Resource Links}
8+
# {http://jsonapi.org/format/#document-links Document Links}
9+
# {http://jsonapi.org/format/#document-resource-object-linkage Document Resource Relationship Linkage}
10+
# {http://jsonapi.org/format/#document-meta Docment Meta}
711
def initialize(parent_serializer, serializer, options = {}, links = {}, meta = nil)
812
@object = parent_serializer.object
913
@scope = parent_serializer.scope

lib/active_model/serializer/adapter/json_api/api_objects/resource_identifier.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ module Adapter
44
class JsonApi
55
module ApiObjects
66
class ResourceIdentifier
7+
# {http://jsonapi.org/format/#document-resource-identifier-objects Resource Identifier Objects}
78
def initialize(serializer)
89
@id = id_for(serializer)
910
@type = type_for(serializer)

lib/active_model/serializer/collection_serializer.rb

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,10 @@ def initialize(resources, options = {})
2222
end
2323
end
2424

25+
def success?
26+
true
27+
end
28+
2529
def json_key
2630
root || derived_root
2731
end
Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
class ActiveModel::Serializer::ErrorSerializer < ActiveModel::Serializer
2+
# @return [Hash<field_name,Array<error_message>>]
3+
def as_json
4+
object.errors.messages
5+
end
6+
7+
def success?
8+
false
9+
end
10+
end
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
require 'active_model/serializer/error_serializer'
2+
class ActiveModel::Serializer::ErrorsSerializer
3+
include Enumerable
4+
delegate :each, to: :@serializers
5+
attr_reader :object, :root
6+
7+
def initialize(resources, options = {})
8+
@root = options[:root]
9+
@object = resources
10+
@serializers = resources.map do |resource|
11+
serializer_class = options.fetch(:serializer) { ActiveModel::Serializer::ErrorSerializer }
12+
serializer_class.new(resource, options.except(:serializer))
13+
end
14+
end
15+
16+
def success?
17+
false
18+
end
19+
20+
def json_key
21+
nil
22+
end
23+
24+
protected
25+
26+
attr_reader :serializers
27+
end

lib/active_model/serializer/lint.rb

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,20 @@ def test_model_name
129129
assert_instance_of resource_class.model_name, ActiveModel::Name
130130
end
131131

132+
def test_active_model_errors
133+
assert_respond_to resource, :errors
134+
end
135+
136+
def test_active_model_errors_human_attribute_name
137+
assert_respond_to resource.class, :human_attribute_name
138+
assert_equal(-2, resource.class.method(:human_attribute_name).arity)
139+
end
140+
141+
def test_active_model_errors_lookup_ancestors
142+
assert_respond_to resource.class, :lookup_ancestors
143+
assert_equal 0, resource.class.method(:lookup_ancestors).arity
144+
end
145+
132146
private
133147

134148
def resource

lib/active_model_serializers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ module ActiveModelSerializers
1010
autoload :Logging
1111
autoload :Test
1212
autoload :Adapter
13+
autoload :JsonPointer
1314

1415
class << self; attr_accessor :logger; end
1516
self.logger = ActiveSupport::TaggedLogging.new(ActiveSupport::Logger.new(STDOUT))

0 commit comments

Comments
 (0)