Skip to content

Commit 9604c35

Browse files
committed
2 parents 2989b0e + fb3bae6 commit 9604c35

File tree

12 files changed

+501
-207
lines changed

12 files changed

+501
-207
lines changed

.gitignore

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,4 +21,3 @@
2121
.byebug_history
2222
test_db
2323
Gemfile.lock
24-

README.md

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ gem 'jsonapi-utils', '~> 0.4.9'
6565
For Rails 5:
6666

6767
```ruby
68-
gem 'jsonapi-utils', '~> 0.6.0.beta'
68+
gem 'jsonapi-utils', '~> 0.7.0'
6969
```
7070

7171
And then execute:
@@ -110,8 +110,8 @@ end
110110

111111
Arguments:
112112

113-
- `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array of Hashes;
114-
- `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered;
113+
- `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array<Hash>;
114+
- `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered;
115115
- `options`:
116116
- `resource`: explicitly points the resource to be used in the serialization. By default, JU will select resources by inferencing from controller's name.
117117
- `count`: explicitly points the total count of records for the request in order to build a proper pagination. By default, JU will count the total number of records.
@@ -155,16 +155,16 @@ It renders a JSON API-compliant error response.
155155

156156
Arguments:
157157
- Exception
158-
- `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array of Hashes or any object which implements the `errors` method;
159-
- `status`: HTTP status code (Integer or Symbol). If ommited a status code will be automatically infered from the error body.
158+
- `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array<Hash> or any object which implements the `errors` method;
159+
- `status`: HTTP status code (Integer, String or Symbol). If ommited a status code will be automatically infered from the error body.
160160

161161
Other examples:
162162

163163
```ruby
164164
# Render errors from a custom exception:
165165
jsonapi_render_errors Exceptions::MyCustomError.new(user)
166166

167-
# Render errors from an Array of Hashes:
167+
# Render errors from an Array<Hash>:
168168
errors = [{ id: 'validation', title: 'Something went wrong', code: '100' }]
169169
jsonapi_render_errors json: errors, status: :unprocessable_entity
170170
```
@@ -188,7 +188,7 @@ end
188188
```
189189

190190
Arguments:
191-
- First: ActiveRecord object, Hash or Array of Hashes;
191+
- First: ActiveRecord object, Hash or Array<Hash>;
192192
- Last: Hash of options (same as `JSONAPI::Utils#jsonapi_render`).
193193

194194
#### Paginators

lib/jsonapi/utils/exceptions.rb

