jsonapi_parameters 1.0.0 → 2.2.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 +48 -214
- data/lib/jsonapi_parameters.rb +2 -2
- data/lib/jsonapi_parameters/core_ext/action_controller/parameters.rb +2 -2
- data/lib/jsonapi_parameters/default_handlers/base_handler.rb +30 -0
- data/lib/jsonapi_parameters/default_handlers/nil_relation_handler.rb +29 -0
- data/lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb +55 -0
- data/lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb +30 -0
- data/lib/jsonapi_parameters/handlers.rb +46 -0
- data/lib/jsonapi_parameters/stack_limit.rb +45 -0
- data/lib/jsonapi_parameters/translator.rb +57 -84
- data/lib/jsonapi_parameters/version.rb +1 -1
- metadata +9 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 8bcf0cd62169a18d8f485540f7fe27e85d2ed8278121fe19309d149b5c59141a
|
4
|
+
data.tar.gz: eef89bf8b7061bf21add79bd09ce3b164c6b0d91e0d8db2898de99e231ab7d36
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fbb1868c672869c4e0b561012dba89ed444271bef9246be22df68b564e397e21d1058860ce5416748c5a662f960bbe5c424db56882e2163bc8ae6516861de00e
|
7
|
+
data.tar.gz: '0940555db68b3b48522dbb33d6f035334ff8b149731b71b597df5e148b0d0d900141dd5213202c76eac4a406b2721c273773a401d90ae94caf44ae90b7a0c34c'
|
data/README.md
CHANGED
@@ -1,17 +1,12 @@
|
|
1
1
|
# JsonApi::Parameters
|
2
|
-
Simple JSON:API compliant parameters translator.
|
2
|
+
Simple [JSON:API](https://jsonapi.org/) compliant parameters translator.
|
3
3
|
|
4
4
|
[](https://badge.fury.io/rb/jsonapi_parameters)
|
5
5
|
[](https://codeclimate.com/github/visualitypl/jsonapi_parameters/maintainability)
|
6
6
|
[](https://codeclimate.com/github/visualitypl/jsonapi_parameters/test_coverage)
|
7
|
+
[](https://circleci.com/gh/visualitypl/jsonapi_parameters)
|
7
8
|
|
8
|
-
|
9
|
-
|
10
|
-
JSON:API standard specifies not only responses (that can be handled nicely, using gems like [fast_jsonapi from Netflix](https://github.com/Netflix/fast_jsonapi)), but also the request structure.
|
11
|
-
|
12
|
-
#### The solution
|
13
|
-
|
14
|
-
As we couldn't find any gem that would make it easier in Rails to use these structures, we decided to create something that will work for us - a translator that transforms JSON:API compliant request parameter strucure into a Railsy structure.
|
9
|
+
[Documentation](https://github.com/visualitypl/jsonapi_parameters/wiki)
|
15
10
|
|
16
11
|
## Usage
|
17
12
|
|
@@ -75,243 +70,82 @@ Relationship parameters are being read from two optional trees:
|
|
75
70
|
|
76
71
|
If you provide any related resources in the `relationships` table, this gem will also look for corresponding, `included` resources and their attributes. Thanks to that this gem supports nested attributes, and will try to translate these included resources and pass them along.
|
77
72
|
|
78
|
-
|
73
|
+
For more examples take a look at [Relationships](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationships) in the wiki documentation.
|
79
74
|
|
80
|
-
Passing a resource that is a single entity in relationships tree will make JsonApi::Parameters assume that it is a `belongs_to` relationship.
|
81
75
|
|
82
|
-
|
83
|
-
Example:
|
84
|
-
|
85
|
-
```
|
86
|
-
class Movie < ActiveRecord::Model
|
87
|
-
belongs_to :director
|
88
|
-
end
|
89
|
-
```
|
76
|
+
### Plain Ruby / outside Rails
|
90
77
|
|
91
|
-
|
78
|
+
```ruby
|
92
79
|
|
93
|
-
|
94
|
-
|
95
|
-
data: {
|
96
|
-
type: 'movies',
|
97
|
-
attributes: {
|
98
|
-
title: 'The Terminator',
|
99
|
-
},
|
100
|
-
relationships: {
|
101
|
-
director: {
|
102
|
-
data: {
|
103
|
-
id: 682, type: 'directors'
|
104
|
-
}
|
105
|
-
}
|
106
|
-
}
|
107
|
-
}
|
80
|
+
params = { # JSON:API compliant parameters here
|
81
|
+
# ...
|
108
82
|
}
|
109
|
-
```
|
110
83
|
|
111
|
-
|
84
|
+
class Translator
|
85
|
+
include JsonApi::Parameters
|
86
|
+
end
|
87
|
+
translator = Translator.new
|
112
88
|
|
89
|
+
translator.jsonapify(params)
|
113
90
|
```
|
114
|
-
|
115
|
-
|
116
|
-
title: 'The Terminator',
|
117
|
-
director_id: 682
|
118
|
-
}
|
119
|
-
}
|
120
|
-
```
|
121
|
-
|
122
|
-
|
123
|
-
###### With included entity:
|
124
|
-
Example:
|
91
|
+
|
92
|
+
## Mime Type
|
125
93
|
|
126
|
-
|
127
|
-
class Movie < ActiveRecord::Model
|
128
|
-
belongs_to :director
|
129
|
-
|
130
|
-
accepts_nested_attributes_for :director
|
131
|
-
end
|
132
|
-
```
|
94
|
+
As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) correct mime type for JSON:API input should be [`application/vnd.api+json`](http://www.iana.org/assignments/media-types/application/vnd.api+json).
|
133
95
|
|
134
|
-
|
135
|
-
```
|
136
|
-
{
|
137
|
-
data: {
|
138
|
-
type: 'movies',
|
139
|
-
attributes: {
|
140
|
-
title: 'The Terminator',
|
141
|
-
},
|
142
|
-
relationships: {
|
143
|
-
director: {
|
144
|
-
data: {
|
145
|
-
id: 682, type: 'directors'
|
146
|
-
}
|
147
|
-
}
|
148
|
-
}
|
149
|
-
},
|
150
|
-
included: [
|
151
|
-
{
|
152
|
-
type: 'directors',
|
153
|
-
id: 682,
|
154
|
-
attributes: {
|
155
|
-
name: 'Some guy'
|
156
|
-
}
|
157
|
-
}
|
158
|
-
]
|
159
|
-
}
|
160
|
-
```
|
96
|
+
This gem's intention is to make input consumption as easy as possible. Hence, it [registers this mime type for you](lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb).
|
161
97
|
|
162
|
-
|
163
|
-
```
|
164
|
-
{
|
165
|
-
movie: {
|
166
|
-
title: 'The Terminator',
|
167
|
-
director_attributes: { id: 682, name: 'Some guy' }
|
168
|
-
}
|
169
|
-
}
|
170
|
-
```
|
98
|
+
## Stack limit
|
171
99
|
|
172
|
-
|
100
|
+
In theory, any payload may consist of infinite amount of relationships (and so each relationship may have its own, included, infinite amount of nested relationships).
|
101
|
+
Because of that, it is a potential vector of attack.
|
173
102
|
|
174
|
-
|
103
|
+
For this reason we have introduced a default limit of stack levels that JsonApi::Parameters will go down through while parsing the payloads.
|
175
104
|
|
176
|
-
|
177
|
-
Example:
|
105
|
+
This default limit is 3, and can be overwritten by specifying the custom limit.
|
178
106
|
|
179
|
-
|
180
|
-
|
181
|
-
|
107
|
+
#### Ruby
|
108
|
+
```
|
109
|
+
class Translator
|
110
|
+
include JsonApi::Parameters
|
182
111
|
end
|
183
|
-
```
|
184
112
|
|
185
|
-
|
113
|
+
translator = Translator.new
|
186
114
|
|
187
|
-
|
188
|
-
{
|
189
|
-
data: {
|
190
|
-
type: 'movies',
|
191
|
-
attributes: {
|
192
|
-
title: 'The Terminator',
|
193
|
-
},
|
194
|
-
relationships: {
|
195
|
-
genres: {
|
196
|
-
data: [{
|
197
|
-
id: 1, type: 'genres'
|
198
|
-
},
|
199
|
-
{
|
200
|
-
id: 2, type: 'genres'
|
201
|
-
}]
|
202
|
-
}
|
203
|
-
}
|
204
|
-
}
|
205
|
-
}
|
206
|
-
```
|
115
|
+
translator.jsonapify(custom_stack_limit: 4)
|
207
116
|
|
208
|
-
|
117
|
+
# OR
|
118
|
+
|
119
|
+
translator.stack_limit = 4
|
120
|
+
translator.jsonapify.(...)
|
121
|
+
```
|
209
122
|
|
123
|
+
#### Rails
|
210
124
|
```
|
211
|
-
|
212
|
-
movie: {
|
213
|
-
title: 'The Terminator',
|
214
|
-
genre_ids: [1, 2]
|
215
|
-
}
|
216
|
-
}
|
217
|
-
```
|
125
|
+
# config/initializers/jsonapi_parameters.rb
|
218
126
|
|
219
|
-
|
220
|
-
|
221
|
-
|
222
|
-
|
223
|
-
```
|
224
|
-
class Movie < ActiveRecord::Model
|
225
|
-
has_many :genres
|
226
|
-
|
227
|
-
accepts_nested_attributes_for :genres
|
127
|
+
def create_params
|
128
|
+
params.from_jsonapi(custom_stack_limit: 4).require(:user).permit(
|
129
|
+
entities_attributes: { subentities_attributes: { ... } }
|
130
|
+
)
|
228
131
|
end
|
229
|
-
```
|
230
132
|
|
231
|
-
|
232
|
-
```
|
233
|
-
{
|
234
|
-
data: {
|
235
|
-
type: 'movies',
|
236
|
-
attributes: {
|
237
|
-
title: 'The Terminator',
|
238
|
-
},
|
239
|
-
relationships: {
|
240
|
-
genres: {
|
241
|
-
data: [{
|
242
|
-
id: 1, type: 'genres'
|
243
|
-
}]
|
244
|
-
}
|
245
|
-
}
|
246
|
-
},
|
247
|
-
included: [
|
248
|
-
{
|
249
|
-
type: 'genres',
|
250
|
-
id: 1,
|
251
|
-
attributes: {
|
252
|
-
name: 'Genre one'
|
253
|
-
}
|
254
|
-
}
|
255
|
-
]
|
256
|
-
}
|
257
|
-
```
|
258
|
-
|
259
|
-
Will translate to:
|
260
|
-
```
|
261
|
-
{
|
262
|
-
movie: {
|
263
|
-
title: 'The Terminator',
|
264
|
-
genres_attributes: [{ id: 1, name: 'Genre one' }]
|
265
|
-
}
|
266
|
-
}
|
267
|
-
```
|
268
|
-
|
269
|
-
|
270
|
-
#### Casing
|
271
|
-
|
272
|
-
If the input is in a different convention than `:snake`, you should specify that.
|
273
|
-
|
274
|
-
You can do it in two ways:
|
275
|
-
* in an initializer, simply create `initializers/jsonapi_parameters.rb` with contents similar to:
|
276
|
-
```ruby
|
277
|
-
# config/initializers/jsonapi_parameters.rb
|
278
|
-
|
279
|
-
JsonApi::Parameters.ensure_underscore_translation = true
|
280
|
-
|
281
|
-
```
|
133
|
+
# OR
|
282
134
|
|
283
|
-
|
284
|
-
|
285
|
-
### Plain Ruby / outside Rails
|
286
|
-
|
287
|
-
```ruby
|
288
|
-
|
289
|
-
params = { # JSON:API compliant parameters here
|
290
|
-
# ...
|
291
|
-
}
|
135
|
+
def create_params
|
136
|
+
params.stack_level = 4
|
292
137
|
|
293
|
-
|
294
|
-
|
138
|
+
params.from_jsonapi.require(:user).permit(entities_attributes: { subentities_attributes: { ... } })
|
139
|
+
ensure
|
140
|
+
params.reset_stack_limit!
|
295
141
|
end
|
296
|
-
translator = Translator.new
|
297
|
-
|
298
|
-
translator.jsonapify(params)
|
299
142
|
```
|
300
143
|
|
301
|
-
|
302
|
-
|
303
|
-
If the input is in a different convention than `:snake`, you should specify that.
|
304
|
-
|
305
|
-
You can do it in two ways:
|
306
|
-
|
307
|
-
* by a global setting: `JsonApi::Parameters.ensure_underscore_translation = true`
|
308
|
-
* while calling `.jsonapify`, for instance: `.jsonapify(params, naming_convention: :camel)`. **The value does not really matter, as anything different than `:snake` will result in deep keys transformation provided by [ActiveSupport](https://apidock.com/rails/v4.1.8/Hash/deep_transform_keys).**
|
309
|
-
|
310
|
-
## Mime Type
|
144
|
+
## Customization
|
311
145
|
|
312
|
-
|
146
|
+
If you need custom relationship handling (for instance, if you have a relationship named `scissors` that is plural, but it actually is a single entity), you can use Handlers to define appropriate behaviour.
|
313
147
|
|
314
|
-
|
148
|
+
Read more at [Relationship Handlers](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationship-handlers).
|
315
149
|
|
316
150
|
## License
|
317
151
|
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/lib/jsonapi_parameters.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'jsonapi_parameters/parameters'
|
2
|
+
require 'jsonapi_parameters/handlers'
|
2
3
|
require 'jsonapi_parameters/translator'
|
3
4
|
require 'jsonapi_parameters/core_ext'
|
5
|
+
require 'jsonapi_parameters/stack_limit'
|
4
6
|
require 'jsonapi_parameters/version'
|
5
|
-
|
6
|
-
require 'active_support/inflector'
|
@@ -9,7 +9,7 @@ class ActionController::Parameters
|
|
9
9
|
from_jsonapi(*args)
|
10
10
|
end
|
11
11
|
|
12
|
-
def from_jsonapi(naming_convention = :snake)
|
13
|
-
@from_jsonapi ||= self.class.new jsonapify(self, naming_convention: naming_convention)
|
12
|
+
def from_jsonapi(naming_convention = :snake, custom_stack_limit: stack_limit)
|
13
|
+
@from_jsonapi ||= self.class.new jsonapify(self, naming_convention: naming_convention, custom_stack_limit: custom_stack_limit)
|
14
14
|
end
|
15
15
|
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
module JsonApi
|
2
|
+
module Parameters
|
3
|
+
module Handlers
|
4
|
+
module DefaultHandlers
|
5
|
+
class BaseHandler
|
6
|
+
attr_reader :relationship_key, :relationship_value, :included
|
7
|
+
|
8
|
+
def self.call(key, val, included)
|
9
|
+
new(key, val, included).handle
|
10
|
+
end
|
11
|
+
|
12
|
+
def initialize(relationship_key, relationship_value, included)
|
13
|
+
@relationship_key = relationship_key
|
14
|
+
@relationship_value = relationship_value
|
15
|
+
@included = included
|
16
|
+
end
|
17
|
+
|
18
|
+
def find_included_object(related_id:, related_type:)
|
19
|
+
included.find do |included_object_enum|
|
20
|
+
included_object_enum[:id] &&
|
21
|
+
included_object_enum[:id] == related_id &&
|
22
|
+
included_object_enum[:type] &&
|
23
|
+
included_object_enum[:type] == related_type
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
require_relative './base_handler'
|
4
|
+
|
5
|
+
module JsonApi
|
6
|
+
module Parameters
|
7
|
+
module Handlers
|
8
|
+
module DefaultHandlers
|
9
|
+
class NilRelationHandler < BaseHandler
|
10
|
+
include ActiveSupport::Inflector
|
11
|
+
|
12
|
+
def handle
|
13
|
+
# Graceful fail if nil on to-many association
|
14
|
+
# in case the relationship key is, for instance, `billable_hours`,
|
15
|
+
# we have to assume that it is a to-many relationship.
|
16
|
+
if pluralize(relationship_key).to_sym == relationship_key
|
17
|
+
raise NotImplementedError.new(
|
18
|
+
'plural resource cannot be nullified - please create a custom handler for this relation'
|
19
|
+
)
|
20
|
+
end
|
21
|
+
|
22
|
+
# Handle with empty hash.
|
23
|
+
ToOneRelationHandler.new(relationship_key, {}, {}).handle
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module JsonApi
|
4
|
+
module Parameters
|
5
|
+
module Handlers
|
6
|
+
module DefaultHandlers
|
7
|
+
class ToManyRelationHandler < BaseHandler
|
8
|
+
include ActiveSupport::Inflector
|
9
|
+
|
10
|
+
attr_reader :with_inclusion, :vals, :key
|
11
|
+
|
12
|
+
def handle
|
13
|
+
@with_inclusion = !relationship_value.empty?
|
14
|
+
|
15
|
+
prepare_relationship_vals
|
16
|
+
|
17
|
+
generate_key
|
18
|
+
|
19
|
+
[key, vals]
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def prepare_relationship_vals
|
25
|
+
@vals = relationship_value.map do |relationship|
|
26
|
+
related_id = relationship.dig(:id)
|
27
|
+
related_type = relationship.dig(:type)
|
28
|
+
|
29
|
+
included_object = find_included_object(
|
30
|
+
related_id: related_id, related_type: related_type
|
31
|
+
) || {}
|
32
|
+
|
33
|
+
# If at least one related object has not been found in `included` tree,
|
34
|
+
# we should not attempt to "#{relationship_key}_attributes" but
|
35
|
+
# "#{relationship_key}_ids" instead.
|
36
|
+
@with_inclusion &= !included_object.empty?
|
37
|
+
|
38
|
+
if with_inclusion
|
39
|
+
{ **(included_object[:attributes] || {}), id: related_id }.tap do |body|
|
40
|
+
body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships
|
41
|
+
end
|
42
|
+
else
|
43
|
+
relationship.dig(:id)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def generate_key
|
49
|
+
@key = (with_inclusion ? "#{pluralize(relationship_key)}_attributes" : "#{singularize(relationship_key)}_ids").to_sym
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'active_support/inflector'
|
2
|
+
|
3
|
+
module JsonApi
|
4
|
+
module Parameters
|
5
|
+
module Handlers
|
6
|
+
module DefaultHandlers
|
7
|
+
class ToOneRelationHandler < BaseHandler
|
8
|
+
include ActiveSupport::Inflector
|
9
|
+
|
10
|
+
def handle
|
11
|
+
related_id = relationship_value.dig(:id)
|
12
|
+
related_type = relationship_value.dig(:type)
|
13
|
+
|
14
|
+
included_object = find_included_object(
|
15
|
+
related_id: related_id, related_type: related_type
|
16
|
+
) || {}
|
17
|
+
|
18
|
+
return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty?
|
19
|
+
|
20
|
+
included_object = { **(included_object[:attributes] || {}), id: related_id }.tap do |body|
|
21
|
+
body[:relationships] = included_object[:relationships] if included_object.key?(:relationships) # Pass nested relationships
|
22
|
+
end
|
23
|
+
|
24
|
+
["#{singularize(relationship_key)}_attributes".to_sym, included_object]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require_relative 'default_handlers/nil_relation_handler'
|
2
|
+
require_relative 'default_handlers/to_many_relation_handler'
|
3
|
+
require_relative 'default_handlers/to_one_relation_handler'
|
4
|
+
|
5
|
+
module JsonApi
|
6
|
+
module Parameters
|
7
|
+
module Handlers
|
8
|
+
include DefaultHandlers
|
9
|
+
|
10
|
+
DEFAULT_HANDLER_SET = {
|
11
|
+
to_many: ToManyRelationHandler,
|
12
|
+
to_one: ToOneRelationHandler,
|
13
|
+
nil: NilRelationHandler
|
14
|
+
}.freeze
|
15
|
+
|
16
|
+
module_function
|
17
|
+
|
18
|
+
def add_handler(handler_name, klass)
|
19
|
+
handlers[handler_name.to_sym] = klass
|
20
|
+
end
|
21
|
+
|
22
|
+
def set_resource_handler(resource_key, handler_key)
|
23
|
+
unless handlers.key?(handler_key)
|
24
|
+
raise NotImplementedError.new(
|
25
|
+
'handler_key does not match any registered handlers'
|
26
|
+
)
|
27
|
+
end
|
28
|
+
|
29
|
+
resource_handlers[resource_key.to_sym] = handler_key.to_sym
|
30
|
+
end
|
31
|
+
|
32
|
+
def reset_handlers
|
33
|
+
@handlers = DEFAULT_HANDLER_SET.dup
|
34
|
+
@resource_handlers = {}
|
35
|
+
end
|
36
|
+
|
37
|
+
def resource_handlers
|
38
|
+
@resource_handlers ||= {}
|
39
|
+
end
|
40
|
+
|
41
|
+
def handlers
|
42
|
+
@handlers ||= DEFAULT_HANDLER_SET.dup
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,45 @@
|
|
1
|
+
module JsonApi
|
2
|
+
module Parameters
|
3
|
+
LIMIT = 3
|
4
|
+
|
5
|
+
class StackLevelTooDeepError < StandardError
|
6
|
+
end
|
7
|
+
|
8
|
+
def stack_limit=(val)
|
9
|
+
@stack_limit = val
|
10
|
+
end
|
11
|
+
|
12
|
+
def stack_limit
|
13
|
+
@stack_limit || LIMIT
|
14
|
+
end
|
15
|
+
|
16
|
+
def reset_stack_limit
|
17
|
+
@stack_limit = LIMIT
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
def initialize_stack(custom_stack_limit)
|
23
|
+
@current_stack_level = 0
|
24
|
+
@stack_limit = custom_stack_limit
|
25
|
+
end
|
26
|
+
|
27
|
+
def increment_stack_level!
|
28
|
+
@current_stack_level += 1
|
29
|
+
|
30
|
+
raise StackLevelTooDeepError.new(stack_exception_message) if @current_stack_level > stack_limit
|
31
|
+
end
|
32
|
+
|
33
|
+
def decrement_stack_level
|
34
|
+
@current_stack_level -= 1
|
35
|
+
end
|
36
|
+
|
37
|
+
def reset_stack_level
|
38
|
+
@current_stack_level = 0
|
39
|
+
end
|
40
|
+
|
41
|
+
def stack_exception_message
|
42
|
+
"Stack level of nested payload is too deep: #{@current_stack_level}/#{stack_limit}. Please see the documentation on how to overwrite the limit."
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
@@ -3,7 +3,9 @@ require 'active_support/inflector'
|
|
3
3
|
module JsonApi::Parameters
|
4
4
|
include ActiveSupport::Inflector
|
5
5
|
|
6
|
-
def jsonapify(params, naming_convention: :snake)
|
6
|
+
def jsonapify(params, naming_convention: :snake, custom_stack_limit: stack_limit)
|
7
|
+
initialize_stack(custom_stack_limit)
|
8
|
+
|
7
9
|
jsonapi_translate(params, naming_convention: naming_convention)
|
8
10
|
end
|
9
11
|
|
@@ -15,7 +17,9 @@ module JsonApi::Parameters
|
|
15
17
|
return params if params.nil? || params.empty?
|
16
18
|
|
17
19
|
@jsonapi_unsafe_hash = if naming_convention != :snake || JsonApi::Parameters.ensure_underscore_translation
|
18
|
-
params.deep_transform_keys { |key| key.to_s.underscore.to_sym }
|
20
|
+
params = params.deep_transform_keys { |key| key.to_s.underscore.to_sym }
|
21
|
+
params[:data][:type] = params[:data][:type].underscore if params.dig(:data, :type)
|
22
|
+
params
|
19
23
|
else
|
20
24
|
params.deep_symbolize_keys
|
21
25
|
end
|
@@ -36,20 +40,11 @@ module JsonApi::Parameters
|
|
36
40
|
def jsonapi_main_body
|
37
41
|
jsonapi_unsafe_params.tap do |param|
|
38
42
|
jsonapi_relationships.each do |relationship_key, relationship_value|
|
39
|
-
|
40
|
-
key, val = case relationship_value
|
41
|
-
when Array
|
42
|
-
handle_to_many_relation(relationship_key, relationship_value)
|
43
|
-
when Hash
|
44
|
-
handle_to_one_relation(relationship_key, relationship_value)
|
45
|
-
when nil
|
46
|
-
handle_nil_relation(relationship_key)
|
47
|
-
else
|
48
|
-
raise jsonapi_not_implemented_err
|
49
|
-
end
|
50
|
-
param[key] = val
|
43
|
+
param = handle_relationships(param, relationship_key, relationship_value)
|
51
44
|
end
|
52
45
|
end
|
46
|
+
ensure
|
47
|
+
reset_stack_level
|
53
48
|
end
|
54
49
|
|
55
50
|
def jsonapi_unsafe_params
|
@@ -60,85 +55,63 @@ module JsonApi::Parameters
|
|
60
55
|
end
|
61
56
|
end
|
62
57
|
|
63
|
-
def jsonapi_included
|
64
|
-
@jsonapi_included ||= @jsonapi_unsafe_hash[:included] || []
|
65
|
-
end
|
66
|
-
|
67
58
|
def jsonapi_relationships
|
68
59
|
@jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
|
69
60
|
end
|
70
61
|
|
71
|
-
def
|
72
|
-
|
73
|
-
|
74
|
-
vals = relationship_value.map do |relationship|
|
75
|
-
related_id = relationship.dig(:id)
|
76
|
-
related_type = relationship.dig(:type)
|
77
|
-
|
78
|
-
included_object = find_included_object(
|
79
|
-
related_id: related_id, related_type: related_type
|
80
|
-
) || {}
|
81
|
-
|
82
|
-
# If at least one related object has not been found in `included` tree,
|
83
|
-
# we should not attempt to "#{relationship_key}_attributes" but
|
84
|
-
# "#{relationship_key}_ids" instead.
|
85
|
-
with_inclusion &= !included_object.empty?
|
86
|
-
|
87
|
-
if with_inclusion
|
88
|
-
included_object.delete(:type)
|
89
|
-
included_object[:attributes].merge(id: related_id)
|
90
|
-
else
|
91
|
-
relationship.dig(:id)
|
92
|
-
end
|
93
|
-
end
|
94
|
-
|
95
|
-
# We may have smells in our value array as `with_inclusion` may have been changed at some point
|
96
|
-
# but not in the beginning.
|
97
|
-
# Because of that we should clear it and make sure the results are unified (e.g. array of ids)
|
98
|
-
unless with_inclusion
|
99
|
-
vals.map do |val|
|
100
|
-
val.dig(:attributes, :id) if val.is_a?(Hash)
|
101
|
-
end
|
102
|
-
end
|
103
|
-
|
104
|
-
key = with_inclusion ? "#{pluralize(relationship_key)}_attributes".to_sym : "#{singularize(relationship_key)}_ids".to_sym
|
105
|
-
|
106
|
-
[key, vals]
|
107
|
-
end
|
108
|
-
|
109
|
-
def handle_to_one_relation(relationship_key, relationship_value)
|
110
|
-
related_id = relationship_value.dig(:id)
|
111
|
-
related_type = relationship_value.dig(:type)
|
112
|
-
|
113
|
-
included_object = find_included_object(
|
114
|
-
related_id: related_id, related_type: related_type
|
115
|
-
) || {}
|
116
|
-
|
117
|
-
return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty?
|
118
|
-
|
119
|
-
included_object.delete(:type)
|
120
|
-
included_object = included_object[:attributes].merge(id: related_id)
|
121
|
-
["#{singularize(relationship_key)}_attributes".to_sym, included_object]
|
62
|
+
def jsonapi_included
|
63
|
+
@jsonapi_included ||= @jsonapi_unsafe_hash[:included] || []
|
122
64
|
end
|
123
65
|
|
124
|
-
def
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
66
|
+
def handle_relationships(param, relationship_key, relationship_value)
|
67
|
+
increment_stack_level!
|
68
|
+
|
69
|
+
relationship_value = relationship_value[:data]
|
70
|
+
handler_args = [relationship_key, relationship_value, jsonapi_included]
|
71
|
+
handler = if Handlers.resource_handlers.key?(relationship_key)
|
72
|
+
Handlers.handlers[Handlers.resource_handlers[relationship_key]]
|
73
|
+
else
|
74
|
+
case relationship_value
|
75
|
+
when Array
|
76
|
+
Handlers.handlers[:to_many]
|
77
|
+
when Hash
|
78
|
+
Handlers.handlers[:to_one]
|
79
|
+
when nil
|
80
|
+
Handlers.handlers[:nil]
|
81
|
+
else
|
82
|
+
raise NotImplementedError.new('relationship resource linkage has to be a type of Array, Hash or nil')
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
key, val = handler.call(*handler_args)
|
87
|
+
|
88
|
+
param[key] = handle_nested_relationships(val)
|
89
|
+
|
90
|
+
param
|
91
|
+
ensure
|
92
|
+
decrement_stack_level
|
130
93
|
end
|
131
94
|
|
132
|
-
def
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
95
|
+
def handle_nested_relationships(val)
|
96
|
+
# We can only consider Hash relationships (which imply to-one relationship) and Array relationships (which imply to-many).
|
97
|
+
# Each type has a different handling method, though in both cases we end up passing the nested relationship recursively to handle_relationship
|
98
|
+
# (and yes, this may go on indefinitely, basically we're going by the relationship levels, deeper and deeper)
|
99
|
+
case val
|
100
|
+
when Array
|
101
|
+
relationships_with_nested_relationships = val.select { |rel| rel.is_a?(Hash) && rel.dig(:relationships) }
|
102
|
+
relationships_with_nested_relationships.each do |relationship_with_nested_relationship|
|
103
|
+
relationship_with_nested_relationship.delete(:relationships).each do |rel_key, rel_val|
|
104
|
+
relationship_with_nested_relationship = handle_relationships(relationship_with_nested_relationship, rel_key, rel_val)
|
105
|
+
end
|
106
|
+
end
|
107
|
+
when Hash
|
108
|
+
if val.key?(:relationships)
|
109
|
+
val.delete(:relationships).each do |rel_key, rel_val|
|
110
|
+
val = handle_relationships(val, rel_key, rel_val)
|
111
|
+
end
|
112
|
+
end
|
138
113
|
end
|
139
|
-
end
|
140
114
|
|
141
|
-
|
142
|
-
NotImplementedError.new('relationship member must either be an Array or a Hash')
|
115
|
+
val
|
143
116
|
end
|
144
117
|
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jsonapi_parameters
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version:
|
4
|
+
version: 2.2.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Visuality
|
@@ -9,7 +9,7 @@ authors:
|
|
9
9
|
autorequire:
|
10
10
|
bindir: bin
|
11
11
|
cert_chain: []
|
12
|
-
date:
|
12
|
+
date: 2020-08-20 00:00:00.000000000 Z
|
13
13
|
dependencies:
|
14
14
|
- !ruby/object:Gem::Dependency
|
15
15
|
name: activesupport
|
@@ -321,7 +321,13 @@ files:
|
|
321
321
|
- lib/jsonapi_parameters/core_ext.rb
|
322
322
|
- lib/jsonapi_parameters/core_ext/action_controller/parameters.rb
|
323
323
|
- lib/jsonapi_parameters/core_ext/action_dispatch/http/mime_type.rb
|
324
|
+
- lib/jsonapi_parameters/default_handlers/base_handler.rb
|
325
|
+
- lib/jsonapi_parameters/default_handlers/nil_relation_handler.rb
|
326
|
+
- lib/jsonapi_parameters/default_handlers/to_many_relation_handler.rb
|
327
|
+
- lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb
|
328
|
+
- lib/jsonapi_parameters/handlers.rb
|
324
329
|
- lib/jsonapi_parameters/parameters.rb
|
330
|
+
- lib/jsonapi_parameters/stack_limit.rb
|
325
331
|
- lib/jsonapi_parameters/translator.rb
|
326
332
|
- lib/jsonapi_parameters/version.rb
|
327
333
|
homepage: https://github.com/visualitypl/jsonapi_parameters
|
@@ -343,7 +349,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
343
349
|
- !ruby/object:Gem::Version
|
344
350
|
version: '0'
|
345
351
|
requirements: []
|
346
|
-
rubygems_version: 3.0.
|
352
|
+
rubygems_version: 3.0.3
|
347
353
|
signing_key:
|
348
354
|
specification_version: 4
|
349
355
|
summary: Translator for JSON:API compliant parameters
|