jsonapi_parameters 1.0.0 → 1.1.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: 6ad28494a05c8bc7f45b3b4408989aa44c311741fbefea777bd9fedb91bba464
4
+ data.tar.gz: 57180a59200f87667ebfdc57c1063e1855ce16777c80e4d8562780bfbbd96bfa
5
5
  SHA512:
6
- metadata.gz: 30da56dbc5654aed7331e5db54f54da59414fdf747a97c75755ba184af9e813a9ef50f7f2251f4af418ca04d11b54d353d1d4ca7c1e12a638e2049fa7bcae972
7
- data.tar.gz: a67da822ec846764be2cc6a999a6a70655b7816b5be9b733770a2f4ec32246a39cf587260a2fdfe6fa6836fe0290cc0e08088b873a8649fb2e61ab1fab59d839
6
+ metadata.gz: 166f3aed4fe6f74806fdad8f17bb924812d4678f8fc2ca55d16cfdff7c2497a43d4c9331280b128b74ac1051ccb6022e16880daae409fb93a402134196c34937
7
+ data.tar.gz: 8556b0aaed3936f78999f7fe7a0438ce1c66af2c7f2d78e499a7ba53c00ff6d60c9c83b949a4096b88a79ddf248bfa0cba89559923fa4f73ce720caf1f49ffec
data/README.md CHANGED
@@ -1,17 +1,11 @@
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
7
 
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.
8
+ [Documentation](https://github.com/visualitypl/jsonapi_parameters/wiki)
15
9
 
16
10
  ## Usage
17
11
 
@@ -75,212 +69,8 @@ Relationship parameters are being read from two optional trees:
75
69
 
76
70
  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
71
 
78
- ##### belongs_to
79
-
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
-
82
- ###### Without included entity
83
- Example:
84
-
85
- ```
86
- class Movie < ActiveRecord::Model
87
- belongs_to :director
88
- end
89
- ```
90
-
91
- Request body:
92
-
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
- }
108
- }
109
- ```
110
-
111
- Will translate to:
112
-
113
- ```
114
- {
115
- movie: {
116
- title: 'The Terminator',
117
- director_id: 682
118
- }
119
- }
120
- ```
121
-
72
+ For more examples take a look at [Relationships](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationships) in the wiki documentation.
122
73
 
123
- ###### With included entity:
124
- Example:
125
-
126
- ```
127
- class Movie < ActiveRecord::Model
128
- belongs_to :director
129
-
130
- accepts_nested_attributes_for :director
131
- end
132
- ```
133
-
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
- ```
161
-
162
- Will translate to:
163
- ```
164
- {
165
- movie: {
166
- title: 'The Terminator',
167
- director_attributes: { id: 682, name: 'Some guy' }
168
- }
169
- }
170
- ```
171
-
172
- ##### has_many
173
-
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.
175
-
176
- ###### Without included entity
177
- Example:
178
-
179
- ```
180
- class Movie < ActiveRecord::Model
181
- has_many :genres
182
- end
183
- ```
184
-
185
- Request body:
186
-
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
- ```
207
-
208
- Will translate to:
209
-
210
- ```
211
- {
212
- movie: {
213
- title: 'The Terminator',
214
- genre_ids: [1, 2]
215
- }
216
- }
217
- ```
218
-
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
228
- end
229
- ```
230
-
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
- ```
282
-
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
74
 
285
75
  ### Plain Ruby / outside Rails
286
76
 
@@ -297,15 +87,6 @@ translator = Translator.new
297
87
 
298
88
  translator.jsonapify(params)
299
89
  ```
300
-
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
90
 
310
91
  ## Mime Type
311
92
 
@@ -313,5 +94,11 @@ As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) corre
313
94
 
314
95
  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).
315
96
 
97
+ ## Customization
98
+
99
+ 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.
100
+
101
+ Read more at [Relationship Handlers]()
102
+
316
103
  ## License
317
104
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -1,4 +1,5 @@
1
1
  require 'jsonapi_parameters/parameters'
2
+ require 'jsonapi_parameters/handlers'
2
3
  require 'jsonapi_parameters/translator'
3
4
  require 'jsonapi_parameters/core_ext'
4
5
  require 'jsonapi_parameters/version'
