jsonapi_parameters 1.0.0 → 2.2.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: daa7f46ec2ca9f23be7199bf5b8ee043a1b40317ed96070f9b0025a624328eeb
4
- data.tar.gz: 29b227bd32c0b86cb6b5e29c368bc4728a445c369f22852037b3ff6f8e929b07
3
+ metadata.gz: 8bcf0cd62169a18d8f485540f7fe27e85d2ed8278121fe19309d149b5c59141a
4
+ data.tar.gz: eef89bf8b7061bf21add79bd09ce3b164c6b0d91e0d8db2898de99e231ab7d36
5
5
  SHA512:
6
- metadata.gz: 30da56dbc5654aed7331e5db54f54da59414fdf747a97c75755ba184af9e813a9ef50f7f2251f4af418ca04d11b54d353d1d4ca7c1e12a638e2049fa7bcae972
7
- data.tar.gz: a67da822ec846764be2cc6a999a6a70655b7816b5be9b733770a2f4ec32246a39cf587260a2fdfe6fa6836fe0290cc0e08088b873a8649fb2e61ab1fab59d839
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
  [![Gem Version](https://badge.fury.io/rb/jsonapi_parameters.svg)](https://badge.fury.io/rb/jsonapi_parameters)
5
5
  [![Maintainability](https://api.codeclimate.com/v1/badges/84fd5b548eea8d7e18af/maintainability)](https://codeclimate.com/github/visualitypl/jsonapi_parameters/maintainability)
6
6
  [![Test Coverage](https://api.codeclimate.com/v1/badges/84fd5b548eea8d7e18af/test_coverage)](https://codeclimate.com/github/visualitypl/jsonapi_parameters/test_coverage)
7
+ [![CircleCI](https://circleci.com/gh/visualitypl/jsonapi_parameters.svg?style=svg)](https://circleci.com/gh/visualitypl/jsonapi_parameters)
7
8
 
8
- #### The problem
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
- ##### belongs_to
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
- ###### Without included entity
83
- Example:
84
-
85
- ```
86
- class Movie < ActiveRecord::Model
87
- belongs_to :director
88
- end
89
- ```
76
+ ### Plain Ruby / outside Rails
90
77
 
91
- Request body:
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
- Will translate to:
84
+ class Translator
85
+ include JsonApi::Parameters
86
+ end
87
+ translator = Translator.new
112
88
 
89
+ translator.jsonapify(params)
113
90
  ```
114
- {
115
- movie: {
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
- Request body:
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
- Will translate to:
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
- ##### has_many
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
- Passing a resource that is a an array of entities in relationships tree will make JsonApi::Parameters assume that it is a `has_many` relationship.
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
- ###### Without included entity
177
- Example:
105
+ This default limit is 3, and can be overwritten by specifying the custom limit.
178
106
 
179
- ```
180
- class Movie < ActiveRecord::Model
181
- has_many :genres
107
+ #### Ruby
108
+ ```
109
+ class Translator
110
+ include JsonApi::Parameters
182
111
  end
183
- ```
184
112
 
185
- Request body:
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
- Will translate to:
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
- ###### With included entity:
221
- Example:
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
- Request body:
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
- * while calling `.from_jsonapi`, for instance: `.from_jsonapi(: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).**
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
- class Translator
294
- include JsonApi::Parameters
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
- #### Casing
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
- 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).
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
- This gems 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).
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).
@@ -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
- relationship_value = relationship_value[:data]
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 handle_to_many_relation(relationship_key, relationship_value)
72
- with_inclusion = !relationship_value.empty?
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 handle_nil_relation(relationship_key)
125
- # Graceful fail if nil on to-many association.
126
- raise jsonapi_not_implemented_err if pluralize(relationship_key).to_sym == relationship_key
127
-
128
- # Handle with empty hash.
129
- handle_to_one_relation(relationship_key, {})
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 find_included_object(related_id:, related_type:)
133
- jsonapi_included.find do |included_object_enum|
134
- included_object_enum[:id] &&
135
- included_object_enum[:id] == related_id &&
136
- included_object_enum[:type] &&
137
- included_object_enum[:type] == related_type
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
- def jsonapi_not_implemented_err
142
- NotImplementedError.new('relationship member must either be an Array or a Hash')
115
+ val
143
116
  end
144
117
  end
@@ -1,5 +1,5 @@
1
1
  module JsonApi
2
2
  module Parameters
3
- VERSION = '1.0.0'.freeze
3
+ VERSION = '2.2.0'.freeze
4
4
  end
5
5
  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: 1.0.0
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: 2019-11-28 00:00:00.000000000 Z
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.4
352
+ rubygems_version: 3.0.3
347
353
  signing_key:
348
354
  specification_version: 4
349
355
  summary: Translator for JSON:API compliant parameters