grape 1.3.1 → 1.3.2

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: 1711dd2fb0f0c86757c7e73d780907efa87e3da54e0a999a4b45c276b5eec92c
4
- data.tar.gz: f5ba49001d1816d92130f70fb1b63ae061d7d9250142605f706d75c207880ad9
3
+ metadata.gz: a1ba205b72fd823b11e700753891784fdfbd6791f591b790a99ac3433cb8fe23
4
+ data.tar.gz: '09e64a0d93415b28fdb179fbb5d7ef0d03e00dcb976fd885c0c655d6ac041345'
5
5
  SHA512:
6
- metadata.gz: 8b2dcf4d4903e3923dd10086a3371258404756cf294cd42c6fb64f3d2520fe7243c0accfe1b68f2e02fbaa85f29396c161d830cde4c5fdedd660b31e8d223a7f
7
- data.tar.gz: 88a4d5e9740495430cd28ed1e213d6aafd92895c1b7966bd6358dd6aeb5b2e1f16eac2e14834d63e7791a6d66e16475ef30cdef5e37d10bca0f38f1d1a22c1e4
6
+ metadata.gz: 06fcd2157bcfcc6e71876df09b34a318c600761b79fb99ad77739521d056950ee8391dfd050efd43618f7f99f11990623e276b32d15a27e92015ec3a56c6c08b
7
+ data.tar.gz: 5463bbd0347950765c9d92e859965e90dd69d7d1f781fd188386cf39597c196f5ba2dc697dbee4f896a70f6498c1e43034a33db8460ea9a719218a7d5df46d6f
@@ -1,3 +1,19 @@
1
+ ### 1.3.2 (2020/04/12)
2
+
3
+ #### Features
4
+ * [#2020](https://github.com/ruby-grape/grape/pull/2020): Reduce array allocation - [@ericproulx](https://github.com/ericproulx).
5
+ * [#2015](https://github.com/ruby-grape/grape/pull/2014): Reduce MatchData allocation - [@ericproulx](https://github.com/ericproulx).
6
+ * [#2014](https://github.com/ruby-grape/grape/pull/2014): Reduce total allocated arrays - [@ericproulx](https://github.com/ericproulx).
7
+ * [#2011](https://github.com/ruby-grape/grape/pull/2011): Reduce total retained regexes - [@ericproulx](https://github.com/ericproulx).
8
+
9
+ #### Fixes
10
+
11
+ * [#2033](https://github.com/ruby-grape/grape/pull/2033): Ensure `Float` params are correctly coerced to `BigDecimal` - [@tlconnor](https://github.com/tlconnor).
12
+ * [#2031](https://github.com/ruby-grape/grape/pull/2031): Fix a regression with an array of a custom type - [@dnesteryuk](https://github.com/dnesteryuk).
13
+ * [#2026](https://github.com/ruby-grape/grape/pull/2026): Fix a regression in `coerce_with` when coercion returns `nil` - [@misdoro](https://github.com/misdoro).
14
+ * [#2025](https://github.com/ruby-grape/grape/pull/2025): Fix Decimal type category - [@kdoya](https://github.com/kdoya).
15
+ * [#2019](https://github.com/ruby-grape/grape/pull/2019): Avoid coercing parameter with multiple types to an empty Array - [@stanhu](https://github.com/stanhu).
16
+
1
17
  ### 1.3.1 (2020/03/11)
2
18
 
3
19
  #### Features
data/README.md CHANGED
@@ -154,7 +154,7 @@ content negotiation, versioning and much more.
154
154
 
155
155
  ## Stable Release
156
156
 
157
- You're reading the documentation for the stable release of Grape, **1.3.1**.
157
+ You're reading the documentation for the stable release of Grape, 1.3.2.
158
158
 
159
159
  ## Project Resources
160
160
 
@@ -3187,14 +3187,13 @@ applies to the current namespace and any children, but not parents.
3187
3187
  ```ruby
3188
3188
  http_basic do |username, password|
3189
3189
  # verify user's password here
3190
- { 'test' => 'password1' }[username] == password
3190
+ # IMPORTANT: make sure you use a comparison method which isn't prone to a timing attack
3191
3191
  end
3192
3192
  ```
3193
3193
 
3194
3194
  ```ruby
3195
3195
  http_digest({ realm: 'Test Api', opaque: 'app secret' }) do |username|
3196
3196
  # lookup the user's password here
3197
- { 'user1' => 'password1' }[username]
3198
3197
  end
3199
3198
  ```
3200
3199
 
@@ -9,38 +9,83 @@ After adding dry-types, Ruby 2.4 or newer is required.
9
9
 
10
10
  #### Coercion
11
11
 
12
- [Virtus](https://github.com/solnic/virtus) has been replaced by [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter coercion. If your project depends on Virtus, explicitly add it to your `Gemfile`. Also, if Virtus is used for defining custom types
12
+ [Virtus](https://github.com/solnic/virtus) has been replaced by
13
+ [dry-types](https://dry-rb.org/gems/dry-types/1.2/) for parameter
14
+ coercion. If your project depends on Virtus outside of Grape, explicitly
15
+ add it to your `Gemfile`.
16
+
17
+ Here's an example of how to migrate a custom type from Virtus to dry-types:
13
18
 
14
19
  ```ruby
15
- class User
16
- include Virtus.model
20
+ # Legacy Grape parser
21
+ class SecureUriType < Virtus::Attribute
22
+ def coerce(input)
23
+ URI.parse value
24
+ end
17
25
 
18
- attribute :id, Integer
19
- attribute :name, String
26
+ def value_coerced?(input)
27
+ value.is_a? String
28
+ end
20
29
  end
21
30
 
22
- # somewhere in your API
23
31
  params do
24
- requires :user, type: User
32
+ requires :secure_uri, type: SecureUri
25
33
  end
26
34
  ```
27
35
 
28
- Add a class-level `parse` method to the model:
36
+ To use dry-types, we need to:
29
37
 
30
- ```ruby
31
- class User
32
- include Virtus.model
38
+ 1. Remove the inheritance of `Virtus::Attribute`
39
+ 1. Rename `coerce` to `self.parse`
40
+ 1. Rename `value_coerced?` to `self.parsed?`
33
41
 
34
- attribute :id, Integer
35
- attribute :name, String
42
+ The custom type must have a class-level `parse` method to the model. A
43
+ class-level `parsed?` is needed if the parsed type differs from the
44
+ defined type. In the example below, since `SecureUri` is not the same
45
+ as `URI::HTTPS`, `self.parsed?` is needed:
36
46
 
37
- def self.parse(attrs)
38
- new(attrs)
47
+ ```ruby
48
+ # New dry-types parser
49
+ class SecureUri
50
+ def self.parse(value)
51
+ URI.parse value
52
+ end
53
+
54
+ def self.parsed?(value)
55
+ value.is_a? URI::HTTPS
39
56
  end
40
57
  end
58
+
59
+ params do
60
+ requires :secure_uri, type: SecureUri
61
+ end
62
+ ```
63
+
64
+ #### Ensure that Array types have explicit coercions
65
+
66
+ Unlike Virtus, dry-types does not perform any implict coercions. If you
67
+ have any uses of `Array[String]`, `Array[Integer]`, etc. be sure they
68
+ use a `coerce_with` block. For example:
69
+
70
+ ```ruby
71
+ requires :values, type: Array[String]
41
72
  ```
42
73
 
43
- Custom types which don't depend on Virtus don't require any changes.
74
+ It's quite common to pass a comma-separated list, such as `tag1,tag2` as
75
+ `values`. Previously Virtus would implicitly coerce this to
76
+ `Array(values)` so that `["tag1,tag2"]` would pass the type checks, but
77
+ with `dry-types` the values are no longer coerced for you. To fix this,
78
+ you might do:
79
+
80
+ ```ruby
81
+ requires :values, type: Array[String], coerce_with: ->(val) { val.split(',').map(&:strip) }
82
+ ```
83
+
84
+ Likewise, for `Array[Integer]`, you might do:
85
+
86
+ ```ruby
87
+ requires :values, type: Array[Integer], coerce_with: ->(val) { val.split(',').map(&:strip).map(&:to_i) }
88
+ ```
44
89
 
45
90
  For more information see [#1920](https://github.com/ruby-grape/grape/pull/1920).
46
91
 
@@ -243,9 +243,13 @@ module Grape
243
243
  # Generate a route that returns an HTTP 405 response for a user defined
244
244
  # path on methods not specified
245
245
  def generate_not_allowed_method(pattern, allowed_methods: [], **attributes)
246
- not_allowed_methods = %w[GET PUT POST DELETE PATCH HEAD] - allowed_methods
247
- not_allowed_methods << Grape::Http::Headers::OPTIONS if self.class.namespace_inheritable(:do_not_route_options)
248
-
246
+ supported_methods =
247
+ if self.class.namespace_inheritable(:do_not_route_options)
248
+ Grape::Http::Headers::SUPPORTED_METHODS
249
+ else
250
+ Grape::Http::Headers::SUPPORTED_METHODS_WITHOUT_OPTIONS
251
+ end
252
+ not_allowed_methods = supported_methods - allowed_methods
249
253
  return if not_allowed_methods.empty?
250
254
 
251
255
  @router.associate_routes(pattern, not_allowed_methods: not_allowed_methods, **attributes)
@@ -21,6 +21,7 @@ module Grape
21
21
  OPTIONS = 'OPTIONS'
22
22
 
23
23
  SUPPORTED_METHODS = [GET, POST, PUT, PATCH, DELETE, HEAD, OPTIONS].freeze
24
+ SUPPORTED_METHODS_WITHOUT_OPTIONS = Grape::Util::LazyObject.new { [GET, POST, PUT, PATCH, DELETE, HEAD].freeze }
24
25
 
25
26
  HTTP_ACCEPT_VERSION = 'HTTP_ACCEPT_VERSION'
26
27
  X_CASCADE = 'X-Cascade'
@@ -63,7 +63,7 @@ module Grape
63
63
 
64
64
  def an_accept_header_with_version_and_vendor_is_present?
65
65
  header.qvalues.keys.any? do |h|
66
- VENDOR_VERSION_HEADER_REGEX =~ h.sub('application/', '')
66
+ VENDOR_VERSION_HEADER_REGEX.match?(h.sub('application/', ''))
67
67
  end
68
68
  end
69
69
 
@@ -3,11 +3,12 @@
3
3
  module Rack
4
4
  module Accept
5
5
  module Header
6
+ ALLOWED_CHARACTERS = %r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$}.freeze
6
7
  class << self
7
8
  # Corrected version of https://github.com/mjackson/rack-accept/blob/master/lib/rack/accept/header.rb#L40-L44
8
9
  def parse_media_type(media_type)
9
10
  # see http://tools.ietf.org/html/rfc6838#section-4.2 for allowed characters in media type names
10
- m = media_type.to_s.match(%r{^([a-z*]+)\/([a-z0-9*\&\^\-_#\$!.+]+)(?:;([a-z0-9=;]+))?$})
11
+ m = media_type&.match(ALLOWED_CHARACTERS)
11
12
  m ? [m[1], m[2], m[3] || ''] : []
12
13
  end
13
14
  end
@@ -42,11 +42,11 @@ module Grape
42
42
  end
43
43
 
44
44
  def namespace?
45
- namespace && namespace.to_s =~ /^\S/ && namespace != '/'
45
+ namespace&.match?(/^\S/) && namespace != '/'
46
46
  end
47
47
 
48
48
  def path?
49
- raw_path && raw_path.to_s =~ /^\S/ && raw_path != '/'
49
+ raw_path&.match?(/^\S/) && raw_path != '/'
50
50
  end
51
51
 
52
52
  def suffix
@@ -8,10 +8,9 @@ module Grape
8
8
  attr_reader :map, :compiled
9
9
 
10
10
  class Any < AttributeTranslator
11
- attr_reader :pattern, :regexp, :index
12
- def initialize(pattern, regexp, index, **attributes)
11
+ attr_reader :pattern, :index
12
+ def initialize(pattern, index, **attributes)
13
13
  @pattern = pattern
14
- @regexp = regexp
15
14
  @index = index
16
15
  super(attributes)
17
16
  end
@@ -39,18 +38,20 @@ module Grape
39
38
 
40
39
  def initialize
41
40
  @neutral_map = []
41
+ @neutral_regexes = []
42
42
  @map = Hash.new { |hash, key| hash[key] = [] }
43
43
  @optimized_map = Hash.new { |hash, key| hash[key] = // }
44
44
  end
45
45
 
46
46
  def compile!
47
47
  return if compiled
48
- @union = Regexp.union(@neutral_map.map(&:regexp))
48
+ @union = Regexp.union(@neutral_regexes)
49
+ @neutral_regexes = nil
49
50
  self.class.supported_methods.each do |method|
50
51
  routes = map[method]
51
52
  @optimized_map[method] = routes.map.with_index do |route, index|
52
53
  route.index = index
53
- route.regexp = Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
54
+ Regexp.new("(?<_#{index}>#{route.pattern.to_regexp})")
54
55
  end
55
56
  @optimized_map[method] = Regexp.union(@optimized_map[method])
56
57
  end
@@ -62,8 +63,8 @@ module Grape
62
63
  end
63
64
 
64
65
  def associate_routes(pattern, **options)
65
- regexp = Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
66
- @neutral_map << Any.new(pattern, regexp, @neutral_map.length, **options)
66
+ @neutral_regexes << Regexp.new("(?<_#{@neutral_map.length}>)#{pattern.to_regexp}")
67
+ @neutral_map << Any.new(pattern, @neutral_map.length, **options)
67
68
  end
68
69
 
69
70
  def call(env)
@@ -12,7 +12,7 @@ module Grape
12
12
  SOURCE_LOCATION_REGEXP = /^(.*?):(\d+?)(?::in `.+?')?$/.freeze
13
13
  FIXED_NAMED_CAPTURES = %w[format version].freeze
14
14
 
15
- attr_accessor :pattern, :translator, :app, :index, :regexp, :options
15
+ attr_accessor :pattern, :translator, :app, :index, :options
16
16
 
17
17
  alias attributes translator
18
18
 
@@ -31,7 +31,7 @@ module Grape
31
31
  end
32
32
 
33
33
  def respond_to_missing?(method_id, _)
34
- ROUTE_ATTRIBUTE_REGEXP.match(method_id.to_s)
34
+ ROUTE_ATTRIBUTE_REGEXP.match?(method_id.to_s)
35
35
  end
36
36
 
37
37
  %i[
@@ -67,7 +67,6 @@ module Grape
67
67
  method_s = method.to_s
68
68
  method_upcase = Grape::Http::Headers.find_supported_method(method_s) || method_s.upcase
69
69
 
70
- @suffix = options[:suffix]
71
70
  @options = options.merge(method: method_upcase)
72
71
  @pattern = Pattern.new(pattern, **options)
73
72
  @translator = AttributeTranslator.new(**options, request_method: method_upcase)
@@ -5,8 +5,7 @@ module Grape
5
5
  # Base for classes which need to operate with own values kept
6
6
  # in the hash and inherited values kept in a Hash-like object.
7
7
  class BaseInheritable
8
- attr_accessor :inherited_values
9
- attr_accessor :new_values
8
+ attr_accessor :inherited_values, :new_values
10
9
 
11
10
  # @param inherited_values [Object] An object implementing an interface
12
11
  # of the Hash class.
@@ -26,10 +25,14 @@ module Grape
26
25
  end
27
26
 
28
27
  def keys
29
- combined = inherited_values.keys
30
- combined.concat(new_values.keys)
31
- combined.uniq!
32
- combined
28
+ if new_values.any?
29
+ combined = inherited_values.keys
30
+ combined.concat(new_values.keys)
31
+ combined.uniq!
32
+ combined
33
+ else
34
+ inherited_values.keys
35
+ end
33
36
  end
34
37
 
35
38
  def key?(name)
@@ -8,8 +8,10 @@ module Grape
8
8
  protected
9
9
 
10
10
  def concat_values(inherited_value, new_value)
11
+ return inherited_value unless new_value
12
+
11
13
  [].tap do |value|
12
- value.concat(new_value) if new_value
14
+ value.concat(new_value)
13
15
  value.concat(inherited_value)
14
16
  end
15
17
  end
@@ -29,9 +29,11 @@ module Grape
29
29
  protected
30
30
 
31
31
  def concat_values(inherited_value, new_value)
32
+ return inherited_value unless new_value
33
+
32
34
  [].tap do |value|
33
35
  value.concat(inherited_value)
34
- value.concat(new_value) if new_value
36
+ value.concat(new_value)
35
37
  end
36
38
  end
37
39
  end
@@ -42,7 +42,6 @@ module Grape
42
42
  Grape::API::Boolean,
43
43
  String,
44
44
  Symbol,
45
- Rack::Multipart::UploadedFile,
46
45
  TrueClass,
47
46
  FalseClass
48
47
  ].freeze
@@ -54,8 +53,7 @@ module Grape
54
53
  Set
55
54
  ].freeze
56
55
 
57
- # Types for which Grape provides special coercion
58
- # and type-checking logic.
56
+ # Special custom types provided by Grape.
59
57
  SPECIAL = {
60
58
  JSON => Json,
61
59
  Array[JSON] => JsonArray,
@@ -130,7 +128,6 @@ module Grape
130
128
  !primitive?(type) &&
131
129
  !structure?(type) &&
132
130
  !multiple?(type) &&
133
- !special?(type) &&
134
131
  type.respond_to?(:parse) &&
135
132
  type.method(:parse).arity == 1
136
133
  end
@@ -143,7 +140,11 @@ module Grape
143
140
  def self.collection_of_custom?(type)
144
141
  (type.is_a?(Array) || type.is_a?(Set)) &&
145
142
  type.length == 1 &&
146
- custom?(type.first)
143
+ (custom?(type.first) || special?(type.first))
144
+ end
145
+
146
+ def self.map_special(type)
147
+ SPECIAL.fetch(type, type)
147
148
  end
148
149
  end
149
150
  end
@@ -42,6 +42,9 @@ module Grape
42
42
  end
43
43
 
44
44
  def self.create_coercer_instance(type, method, strict)
45
+ # Maps a custom type provided by Grape, it doesn't map types wrapped by collections!!!
46
+ type = Types.map_special(type)
47
+
45
48
  # Use a special coercer for multiply-typed parameters.
46
49
  if Types.multiple?(type)
47
50
  MultipleTypeCoercer.new(type, method)
@@ -55,10 +58,8 @@ module Grape
55
58
  # method is supplied.
56
59
  elsif Types.collection_of_custom?(type)
57
60
  Types::CustomTypeCollectionCoercer.new(
58
- type.first, type.is_a?(Set)
61
+ Types.map_special(type.first), type.is_a?(Set)
59
62
  )
60
- elsif Types.special?(type)
61
- Types::SPECIAL[type].new
62
63
  elsif type.is_a?(Array)
63
64
  ArrayCoercer.new type, strict
64
65
  elsif type.is_a?(Set)
@@ -60,7 +60,7 @@ module Grape
60
60
  end
61
61
 
62
62
  def coerced?(val)
63
- @type_check.call val
63
+ val.nil? || @type_check.call(val)
64
64
  end
65
65
 
66
66
  private
@@ -7,21 +7,23 @@ module Grape
7
7
  # Actual handling of these objects is provided by +Rack::Request+;
8
8
  # this class is here only to assert that rack's handling has succeeded.
9
9
  class File
10
- def call(input)
11
- return if input.nil?
12
- return InvalidValue.new unless coerced?(input)
10
+ class << self
11
+ def parse(input)
12
+ return if input.nil?
13
+ return InvalidValue.new unless parsed?(input)
13
14
 
14
- # Processing of multipart file objects
15
- # is already taken care of by Rack::Request.
16
- # Nothing to do here.
17
- input
18
- end
15
+ # Processing of multipart file objects
16
+ # is already taken care of by Rack::Request.
17
+ # Nothing to do here.
18
+ input
19
+ end
19
20
 
20
- def coerced?(value)
21
- # Rack::Request creates a Hash with filename,
22
- # content type and an IO object. Do a bit of basic
23
- # duck-typing.
24
- value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
21
+ def parsed?(value)
22
+ # Rack::Request creates a Hash with filename,
23
+ # content type and an IO object. Do a bit of basic
24
+ # duck-typing.
25
+ value.is_a?(::Hash) && value.key?(:tempfile) && value[:tempfile].is_a?(Tempfile)
26
+ end
25
27
  end
26
28
  end
27
29
  end
@@ -12,35 +12,37 @@ module Grape
12
12
  # validation system will apply nested validation rules to
13
13
  # all returned objects.
14
14
  class Json
15
- # Coerce the input into a JSON-like data structure.
16
- #
17
- # @param input [String] a JSON-encoded parameter value
18
- # @return [Hash,Array<Hash>,nil]
19
- def call(input)
20
- return input if coerced?(input)
15
+ class << self
16
+ # Coerce the input into a JSON-like data structure.
17
+ #
18
+ # @param input [String] a JSON-encoded parameter value
19
+ # @return [Hash,Array<Hash>,nil]
20
+ def parse(input)
21
+ return input if parsed?(input)
21
22
 
22
- # Allow nulls and blank strings
23
- return if input.nil? || input =~ /^\s*$/
24
- JSON.parse(input, symbolize_names: true)
25
- end
23
+ # Allow nulls and blank strings
24
+ return if input.nil? || input.match?(/^\s*$/)
25
+ JSON.parse(input, symbolize_names: true)
26
+ end
26
27
 
27
- # Checks that the input was parsed successfully
28
- # and isn't something odd such as an array of primitives.
29
- #
30
- # @param value [Object] result of {#coerce}
31
- # @return [true,false]
32
- def coerced?(value)
33
- value.is_a?(::Hash) || coerced_collection?(value)
34
- end
28
+ # Checks that the input was parsed successfully
29
+ # and isn't something odd such as an array of primitives.
30
+ #
31
+ # @param value [Object] result of {#parse}
32
+ # @return [true,false]
33
+ def parsed?(value)
34
+ value.is_a?(::Hash) || coerced_collection?(value)
35
+ end
35
36
 
36
- protected
37
+ protected
37
38
 
38
- # Is the value an array of JSON-like objects?
39
- #
40
- # @param value [Object] result of {#coerce}
41
- # @return [true,false]
42
- def coerced_collection?(value)
43
- value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
39
+ # Is the value an array of JSON-like objects?
40
+ #
41
+ # @param value [Object] result of {#parse}
42
+ # @return [true,false]
43
+ def coerced_collection?(value)
44
+ value.is_a?(::Array) && value.all? { |i| i.is_a? ::Hash }
45
+ end
44
46
  end
45
47
  end
46
48
 
@@ -49,18 +51,20 @@ module Grape
49
51
  # objects and arrays of objects, but wraps single objects
50
52
  # in an Array.
51
53
  class JsonArray < Json
52
- # See {Json#coerce}. Wraps single objects in an array.
53
- #
54
- # @param input [String] JSON-encoded parameter value
55
- # @return [Array<Hash>]
56
- def call(input)
57
- json = super
58
- Array.wrap(json) unless json.nil?
59
- end
54
+ class << self
55
+ # See {Json#parse}. Wraps single objects in an array.
56
+ #
57
+ # @param input [String] JSON-encoded parameter value
58
+ # @return [Array<Hash>]
59
+ def parse(input)
60
+ json = super
61
+ Array.wrap(json) unless json.nil?
62
+ end
60
63
 
61
- # See {Json#coerced_collection?}
62
- def coerced?(value)
63
- coerced_collection? value
64
+ # See {Json#coerced_collection?}
65
+ def parsed?(value)
66
+ coerced_collection? value
67
+ end
64
68
  end
65
69
  end
66
70
  end
@@ -11,10 +11,10 @@ module Grape
11
11
  class PrimitiveCoercer < DryTypeCoercer
12
12
  MAPPING = {
13
13
  Grape::API::Boolean => DryTypes::Params::Bool,
14
+ BigDecimal => DryTypes::Params::Decimal,
14
15
 
15
16
  # unfortunately, a +Params+ scope doesn't contain String
16
- String => DryTypes::Coercible::String,
17
- BigDecimal => DryTypes::Coercible::Decimal
17
+ String => DryTypes::Coercible::String
18
18
  }.freeze
19
19
 
20
20
  STRICT_MAPPING = {
@@ -47,7 +47,9 @@ module Grape
47
47
  # h[:list] = list
48
48
  # h
49
49
  # => #<Hashie::Mash list=[1, 2, 3, 4]>
50
- params[attr_name] = new_value unless params[attr_name] == new_value
50
+ return if params[attr_name].class == new_value.class && params[attr_name] == new_value
51
+
52
+ params[attr_name] = new_value
51
53
  end
52
54
 
53
55
  private
@@ -65,21 +67,21 @@ module Grape
65
67
  end
66
68
 
67
69
  def coerce_value(val)
68
- # define default values for structures, the dry-types lib which is used
69
- # for coercion doesn't accept nil as a value, so it would fail
70
- if val.nil?
71
- return [] if type == Array || type.is_a?(Array)
72
- return Set.new if type == Set
73
- return {} if type == Hash
74
- end
75
-
76
- converter.call(val)
77
-
70
+ val.nil? ? coerce_nil(val) : converter.call(val)
78
71
  # Some custom types might fail, so it should be treated as an invalid value
79
72
  rescue StandardError
80
73
  Types::InvalidValue.new
81
74
  end
82
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
+
83
85
  # Type to which the parameter will be coerced.
84
86
  #
85
87
  # @return [Class]
@@ -5,7 +5,7 @@ module Grape
5
5
  class RegexpValidator < Base
6
6
  def validate_param!(attr_name, params)
7
7
  return unless params.respond_to?(:key?) && params.key?(attr_name)
8
- return if Array.wrap(params[attr_name]).all? { |param| param.nil? || (param.to_s =~ (options_key?(:value) ? @option[:value] : @option)) }
8
+ return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.match?((options_key?(:value) ? @option[:value] : @option)) }
9
9
  raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp))
10
10
  end
11
11
  end
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '1.3.1'
5
+ VERSION = '1.3.2'
6
6
  end
@@ -87,12 +87,12 @@ module Grape
87
87
  describe '#namespace?' do
88
88
  it 'is false when the namespace is nil' do
89
89
  path = Path.new(anything, nil, anything)
90
- expect(path.namespace?).to be nil
90
+ expect(path.namespace?).to be_falsey
91
91
  end
92
92
 
93
93
  it 'is false when the namespace starts with whitespace' do
94
94
  path = Path.new(anything, ' /foo', anything)
95
- expect(path.namespace?).to be nil
95
+ expect(path.namespace?).to be_falsey
96
96
  end
97
97
 
98
98
  it 'is false when the namespace is the root path' do
@@ -109,12 +109,12 @@ module Grape
109
109
  describe '#path?' do
110
110
  it 'is false when the path is nil' do
111
111
  path = Path.new(nil, anything, anything)
112
- expect(path.path?).to be nil
112
+ expect(path.path?).to be_falsey
113
113
  end
114
114
 
115
115
  it 'is false when the path starts with whitespace' do
116
116
  path = Path.new(' /foo', anything, anything)
117
- expect(path.path?).to be nil
117
+ expect(path.path?).to be_falsey
118
118
  end
119
119
 
120
120
  it 'is false when the path is the root path' do
@@ -17,7 +17,7 @@ describe Grape::Validations::Types do
17
17
  [
18
18
  Integer, Float, Numeric, BigDecimal,
19
19
  Grape::API::Boolean, String, Symbol,
20
- Date, DateTime, Time, Rack::Multipart::UploadedFile
20
+ Date, DateTime, Time
21
21
  ].each do |type|
22
22
  it "recognizes #{type} as a primitive" do
23
23
  expect(described_class.primitive?(type)).to be_truthy
@@ -154,6 +154,36 @@ describe Grape::Validations::CoerceValidator do
154
154
  end
155
155
 
156
156
  context 'coerces' do
157
+ context 'json' do
158
+ let(:headers) { { 'CONTENT_TYPE' => 'application/json', 'ACCEPT' => 'application/json' } }
159
+
160
+ it 'BigDecimal' do
161
+ subject.params do
162
+ requires :bigdecimal, type: BigDecimal
163
+ end
164
+ subject.post '/bigdecimal' do
165
+ "#{params[:bigdecimal].class} #{params[:bigdecimal].to_f}"
166
+ end
167
+
168
+ post '/bigdecimal', { bigdecimal: 45.1 }.to_json, headers
169
+ expect(last_response.status).to eq(201)
170
+ expect(last_response.body).to eq('BigDecimal 45.1')
171
+ end
172
+
173
+ it 'Boolean' do
174
+ subject.params do
175
+ requires :boolean, type: Boolean
176
+ end
177
+ subject.post '/boolean' do
178
+ params[:boolean]
179
+ end
180
+
181
+ post '/boolean', { boolean: 'true' }.to_json, headers
182
+ expect(last_response.status).to eq(201)
183
+ expect(last_response.body).to eq('true')
184
+ end
185
+ end
186
+
157
187
  it 'BigDecimal' do
158
188
  subject.params do
159
189
  requires :bigdecimal, coerce: BigDecimal
@@ -180,6 +210,23 @@ describe Grape::Validations::CoerceValidator do
180
210
  expect(last_response.body).to eq(integer_class_name)
181
211
  end
182
212
 
213
+ it 'String' do
214
+ subject.params do
215
+ requires :string, coerce: String
216
+ end
217
+ subject.get '/string' do
218
+ params[:string].class
219
+ end
220
+
221
+ get '/string', string: 45
222
+ expect(last_response.status).to eq(200)
223
+ expect(last_response.body).to eq('String')
224
+
225
+ get '/string', string: nil
226
+ expect(last_response.status).to eq(200)
227
+ expect(last_response.body).to eq('NilClass')
228
+ end
229
+
183
230
  it 'is a custom type' do
184
231
  subject.params do
185
232
  requires :uri, coerce: SecureURIOnly
@@ -307,42 +354,60 @@ describe Grape::Validations::CoerceValidator do
307
354
  expect(last_response.body).to eq('TrueClass')
308
355
  end
309
356
 
310
- it 'Rack::Multipart::UploadedFile' do
311
- subject.params do
312
- requires :file, type: Rack::Multipart::UploadedFile
313
- end
314
- subject.post '/upload' do
315
- params[:file][:filename]
316
- end
357
+ context 'File' do
358
+ let(:file) { Rack::Test::UploadedFile.new(__FILE__) }
359
+ let(:filename) { File.basename(__FILE__).to_s }
317
360
 
318
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
319
- expect(last_response.status).to eq(201)
320
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
361
+ it 'Rack::Multipart::UploadedFile' do
362
+ subject.params do
363
+ requires :file, type: Rack::Multipart::UploadedFile
364
+ end
365
+ subject.post '/upload' do
366
+ params[:file][:filename]
367
+ end
321
368
 
322
- post '/upload', file: 'not a file'
323
- expect(last_response.status).to eq(400)
324
- expect(last_response.body).to eq('file is invalid')
325
- end
369
+ post '/upload', file: file
370
+ expect(last_response.status).to eq(201)
371
+ expect(last_response.body).to eq(filename)
326
372
 
327
- it 'File' do
328
- subject.params do
329
- requires :file, coerce: File
330
- end
331
- subject.post '/upload' do
332
- params[:file][:filename]
373
+ post '/upload', file: 'not a file'
374
+ expect(last_response.status).to eq(400)
375
+ expect(last_response.body).to eq('file is invalid')
333
376
  end
334
377
 
335
- post '/upload', file: Rack::Test::UploadedFile.new(__FILE__)
336
- expect(last_response.status).to eq(201)
337
- expect(last_response.body).to eq(File.basename(__FILE__).to_s)
378
+ it 'File' do
379
+ subject.params do
380
+ requires :file, coerce: File
381
+ end
382
+ subject.post '/upload' do
383
+ params[:file][:filename]
384
+ end
385
+
386
+ post '/upload', file: file
387
+ expect(last_response.status).to eq(201)
388
+ expect(last_response.body).to eq(filename)
338
389
 
339
- post '/upload', file: 'not a file'
340
- expect(last_response.status).to eq(400)
341
- expect(last_response.body).to eq('file is invalid')
390
+ post '/upload', file: 'not a file'
391
+ expect(last_response.status).to eq(400)
392
+ expect(last_response.body).to eq('file is invalid')
342
393
 
343
- post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
344
- expect(last_response.status).to eq(400)
345
- expect(last_response.body).to eq('file is invalid')
394
+ post '/upload', file: { filename: 'fake file', tempfile: '/etc/passwd' }
395
+ expect(last_response.status).to eq(400)
396
+ expect(last_response.body).to eq('file is invalid')
397
+ end
398
+
399
+ it 'collection' do
400
+ subject.params do
401
+ requires :files, type: Array[File]
402
+ end
403
+ subject.post '/upload' do
404
+ params[:files].first[:filename]
405
+ end
406
+
407
+ post '/upload', files: [file]
408
+ expect(last_response.status).to eq(201)
409
+ expect(last_response.body).to eq(filename)
410
+ end
346
411
  end
347
412
 
348
413
  it 'Nests integers' do
@@ -478,6 +543,46 @@ describe Grape::Validations::CoerceValidator do
478
543
  expect(last_response.body).to eq('3')
479
544
  end
480
545
 
546
+ context 'Integer type and coerce_with potentially returning nil' do
547
+ before do
548
+ subject.params do
549
+ requires :int, type: Integer, coerce_with: (lambda do |val|
550
+ if val == '0'
551
+ nil
552
+ elsif val.match?(/^-?\d+$/)
553
+ val.to_i
554
+ else
555
+ val
556
+ end
557
+ end)
558
+ end
559
+ subject.get '/' do
560
+ params[:int].class.to_s
561
+ end
562
+ end
563
+
564
+ it 'accepts value that coerces to nil' do
565
+ get '/', int: '0'
566
+
567
+ expect(last_response.status).to eq(200)
568
+ expect(last_response.body).to eq('NilClass')
569
+ end
570
+
571
+ it 'coerces to Integer' do
572
+ get '/', int: '1'
573
+
574
+ expect(last_response.status).to eq(200)
575
+ expect(last_response.body).to eq('Integer')
576
+ end
577
+
578
+ it 'returns invalid value if coercion returns a wrong type' do
579
+ get '/', int: 'lol'
580
+
581
+ expect(last_response.status).to eq(400)
582
+ expect(last_response.body).to eq('int is invalid')
583
+ end
584
+ end
585
+
481
586
  it 'must be supplied with :type or :coerce' do
482
587
  expect do
483
588
  subject.params do
@@ -647,6 +752,19 @@ describe Grape::Validations::CoerceValidator do
647
752
  expect(last_response.body).to eq('String')
648
753
  end
649
754
 
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
+
650
768
  it 'fails when no coercion is possible' do
651
769
  subject.params do
652
770
  requires :a, types: [Boolean, Integer]
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.1
4
+ version: 1.3.2
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-03-11 00:00:00.000000000 Z
11
+ date: 2020-04-12 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: activesupport
@@ -364,9 +364,9 @@ licenses:
364
364
  - MIT
365
365
  metadata:
366
366
  bug_tracker_uri: https://github.com/ruby-grape/grape/issues
367
- changelog_uri: https://github.com/ruby-grape/grape/blob/v1.3.1/CHANGELOG.md
368
- documentation_uri: https://www.rubydoc.info/gems/grape/1.3.1
369
- source_code_uri: https://github.com/ruby-grape/grape/tree/v1.3.1
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
370
370
  post_install_message:
371
371
  rdoc_options: []
372
372
  require_paths: