Skip to content

Commit 85f5c7c

Browse files
committed
RFC: Json Api Errors (WIP)
1 parent 39d6dab commit 85f5c7c

File tree

7 files changed

+168
-2
lines changed

7 files changed

+168
-2
lines changed

lib/active_model/serializer.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ class Serializer
66

77
autoload :Configuration
88
autoload :ArraySerializer
9+
autoload :ErrorSerializer
910
autoload :Adapter
1011
autoload :Lint
1112
autoload :Associations

lib/active_model/serializer/adapter/flatten_json.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ module ActiveModel
22
class Serializer
33
class Adapter
44
class FlattenJson < Json
5-
def serializable_hash(options = {})
5+
def serializable_hash(options = nil)
66
super
77
@result
88
end

lib/active_model/serializer/adapter/json_api.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
require 'active_model/serializer/adapter/json_api/fragment_cache'
22
require 'active_model/serializer/adapter/json_api/pagination_links'
3+
require 'active_model/serializer/adapter/json_api/error'
34

45
module ActiveModel
56
class Serializer
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
class ActiveModel::Serializer::Adapter::JsonApi::Error < ActiveModel::Serializer::Adapter
2+
def serializable_hash(options = nil)
3+
options ||= {}
4+
5+
6+
@result = []
7+
object_errors.each do |attribute_name, attribute_errors|
8+
attribute_errors.each do |attribute_error|
9+
@result << {
10+
source: { pointer: "data/attributes/#{attribute_name}" },
11+
detail: attribute_error,
12+
}
13+
end
14+
end
15+
16+
{ root => @result }
17+
18+
@result
19+
end
20+
21+
def fragment_cache(cached_hash, non_cached_hash)
22+
Json::FragmentCache.new().fragment_cache(cached_hash, non_cached_hash)
23+
end
24+
25+
def root
26+
"errors".freeze
27+
end
28+
29+
private
30+
31+
def object_errors
32+
cache_check(serializer) do
33+
serializer.object.errors.messages
34+
end
35+
end
36+
end
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
class ActiveModel::Serializer::ErrorSerializer < ActiveModel::Serializer
2+
end

lib/active_model/serializer/lint.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,17 @@ def test_serializable_hash
3636
# Typically, it is implemented by including ActiveModel::Serialization.
3737
def test_read_attribute_for_serialization
3838
assert_respond_to resource, :read_attribute_for_serialization, "The resource should respond to read_attribute_for_serialization"
39-
assert_equal resource.method(:read_attribute_for_serialization).arity, 1
39+
actual_arity = resource.method(:read_attribute_for_serialization).arity
40+
if defined?(::Rubinius)
41+
# 1 for def read_attribute_for_serialization(name); end
42+
# -2 for alias :read_attribute_for_serialization :send for rbx because :shrug:
43+
assert_includes [1, -2], actual_arity, "expected #{actual_arity.inspect} to be 1 or -2"
44+
else
45+
# using absolute value since arity is:
46+
# 1 for def read_attribute_for_serialization(name); end
47+
# -1 for alias :read_attribute_for_serialization :send
48+
assert_includes [1, -1], actual_arity, "expected #{actual_arity.inspect} to be 1 or -1"
49+
end
4050
end
4151

4252
# Passes if the object responds to <tt>as_json</tt> and if it takes

test/adapter/json_api/errors_test.rb

Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
require 'test_helper'
2+
3+
=begin
4+
## http://jsonapi.org/format/#document-top-level
5+
6+
A document MUST contain at least one of the following top-level members:
7+
8+
- data: the document's "primary data"
9+
- errors: an array of error objects
10+
- meta: a meta object that contains non-standard meta-information.
11+
12+
The members data and errors MUST NOT coexist in the same document.
13+
14+
## http://jsonapi.org/format/#error-objects
15+
16+
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.
17+
18+
An error object MAY have the following members:
19+
20+
- id: a unique identifier for this particular occurrence of the problem.
21+
- links: a links object containing the following members:
22+
- about: a link that leads to further details about this particular occurrence of the problem.
23+
- status: the HTTP status code applicable to this problem, expressed as a string value.
24+
- code: an application-specific error code, expressed as a string value.
25+
- 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.
26+
- detail: a human-readable explanation specific to this occurrence of the problem.
27+
- source: an object containing references to the source of the error, optionally including any of the following members:
28+
- 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].
29+
- parameter: a string indicating which query parameter caused the error.
30+
- meta: a meta object containing non-standard meta-information about the error.
31+
32+
=end
33+
34+
module ActiveModel
35+
class Serializer
36+
class Adapter
37+
class JsonApi
38+
class ErrorsTest < Minitest::Test
39+
include ActiveModel::Serializer::Lint::Tests
40+
41+
# see
42+
# https://github.com/rails/rails/blob/4-2-stable/activemodel/lib/active_model/errors.rb
43+
# The below allows you to do:
44+
#
45+
# model = ModelWithErrors.new
46+
# model.validate! # => ["cannot be nil"]
47+
# model.errors.full_messages # => ["name cannot be nil"]
48+
class ModelWithErrors
49+
include ActiveModel::Serialization
50+
def attributes
51+
{
52+
errors: errors,
53+
name: name,
54+
}
55+
end
56+
57+
def id
58+
object_id
59+
end
60+
61+
def cache_key
62+
"#{self.class.name.downcase}/#{id}-#{Time.now.utc.strftime("%Y%m%d%H%M%S%9N")}"
63+
end
64+
65+
# Required dependency for ActiveModel::Errors
66+
extend ActiveModel::Naming
67+
def initialize
68+
@errors = ActiveModel::Errors.new(self)
69+
end
70+
attr_accessor :name
71+
attr_reader :errors
72+
73+
# The following methods are needed to be minimally implemented
74+
75+
def read_attribute_for_validation(attr)
76+
send(attr)
77+
end
78+
79+
def self.human_attribute_name(attr, options = {})
80+
attr
81+
end
82+
83+
def self.lookup_ancestors
84+
[self]
85+
end
86+
end
87+
88+
def setup
89+
@resource = ModelWithErrors.new
90+
end
91+
92+
def test_active_model_errors
93+
options = {
94+
serializer: ActiveModel::Serializer::ErrorSerializer,
95+
adapter: :'json_api/error',
96+
}
97+
98+
@resource.errors.add(:name, "cannot be nil")
99+
100+
serializable_resource = ActiveModel::SerializableResource.serialize(@resource, options)
101+
assert_equal serializable_resource.serializer_instance.attributes, { }
102+
assert_equal serializable_resource.serializer_instance.object, @resource
103+
104+
expected_errors_object = [
105+
{
106+
source: { pointer: 'data/attributes/name' },
107+
detail: 'cannot be nil'
108+
}
109+
]
110+
assert_equal serializable_resource.as_json, expected_errors_object
111+
end
112+
end
113+
end
114+
end
115+
end
116+
end

0 commit comments

Comments
 (0)