jsonapi_parameters 0.4.4 → 2.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: 84858e2d499d73db01b7438b76e694d0a9e00f0a09c7c60d39d5ab8f2b7cc93a
4
- data.tar.gz: c9dd1b24e02193aa7bd2fb820510d4deb35e7da1dca5dea6b83370fb0341c603
3
+ metadata.gz: 514048af1321f2eb0d1c17e260d8bfc0ce3dbf3b0d8c70fc91f42724fdc6a0c1
4
+ data.tar.gz: 46a52ac2fa28952b732d11479b9c1a2a3c2ff88bafd37816a920626b32ceda31
5
5
  SHA512:
6
- metadata.gz: fcde1c307a79942c9dc2725ad77855255102cba66f7d18cd8dc6c87f439e3c8e279ab8817ad2f3bfd2a8cfe044224c6e7e0e9f61011b0f9fa4ad2a1e9350e6f8
7
- data.tar.gz: b4e8ffc48a859faa83c5224d0c384482ada8f3fddcc4ed07f298e3d9c9c6ce542df20db99f44b73bf8417320c06ba9391b721b14ab7085adaf3bc5c3ff371da3
6
+ metadata.gz: 406ea510828fb14bb8b4ba1298ad4a7f1c68a93391b8dce48a0e9132d8b7e05343febc0da9fd8f2e7aa8b6bb81f1a48a5cc23c51f01164d8d13e572b4f2b1c1e
7
+ data.tar.gz: b455719c009aae879edf3e457f34222cee455a3583ae9c1c58348f4784efafe92c1b2407732500a35d8f5da026bdce9dff5624482c90950a0d3a8d4430c48d81
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,212 +70,8 @@ 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
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
-
73
+ For more examples take a look at [Relationships](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationships) in the wiki documentation.
122
74
 
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
- data: {
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: 'genre',
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
75
 
285
76
  ### Plain Ruby / outside Rails
286
77
 
@@ -297,15 +88,6 @@ translator = Translator.new
297
88
 
298
89
  translator.jsonapify(params)
299
90
  ```
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
91
 
310
92
  ## Mime Type
311
93
 
@@ -313,5 +95,11 @@ As [stated in the JSON:API specification](https://jsonapi.org/#mime-types) corre
313
95
 
314
96
  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
97
 
98
+ ## Customization
99
+
100
+ 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.
101
+
102
+ Read more at [Relationship Handlers](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationship-handlers).
103
+
316
104
  ## License
317
105
  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,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,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: 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
@@ -15,7 +15,9 @@ module JsonApi::Parameters
15
15
  return params if params.nil? || params.empty?
16
16
 
17
17
  @jsonapi_unsafe_hash = if naming_convention != :snake || JsonApi::Parameters.ensure_underscore_translation
18
- params.deep_transform_keys { |key| key.to_s.underscore.to_sym }
18
+ params = params.deep_transform_keys { |key| key.to_s.underscore.to_sym }
19
+ params[:data][:type] = params[:data][:type].underscore if params.dig(:data, :type)
20
+ params
19
21
  else
20
22
  params.deep_symbolize_keys
21
23
  end
@@ -37,16 +39,23 @@ module JsonApi::Parameters
37
39
  jsonapi_unsafe_params.tap do |param|
38
40
  jsonapi_relationships.each do |relationship_key, relationship_value|
39
41
  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
42
+ handler_args = [relationship_key, relationship_value, jsonapi_included]
43
+ handler = if Handlers.resource_handlers.key?(relationship_key)
44
+ Handlers.handlers[Handlers.resource_handlers[relationship_key]]
45
+ else
46
+ case relationship_value
47
+ when Array
48
+ Handlers.handlers[:to_many]
49
+ when Hash
50
+ Handlers.handlers[:to_one]
51
+ when nil
52
+ Handlers.handlers[:nil]
53
+ else
54
+ raise NotImplementedError.new('relationship resource linkage has to be a type of Array, Hash or nil')
55
+ end
56
+ end
57
+
58
+ key, val = handler.call(*handler_args)
50
59
  param[key] = val
51
60
  end
52
61
  end
@@ -67,78 +76,4 @@ module JsonApi::Parameters
67
76
  def jsonapi_relationships
68
77
  @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
69
78
  end
70
-
71
- def handle_to_many_relation(relationship_key, relationship_value)
72
- with_inclusion = true
73
-
74
- vals = relationship_value.map do |relationship_value|
75
- related_id = relationship_value.dig(:id)
76
- related_type = relationship_value.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 = with_inclusion ? !included_object.empty? : with_inclusion
86
-
87
- if with_inclusion
88
- included_object.delete(:type)
89
- included_object[:attributes].merge(id: related_id)
90
- else
91
- relationship_value.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
79
  end
@@ -1,5 +1,5 @@
1
1
  module JsonApi
2
2
  module Parameters
3
- VERSION = '0.4.4'.freeze
3
+ VERSION = '2.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: 0.4.4
4
+ version: 2.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-08 00:00:00.000000000 Z
12
+ date: 2020-06-30 00:00:00.000000000 Z
13
13
  dependencies:
14
14
  - !ruby/object:Gem::Dependency
15
15
  name: activesupport
@@ -45,14 +45,14 @@ dependencies:
45
45
  requirements:
46
46
  - - "~>"
47
47
  - !ruby/object:Gem::Version
48
- version: 1.10.4
49
- type: :runtime
48
+ version: 1.10.5
49
+ type: :development
50
50
  prerelease: false
51
51
  version_requirements: !ruby/object:Gem::Requirement
52
52
  requirements:
53
53
  - - "~>"
54
54
  - !ruby/object:Gem::Version
55
- version: 1.10.4
55
+ version: 1.10.5
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: json
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -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
@@ -343,7 +348,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
343
348
  - !ruby/object:Gem::Version
344
349
  version: '0'
345
350
  requirements: []
346
- rubygems_version: 3.0.4
351
+ rubygems_version: 3.0.3
347
352
  signing_key:
348
353
  specification_version: 4
349
354
  summary: Translator for JSON:API compliant parameters