@@ -0,0 +1,26 @@
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 initialize(relationship_key, relationship_value, included)
9
+ @relationship_key = relationship_key
10
+ @relationship_value = relationship_value
11
+ @included = included
12
+ end
13
+
14
+ def find_included_object(related_id:, related_type:)
15
+ included.find do |included_object_enum|
16
+ included_object_enum[:id] &&
17
+ included_object_enum[:id] == related_id &&
18
+ included_object_enum[:type] &&
19
+ included_object_enum[:type] == related_type
20
+ end
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,27 @@
1
+ require_relative './base_handler'
2
+
3
+ module JsonApi
4
+ module Parameters
5
+ module Handlers
6
+ module DefaultHandlers
7
+ class NilRelationHandler < BaseHandler
8
+ include ActiveSupport::Inflector
9
+
10
+ def handle
11
+ # Graceful fail if nil on to-many association
12
+ # in case the relationship key is, for instance, `billable_hours`,
13
+ # we have to assume that it is a to-many relationship.
14
+ if pluralize(relationship_key).to_sym == relationship_key
15
+ raise NotImplementedError.new(
16
+ 'plural resource cannot be nullified - please create a custom handler for this relation'
17
+ )
18
+ end
19
+
20
+ # Handle with empty hash.
21
+ ToOneRelationHandler.new(relationship_key, {}, {}).handle
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,52 @@
1
+ module JsonApi
2
+ module Parameters
3
+ module Handlers
4
+ module DefaultHandlers
5
+ class ToManyRelationHandler < BaseHandler
6
+ include ActiveSupport::Inflector
7
+
8
+ attr_reader :with_inclusion, :vals, :key
9
+
10
+ def handle
11
+ @with_inclusion = !relationship_value.empty?
12
+
13
+ prepare_relationship_vals
14
+
15
+ generate_key
16
+
17
+ [key, vals]
18
+ end
19
+
20
+ private
21
+
22
+ def prepare_relationship_vals
23
+ @vals = relationship_value.map do |relationship|
24
+ related_id = relationship.dig(:id)
25
+ related_type = relationship.dig(:type)
26
+
27
+ included_object = find_included_object(
28
+ related_id: related_id, related_type: related_type
29
+ ) || {}
30
+
31
+ # If at least one related object has not been found in `included` tree,
32
+ # we should not attempt to "#{relationship_key}_attributes" but
33
+ # "#{relationship_key}_ids" instead.
34
+ @with_inclusion &= !included_object.empty?
35
+
36
+ if with_inclusion
37
+ included_object.delete(:type)
38
+ included_object[:attributes].merge(id: related_id)
39
+ else
40
+ relationship.dig(:id)
41
+ end
42
+ end
43
+ end
44
+
45
+ def generate_key
46
+ @key = (with_inclusion ? "#{pluralize(relationship_key)}_attributes" : "#{singularize(relationship_key)}_ids").to_sym
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,26 @@
1
+ module JsonApi
2
+ module Parameters
3
+ module Handlers
4
+ module DefaultHandlers
5
+ class ToOneRelationHandler < BaseHandler
6
+ include ActiveSupport::Inflector
7
+
8
+ def handle
9
+ related_id = relationship_value.dig(:id)
10
+ related_type = relationship_value.dig(:type)
11
+
12
+ included_object = find_included_object(
13
+ related_id: related_id, related_type: related_type
14
+ ) || {}
15
+
16
+ return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty?
17
+
18
+ included_object.delete(:type)
19
+ included_object = included_object[:attributes].merge(id: related_id)
20
+ ["#{singularize(relationship_key)}_attributes".to_sym, included_object]
21
+ end
22
+ end
23
+ end
24
+ end
25
+ end
26
+ 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: ->(k, v, included) { ToManyRelationHandler.new(k, v, included).handle },
12
+ to_one: ->(k, v, included) { ToOneRelationHandler.new(k, v, included).handle },
13
+ nil: ->(k, v, included) { NilRelationHandler.new(k, v, included).handle }
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
@@ -37,16 +37,23 @@ module JsonApi::Parameters
37
37
  jsonapi_unsafe_params.tap do |param|
38
38
  jsonapi_relationships.each do |relationship_key, relationship_value|
39
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
40
+ handler_args = [relationship_key, relationship_value, jsonapi_included]
41
+ handler = if Handlers.resource_handlers.key?(relationship_key)
42
+ Handlers.handlers[Handlers.resource_handlers[relationship_key]]
43
+ else
44
+ case relationship_value
45
+ when Array
46
+ Handlers.handlers[:to_many]
47
+ when Hash
48
+ Handlers.handlers[:to_one]
49
+ when nil
50
+ Handlers.handlers[:nil]
51
+ else
52
+ raise NotImplementedError.new('relationship resource linkage has to be a type of Array, Hash or nil')
53
+ end
54
+ end
55
+
56
+ key, val = handler.call(*handler_args)
50
57
  param[key] = val
51
58
  end
52
59
  end
@@ -67,78 +74,4 @@ module JsonApi::Parameters
67
74
  def jsonapi_relationships
68
75
  @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
69
76
  end
70
-
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]
122
- end
123
-
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, {})
130
- end
131
-
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
138
- end
139
- end
140
-
141
- def jsonapi_not_implemented_err
142
- NotImplementedError.new('relationship member must either be an Array or a Hash')
143
- end
144
77
  end
@@ -1,5 +1,5 @@
1
1
  module JsonApi
2
2
  module Parameters
3
- VERSION = '1.0.0'.freeze
3
+ VERSION = '1.1.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: 1.1.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: 2019-12-06 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -321,6 +321,11 @@ 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
325
330
  - lib/jsonapi_parameters/translator.rb
326
331
  - lib/jsonapi_parameters/version.rb