jsonapi_parameters 1.1.0 → 2.3.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: 6ad28494a05c8bc7f45b3b4408989aa44c311741fbefea777bd9fedb91bba464
4
- data.tar.gz: 57180a59200f87667ebfdc57c1063e1855ce16777c80e4d8562780bfbbd96bfa
3
+ metadata.gz: 414c6d7e91cd71641096a2fc6dcb6d7055f9154cab1db06be6dbb188fddb3c6a
4
+ data.tar.gz: 483e0f4576c935632ca3cdbe48b10050d232dbb67e202a4db0fca2d4f0004721
5
5
  SHA512:
6
- metadata.gz: 166f3aed4fe6f74806fdad8f17bb924812d4678f8fc2ca55d16cfdff7c2497a43d4c9331280b128b74ac1051ccb6022e16880daae409fb93a402134196c34937
7
- data.tar.gz: 8556b0aaed3936f78999f7fe7a0438ce1c66af2c7f2d78e499a7ba53c00ff6d60c9c83b949a4096b88a79ddf248bfa0cba89559923fa4f73ce720caf1f49ffec
6
+ metadata.gz: 5a90b5d5a44fcbe5ee66e13308ab190e73a7a84409727c6c7530d66d5105baf4754987d634b71ebe4f7674d3b491e95a7ed8bb8e5fd399f4a638cb2bb272f087
7
+ data.tar.gz: 89d0c3dc2b6b100c53f1c446b80d9bd8acfc5e9db33587330c5fd57c362d9c92d8b9866f5c2afb95aa86233bb5c40ce72c61baac6cafa2b40d53df37d39dc47d
data/README.md CHANGED
@@ -4,6 +4,7 @@ Simple [JSON:API](https://jsonapi.org/) compliant parameters translator.
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
9
  [Documentation](https://github.com/visualitypl/jsonapi_parameters/wiki)
9
10
 
@@ -92,13 +93,63 @@ translator.jsonapify(params)
92
93
 
93
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).
94
95
 
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).
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).
97
+
98
+ ## Stack limit
99
+
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.
102
+
103
+ For this reason we have introduced a default limit of stack levels that JsonApi::Parameters will go down through while parsing the payloads.
104
+
105
+ This default limit is 3, and can be overwritten by specifying the custom limit.
106
+
107
+ #### Ruby
108
+ ```ruby
109
+ class Translator
110
+ include JsonApi::Parameters
111
+ end
112
+
113
+ translator = Translator.new
114
+
115
+ translator.jsonapify(custom_stack_limit: 4)
116
+
117
+ # OR
118
+
119
+ translator.stack_limit = 4
120
+ translator.jsonapify.(...)
121
+ ```
122
+
123
+ #### Rails
124
+ ```ruby
125
+ def create_params
126
+ params.from_jsonapi(custom_stack_limit: 4).require(:user).permit(
127
+ entities_attributes: { subentities_attributes: { ... } }
128
+ )
129
+ end
130
+
131
+ # OR
132
+
133
+ def create_params
134
+ params.stack_level = 4
135
+
136
+ params.from_jsonapi.require(:user).permit(entities_attributes: { subentities_attributes: { ... } })
137
+ ensure
138
+ params.reset_stack_limit!
139
+ end
140
+ ```
96
141
 
97
142
  ## Customization
98
143
 
99
144
  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
145
 