Lines changed: 2 additions & 126 deletions
Original file line numberDiff line numberDiff line change
@@ -1,126 +1,2 @@
1-
require 'jsonapi/utils/version'
2-
3-
module JSONAPI
4-
module Utils
5-
module Exceptions
6-
class ActiveRecord < ::JSONAPI::Exceptions::Error
7-
attr_reader :object, :resource, :relationships, :relationship_keys, :foreign_keys
8-
9-
def initialize(object, resource_klass, context)
10-
@object = object
11-
@resource = resource_klass.new(object, context)
12-
13-
# Need to reflect on resource's relationships for error reporting.
14-
@relationships = resource_klass._relationships.values
15-
@relationship_keys = @relationships.map(&:name).map(&:to_sym)
16-
@foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym)
17-
end
18-
19-
def errors
20-
object.errors.messages.flat_map do |key, messages|
21-
messages.map do |message|
22-
error_meta = error_base
23-
.merge(title: title_member(key, message))
24-
.merge(id: id_member(key))
25-
.merge(source_member(key))
26-
27-
JSONAPI::Error.new(error_meta)
28-
end
29-
end
30-
end
31-
32-
private
33-
34-
def id_member(key)
35-
id = resource_key_for(key)
36-
key_formatter = JSONAPI.configuration.key_formatter
37-
key_formatter.format(id).to_sym
38-
end
39-
40-
# Determine if this is a foreign key, which will need to look up its
41-
# matching association name.
42-
def resource_key_for(key)
43-
if foreign_keys.include?(key)
44-
relationships.find { |r| r.foreign_key == key }.name.to_sym
45-
else
46-
key
47-
end
48-
end
49-
50-
def source_member(key)
51-
Hash.new.tap do |hash|
52-
resource_key = resource_key_for(key)
53-
54-
# Pointer should only be created for whitelisted attributes.
55-
return hash unless resource.fetchable_fields.include?(resource_key) || key == :base
56-
57-
id = id_member(key)
58-
59-
hash[:source] = {}
60-
hash[:source][:pointer] =
61-
# Relationship
62-
if relationship_keys.include?(resource_key)
63-
"/data/relationships/#{id}"
64-
# Base
65-
elsif key == :base
66-
'/data'
67-
# Attribute
68-
else
69-
"/data/attributes/#{id}"
70-
end
71-
end
72-
end
73-
74-
def title_member(key, message)
75-
if key == :base
76-
message
77-
else
78-
resource_key = resource_key_for(key)
79-
[translation_for(resource_key), message].join(' ')
80-
end
81-
end
82-
83-
def translation_for(key)
84-
object.class.human_attribute_name(key)
85-
end
86-
87-
def error_base
88-
{
89-
code: JSONAPI::VALIDATION_ERROR,
90-
status: :unprocessable_entity
91-
}
92-
end
93-
end
94-
95-
class BadRequest < ::JSONAPI::Exceptions::Error
96-
def code
97-
'400'
98-
end
99-
100-
def errors
101-
[JSONAPI::Error.new(
102-
code: code,
103-
status: :bad_request,
104-
title: 'Bad Request',
105-
detail: 'This request is not supported.'
106-
)]
107-
end
108-
end
109-
110-
class InternalServerError < ::JSONAPI::Exceptions::Error
111-
def code
112-
'500'
113-
end
114-
115-
def errors
116-
[JSONAPI::Error.new(
117-
code: code,
118-
status: :internal_server_error,
119-
title: 'Internal Server Error',
120-
detail: 'An internal error ocurred while processing the request.'
121-
)]
122-
end
123-
end
124-
end
125-
end
126-
end
1+
require 'jsonapi/utils/exceptions/active_record'
2+
require 'jsonapi/utils/exceptions/internal_server_error'
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
module JSONAPI
2+
module Utils
3+
module Exceptions
4+
class ActiveRecord < ::JSONAPI::Exceptions::Error
5+
attr_reader :object, :resource, :relationships, :relationship_names, :foreign_keys
6+
7+
# Construct an error decorator over ActiveRecord objects.
8+
#
9+
# @param object [ActiveRecord::Base] Invalid ActiveRecord object.
10+
# e.g.: User.new(name: nil).tap(&:save)
11+
#
12+
# @param resource_klass [JSONAPI::Resource] Resource class to be used for reflection.
13+
# e.g.: UserResuource
14+
#
15+
# @return [JSONAPI::Utils::Exceptions::ActiveRecord]
16+
#
17+
# @api public
18+
def initialize(object, resource_klass, context)
19+
@object = object
20+
@resource = resource_klass.new(object, context)
21+
22+
# Need to reflect on resource's relationships for error reporting.
23+
@relationships = resource_klass._relationships.values
24+
@relationship_names = @relationships.map(&:name).map(&:to_sym)
25+
@foreign_keys = @relationships.map(&:foreign_key).map(&:to_sym)
26+
@resource_key_for = {}
27+
@formatted_key = {}
28+
end
29+
30+
# Decorate errors for AR invalid objects.
31+
#
32+
# @note That's the method used by formatters to build the response's error body.
33+
#
34+
# @return [Array]
35+
#
36+
# @api public
37+
def errors
38+
object.errors.messages.flat_map do |field, messages|
39+
messages.map.with_index do |message, index|
40+
build_error(field, message, index)
41+
end
42+
end
43+
end
44+
45+
private
46+
47+
# Turn AR error into JSONAPI::Error.
48+
#
49+
# @param field [Symbol] Name of the invalid field
50+
# e.g.: :title
51+
#
52+
# @param message [String] Error message
53+
# e.g.: "can't be blank"
54+
#
55+
# @param index [Integer] Index of the error detail
56+
#
57+
# @return [JSONAPI::Error]
58+
#
59+
# @api private
60+
def build_error(field, message, index = 0)
61+
error = error_base
62+
.merge(
63+
id: id_member(field, index),
64+
title: message,
65+
detail: detail_member(field, message)
66+
).merge(source_member(field))
67+
JSONAPI::Error.new(error)
68+
end
69+
70+
# Build the "id" member value for the JSON API error object.
71+
# e.g.: for :first_name, :too_short => "first-name#too-short"
72+
#
73+
# @note The returned value depends on the key formatter type defined
74+
# via configuration, e.g.: config.json_key_format = :dasherized_key
75+
#
76+
# @param field [Symbol] Name of the invalid field
77+
# e.g.: :first_name
78+
#
79+
# @param index [Integer] Index of the error detail
80+
#
81+
# @return [String]
82+
#
83+
# @api private
84+
def id_member(field, index)
85+
[
86+
key_format(field),
87+
key_format(
88+
object.errors.details
89+
.dig(field, index, :error)
90+
.to_s.downcase
91+
.split
92+
.join('_')
93+
)
94+
].join('#')
95+
end
96+
97+
# Bring the formatted resource key for a given field.
98+
# e.g.: for :first_name => :"first-name"
99+
#
100+
# @note The returned value depends on the key formatter type defined
101+
# via configuration, e.g.: config.json_key_format = :dasherized_key
102+
#
103+
# @param field [Symbol] Name of the invalid field
104+
# e.g.: :title
105+
#
106+
# @return [Symbol]
107+
#
108+
# @api private
109+
def key_format(field)
110+
@formatted_key[field] ||= JSONAPI.configuration
111+
.key_formatter
112+
.format(resource_key_for(field))
113+
.to_sym
114+
end
115+
116+
# Build the "source" member value for the JSON API error object.
117+
# e.g.: :title => "/data/attributes/title"
118+
#
119+
# @param field [Symbol] Name of the invalid field
120+
# e.g.: :title
121+
#
122+
# @return [Hash]
123+
#
124+
# @api private
125+
def source_member(field)
126+
resource_key = resource_key_for(field)
127+
return {} unless field == :base || resource.fetchable_fields.include?(resource_key)
128+
id = key_format(field)
129+
130+
pointer =
131+
if field == :base then '/data'
132+
elsif relationship_names.include?(resource_key) then "/data/relationships/#{id}"
133+
else "/data/attributes/#{id}"
134+
end
135+
136+
{ source: { pointer: pointer } }
137+
end
138+
139+
# Build the "detail" member value for the JSON API error object.
140+
# e.g.: :first_name, "can't be blank" => "First name can't be blank"
141+
#
142+
# @param field [Symbol] Name of the invalid field
143+
# e.g.: :first_name
144+
#
145+
# @return [String]
146+
#
147+
# @api private
148+
def detail_member(field, message)
149+
return message if field == :base
150+
resource_key = resource_key_for(field)
151+
[translation_for(resource_key), message].join(' ')
152+
end
153+
154+
# Return the resource's attribute or relationship key name for a given field name.
155+
# e.g.: :title => :title, :user_id => :author
156+
#
157+
# @param field [Symbol] Name of the invalid field
158+
# e.g.: :title
159+
#
160+
# @return [Symbol]
161+
#
162+
# @api private
163+
def resource_key_for(field)
164+
@resource_key_for[field] ||= begin
165+
return field unless foreign_keys.include?(field)
166+
relationships.find { |r| r.foreign_key == field }.name.to_sym
167+
end
168+
end
169+
170+
# Turn the field name into human-friendly one.
171+
# e.g.: :first_name => "First name"
172+
#
173+
# @param field [Symbol] Name of the invalid field
174+
# e.g.: :first_name
175+
#
176+
# @return [String]
177+
#
178+
# @api private
179+
def translation_for(field)
180+
object.class.human_attribute_name(field)
181+
end
182+
183+
# Return the base data used for all errors of this kind.
184+
#
185+
# @return [Hash]
186+
#
187+
# @api private
188+
def error_base
189+
{
190+
code: JSONAPI::VALIDATION_ERROR,
191+
status: :unprocessable_entity
192+
}
193+
end
194+
end
195+
end
196+
end
197+
end

0 commit comments

Comments
 (0)