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