grape 1.3.1 → 1.5.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +82 -1
- data/LICENSE +1 -1
- data/README.md +104 -21
- data/UPGRADING.md +265 -39
- data/lib/grape.rb +2 -2
- data/lib/grape/api.rb +2 -2
- data/lib/grape/api/instance.rb +29 -28
- data/lib/grape/dsl/helpers.rb +1 -0
- data/lib/grape/dsl/inside_route.rb +69 -36
- data/lib/grape/dsl/parameters.rb +7 -3
- data/lib/grape/dsl/routing.rb +2 -4
- data/lib/grape/dsl/validations.rb +18 -1
- data/lib/grape/eager_load.rb +1 -1
- data/lib/grape/endpoint.rb +8 -6
- data/lib/grape/http/headers.rb +1 -0
- data/lib/grape/middleware/base.rb +2 -1
- data/lib/grape/middleware/error.rb +10 -12
- data/lib/grape/middleware/formatter.rb +3 -3
- data/lib/grape/middleware/stack.rb +21 -8
- 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/request.rb +1 -1
- data/lib/grape/router.rb +30 -43
- data/lib/grape/router/attribute_translator.rb +26 -5
- data/lib/grape/router/route.rb +3 -22
- data/lib/grape/{serve_file → serve_stream}/file_body.rb +1 -1
- data/lib/grape/{serve_file → serve_stream}/sendfile_response.rb +1 -1
- data/lib/grape/{serve_file/file_response.rb → serve_stream/stream_response.rb} +8 -8
- data/lib/grape/util/base_inheritable.rb +11 -8
- data/lib/grape/util/lazy_value.rb +1 -0
- data/lib/grape/util/reverse_stackable_values.rb +3 -1
- data/lib/grape/util/stackable_values.rb +3 -1
- data/lib/grape/validations/attributes_iterator.rb +8 -0
- data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
- data/lib/grape/validations/params_scope.rb +8 -6
- data/lib/grape/validations/single_attribute_iterator.rb +1 -1
- data/lib/grape/validations/types.rb +6 -5
- data/lib/grape/validations/types/array_coercer.rb +14 -5
- data/lib/grape/validations/types/build_coercer.rb +5 -8
- data/lib/grape/validations/types/custom_type_coercer.rb +14 -2
- data/lib/grape/validations/types/dry_type_coercer.rb +36 -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 +11 -5
- data/lib/grape/validations/types/set_coercer.rb +6 -4
- data/lib/grape/validations/types/variant_collection_coercer.rb +1 -1
- data/lib/grape/validations/validator_factory.rb +1 -1
- data/lib/grape/validations/validators/as.rb +1 -1
- data/lib/grape/validations/validators/base.rb +4 -5
- data/lib/grape/validations/validators/coerce.rb +3 -10
- data/lib/grape/validations/validators/default.rb +3 -5
- data/lib/grape/validations/validators/except_values.rb +1 -1
- data/lib/grape/validations/validators/multiple_params_base.rb +2 -1
- data/lib/grape/validations/validators/regexp.rb +1 -1
- data/lib/grape/validations/validators/values.rb +1 -1
- data/lib/grape/version.rb +1 -1
- data/spec/grape/api/instance_spec.rb +50 -0
- data/spec/grape/api_spec.rb +75 -0
- data/spec/grape/dsl/inside_route_spec.rb +182 -33
- data/spec/grape/endpoint/declared_spec.rb +601 -0
- data/spec/grape/endpoint_spec.rb +0 -521
- data/spec/grape/entity_spec.rb +6 -0
- data/spec/grape/integration/rack_sendfile_spec.rb +12 -8
- data/spec/grape/middleware/auth/strategies_spec.rb +1 -1
- data/spec/grape/middleware/error_spec.rb +1 -1
- data/spec/grape/middleware/formatter_spec.rb +1 -1
- data/spec/grape/middleware/stack_spec.rb +3 -1
- data/spec/grape/path_spec.rb +4 -4
- data/spec/grape/validations/multiple_attributes_iterator_spec.rb +13 -3
- data/spec/grape/validations/params_scope_spec.rb +26 -0
- data/spec/grape/validations/single_attribute_iterator_spec.rb +17 -6
- data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
- data/spec/grape/validations/types/primitive_coercer_spec.rb +65 -5
- data/spec/grape/validations/types/set_coercer_spec.rb +34 -0
- data/spec/grape/validations/types_spec.rb +1 -1
- data/spec/grape/validations/validators/coerce_spec.rb +317 -29
- data/spec/grape/validations/validators/default_spec.rb +170 -0
- data/spec/grape/validations/validators/except_values_spec.rb +1 -0
- data/spec/grape/validations/validators/values_spec.rb +1 -1
- data/spec/grape/validations_spec.rb +290 -18
- data/spec/integration/eager_load/eager_load_spec.rb +15 -0
- data/spec/spec_helper.rb +0 -10
- data/spec/support/chunks.rb +14 -0
- data/spec/support/versioned_helpers.rb +3 -5
- metadata +18 -8
@@ -5,13 +5,12 @@ 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.
|
13
|
-
def initialize(inherited_values =
|
14
|
-
@inherited_values = inherited_values
|
12
|
+
def initialize(inherited_values = nil)
|
13
|
+
@inherited_values = inherited_values || {}
|
15
14
|
@new_values = {}
|
16
15
|
end
|
17
16
|
|
@@ -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
|
@@ -48,6 +48,14 @@ module Grape
|
|
48
48
|
def yield_attributes(_resource_params, _attrs)
|
49
49
|
raise NotImplementedError
|
50
50
|
end
|
51
|
+
|
52
|
+
# This is a special case so that we can ignore tree's where option
|
53
|
+
# values are missing lower down. Unfortunately we can remove this
|
54
|
+
# are the parameter parsing stage as they are required to ensure
|
55
|
+
# the correct indexing is maintained
|
56
|
+
def skip?(val)
|
57
|
+
val == Grape::DSL::Parameters::EmptyOptionalValue
|
58
|
+
end
|
51
59
|
end
|
52
60
|
end
|
53
61
|
end
|
@@ -54,14 +54,16 @@ module Grape
|
|
54
54
|
end
|
55
55
|
|
56
56
|
def meets_dependency?(params, request_params)
|
57
|
+
return true unless @dependent_on
|
58
|
+
|
57
59
|
if @parent.present? && !@parent.meets_dependency?(@parent.params(request_params), request_params)
|
58
60
|
return false
|
59
61
|
end
|
60
62
|
|
61
|
-
return true unless @dependent_on
|
62
63
|
return params.any? { |param| meets_dependency?(param, request_params) } if params.is_a?(Array)
|
63
|
-
|
64
|
-
params
|
64
|
+
|
65
|
+
# params might be anything what looks like a hash, so it must implement a `key?` method
|
66
|
+
return false unless params.respond_to?(:key?)
|
65
67
|
|
66
68
|
@dependent_on.each do |dependency|
|
67
69
|
if dependency.is_a?(Hash)
|
@@ -237,10 +239,10 @@ module Grape
|
|
237
239
|
@parent.push_declared_params [element => @declared_params]
|
238
240
|
else
|
239
241
|
@api.namespace_stackable(:declared_params, @declared_params)
|
240
|
-
|
241
|
-
@api.route_setting(:declared_params, []) unless @api.route_setting(:declared_params)
|
242
|
-
@api.route_setting(:declared_params, @api.namespace_stackable(:declared_params).flatten)
|
243
242
|
end
|
243
|
+
|
244
|
+
# params were stored in settings, it can be cleaned from the params scope
|
245
|
+
@declared_params = nil
|
244
246
|
end
|
245
247
|
|
246
248
|
def validates(attrs, validations)
|
@@ -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
|
@@ -6,7 +6,7 @@ module Grape
|
|
6
6
|
module Validations
|
7
7
|
module Types
|
8
8
|
# Coerces elements in an array. It might be an array of strings or integers or
|
9
|
-
#
|
9
|
+
# an array of arrays of integers.
|
10
10
|
#
|
11
11
|
# It could've been possible to use an +of+
|
12
12
|
# method (https://dry-rb.org/gems/dry-types/1.2/array-with-member/)
|
@@ -14,16 +14,17 @@ module Grape
|
|
14
14
|
# behavior of Virtus which was used earlier, a `Grape::Validations::Types::PrimitiveCoercer`
|
15
15
|
# maintains Virtus behavior in coercing.
|
16
16
|
class ArrayCoercer < DryTypeCoercer
|
17
|
+
register_collection Array
|
18
|
+
|
17
19
|
def initialize(type, strict = false)
|
18
20
|
super
|
19
21
|
|
20
22
|
@coercer = scope::Array
|
21
|
-
@
|
23
|
+
@subtype = type.first
|
22
24
|
end
|
23
25
|
|
24
26
|
def call(_val)
|
25
27
|
collection = super
|
26
|
-
|
27
28
|
return collection if collection.is_a?(InvalidValue)
|
28
29
|
|
29
30
|
coerce_elements collection
|
@@ -31,11 +32,15 @@ module Grape
|
|
31
32
|
|
32
33
|
protected
|
33
34
|
|
35
|
+
attr_reader :subtype
|
36
|
+
|
34
37
|
def coerce_elements(collection)
|
38
|
+
return if collection.nil?
|
39
|
+
|
35
40
|
collection.each_with_index do |elem, index|
|
36
41
|
return InvalidValue.new if reject?(elem)
|
37
42
|
|
38
|
-
coerced_elem =
|
43
|
+
coerced_elem = elem_coercer.call(elem)
|
39
44
|
|
40
45
|
return coerced_elem if coerced_elem.is_a?(InvalidValue)
|
41
46
|
|
@@ -45,11 +50,15 @@ module Grape
|
|
45
50
|
collection
|
46
51
|
end
|
47
52
|
|
48
|
-
# This method
|
53
|
+
# This method maintains logic which was defined by Virtus for arrays.
|
49
54
|
# Virtus doesn't allow nil in arrays.
|
50
55
|
def reject?(val)
|
51
56
|
val.nil?
|
52
57
|
end
|
58
|
+
|
59
|
+
def elem_coercer
|
60
|
+
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict)
|
61
|
+
end
|
53
62
|
end
|
54
63
|
end
|
55
64
|
end
|
@@ -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,16 +58,10 @@ 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
|
-
elsif type.is_a?(Array)
|
63
|
-
ArrayCoercer.new type, strict
|
64
|
-
elsif type.is_a?(Set)
|
65
|
-
SetCoercer.new type, strict
|
66
63
|
else
|
67
|
-
|
64
|
+
DryTypeCoercer.coercer_instance_for(type, strict)
|
68
65
|
end
|
69
66
|
end
|
70
67
|
|
@@ -60,7 +60,7 @@ module Grape
|
|
60
60
|
end
|
61
61
|
|
62
62
|
def coerced?(val)
|
63
|
-
@type_check.call
|
63
|
+
val.nil? || @type_check.call(val)
|
64
64
|
end
|
65
65
|
|
66
66
|
private
|
@@ -103,13 +103,25 @@ module Grape
|
|
103
103
|
# passed, or if the type also implements a parse() method.
|
104
104
|
type
|
105
105
|
elsif type.is_a?(Enumerable)
|
106
|
-
|
106
|
+
lambda do |value|
|
107
|
+
value.is_a?(Enumerable) && value.all? do |val|
|
108
|
+
recursive_type_check(type.first, val)
|
109
|
+
end
|
110
|
+
end
|
107
111
|
else
|
108
112
|
# By default, do a simple type check
|
109
113
|
->(value) { value.is_a? type }
|
110
114
|
end
|
111
115
|
end
|
112
116
|
|
117
|
+
def recursive_type_check(type, value)
|
118
|
+
if type.is_a?(Enumerable) && value.is_a?(Enumerable)
|
119
|
+
value.all? { |val| recursive_type_check(type.first, val) }
|
120
|
+
else
|
121
|
+
!type.is_a?(Enumerable) && value.is_a?(type)
|
122
|
+
end
|
123
|
+
end
|
124
|
+
|
113
125
|
# Enforce symbolized keys for complex types
|
114
126
|
# by wrapping the coercion method such that
|
115
127
|
# any Hash objects in the immediate heirarchy
|
@@ -17,8 +17,41 @@ module Grape
|
|
17
17
|
# but check its type. More information there
|
18
18
|
# https://dry-rb.org/gems/dry-types/1.2/built-in-types/
|
19
19
|
class DryTypeCoercer
|
20
|
+
class << self
|
21
|
+
# Registers a collection coercer which could be found by a type,
|
22
|
+
# see +collection_coercer_for+ method below. This method is meant for inheritors.
|
23
|
+
def register_collection(type)
|
24
|
+
DryTypeCoercer.collection_coercers[type] = self
|
25
|
+
end
|
26
|
+
|
27
|
+
# Returns a collection coercer which corresponds to a given type.
|
28
|
+
# Example:
|
29
|
+
#
|
30
|
+
# collection_coercer_for(Array)
|
31
|
+
# #=> Grape::Validations::Types::ArrayCoercer
|
32
|
+
def collection_coercer_for(type)
|
33
|
+
collection_coercers[type]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns an instance of a coercer for a given type
|
37
|
+
def coercer_instance_for(type, strict = false)
|
38
|
+
return PrimitiveCoercer.new(type, strict) if type.class == Class
|
39
|
+
|
40
|
+
# in case of a collection (Array[Integer]) the type is an instance of a collection,
|
41
|
+
# so we need to figure out the actual type
|
42
|
+
collection_coercer_for(type.class).new(type, strict)
|
43
|
+
end
|
44
|
+
|
45
|
+
protected
|
46
|
+
|
47
|
+
def collection_coercers
|
48
|
+
@collection_coercers ||= {}
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
20
52
|
def initialize(type, strict = false)
|
21
53
|
@type = type
|
54
|
+
@strict = strict
|
22
55
|
@scope = strict ? DryTypes::Strict : DryTypes::Params
|
23
56
|
end
|
24
57
|
|
@@ -27,6 +60,8 @@ module Grape
|
|
27
60
|
#
|
28
61
|
# @param val [Object]
|
29
62
|
def call(val)
|
63
|
+
return if val.nil?
|
64
|
+
|
30
65
|
@coercer[val]
|
31
66
|
rescue Dry::Types::CoercionError => _e
|
32
67
|
InvalidValue.new
|
@@ -34,7 +69,7 @@ module Grape
|
|
34
69
|
|
35
70
|
protected
|
36
71
|
|
37
|
-
attr_reader :scope, :type
|
72
|
+
attr_reader :scope, :type, :strict
|
38
73
|
end
|
39
74
|
end
|
40
75
|
end
|
@@ -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
|