grape 1.3.1 → 1.3.2

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