grape 1.3.1 → 1.5.1
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 +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
|