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 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