101
- Read more at [Relationship Handlers]()
146
+ Read more at [Relationship Handlers](https://github.com/visualitypl/jsonapi_parameters/wiki/Relationship-handlers).
147
+
148
+ ## Team
149
+
150
+ Project started by [Jasiek Matusz](https://github.com/Marahin).
151
+
152
+ Currently, jsonapi_parameters is maintained by Visuality's [Open Source Commitee](https://www.visuality.pl/open-source).
102
153
 
103
154
  ## License
104
155
  The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
@@ -2,6 +2,5 @@ require 'jsonapi_parameters/parameters'
2
2
  require 'jsonapi_parameters/handlers'
3
3
  require 'jsonapi_parameters/translator'
4
4
  require 'jsonapi_parameters/core_ext'
5
+ require 'jsonapi_parameters/stack_limit'
5
6
  require 'jsonapi_parameters/version'
6
-
7
- 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
@@ -5,6 +5,10 @@ module JsonApi
5
5
  class BaseHandler
6
6
  attr_reader :relationship_key, :relationship_value, :included
7
7
 
8
+ def self.call(key, val, included)
9
+ new(key, val, included).handle
10
+ end
11
+
8
12
  def initialize(relationship_key, relationship_value, included)
9
13
  @relationship_key = relationship_key
10
14
  @relationship_value = relationship_value
@@ -1,3 +1,5 @@
1
+ require 'active_support/inflector'
2
+
1
3
  require_relative './base_handler'
2
4
 
3
5
  module JsonApi
@@ -1,3 +1,5 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module JsonApi
2
4
  module Parameters
3
5
  module Handlers
@@ -34,8 +36,9 @@ module JsonApi
34
36
  @with_inclusion &= !included_object.empty?
35
37
 
36
38
  if with_inclusion
37
- included_object.delete(:type)
38
- included_object[:attributes].merge(id: related_id)
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
39
42
  else
40
43
  relationship.dig(:id)
41
44
  end
@@ -1,3 +1,5 @@
1
+ require 'active_support/inflector'
2
+
1
3
  module JsonApi
2
4
  module Parameters
3
5
  module Handlers
@@ -15,8 +17,10 @@ module JsonApi
15
17
 
16
18
  return ["#{singularize(relationship_key)}_id".to_sym, related_id] if included_object.empty?
17
19
 
18
- included_object.delete(:type)
19
- included_object = included_object[:attributes].merge(id: related_id)
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
+
20
24
  ["#{singularize(relationship_key)}_attributes".to_sym, included_object]
21
25
  end
22
26
  end
@@ -8,9 +8,9 @@ module JsonApi
8
8
  include DefaultHandlers
9
9
 
10
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 }
11
+ to_many: ToManyRelationHandler,
12
+ to_one: ToOneRelationHandler,
13
+ nil: NilRelationHandler
14
14
  }.freeze
15
15
 
16
16
  module_function
@@ -29,7 +29,7 @@ module JsonApi
29
29
  resource_handlers[resource_key.to_sym] = handler_key.to_sym
30
30
  end
31
31
 
32
- def reset_handlers!
32
+ def reset_handlers
33
33
  @handlers = DEFAULT_HANDLER_SET.dup
34
34
  @resource_handlers = {}
35
35
  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,27 +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
- 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)
57
- param[key] = val
43
+ param = handle_relationships(param, relationship_key, relationship_value)
58
44
  end
59
45
  end
46
+ ensure
47
+ reset_stack_level
60
48
  end
61
49
 
62
50
  def jsonapi_unsafe_params
@@ -67,11 +55,63 @@ module JsonApi::Parameters
67
55
  end
68
56
  end
69
57
 
58
+ def jsonapi_relationships
59
+ @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
60
+ end
61
+
70
62
  def jsonapi_included
71
63
  @jsonapi_included ||= @jsonapi_unsafe_hash[:included] || []
72
64
  end
73
65
 
74
- def jsonapi_relationships
75
- @jsonapi_relationships ||= @jsonapi_unsafe_hash.dig(:data, :relationships) || []
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
93
+ end
94
+
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
113
+ end
114
+
115
+ val
76
116
  end
77
117
  end
@@ -1,5 +1,5 @@
1
1
  module JsonApi
2
2
  module Parameters
3
- VERSION = '1.1.0'.freeze
3
+ VERSION = '2.3.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.1.0
4
+ version: 2.3.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-12-06 00:00:00.000000000 Z
12
+ date: 2021-01-29 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.5
48
+ version: '1.11'
49
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.5
55
+ version: '1.11'
56
56
  - !ruby/object:Gem::Dependency
57
57
  name: json
58
58
  requirement: !ruby/object:Gem::Requirement
@@ -327,6 +327,7 @@ files:
327
327
  - lib/jsonapi_parameters/default_handlers/to_one_relation_handler.rb
328
328
  - lib/jsonapi_parameters/handlers.rb
329
329
  - lib/jsonapi_parameters/parameters.rb
330
+ - lib/jsonapi_parameters/stack_limit.rb
330
331
  - lib/jsonapi_parameters/translator.rb
331
332
  - lib/jsonapi_parameters/version.rb
332
333
  homepage: https://github.com/visualitypl/jsonapi_parameters
@@ -341,14 +342,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
341
342
  requirements:
342
343
  - - ">="
343
344
  - !ruby/object:Gem::Version
344
- version: 2.3.0
345
+ version: 2.5.0
345
346
  required_rubygems_version: !ruby/object:Gem::Requirement
346
347
  requirements:
347
348
  - - ">="
348
349
  - !ruby/object:Gem::Version
349
350
  version: '0'
350
351
  requirements: []
351
- rubygems_version: 3.0.4
352
+ rubygems_version: 3.1.2
352
353
  signing_key:
353
354
  specification_version: 4
354
355
  summary: Translator for JSON:API compliant parameters