grape 1.3.2 → 1.3.3

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: a1ba205b72fd823b11e700753891784fdfbd6791f591b790a99ac3433cb8fe23
4
- data.tar.gz: '09e64a0d93415b28fdb179fbb5d7ef0d03e00dcb976fd885c0c655d6ac041345'
3
+ metadata.gz: d63fb79e412ead32064ad4994e171e65962131a04afb20d12957942c744f4df8
4
+ data.tar.gz: 9a9f4fb654e346eabb8a0b902d5e49ae54dc567797789c98c76f2a2fe2e2daa9
5
5
  SHA512:
6
- metadata.gz: 06fcd2157bcfcc6e71876df09b34a318c600761b79fb99ad77739521d056950ee8391dfd050efd43618f7f99f11990623e276b32d15a27e92015ec3a56c6c08b
7
- data.tar.gz: 5463bbd0347950765c9d92e859965e90dd69d7d1f781fd188386cf39597c196f5ba2dc697dbee4f896a70f6498c1e43034a33db8460ea9a719218a7d5df46d6f
6
+ metadata.gz: b75afa355e6a2b8200d72a2c4407bc78646648832a94d576bff06bdebde90d54b1897df09a560665bd9f58e735f9832a110b63bbcaabe14a4e2ee3c97e743b57
7
+ data.tar.gz: 18a4ea057230ae0e6cfe16f92e5043339b66026a94d29bd8c897d2482577cb279ec4e202f41a643605a3ea09c748fa159fddc36977a3c4828b5b64daef080272
@@ -1,6 +1,24 @@
1
+ ### 1.3.3 (2020/05/23)
2
+
3
+ #### Features
4
+
5
+ * [#2048](https://github.com/ruby-grape/grape/issues/2034): Grape Enterprise support is now available [via TideLift](https://tidelift.com/subscription/request-a-demo?utm_source=rubygems-grape&utm_medium=referral&utm_campaign=enterprise) - [@dblock](https://github.com/dblock).
6
+ * [#2039](https://github.com/ruby-grape/grape/pull/2039): Travis - update rails versions - [@ericproulx](https://github.com/ericproulx).
7
+ * [#2038](https://github.com/ruby-grape/grape/pull/2038): Travis - update ruby versions - [@ericproulx](https://github.com/ericproulx).
8
+ * [#2050](https://github.com/ruby-grape/grape/pull/2050): Refactor route public_send to AttributeTranslator - [@ericproulx](https://github.com/ericproulx).
9
+
10
+ #### Fixes
11
+
12
+ * [#2049](https://github.com/ruby-grape/grape/pull/2049): Coerce an empty string to nil in case of the bool type - [@dnesteryuk](https://github.com/dnesteryuk).
13
+ * [#2043](https://github.com/ruby-grape/grape/pull/2043): Modify declared for nested array and hash - [@kadotami](https://github.com/kadotami).
14
+ * [#2040](https://github.com/ruby-grape/grape/pull/2040): Fix a regression with Array of type nil - [@ericproulx](https://github.com/ericproulx).
15
+ * [#2054](https://github.com/ruby-grape/grape/pull/2054): Coercing of nested arrays - [@dnesteryuk](https://github.com/dnesteryuk).
16
+ * [#2050](https://github.com/ruby-grape/grape/pull/2053): Fix broken multiple mounts - [@Jack12816](https://github.com/Jack12816).
17
+
1
18
  ### 1.3.2 (2020/04/12)
2
19
 
3
20
  #### Features
21
+
4
22
  * [#2020](https://github.com/ruby-grape/grape/pull/2020): Reduce array allocation - [@ericproulx](https://github.com/ericproulx).
5
23
  * [#2015](https://github.com/ruby-grape/grape/pull/2014): Reduce MatchData allocation - [@ericproulx](https://github.com/ericproulx).
6
24
  * [#2014](https://github.com/ruby-grape/grape/pull/2014): Reduce total allocated arrays - [@ericproulx](https://github.com/ericproulx).
data/LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2010-2019 Michael Bleigh, Intridea Inc. and Contributors.
1
+ Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors.
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.md CHANGED
@@ -12,6 +12,7 @@
12
12
  - [What is Grape?](#what-is-grape)
13
13
  - [Stable Release](#stable-release)
14
14
  - [Project Resources](#project-resources)
15
+ - [Grape for Enterprise](#grape-for-enterprise)
15
16
  - [Installation](#installation)
16
17
  - [Basic Usage](#basic-usage)
17
18
  - [Mounting](#mounting)
@@ -141,6 +142,7 @@
141
142
  - [format_response.grape](#format_responsegrape)
142
143
  - [Monitoring Products](#monitoring-products)
143
144
  - [Contributing to Grape](#contributing-to-grape)
145
+ - [Security](#security)
144
146
  - [License](#license)
145
147
  - [Copyright](#copyright)
146
148
 
@@ -154,7 +156,7 @@ content negotiation, versioning and much more.
154
156
 
155
157
  ## Stable Release
156
158
 
157
- You're reading the documentation for the stable release of Grape, 1.3.2.
159
+ You're reading the documentation for the stable release of Grape, **1.3.3**.
158
160
 
159
161
  ## Project Resources
160
162
 
@@ -163,6 +165,14 @@ You're reading the documentation for the stable release of Grape, 1.3.2.
163
165
  * Need help? Try [Grape Google Group](http://groups.google.com/group/ruby-grape) or [Gitter](https://gitter.im/ruby-grape/grape)
164
166
  * [Follow us on Twitter](https://twitter.com/grapeframework)
165
167
 
168
+ ## Grape for Enterprise
169
+
170
+ Available as part of the Tidelift Subscription.
171
+
172
+ The maintainers of Grape are working with Tidelift to deliver commercial support and maintenance. Save time, reduce risk, and improve code health, while paying the maintainers of Grape. Click [here](https://tidelift.com/subscription/request-a-demo?utm_source=rubygems-grape&utm_medium=referral&utm_campaign=enterprise) for more details.
173
+
174
+ In 2020, we plan to use the money towards gathering Grape contributors for dinner in New York City.
175
+
166
176
  ## Installation
167
177
 
168
178
  Ruby 2.4 or newer is required.
@@ -3850,7 +3860,7 @@ Grape integrates with following third-party tools:
3850
3860
  * **Librato Metrics** - [grape-librato](https://github.com/seanmoon/grape-librato) gem
3851
3861
  * **[Skylight](https://www.skylight.io/)** - [skylight](https://github.com/skylightio/skylight-ruby) gem, [documentation](https://docs.skylight.io/grape/)
3852
3862
  * **[AppSignal](https://www.appsignal.com)** - [appsignal-ruby](https://github.com/appsignal/appsignal-ruby) gem, [documentation](http://docs.appsignal.com/getting-started/supported-frameworks.html#grape)
3853
- * **[ElasticAPM](https://www.elastic.co/products/apm) - [elastic-apm](https://github.com/elastic/apm-agent-ruby) gem, [documentation](https://www.elastic.co/guide/en/apm/agent/ruby/3.x/getting-started-rack.html#getting-started-grape)
3863
+ * **[ElasticAPM](https://www.elastic.co/products/apm)** - [elastic-apm](https://github.com/elastic/apm-agent-ruby) gem, [documentation](https://www.elastic.co/guide/en/apm/agent/ruby/3.x/getting-started-rack.html#getting-started-grape)
3854
3864
 
3855
3865
  ## Contributing to Grape
3856
3866
 
@@ -3859,10 +3869,14 @@ features and discuss issues.
3859
3869
 
3860
3870
  See [CONTRIBUTING](CONTRIBUTING.md).
3861
3871
 
3872
+ ## Security
3873
+
3874
+ See [SECURITY](SECURITY.md) for details.
3875
+
3862
3876
  ## License
3863
3877
 
3864
- MIT License. See LICENSE for details.
3878
+ MIT License. See [LICENSE](LICENSE) for details.
3865
3879
 
3866
3880
  ## Copyright
3867
3881
 
3868
- Copyright (c) 2010-2019 Michael Bleigh, Intridea Inc. and Contributors.
3882
+ Copyright (c) 2010-2020 Michael Bleigh, Intridea Inc. and Contributors.
@@ -1,6 +1,65 @@
1
1
  Upgrading Grape
2
2
  ===============
3
3
 
4
+ ### Upgrading to >= 1.4.0
5
+
6
+ #### Nil values for structures
7
+
8
+ Nil values always been a special case when dealing with types especially with the following structures:
9
+ - Array
10
+ - Hash
11
+ - Set
12
+
13
+ The behaviour for these structures has change through out the latest releases. For instance:
14
+
15
+ ```ruby
16
+ class Api < Grape::API
17
+ params do
18
+ require :my_param, type: Array[Integer]
19
+ end
20
+
21
+ get 'example' do
22
+ params[:my_param]
23
+ end
24
+ get '/example', params: { my_param: nil }
25
+ # 1.3.1 = []
26
+ # 1.3.2 = nil
27
+ end
28
+ ```
29
+ For now on, `nil` values stay `nil` values for all types, including arrays, sets and hashes.
30
+
31
+ If you want to have the same behavior as 1.3.1, apply a `default` validator
32
+
33
+ ```ruby
34
+ class Api < Grape::API
35
+ params do
36
+ require :my_param, type: Array[Integer], default: []
37
+ end
38
+
39
+ get 'example' do
40
+ params[:my_param]
41
+ end
42
+ get '/example', params: { my_param: nil } # => []
43
+ end
44
+ ```
45
+
46
+ #### Default validator
47
+
48
+ Default validator is now applied for `nil` values.
49
+
50
+ ```ruby
51
+ class Api < Grape::API
52
+ params do
53
+ requires :my_param, type: Integer, default: 0
54
+ end
55
+
56
+ get 'example' do
57
+ params[:my_param]
58
+ end
59
+ get '/example', params: { my_param: nil } #=> before: nil, after: 0
60
+ end
61
+ ```
62
+
4
63
  ### Upgrading to >= 1.3.0
5
64
 
6
65
  #### Ruby
@@ -205,11 +205,12 @@ module Grape
205
205
  route_settings[:requirements] = route.requirements
206
206
  route_settings[:path] = route.origin
207
207
  route_settings[:methods] ||= []
208
- route_settings[:methods] << route.request_method
208
+ if route.request_method == '*' || route_settings[:methods].include?('*')
209
+ route_settings[:methods] = Grape::Http::Headers::SUPPORTED_METHODS
210
+ else
211
+ route_settings[:methods] << route.request_method
212
+ end
209
213
  route_settings[:endpoint] = route.app
210
-
211
- # using the :any shorthand produces [nil] for route methods, substitute all manually
212
- route_settings[:methods] = Grape::Http::Headers::SUPPORTED_METHODS if route_settings[:methods].include?('*')
213
214
  end
214
215
  end
215
216
 
@@ -28,36 +28,38 @@ module Grape
28
28
  # Methods which should not be available in filters until the before filter
29
29
  # has completed
30
30
  module PostBeforeFilter
31
- def declared(passed_params, options = {}, declared_params = nil)
31
+ def declared(passed_params, options = {}, declared_params = nil, params_nested_path = [])
32
32
  options = options.reverse_merge(include_missing: true, include_parent_namespaces: true)
33
33
  declared_params ||= optioned_declared_params(**options)
34
34
 
35
35
  if passed_params.is_a?(Array)
36
- declared_array(passed_params, options, declared_params)
36
+ declared_array(passed_params, options, declared_params, params_nested_path)
37
37
  else
38
- declared_hash(passed_params, options, declared_params)
38
+ declared_hash(passed_params, options, declared_params, params_nested_path)
39
39
  end
40
40
  end
41
41
 
42
42
  private
43
43
 
44
- def declared_array(passed_params, options, declared_params)
44
+ def declared_array(passed_params, options, declared_params, params_nested_path)
45
45
  passed_params.map do |passed_param|
46
- declared(passed_param || {}, options, declared_params)
46
+ declared(passed_param || {}, options, declared_params, params_nested_path)
47
47
  end
48
48
  end
49
49
 
50
- def declared_hash(passed_params, options, declared_params)
50
+ def declared_hash(passed_params, options, declared_params, params_nested_path)
51
51
  declared_params.each_with_object(passed_params.class.new) do |declared_param, memo|
52
52
  if declared_param.is_a?(Hash)
53
53
  declared_param.each_pair do |declared_parent_param, declared_children_params|
54
+ params_nested_path_dup = params_nested_path.dup
55
+ params_nested_path_dup << declared_parent_param.to_s
54
56
  next unless options[:include_missing] || passed_params.key?(declared_parent_param)
55
57
 
56
58
  passed_children_params = passed_params[declared_parent_param] || passed_params.class.new
57
59
  memo_key = optioned_param_key(declared_parent_param, options)
58
60
 
59
- memo[memo_key] = handle_passed_param(declared_parent_param, passed_children_params) do
60
- declared(passed_children_params, options, declared_children_params)
61
+ memo[memo_key] = handle_passed_param(passed_children_params, params_nested_path_dup) do
62
+ declared(passed_children_params, options, declared_children_params, params_nested_path_dup)
61
63
  end
62
64
  end
63
65
  else
@@ -77,19 +79,34 @@ module Grape
77
79
  end
78
80
  end
79
81
 
80
- def handle_passed_param(declared_param, passed_children_params, &_block)
81
- should_be_empty_array?(declared_param, passed_children_params) ? [] : yield
82
+ def handle_passed_param(passed_children_params, params_nested_path, &_block)
83
+ if should_be_empty_hash?(passed_children_params, params_nested_path)
84
+ {}
85
+ elsif should_be_empty_array?(passed_children_params, params_nested_path)
86
+ []
87
+ else
88
+ yield
89
+ end
82
90
  end
83
91
 
84
- def should_be_empty_array?(declared_param, passed_children_params)
85
- declared_param_is_array?(declared_param) && passed_children_params.empty?
92
+ def should_be_empty_array?(passed_children_params, params_nested_path)
93
+ passed_children_params.empty? && declared_param_is_array?(params_nested_path)
86
94
  end
87
95
 
88
- def declared_param_is_array?(declared_param)
89
- key = declared_param.to_s
96
+ def declared_param_is_array?(params_nested_path)
97
+ key = route_options_params_key(params_nested_path)
90
98
  route_options_params[key] && route_options_params[key][:type] == 'Array'
91
99
  end
92
100
 
101
+ def should_be_empty_hash?(passed_children_params, params_nested_path)
102
+ passed_children_params.empty? && declared_param_is_hash?(params_nested_path)
103
+ end
104
+
105
+ def declared_param_is_hash?(params_nested_path)
106
+ key = route_options_params_key(params_nested_path)
107
+ route_options_params[key] && route_options_params[key][:type] == 'Hash'
108
+ end
109
+
93
110
  def route_options_params
94
111
  options[:route_options][:params] || {}
95
112
  end
@@ -98,6 +115,12 @@ module Grape
98
115
  options[:stringify] ? declared_param.to_s : declared_param.to_sym
99
116
  end
100
117
 
118
+ def route_options_params_key(params_nested_path)
119
+ key = params_nested_path[0]
120
+ key += '[' + params_nested_path[1..-1].join('][') + ']' if params_nested_path.size > 1
121
+ key
122
+ end
123
+
101
124
  def optioned_declared_params(**options)
102
125
  declared_params = if options[:include_parent_namespaces]
103
126
  # Declared params including parent namespaces
@@ -7,15 +7,6 @@ module Grape
7
7
  class Router
8
8
  attr_reader :map, :compiled
9
9
 
10
- class Any < AttributeTranslator
11
- attr_reader :pattern, :index
12
- def initialize(pattern, index, **attributes)
13
- @pattern = pattern
14
- @index = index
15
- super(attributes)
16
- end
17
- end
18
-
19
10
  class NormalizePathCache < Grape::Util::Cache
20
11
  def initialize
21
12
  @cache = Hash.new do |h, path|
@@ -64,7 +55,7 @@ module Grape
64
55
 
65
56
  def associate_routes(pattern, **options)
66
57
  @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
67
- @neutral_map << Any.new(pattern, @neutral_map.length, **options)
58
+ @neutral_map << Grape::Router::AttributeTranslator.new(options.merge(pattern: pattern, index: @neutral_map.length))
68
59
  end
69
60
 
70
61
  def call(env)
@@ -6,10 +6,31 @@ module Grape
6
6
  class AttributeTranslator
7
7
  attr_reader :attributes, :request_method, :requirements
8
8
 
9
+ ROUTE_ATTRIBUTES = %i[
10
+ prefix
11
+ version
12
+ settings
13
+ format
14
+ description
15
+ http_codes
16
+ headers
17
+ entity
18
+ details
19
+ requirements
20
+ request_method
21
+ namespace
22
+ ].freeze
23
+
24
+ ROUTER_ATTRIBUTES = %i[pattern index].freeze
25
+
9
26
  def initialize(attributes = {})
10
27
  @attributes = attributes
11
- @request_method = attributes[:request_method]
12
- @requirements = attributes[:requirements]
28
+ end
29
+
30
+ (ROUTER_ATTRIBUTES + ROUTE_ATTRIBUTES).each do |attr|
31
+ define_method attr do
32
+ attributes[attr]
33
+ end
13
34
  end
14
35
 
15
36
  def to_h
@@ -18,6 +18,7 @@ module Grape
18
18
 
19
19
  extend Forwardable
20
20
  def_delegators :pattern, :path, :origin
21
+ delegate Grape::Router::AttributeTranslator::ROUTE_ATTRIBUTES => :attributes
21
22
 
22
23
  def method_missing(method_id, *arguments)
23
24
  match = ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
@@ -34,25 +35,6 @@ module Grape
34
35
  ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
35
36
  end
36
37
 
37
- %i[
38
- prefix
39
- version
40
- settings
41
- format
42
- description
43
- http_codes
44
- headers
45
- entity
46
- details
47
- requirements
48
- request_method
49
- namespace
50
- ].each do |method_name|
51
- define_method method_name do
52
- attributes.public_send method_name
53
- end
54
- end
55
-
56
38
  def route_method
57
39
  warn_route_methods(:method, caller(1).shift, :request_method)
58
40
  request_method
@@ -6,7 +6,7 @@ module Grape
6
6
  module Validations
7
7
  module Types
8
8
  # Coerces elements in an array. It might be an array of strings or integers or
9
- # anything else.
9
+ # an array of arrays of integers.
10
10
  #
11
11
  # It could've been possible to use an +of+
12
12
  # method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
@@ -14,16 +14,17 @@ module Grape
14
14
  # behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
15
15
  # maintains Virtus behavior in coercing.
16
16
  class ArrayCoercer < DryTypeCoercer
17
+ register_collection Array
18
+
17
19
  def initialize(type, strict = false)
18
20
  super
19
21
 
20
22
  @coercer = scope::Array
21
- @elem_coercer = PrimitiveCoercer.new(type.first, strict)
23
+ @subtype = type.first
22
24
  end
23
25
 
24
26
  def call(_val)
25
27
  collection = super
26
-
27
28
  return collection if collection.is_a?(InvalidValue)
28
29
 
29
30
  coerce_elements collection
@@ -31,11 +32,15 @@ module Grape
31
32
 
32
33
  protected
33
34
 
35
+ attr_reader :subtype
36
+
34
37
  def coerce_elements(collection)
38
+ return if collection.nil?
39
+
35
40
  collection.each_with_index do |elem, index|
36
41
  return InvalidValue.new if reject?(elem)
37
42
 
38
- coerced_elem = @elem_coercer.call(elem)
43
+ coerced_elem = elem_coercer.call(elem)
39
44
 
40
45
  return coerced_elem if coerced_elem.is_a?(InvalidValue)
41
46
 
@@ -45,11 +50,15 @@ module Grape
45
50
  collection
46
51
  end
47
52
 
48
- # This method maintaine logic which was defined by Virtus for arrays.
53
+ # This method maintains logic which was defined by Virtus for arrays.
49
54
  # Virtus doesn't allow nil in arrays.
50
55
  def reject?(val)
51
56
  val.nil?
52
57
  end
58
+
59
+ def elem_coercer
60
+ @elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
61
+ end
53
62
  end
54
63
  end
55
64
  end
@@ -60,12 +60,8 @@ module Grape
60
60
  Types::CustomTypeCollectionCoercer.new(
61
61
  Types.map_special(type.first), type.is_a?(Set)
62
62
  )
63
- elsif type.is_a?(Array)
64
- ArrayCoercer.new type, strict
65
- elsif type.is_a?(Set)
66
- SetCoercer.new type, strict
67
63
  else
68
- PrimitiveCoercer.new type, strict
64
+ DryTypeCoercer.coercer_instance_for(type, strict)
69
65
  end
70
66
  end
71
67
 
@@ -17,8 +17,41 @@ module Grape
17
17
  # but check its type. More information there
18
18
  # https://dry-rb.org/gems/dry-types/1.2/built-in-types/
19
19
  class DryTypeCoercer
20
+ class << self
21
+ # Registers a collection coercer which could be found by a type,
22
+ # see +collection_coercer_for+ method below. This method is meant for inheritors.
23
+ def register_collection(type)
24
+ DryTypeCoercer.collection_coercers[type] = self
25
+ end
26
+
27
+ # Returns a collection coercer which corresponds to a given type.
28
+ # Example:
29
+ #
30
+ # collection_coercer_for(Array)
31
+ # #=> Grape::Validations::Types::ArrayCoercer
32
+ def collection_coercer_for(type)
33
+ collection_coercers[type]
34
+ end
35
+
36
+ # Returns an instance of a coercer for a given type
37
+ def coercer_instance_for(type, strict = false)
38
+ return PrimitiveCoercer.new(type, strict) if type.class == Class
39
+
40
+ # in case of a collection (Array[Integer]) the type is an instance of a collection,
41
+ # so we need to figure out the actual type
42
+ collection_coercer_for(type.class).new(type, strict)
43
+ end
44
+
45
+ protected
46
+
47
+ def collection_coercers
48
+ @collection_coercers ||= {}
49
+ end
50
+ end
51
+
20
52
  def initialize(type, strict = false)
21
53
  @type = type
54
+ @strict = strict
22
55
  @scope = strict ? DryTypes::Strict : DryTypes::Params
23
56
  end
24
57
 
@@ -27,6 +60,8 @@ module Grape
27
60
  #
28
61
  # @param val [Object]
29
62
  def call(val)
63
+ return if val.nil?
64
+
30
65
  @coercer[val]
31
66
  rescue Dry::Types::CoercionError => _e
32
67
  InvalidValue.new
@@ -34,7 +69,7 @@ module Grape
34
69
 
35
70
  protected
36
71
 
37
- attr_reader :scope, :type
72
+ attr_reader :scope, :type, :strict
38
73
  end
39
74
  end
40
75
  end
@@ -36,7 +36,7 @@ module Grape
36
36
 
37
37
  def call(val)
38
38
  return InvalidValue.new if reject?(val)
39
- return nil if val.nil?
39
+ return nil if val.nil? || treat_as_nil?(val)
40
40
  return '' if val == ''
41
41
 
42
42
  super
@@ -46,7 +46,7 @@ module Grape
46
46
 
47
47
  attr_reader :type
48
48
 
49
- # This method maintaine logic which was defined by Virtus. For example,
49
+ # This method maintains logic which was defined by Virtus. For example,
50
50
  # dry-types is ok to convert an array or a hash to a string, it is supported,
51
51
  # but Virtus wouldn't accept it. So, this method only exists to not introduce
52
52
  # breaking changes.
@@ -55,6 +55,13 @@ module Grape
55
55
  (val.is_a?(String) && type == Hash) ||
56
56
  (val.is_a?(Hash) && type == String)
57
57
  end
58
+
59
+ # Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
60
+ # absence of a value and coerces it into nil. See a discussion there
61
+ # https://github.com/ruby-grape/grape/pull/2045
62
+ def treat_as_nil?(val)
63
+ val == '' && type == Grape::API::Boolean
64
+ end
58
65
  end
59
66
  end
60
67
  end
@@ -1,18 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'set'
4
- require_relative 'dry_type_coercer'
4
+ require_relative 'array_coercer'
5
5
 
6
6
  module Grape
7
7
  module Validations
8
8
  module Types
9
9
  # Takes the given array and converts it to a set. Every element of the set
10
10
  # is also coerced.
11
- class SetCoercer < DryTypeCoercer
11
+ class SetCoercer < ArrayCoercer
12
+ register_collection Set
13
+
12
14
  def initialize(type, strict = false)
13
15
  super
14
16
 
15
- @elem_coercer = PrimitiveCoercer.new(type.first, strict)
17
+ @coercer = nil
16
18
  end
17
19
 
18
20
  def call(value)
@@ -25,7 +27,7 @@ module Grape
25
27
 
26
28
  def coerce_elements(collection)
27
29
  collection.each_with_object(Set.new) do |elem, memo|
28
- coerced_elem = @elem_coercer.call(elem)
30
+ coerced_elem = elem_coercer.call(elem)
29
31
 
30
32
  return coerced_elem if coerced_elem.is_a?(InvalidValue)
31
33
 
@@ -33,7 +33,7 @@ module Grape
33
33
  # the coerced result, or an instance
34
34
  # of {InvalidValue} if the value could not be coerced.
35
35
  def call(value)
36
- return InvalidValue.new unless value.is_a? Array
36
+ return unless value.is_a? Array
37
37
 
38
38
  value =
39
39
  if @method
@@ -67,21 +67,12 @@ module Grape
67
67
  end
68
68
 
69
69
  def coerce_value(val)
70
- val.nil? ? coerce_nil(val) : converter.call(val)
70
+ converter.call(val)
71
71
  # Some custom types might fail, so it should be treated as an invalid value
72
72
  rescue StandardError
73
73
  Types::InvalidValue.new
74
74
  end
75
75
 
76
- def coerce_nil(val)
77
- # define default values for structures, the dry-types lib which is used
78
- # for coercion doesn't accept nil as a value, so it would fail
79
- return [] if type == Array
80
- return Set.new if type == Set
81
- return {} if type == Hash
82
- val
83
- end
84
-
85
76
  # Type to which the parameter will be coerced.
86
77
  #
87
78
  # @return [Class]
@@ -9,7 +9,6 @@ module Grape
9
9
  end
10
10
 
11
11
  def validate_param!(attr_name, params)
12
- return if params.key? attr_name
13
12
  params[attr_name] = if @default.is_a? Proc
14
13
  @default.call
15
14
  elsif @default.frozen? || !duplicatable?(@default)
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.3.2'
5
+ VERSION = '1.3.3'
6
6
  end
@@ -51,4 +51,54 @@ describe Grape::API::Instance do
51
51
  expect(an_instance.top_level_setting.parent).to be_nil
52
52
  end
53
53
  end
54
+
55
+ context 'with multiple moutes' do
56
+ let(:first) do
57
+ Class.new(Grape::API::Instance) do
58
+ namespace(:some_namespace) do
59
+ route :any, '*path' do
60
+ error!('Not found! (1)', 404)
61
+ end
62
+ end
63
+ end
64
+ end
65
+ let(:second) do
66
+ Class.new(Grape::API::Instance) do
67
+ namespace(:another_namespace) do
68
+ route :any, '*path' do
69
+ error!('Not found! (2)', 404)
70
+ end
71
+ end
72
+ end
73
+ end
74
+ let(:root_api) do
75
+ first_instance = first
76
+ second_instance = second
77
+ Class.new(Grape::API) do
78
+ mount first_instance
79
+ mount first_instance
80
+ mount second_instance
81
+ end
82
+ end
83
+
84
+ it 'does not raise a FrozenError on first instance' do
85
+ expect { patch '/some_namespace/anything' }.not_to \
86
+ raise_error
87
+ end
88
+
89
+ it 'responds the correct body at the first instance' do
90
+ patch '/some_namespace/anything'
91
+ expect(last_response.body).to eq 'Not found! (1)'
92
+ end
93
+
94
+ it 'does not raise a FrozenError on second instance' do
95
+ expect { get '/another_namespace/other' }.not_to \
96
+ raise_error
97
+ end
98
+
99
+ it 'responds the correct body at the second instance' do
100
+ get '/another_namespace/foobar'
101
+ expect(last_response.body).to eq 'Not found! (2)'
102
+ end
103
+ end
54
104
  end
@@ -296,9 +296,12 @@ describe Grape::Endpoint do
296
296
  optional :seventh
297
297
  end
298
298
  end
299
+ optional :nested_arr, type: Array do
300
+ optional :eighth
301
+ end
299
302
  end
300
- optional :nested_arr, type: Array do
301
- optional :eighth
303
+ optional :arr, type: Array do
304
+ optional :nineth
302
305
  end
303
306
  end
304
307
  end
@@ -390,7 +393,7 @@ describe Grape::Endpoint do
390
393
 
391
394
  get '/declared?first=present&nested[fourth]=1'
392
395
  expect(last_response.status).to eq(200)
393
- expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 3
396
+ expect(JSON.parse(last_response.body)['nested'].keys.size).to eq 4
394
397
  end
395
398
 
396
399
  it 'builds nested params when given array' do
@@ -421,7 +424,7 @@ describe Grape::Endpoint do
421
424
 
422
425
  get '/declared?first=present'
423
426
  expect(last_response.status).to eq(200)
424
- expect(JSON.parse(last_response.body)['nested']).to be_a(Hash)
427
+ expect(JSON.parse(last_response.body)['nested']).to eq({})
425
428
  end
426
429
 
427
430
  it 'to be an array when include_missing is true' do
@@ -431,7 +434,17 @@ describe Grape::Endpoint do
431
434
 
432
435
  get '/declared?first=present'
433
436
  expect(last_response.status).to eq(200)
434
- expect(JSON.parse(last_response.body)['nested_arr']).to be_a(Array)
437
+ expect(JSON.parse(last_response.body)['arr']).to be_a(Array)
438
+ end
439
+
440
+ it 'to be an array when nested and include_missing is true' do
441
+ subject.get '/declared' do
442
+ declared(params, include_missing: true)
443
+ end
444
+
445
+ get '/declared?first=present&nested[fourth]=1'
446
+ expect(last_response.status).to eq(200)
447
+ expect(JSON.parse(last_response.body)['nested']['nested_arr']).to be_a(Array)
435
448
  end
436
449
 
437
450
  it 'to be nil when include_missing is false' do
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::ArrayCoercer do
6
+ subject { described_class.new(type) }
7
+
8
+ describe '#call' do
9
+ context 'an array of primitives' do
10
+ let(:type) { Array[String] }
11
+
12
+ it 'coerces elements in the array' do
13
+ expect(subject.call([10, 20])).to eq(%w[10 20])
14
+ end
15
+ end
16
+
17
+ context 'an array of arrays' do
18
+ let(:type) { Array[Array[Integer]] }
19
+
20
+ it 'coerces elements in the nested array' do
21
+ expect(subject.call([%w[10 20]])).to eq([[10, 20]])
22
+ expect(subject.call([['10'], ['20']])).to eq([[10], [20]])
23
+ end
24
+ end
25
+
26
+ context 'an array of sets' do
27
+ let(:type) { Array[Set[Integer]] }
28
+
29
+ it 'coerces elements in the nested set' do
30
+ expect(subject.call([%w[10 20]])).to eq([Set[10, 20]])
31
+ expect(subject.call([['10'], ['20']])).to eq([Set[10], Set[20]])
32
+ end
33
+ end
34
+ end
35
+ end
@@ -7,7 +7,7 @@ describe Grape::Validations::Types::PrimitiveCoercer do
7
7
 
8
8
  subject { described_class.new(type, strict) }
9
9
 
10
- describe '.call' do
10
+ describe '#call' do
11
11
  context 'Boolean' do
12
12
  let(:type) { Grape::API::Boolean }
13
13
 
@@ -26,6 +26,10 @@ describe Grape::Validations::Types::PrimitiveCoercer do
26
26
  it 'returns an error when the given value cannot be coerced' do
27
27
  expect(subject.call(123)).to be_instance_of(Grape::Validations::Types::InvalidValue)
28
28
  end
29
+
30
+ it 'coerces an empty string to nil' do
31
+ expect(subject.call('')).to be_nil
32
+ end
29
33
  end
30
34
 
31
35
  context 'String' do
@@ -0,0 +1,34 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'spec_helper'
4
+
5
+ describe Grape::Validations::Types::SetCoercer do
6
+ subject { described_class.new(type) }
7
+
8
+ describe '#call' do
9
+ context 'a set of primitives' do
10
+ let(:type) { Set[String] }
11
+
12
+ it 'coerces elements to the set' do
13
+ expect(subject.call([10, 20])).to eq(Set['10', '20'])
14
+ end
15
+ end
16
+
17
+ context 'a set of sets' do
18
+ let(:type) { Set[Set[Integer]] }
19
+
20
+ it 'coerces elements in the nested set' do
21
+ expect(subject.call([%w[10 20]])).to eq(Set[Set[10, 20]])
22
+ expect(subject.call([['10'], ['20']])).to eq(Set[Set[10], Set[20]])
23
+ end
24
+ end
25
+
26
+ context 'a set of sets of arrays' do
27
+ let(:type) { Set[Set[Array[Integer]]] }
28
+
29
+ it 'coerces elements in the nested set' do
30
+ expect(subject.call([[['10'], ['20']]])).to eq(Set[Set[Array[10], Array[20]]])
31
+ end
32
+ end
33
+ end
34
+ end
@@ -424,6 +424,79 @@ describe Grape::Validations::CoerceValidator do
424
424
  expect(last_response.status).to eq(200)
425
425
  expect(last_response.body).to eq(integer_class_name)
426
426
  end
427
+
428
+ context 'nil values' do
429
+ context 'primitive types' do
430
+ Grape::Validations::Types::PRIMITIVES.each do |type|
431
+ it 'respects the nil value' do
432
+ subject.params do
433
+ requires :param, type: type
434
+ end
435
+ subject.get '/nil_value' do
436
+ params[:param].class
437
+ end
438
+
439
+ get '/nil_value', param: nil
440
+ expect(last_response.status).to eq(200)
441
+ expect(last_response.body).to eq('NilClass')
442
+ end
443
+ end
444
+ end
445
+
446
+ context 'structures types' do
447
+ Grape::Validations::Types::STRUCTURES.each do |type|
448
+ it 'respects the nil value' do
449
+ subject.params do
450
+ requires :param, type: type
451
+ end
452
+ subject.get '/nil_value' do
453
+ params[:param].class
454
+ end
455
+
456
+ get '/nil_value', param: nil
457
+ expect(last_response.status).to eq(200)
458
+ expect(last_response.body).to eq('NilClass')
459
+ end
460
+ end
461
+ end
462
+
463
+ context 'special types' do
464
+ Grape::Validations::Types::SPECIAL.each_key do |type|
465
+ it 'respects the nil value' do
466
+ subject.params do
467
+ requires :param, type: type
468
+ end
469
+ subject.get '/nil_value' do
470
+ params[:param].class
471
+ end
472
+
473
+ get '/nil_value', param: nil
474
+ expect(last_response.status).to eq(200)
475
+ expect(last_response.body).to eq('NilClass')
476
+ end
477
+ end
478
+
479
+ context 'variant-member-type collections' do
480
+ [
481
+ Array[Integer, String],
482
+ [Integer, String, Array[Integer, String]]
483
+ ].each do |type|
484
+ it 'respects the nil value' do
485
+ subject.params do
486
+ requires :param, type: type
487
+ end
488
+ subject.get '/nil_value' do
489
+ params[:param].class
490
+ end
491
+
492
+ get '/nil_value', param: nil
493
+ expect(last_response.status).to eq(200)
494
+ expect(last_response.body).to eq('NilClass')
495
+ end
496
+ end
497
+ end
498
+ end
499
+ end
427
500
  end
428
501
 
429
502
  context 'using coerce_with' do
@@ -752,19 +825,6 @@ describe Grape::Validations::CoerceValidator do
752
825
  expect(last_response.body).to eq('String')
753
826
  end
754
827
 
755
- it 'respects nil values' do
756
- subject.params do
757
- optional :a, types: [File, String]
758
- end
759
- subject.get '/' do
760
- params[:a].class.to_s
761
- end
762
-
763
- get '/', a: nil
764
- expect(last_response.status).to eq(200)
765
- expect(last_response.body).to eq('NilClass')
766
- end
767
-
768
828
  it 'fails when no coercion is possible' do
769
829
  subject.params do
770
830
  requires :a, types: [Boolean, Integer]
@@ -298,4 +298,125 @@ describe Grape::Validations::DefaultValidator do
298
298
  end
299
299
  end
300
300
  end
301
+
302
+ context 'optional with nil as value' do
303
+ subject do
304
+ Class.new(Grape::API) do
305
+ default_format :json
306
+ end
307
+ end
308
+
309
+ def app
310
+ subject
311
+ end
312
+
313
+ context 'primitive types' do
314
+ [
315
+ [Integer, 0],
316
+ [Integer, 42],
317
+ [Float, 0.0],
318
+ [Float, 4.2],
319
+ [BigDecimal, 0.0],
320
+ [BigDecimal, 4.2],
321
+ [Numeric, 0],
322
+ [Numeric, 42],
323
+ [Date, Date.today],
324
+ [DateTime, DateTime.now],
325
+ [Time, Time.now],
326
+ [Time, Time.at(0)],
327
+ [Grape::API::Boolean, false],
328
+ [String, ''],
329
+ [String, 'non-empty-string'],
330
+ [Symbol, :symbol],
331
+ [TrueClass, true],
332
+ [FalseClass, false]
333
+ ].each do |type, default|
334
+ it 'respects the default value' do
335
+ subject.params do
336
+ optional :param, type: type, default: default
337
+ end
338
+ subject.get '/default_value' do
339
+ params[:param]
340
+ end
341
+
342
+ get '/default_value', param: nil
343
+ expect(last_response.status).to eq(200)
344
+ expect(last_response.body).to eq(default.to_json)
345
+ end
346
+ end
347
+ end
348
+
349
+ context 'structures types' do
350
+ [
351
+ [Hash, {}],
352
+ [Hash, { test: 'non-empty' }],
353
+ [Array, []],
354
+ [Array, ['non-empty']],
355
+ [Array[Integer], []],
356
+ [Set, []],
357
+ [Set, [1]]
358
+ ].each do |type, default|
359
+ it 'respects the default value' do
360
+ subject.params do
361
+ optional :param, type: type, default: default
362
+ end
363
+ subject.get '/default_value' do
364
+ params[:param]
365
+ end
366
+
367
+ get '/default_value', param: nil
368
+ expect(last_response.status).to eq(200)
369
+ expect(last_response.body).to eq(default.to_json)
370
+ end
371
+ end
372
+ end
373
+
374
+ context 'special types' do
375
+ [
376
+ [JSON, ''],
377
+ [JSON, { test: 'non-empty-string' }.to_json],
378
+ [Array[JSON], []],
379
+ [Array[JSON], [{ test: 'non-empty-string' }.to_json]],
380
+ [::File, ''],
381
+ [::File, { test: 'non-empty-string' }.to_json],
382
+ [Rack::Multipart::UploadedFile, ''],
383
+ [Rack::Multipart::UploadedFile, { test: 'non-empty-string' }.to_json]
384
+ ].each do |type, default|
385
+ it 'respects the default value' do
386
+ subject.params do
387
+ optional :param, type: type, default: default
388
+ end
389
+ subject.get '/default_value' do
390
+ params[:param]
391
+ end
392
+
393
+ get '/default_value', param: nil
394
+ expect(last_response.status).to eq(200)
395
+ expect(last_response.body).to eq(default.to_json)
396
+ end
397
+ end
398
+ end
399
+
400
+ context 'variant-member-type collections' do
401
+ [
402
+ [Array[Integer, String], [0, '']],
403
+ [Array[Integer, String], [42, 'non-empty-string']],
404
+ [[Integer, String, Array[Integer, String]], [0, '', [0, '']]],
405
+ [[Integer, String, Array[Integer, String]], [42, 'non-empty-string', [42, 'non-empty-string']]]
406
+ ].each do |type, default|
407
+ it 'respects the default value' do
408
+ subject.params do
409
+ optional :param, type: type, default: default
410
+ end
411
+ subject.get '/default_value' do
412
+ params[:param]
413
+ end
414
+
415
+ get '/default_value', param: nil
416
+ expect(last_response.status).to eq(200)
417
+ expect(last_response.body).to eq(default.to_json)
418
+ end
419
+ end
420
+ end
421
+ end
301
422
  end
@@ -319,7 +319,7 @@ describe Grape::Validations::ValuesValidator do
319
319
  expect(last_response.status).to eq 200
320
320
  end
321
321
 
322
- it 'allows for an optional param with a list of values' do
322
+ it 'accepts for an optional param with a list of values' do
323
323
  put('/optional_with_array_of_string_values', optional: nil)
324
324
  expect(last_response.status).to eq 200
325
325
  end
@@ -574,7 +574,7 @@ describe Grape::Validations do
574
574
  # NOTE: with body parameters in json or XML or similar this
575
575
  # should actually fail with: children[parents][name] is missing.
576
576
  expect(last_response.status).to eq(400)
577
- expect(last_response.body).to eq('children[1][parents] is missing')
577
+ expect(last_response.body).to eq('children[1][parents] is missing, children[0][parents][1][name] is missing, children[0][parents][1][name] is empty')
578
578
  end
579
579
 
580
580
  it 'errors when a parameter is not present in array within array' do
@@ -615,7 +615,7 @@ describe Grape::Validations do
615
615
 
616
616
  get '/within_array', children: [name: 'Jay']
617
617
  expect(last_response.status).to eq(400)
618
- expect(last_response.body).to eq('children[0][parents] is missing')
618
+ expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing, children[0][parents][0][name] is empty')
619
619
  end
620
620
 
621
621
  it 'errors when param is not an Array' do
@@ -763,7 +763,7 @@ describe Grape::Validations do
763
763
  expect(last_response.status).to eq(200)
764
764
  put_with_json '/within_array', children: [name: 'Jay']
765
765
  expect(last_response.status).to eq(400)
766
- expect(last_response.body).to eq('children[0][parents] is missing')
766
+ expect(last_response.body).to eq('children[0][parents] is missing, children[0][parents][0][name] is missing')
767
767
  end
768
768
  end
769
769
 
@@ -838,7 +838,7 @@ describe Grape::Validations do
838
838
  it 'does internal validations if the outer group is present' do
839
839
  get '/nested_optional_group', items: [{ key: 'foo' }]
840
840
  expect(last_response.status).to eq(400)
841
- expect(last_response.body).to eq('items[0][required_subitems] is missing')
841
+ expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing')
842
842
 
843
843
  get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
844
844
  expect(last_response.status).to eq(200)
@@ -858,7 +858,7 @@ describe Grape::Validations do
858
858
  it 'handles validation within arrays' do
859
859
  get '/nested_optional_group', items: [{ key: 'foo' }]
860
860
  expect(last_response.status).to eq(400)
861
- expect(last_response.body).to eq('items[0][required_subitems] is missing')
861
+ expect(last_response.body).to eq('items[0][required_subitems] is missing, items[0][required_subitems][0][value] is missing')
862
862
 
863
863
  get '/nested_optional_group', items: [{ key: 'foo', required_subitems: [{ value: 'bar' }] }]
864
864
  expect(last_response.status).to eq(200)
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: grape
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.3.2
4
+ version: 1.3.3
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michael Bleigh
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2020-04-12 00:00:00.000000000 Z
11
+ date: 2020-05-23 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -332,7 +332,9 @@ files:
332
332
  - spec/grape/validations/multiple_attributes_iterator_spec.rb
333
333
  - spec/grape/validations/params_scope_spec.rb
334
334
  - spec/grape/validations/single_attribute_iterator_spec.rb
335
+ - spec/grape/validations/types/array_coercer_spec.rb
335
336
  - spec/grape/validations/types/primitive_coercer_spec.rb
337
+ - spec/grape/validations/types/set_coercer_spec.rb
336
338
  - spec/grape/validations/types_spec.rb
337
339
  - spec/grape/validations/validators/all_or_none_spec.rb
338
340
  - spec/grape/validations/validators/allow_blank_spec.rb
@@ -364,9 +366,9 @@ licenses:
364
366
  - MIT
365
367
  metadata:
366
368
  bug_tracker_uri: https://github.com/ruby-grape/grape/issues
367
- changelog_uri: https://github.com/ruby-grape/grape/blob/v1.3.2/CHANGELOG.md
368
- documentation_uri: https://www.rubydoc.info/gems/grape/1.3.2
369
- source_code_uri: https://github.com/ruby-grape/grape/tree/v1.3.2
369
+ changelog_uri: https://github.com/ruby-grape/grape/blob/v1.3.3/CHANGELOG.md
370
+ documentation_uri: https://www.rubydoc.info/gems/grape/1.3.3
371
+ source_code_uri: https://github.com/ruby-grape/grape/tree/v1.3.3
370
372
  post_install_message:
371
373
  rdoc_options: []
372
374
  require_paths:
@@ -415,6 +417,8 @@ test_files:
415
417
  - spec/grape/api_remount_spec.rb
416
418
  - spec/grape/validations/types_spec.rb
417
419
  - spec/grape/validations/attributes_iterator_spec.rb
420
+ - spec/grape/validations/types/array_coercer_spec.rb
421
+ - spec/grape/validations/types/set_coercer_spec.rb
418
422
  - spec/grape/validations/types/primitive_coercer_spec.rb
419
423
  - spec/grape/validations/validators/regexp_spec.rb
420
424
  - spec/grape/validations/validators/default_spec.rb