jsonapi_parameters 0.4.4 → 2.1.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: 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