jsonapi-utils 0.6.0 → 0.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +7 -7
- data/lib/jsonapi/utils/exceptions.rb +2 -126
- data/lib/jsonapi/utils/exceptions/active_record.rb +197 -0
- data/lib/jsonapi/utils/exceptions/internal_server_error.rb +30 -0
- data/lib/jsonapi/utils/response/formatters.rb +170 -36
- data/lib/jsonapi/utils/response/renders.rb +57 -6
- data/lib/jsonapi/utils/support/sort.rb +3 -3
- data/lib/jsonapi/utils/version.rb +1 -1
- metadata +16 -14
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: fa0fb8cc338bd774c92dce69ce09c955e50449c0
|
4
|
+
data.tar.gz: 661f6402c7943d0ab6ce6ea6fc6c5f67260c2828
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: f7813a7385d284f6aba51027410645fae4b38563ca611482cde9fd5f4dde80fce225b8ccf82c34b9a43fcbcf2a825c757fbdbe1607c1dc62b1b715f2672f447e
|
7
|
+
data.tar.gz: cde1ac2b6cc5a5f43b0d7da7e8cac598d527821dde18dcbc9ee633acbece6411e5cefafbf81d5c4cc791af78abdf72e0f371e67c945aec4611fe8e91bb888d9a
|
data/README.md
CHANGED
@@ -65,7 +65,7 @@ gem 'jsonapi-utils', '~> 0.4.9'
|
|
65
65
|
For Rails 5:
|
66
66
|
|
67
67
|
```ruby
|
68
|
-
gem 'jsonapi-utils', '~> 0.
|
68
|
+
gem 'jsonapi-utils', '~> 0.7.0'
|
69
69
|
```
|
70
70
|
|
71
71
|
And then execute:
|
@@ -110,8 +110,8 @@ end
|
|
110
110
|
|
111
111
|
Arguments:
|
112
112
|
|
113
|
-
- `json`: object to be rendered as a JSON document: ActiveRecord object, Hash or Array
|
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;
|
115
115
|
- `options`:
|
116
116
|
- `resource`: explicitly points the resource to be used in the serialization. By default, JU will select resources by inferencing from controller's name.
|
117
117
|
- `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,8 +155,8 @@ It renders a JSON API-compliant error response.
|
|
155
155
|
|
156
156
|
Arguments:
|
157
157
|
- Exception
|
158
|
-
- `json`: object to be rendered as a JSON document: ActiveRecord, Exception, Array
|
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.
|
160
160
|
|
161
161
|
Other examples:
|
162
162
|
|
@@ -164,7 +164,7 @@ Other examples:
|
|
164
164
|
# Render errors from a custom exception:
|
165
165
|
jsonapi_render_errors Exceptions::MyCustomError.new(user)
|
166
166
|
|
167
|
-
# Render errors from an Array
|
167
|
+
# Render errors from an Array<Hash>:
|
168
168
|
errors = [{ id: 'validation', title: 'Something went wrong', code: '100' }]
|
169
169
|
jsonapi_render_errors json: errors, status: :unprocessable_entity
|
170
170
|
```
|
@@ -188,7 +188,7 @@ end
|
|
188
188
|
```
|
189
189
|
|
190
190
|
Arguments:
|
191
|
-
- First: ActiveRecord object, Hash or Array
|
191
|
+
- First: ActiveRecord object, Hash or Array<Hash>;
|
192
192
|
- Last: Hash of options (same as `JSONAPI::Utils#jsonapi_render`).
|
193
193
|
|
194
194
|
#### Paginators
|
@@ -1,126 +1,2 @@
|
|
1
|
-
require 'jsonapi/utils/
|
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'
|
@@ -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,0 +1,30 @@
|
|
1
|
+
module JSONAPI
|
2
|
+
module Utils
|
3
|
+
module Exceptions
|
4
|
+
class InternalServerError < ::JSONAPI::Exceptions::Error
|
5
|
+
# HTTP status code
|
6
|
+
#
|
7
|
+
# @return [String]
|
8
|
+
#
|
9
|
+
# @api public
|
10
|
+
def code
|
11
|
+
'500'
|
12
|
+
end
|
13
|
+
|
14
|
+
# Decorate errors for 500 responses.
|
15
|
+
#
|
16
|
+
# @return [Array]
|
17
|
+
#
|
18
|
+
# @api public
|
19
|
+
def errors
|
20
|
+
[JSONAPI::Error.new(
|
21
|
+
code: code,
|
22
|
+
status: :internal_server_error,
|
23
|
+
title: 'Internal Server Error',
|
24
|
+
detail: 'An internal error ocurred while processing the request.'
|
25
|
+
)]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -2,72 +2,168 @@ module JSONAPI
|
|
2
2
|
module Utils
|
3
3
|
module Response
|
4
4
|
module Formatters
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
5
|
+
# Helper method to format ActiveRecord or Hash objects into JSON API-compliant ones.
|
6
|
+
#
|
7
|
+
# @note The return of this method represents what will actually be displayed in the response body.
|
8
|
+
# @note It can also be called as #jsonapi_serialize due to backward compatibility issues.
|
9
|
+
#
|
10
|
+
# @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
|
11
|
+
# Object to be formatted into JSON
|
12
|
+
# e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } },
|
13
|
+
# [{ data: { id: 1, first_name: 'Tiago' } }]
|
14
|
+
#
|
15
|
+
# @option options [JSONAPI::Resource] resource: it tells the formatter which resource
|
16
|
+
# class to be used rather than use an infered one (default behaviour)
|
17
|
+
#
|
18
|
+
# @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated
|
19
|
+
# when a Hash or Array of Hashes is passed as the "object" argument
|
20
|
+
#
|
21
|
+
# @option options [Integer] count: if it's rendering a collection of resources, the default
|
22
|
+
# gem's counting method can be bypassed by the use of this options. It's shows then the total
|
23
|
+
# records resulting from that request and also calculates the pagination.
|
24
|
+
#
|
25
|
+
# @return [Hash]
|
26
|
+
#
|
27
|
+
# @api public
|
28
|
+
def jsonapi_format(object, options = {})
|
29
|
+
if object.is_a?(Hash)
|
30
|
+
hash = object.with_indifferent_access
|
31
|
+
object = hash_to_active_record(hash[:data], options[:model])
|
9
32
|
end
|
10
|
-
|
11
|
-
build_response_document(
|
33
|
+
fix_custom_request_options(object)
|
34
|
+
build_response_document(object, options).contents
|
12
35
|
end
|
13
36
|
|
14
37
|
alias_method :jsonapi_serialize, :jsonapi_format
|
15
38
|
|
16
|
-
|
17
|
-
|
18
|
-
|
39
|
+
# Helper method to format ActiveRecord or any object that responds to #errors
|
40
|
+
# into JSON API-compliant error response bodies.
|
41
|
+
#
|
42
|
+
# @note The return of this method represents what will actually be displayed in the response body.
|
43
|
+
# @note It can also be called as #jsonapi_serialize_errors due to backward compatibility issues.
|
44
|
+
#
|
45
|
+
# @param object [ActiveRecord::Base or any object that responds to #errors]
|
46
|
+
# Error object to be serialized into JSON
|
47
|
+
# e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object)
|
48
|
+
#
|
49
|
+
# @return [Array]
|
50
|
+
#
|
51
|
+
# @api public
|
52
|
+
def jsonapi_format_errors(object)
|
53
|
+
if active_record_obj?(object)
|
54
|
+
object = JSONAPI::Utils::Exceptions::ActiveRecord.new(object, @request.resource_klass, context)
|
55
|
+
end
|
56
|
+
errors = object.respond_to?(:errors) ? object.errors : object
|
19
57
|
JSONAPI::Utils::Support::Error.sanitize(errors).uniq
|
20
58
|
end
|
21
59
|
|
60
|
+
alias_method :jsonapi_serialize_errors, :jsonapi_format_errors
|
61
|
+
|
22
62
|
private
|
23
63
|
|
24
|
-
|
25
|
-
|
64
|
+
# Check whether the given object is an ActiveRecord-like one.
|
65
|
+
#
|
66
|
+
# @param object [Object] Object to be checked
|
67
|
+
#
|
68
|
+
# @return [TrueClass, FalseClass]
|
69
|
+
#
|
70
|
+
# @api private
|
71
|
+
def active_record_obj?(object)
|
72
|
+
defined?(ActiveRecord::Base) &&
|
73
|
+
(object.is_a?(ActiveRecord::Base) ||
|
74
|
+
object.singleton_class.include?(ActiveModel::Model))
|
26
75
|
end
|
27
76
|
|
28
|
-
|
77
|
+
# Build the full response document.
|
78
|
+
#
|
79
|
+
# @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
|
80
|
+
# Object to be formatted into JSON
|
81
|
+
# e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } },
|
82
|
+
# [{ data: { id: 1, first_name: 'Tiago' } }]
|
83
|
+
#
|
84
|
+
# @option options [JSONAPI::Resource] resource: it tells the builder which resource
|
85
|
+
# class to be used rather than use an infered one (default behaviour)
|
86
|
+
#
|
87
|
+
# @option options [Integer] count: if it's rendering a collection of resources, the default
|
88
|
+
# gem's counting method can be bypassed by the use of this options. It's shows then the total
|
89
|
+
# records resulting from that request and also calculates the pagination.
|
90
|
+
#
|
91
|
+
# @return [JSONAPI::ResponseDocument]
|
92
|
+
#
|
93
|
+
# @api private
|
94
|
+
def build_response_document(object, options)
|
29
95
|
results = JSONAPI::OperationResults.new
|
30
96
|
|
31
|
-
if
|
32
|
-
|
33
|
-
results.add_result(JSONAPI::ResourcesOperationResult.new(:ok,
|
97
|
+
if object.respond_to?(:to_ary)
|
98
|
+
records = build_collection(object, options)
|
99
|
+
results.add_result(JSONAPI::ResourcesOperationResult.new(:ok, records, result_options(object, options)))
|
34
100
|
else
|
35
|
-
|
36
|
-
results.add_result(JSONAPI::ResourceOperationResult.new(:ok,
|
101
|
+
record = turn_into_resource(object, options)
|
102
|
+
results.add_result(JSONAPI::ResourceOperationResult.new(:ok, record))
|
37
103
|
end
|
38
104
|
|
39
105
|
@_response_document = create_response_document(results)
|
40
106
|
end
|
41
107
|
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
108
|
+
# Apply a proper action setup for custom requests/actions.
|
109
|
+
#
|
110
|
+
# @note The setup_(index|show)_action comes from JSONAPI::Resources' API.
|
111
|
+
#
|
112
|
+
# @param object [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
|
113
|
+
# It's checked whether this object refers to a collection or not.
|
114
|
+
#
|
115
|
+
# @api private
|
116
|
+
def fix_custom_request_options(object)
|
117
|
+
return unless custom_get_request_with_params?
|
118
|
+
action = object.respond_to?(:to_ary) ? 'index' : 'show'
|
47
119
|
@request.send("setup_#{action}_action", params)
|
48
120
|
end
|
49
121
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
if JSONAPI.configuration.top_level_meta_include_record_count
|
58
|
-
data[:record_count] = count_records(records, options)
|
59
|
-
end
|
60
|
-
end
|
122
|
+
# Check whether it's a custom GET request with params.
|
123
|
+
#
|
124
|
+
# @return [TrueClass, FalseClass]
|
125
|
+
#
|
126
|
+
# @api private
|
127
|
+
def custom_get_request_with_params?
|
128
|
+
request.method =~ /get/i && !%w(index show).include?(params[:action]) && !params.nil?
|
61
129
|
end
|
62
130
|
|
63
|
-
|
131
|
+
# Turn a collection of AR or Hash objects into a collection of JSONAPI::Resource ones.
|
132
|
+
#
|
133
|
+
# @param records [ActiveRecord::Relation, Hash, Array<Hash>]
|
134
|
+
# Objects to be instantiated as JSONAPI::Resource ones.
|
135
|
+
# e.g.: User.all, [{ data: { id: 1, first_name: 'Tiago' } }]
|
136
|
+
#
|
137
|
+
# @option options [JSONAPI::Resource] resource: it tells the buider which resource
|
138
|
+
# class to be used rather than use an infered one (default behaviour)
|
139
|
+
#
|
140
|
+
# @option options [Integer] count: if it's rendering a collection of resources, the default
|
141
|
+
# gem's counting method can be bypassed by the use of this options. It's shows then the total
|
142
|
+
# records resulting from that request and also calculates the pagination.
|
143
|
+
#
|
144
|
+
# @return [Array]
|
145
|
+
#
|
146
|
+
# @api private
|
147
|
+
def build_collection(records, options)
|
64
148
|
records = apply_filter(records, options)
|
65
|
-
records = apply_pagination(records, options)
|
66
149
|
records = apply_sort(records)
|
150
|
+
records = apply_pagination(records, options)
|
67
151
|
records.respond_to?(:to_ary) ? records.map { |record| turn_into_resource(record, options) } : []
|
68
152
|
end
|
69
153
|
|
70
|
-
|
154
|
+
# Turn an AR or Hash object into a JSONAPI::Resource one.
|
155
|
+
#
|
156
|
+
# @param records [ActiveRecord::Relation, Hash, Array<Hash>]
|
157
|
+
# Object to be instantiated as a JSONAPI::Resource one.
|
158
|
+
# e.g.: User.first, { data: { id: 1, first_name: 'Tiago' } }
|
159
|
+
#
|
160
|
+
# @option options [JSONAPI::Resource] resource: it tells which resource
|
161
|
+
# class to be used rather than use an infered one (default behaviour)
|
162
|
+
#
|
163
|
+
# @return [JSONAPI::Resource]
|
164
|
+
#
|
165
|
+
# @api private
|
166
|
+
def turn_into_resource(record, options)
|
71
167
|
if options[:resource]
|
72
168
|
options[:resource].to_s.constantize.new(record, context)
|
73
169
|
else
|
@@ -75,6 +171,44 @@ module JSONAPI
|
|
75
171
|
end
|
76
172
|
end
|
77
173
|
|
174
|
+
# Apply some result options like pagination params and count to a collection response.
|
175
|
+
#
|
176
|
+
# @param records [ActiveRecord::Relation, Hash, Array<Hash>]
|
177
|
+
# Object to be formatted into JSON
|
178
|
+
# e.g.: User.all, [{ data: { id: 1, first_name: 'Tiago' } }]
|
179
|
+
#
|
180
|
+
# @option options [Integer] count: if it's rendering a collection of resources, the default
|
181
|
+
# gem's counting method can be bypassed by the use of this options. It's shows then the total
|
182
|
+
# records resulting from that request and also calculates the pagination.
|
183
|
+
#
|
184
|
+
# @return [Hash]
|
185
|
+
#
|
186
|
+
# @api private
|
187
|
+
def result_options(records, options)
|
188
|
+
{}.tap do |data|
|
189
|
+
if JSONAPI.configuration.default_paginator != :none &&
|
190
|
+
JSONAPI.configuration.top_level_links_include_pagination
|
191
|
+
data[:pagination_params] = pagination_params(records, options)
|
192
|
+
end
|
193
|
+
|
194
|
+
if JSONAPI.configuration.top_level_meta_include_record_count
|
195
|
+
data[:record_count] = count_records(records, options)
|
196
|
+
end
|
197
|
+
end
|
198
|
+
end
|
199
|
+
|
200
|
+
# Convert Hash or collection of Hashes into AR objects.
|
201
|
+
#
|
202
|
+
# @param data [Hash, Array<Hash>] Hash or collection to be converted
|
203
|
+
# e.g.: { data: { id: 1, first_name: 'Tiago' } },
|
204
|
+
# [{ data: { id: 1, first_name: 'Tiago' } }],
|
205
|
+
#
|
206
|
+
# @option options [ActiveRecord::Base] model: ActiveRecord model class to be
|
207
|
+
# used as base for the objects' intantialization.
|
208
|
+
#
|
209
|
+
# @return [ActiveRecord::Base, ActiveRecord::Relation]
|
210
|
+
#
|
211
|
+
# @api private
|
78
212
|
def hash_to_active_record(data, model)
|
79
213
|
return data if model.nil?
|
80
214
|
coerced = [data].flatten.map { |hash| model.new(hash) }
|
@@ -2,36 +2,87 @@ module JSONAPI
|
|
2
2
|
module Utils
|
3
3
|
module Response
|
4
4
|
module Renders
|
5
|
+
# Helper method to render JSON API-compliant responses.
|
6
|
+
#
|
7
|
+
# @param json [ActiveRecord::Base, ActiveRecord::Relation, Hash, Array<Hash>]
|
8
|
+
# Object to be serialized into JSON
|
9
|
+
# e.g.: User.first, User.all, { data: { id: 1, first_name: 'Tiago' } },
|
10
|
+
# [{ data: { id: 1, first_name: 'Tiago' } }]
|
11
|
+
#
|
12
|
+
# @param status [Integer, String, Symbol] HTTP status code
|
13
|
+
# e.g.: 201, '201', :created
|
14
|
+
#
|
15
|
+
# @option options [JSONAPI::Resource] resource: it tells the render which resource
|
16
|
+
# class to be used rather than use an infered one (default behaviour)
|
17
|
+
#
|
18
|
+
# @option options [ActiveRecord::Base] model: ActiveRecord model class to be instantiated
|
19
|
+
# when a Hash or Array of Hashes is passed to the "json" key argument
|
20
|
+
#
|
21
|
+
# @option options [Integer] count: if it's rendering a collection of resources, the default
|
22
|
+
# gem's counting method can be bypassed by the use of this options. It's shows then the total
|
23
|
+
# records resulting from that request and also calculates the pagination.
|
24
|
+
#
|
25
|
+
# @return [String]
|
26
|
+
#
|
27
|
+
# @api public
|
5
28
|
def jsonapi_render(json:, status: nil, options: {})
|
6
29
|
body = jsonapi_format(json, options)
|
7
|
-
render json: body, status: status || @_response_document.status
|
30
|
+
render json: body, status: (status || @_response_document.status)
|
8
31
|
rescue => e
|
9
|
-
handle_exceptions(e)
|
32
|
+
handle_exceptions(e) # http://bit.ly/2sEEGTN
|
10
33
|
ensure
|
11
34
|
correct_media_type
|
12
35
|
end
|
13
36
|
|
14
|
-
|
15
|
-
|
16
|
-
|
37
|
+
# Helper method to render JSON API-compliant error responses.
|
38
|
+
#
|
39
|
+
# @param error [ActiveRecord::Base or any object that responds to #errors]
|
40
|
+
# Error object to be serialized into JSON
|
41
|
+
# e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object)
|
42
|
+
#
|
43
|
+
# @param json [ActiveRecord::Base or any object that responds to #errors]
|
44
|
+
# Error object to be serialized into JSON
|
45
|
+
# e.g.: User.new(name: nil).tap(&:save), MyErrorDecorator.new(invalid_object)
|
46
|
+
#
|
47
|
+
# @param status [Integer, String, Symbol] HTTP status code
|
48
|
+
# e.g.: 422, '422', :unprocessable_entity
|
49
|
+
#
|
50
|
+
# @return [String]
|
51
|
+
#
|
52
|
+
# @api public
|
53
|
+
def jsonapi_render_errors(error = nil, json: nil, status: nil)
|
54
|
+
body = jsonapi_format_errors(error || json)
|
55
|
+
status = status || body.try(:first).try(:[], :status) || :bad_request
|
17
56
|
render json: { errors: body }, status: status
|
18
57
|
ensure
|
19
58
|
correct_media_type
|
20
59
|
end
|
21
60
|
|
61
|
+
# Helper method to render HTTP 500 Interval Server Error.
|
62
|
+
#
|
63
|
+
# @api public
|
22
64
|
def jsonapi_render_internal_server_error
|
23
65
|
jsonapi_render_errors(::JSONAPI::Utils::Exceptions::InternalServerError.new)
|
24
66
|
end
|
25
67
|
|
68
|
+
# Helper method to render HTTP 400 Bad Request.
|
69
|
+
#
|
70
|
+
# @api public
|
26
71
|
def jsonapi_render_bad_request
|
27
72
|
jsonapi_render_errors(::JSONAPI::Utils::Exceptions::BadRequest.new)
|
28
73
|
end
|
29
74
|
|
75
|
+
# Helper method to render HTTP 404 Bad Request.
|
76
|
+
#
|
77
|
+
# @api public
|
30
78
|
def jsonapi_render_not_found(exception)
|
31
|
-
id = exception.message
|
79
|
+
id = exception.message =~ /=([\w-]+)/ && $1 || '(no identifier)'
|
32
80
|
jsonapi_render_errors(JSONAPI::Exceptions::RecordNotFound.new(id))
|
33
81
|
end
|
34
82
|
|
83
|
+
# Helper method to render HTTP 404 Bad Request with null "data".
|
84
|
+
#
|
85
|
+
# @api public
|
35
86
|
def jsonapi_render_not_found_with_null
|
36
87
|
render json: { data: nil }, status: 200
|
37
88
|
end
|
@@ -43,9 +43,9 @@ module JSONAPI::Utils::Support
|
|
43
43
|
@_sort_params ||=
|
44
44
|
if params[:sort].present?
|
45
45
|
params[:sort].split(',').each_with_object({}) do |field, hash|
|
46
|
-
unformatted_field
|
47
|
-
desc, field
|
48
|
-
hash[field]
|
46
|
+
unformatted_field = @request.unformat_key(field)
|
47
|
+
desc, field = unformatted_field.to_s.match(/^([-_])?(\w+)$/i)[1..2]
|
48
|
+
hash[field.to_sym] = desc.present? ? :desc : :asc
|
49
49
|
end
|
50
50
|
end
|
51
51
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi-utils
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Tiago Guedes
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: exe
|
11
11
|
cert_chain: []
|
12
|
-
date: 2017-
|
12
|
+
date: 2017-07-16 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: jsonapi-resources
|
@@ -54,33 +54,33 @@ dependencies:
|
|
54
54
|
- !ruby/object:Gem::Version
|
55
55
|
version: '10.0'
|
56
56
|
- !ruby/object:Gem::Dependency
|
57
|
-
name:
|
57
|
+
name: rails
|
58
58
|
requirement: !ruby/object:Gem::Requirement
|
59
59
|
requirements:
|
60
|
-
- - "
|
60
|
+
- - "~>"
|
61
61
|
- !ruby/object:Gem::Version
|
62
|
-
version: '
|
62
|
+
version: '5.1'
|
63
63
|
type: :development
|
64
64
|
prerelease: false
|
65
65
|
version_requirements: !ruby/object:Gem::Requirement
|
66
66
|
requirements:
|
67
|
-
- - "
|
67
|
+
- - "~>"
|
68
68
|
- !ruby/object:Gem::Version
|
69
|
-
version: '
|
69
|
+
version: '5.1'
|
70
70
|
- !ruby/object:Gem::Dependency
|
71
|
-
name:
|
71
|
+
name: sqlite3
|
72
72
|
requirement: !ruby/object:Gem::Requirement
|
73
73
|
requirements:
|
74
|
-
- -
|
74
|
+
- - ">="
|
75
75
|
- !ruby/object:Gem::Version
|
76
|
-
version:
|
76
|
+
version: '0'
|
77
77
|
type: :development
|
78
78
|
prerelease: false
|
79
79
|
version_requirements: !ruby/object:Gem::Requirement
|
80
80
|
requirements:
|
81
|
-
- -
|
81
|
+
- - ">="
|
82
82
|
- !ruby/object:Gem::Version
|
83
|
-
version:
|
83
|
+
version: '0'
|
84
84
|
- !ruby/object:Gem::Dependency
|
85
85
|
name: rspec-rails
|
86
86
|
requirement: !ruby/object:Gem::Requirement
|
@@ -115,14 +115,14 @@ dependencies:
|
|
115
115
|
requirements:
|
116
116
|
- - "~>"
|
117
117
|
- !ruby/object:Gem::Version
|
118
|
-
version: 0.1.
|
118
|
+
version: 0.1.6
|
119
119
|
type: :development
|
120
120
|
prerelease: false
|
121
121
|
version_requirements: !ruby/object:Gem::Requirement
|
122
122
|
requirements:
|
123
123
|
- - "~>"
|
124
124
|
- !ruby/object:Gem::Version
|
125
|
-
version: 0.1.
|
125
|
+
version: 0.1.6
|
126
126
|
- !ruby/object:Gem::Dependency
|
127
127
|
name: pry
|
128
128
|
requirement: !ruby/object:Gem::Requirement
|
@@ -167,6 +167,8 @@ files:
|
|
167
167
|
- bin/setup
|
168
168
|
- lib/jsonapi/utils.rb
|
169
169
|
- lib/jsonapi/utils/exceptions.rb
|
170
|
+
- lib/jsonapi/utils/exceptions/active_record.rb
|
171
|
+
- lib/jsonapi/utils/exceptions/internal_server_error.rb
|
170
172
|
- lib/jsonapi/utils/request.rb
|
171
173
|
- lib/jsonapi/utils/response.rb
|
172
174
|
- lib/jsonapi/utils/response/formatters.rb
|