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 +4 -4
- data/CHANGELOG.md +16 -0
- data/README.md +2 -3
- data/UPGRADING.md +61 -16
- data/lib/grape/api/instance.rb +7 -3
- data/lib/grape/http/headers.rb +1 -0
- data/lib/grape/middleware/versioner/header.rb +1 -1
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
- data/lib/grape/path.rb +2 -2
- data/lib/grape/router.rb +8 -7
- data/lib/grape/router/route.rb +2 -3
- data/lib/grape/util/base_inheritable.rb +9 -6
- data/lib/grape/util/reverse_stackable_values.rb +3 -1
- data/lib/grape/util/stackable_values.rb +3 -1
- data/lib/grape/validations/types.rb +6 -5
- data/lib/grape/validations/types/build_coercer.rb +4 -3
- data/lib/grape/validations/types/custom_type_coercer.rb +1 -1
- data/lib/grape/validations/types/file.rb +15 -13
- data/lib/grape/validations/types/json.rb +40 -36
- data/lib/grape/validations/types/primitive_coercer.rb +2 -2
- data/lib/grape/validations/validators/coerce.rb +13 -11
- data/lib/grape/validations/validators/regexp.rb +1 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/path_spec.rb +4 -4
- data/spec/grape/validations/types_spec.rb +1 -1
- data/spec/grape/validations/validators/coerce_spec.rb +147 -29
- metadata +5 -5
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: a1ba205b72fd823b11e700753891784fdfbd6791f591b790a99ac3433cb8fe23
|
4
|
+
data.tar.gz: '09e64a0d93415b28fdb179fbb5d7ef0d03e00dcb976fd885c0c655d6ac041345'
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 06fcd2157bcfcc6e71876df09b34a318c600761b79fb99ad77739521d056950ee8391dfd050efd43618f7f99f11990623e276b32d15a27e92015ec3a56c6c08b
|
7
|
+
data.tar.gz: 5463bbd0347950765c9d92e859965e90dd69d7d1f781fd188386cf39597c196f5ba2dc697dbee4f896a70f6498c1e43034a33db8460ea9a719218a7d5df46d6f
|
data/CHANGELOG.md
CHANGED
@@ -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,
|
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
|
-
|
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
|
|
data/UPGRADING.md
CHANGED
@@ -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
|
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
|
-
|
16
|
-
|
20
|
+
# Legacy Grape parser
|
21
|
+
class SecureUriType < Virtus::Attribute
|
22
|
+
def coerce(input)
|
23
|
+
URI.parse value
|
24
|
+
end
|
17
25
|
|
18
|
-
|
19
|
-
|
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 :
|
32
|
+
requires :secure_uri, type: SecureUri
|
25
33
|
end
|
26
34
|
```
|
27
35
|
|
28
|
-
|
36
|
+
To use dry-types, we need to:
|
29
37
|
|
30
|
-
|
31
|
-
|
32
|
-
|
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
|
-
|
35
|
-
|
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
|
-
|
38
|
-
|
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
|
-
|
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
|
|
data/lib/grape/api/instance.rb
CHANGED
@@ -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
|
-
|
247
|
-
|
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)
|
data/lib/grape/http/headers.rb
CHANGED
@@ -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'
|
@@ -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
|
11
|
+
m = media_type&.match(ALLOWED_CHARACTERS)
|
11
12
|
m ? [m[1], m[2], m[3] || ''] : []
|
12
13
|
end
|
13
14
|
end
|
data/lib/grape/path.rb
CHANGED
@@ -42,11 +42,11 @@ module Grape
|
|
42
42
|
end
|
43
43
|
|
44
44
|
def namespace?
|
45
|
-
namespace
|
45
|
+
namespace&.match?(/^\S/) && namespace != '/'
|
46
46
|
end
|
47
47
|
|
48
48
|
def path?
|
49
|
-
raw_path
|
49
|
+
raw_path&.match?(/^\S/) && raw_path != '/'
|
50
50
|
end
|
51
51
|
|
52
52
|
def suffix
|
data/lib/grape/router.rb
CHANGED
@@ -8,10 +8,9 @@ module Grape
|
|
8
8
|
attr_reader :map, :compiled
|
9
9
|
|
10
10
|
class Any < AttributeTranslator
|
11
|
-
attr_reader :pattern, :
|
12
|
-
def initialize(pattern,
|
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(@
|
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
|
-
|
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
|
-
|
66
|
-
@neutral_map << Any.new(pattern,
|
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)
|
data/lib/grape/router/route.rb
CHANGED
@@ -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, :
|
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
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
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)
|
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)
|
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
|
-
#
|
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)
|
@@ -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
|
-
|
11
|
-
|
12
|
-
|
10
|
+
class << self
|
11
|
+
def parse(input)
|
12
|
+
return if input.nil?
|
13
|
+
return InvalidValue.new unless parsed?(input)
|
13
14
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
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
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
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
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
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
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
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
|
-
|
37
|
+
protected
|
37
38
|
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
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
|
-
|
62
|
-
|
63
|
-
|
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]
|
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
|
-
|
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? ||
|
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
|
data/lib/grape/version.rb
CHANGED
data/spec/grape/path_spec.rb
CHANGED
@@ -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
|
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
|
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
|
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
|
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
|
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
|
-
|
311
|
-
|
312
|
-
|
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
|
-
|
319
|
-
|
320
|
-
|
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
|
-
|
323
|
-
|
324
|
-
|
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
|
-
|
328
|
-
|
329
|
-
|
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
|
-
|
336
|
-
|
337
|
-
|
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
|
-
|
340
|
-
|
341
|
-
|
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
|
-
|
344
|
-
|
345
|
-
|
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.
|
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-
|
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.
|
368
|
-
documentation_uri: https://www.rubydoc.info/gems/grape/1.3.
|
369
|
-
source_code_uri: https://github.com/ruby-grape/grape/tree/v1.3.
|
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:
|