grape 1.3.2 → 1.3.3

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: 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