grape 1.3.0 → 1.5.0
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 +90 -0
- data/LICENSE +1 -1
- data/README.md +104 -21
- data/UPGRADING.md +243 -39
- data/lib/grape.rb +4 -5
- data/lib/grape/api.rb +4 -4
- data/lib/grape/api/instance.rb +32 -31
- data/lib/grape/content_types.rb +34 -0
- data/lib/grape/dsl/helpers.rb +2 -1
- data/lib/grape/dsl/inside_route.rb +76 -42
- data/lib/grape/dsl/parameters.rb +4 -4
- data/lib/grape/dsl/routing.rb +8 -8
- 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/exceptions/base.rb +0 -4
- data/lib/grape/exceptions/validation_errors.rb +11 -12
- data/lib/grape/http/headers.rb +26 -0
- data/lib/grape/middleware/base.rb +3 -4
- data/lib/grape/middleware/error.rb +10 -12
- data/lib/grape/middleware/formatter.rb +3 -3
- data/lib/grape/middleware/stack.rb +19 -5
- data/lib/grape/middleware/versioner/header.rb +4 -4
- data/lib/grape/middleware/versioner/parse_media_type_patch.rb +2 -1
- data/lib/grape/middleware/versioner/path.rb +1 -1
- data/lib/grape/namespace.rb +12 -2
- data/lib/grape/path.rb +13 -3
- data/lib/grape/request.rb +13 -8
- data/lib/grape/router.rb +26 -30
- data/lib/grape/router/attribute_translator.rb +25 -4
- data/lib/grape/router/pattern.rb +17 -16
- data/lib/grape/router/route.rb +5 -24
- 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 +15 -8
- data/lib/grape/util/cache.rb +20 -0
- data/lib/grape/util/lazy_object.rb +43 -0
- data/lib/grape/util/lazy_value.rb +1 -0
- data/lib/grape/util/reverse_stackable_values.rb +2 -0
- data/lib/grape/util/stackable_values.rb +7 -20
- data/lib/grape/validations/params_scope.rb +6 -5
- 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 -12
- data/lib/grape/validations/types/json.rb +40 -36
- data/lib/grape/validations/types/primitive_coercer.rb +15 -6
- 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/validators/as.rb +1 -1
- data/lib/grape/validations/validators/base.rb +2 -4
- data/lib/grape/validations/validators/coerce.rb +4 -11
- data/lib/grape/validations/validators/default.rb +3 -5
- data/lib/grape/validations/validators/exactly_one_of.rb +4 -2
- data/lib/grape/validations/validators/except_values.rb +1 -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 +82 -6
- data/spec/grape/dsl/inside_route_spec.rb +182 -33
- data/spec/grape/endpoint/declared_spec.rb +590 -0
- data/spec/grape/endpoint_spec.rb +0 -521
- data/spec/grape/entity_spec.rb +6 -0
- data/spec/grape/exceptions/validation_errors_spec.rb +2 -2
- 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 +3 -3
- data/spec/grape/middleware/stack_spec.rb +12 -1
- data/spec/grape/path_spec.rb +4 -4
- data/spec/grape/validations/instance_behaivour_spec.rb +1 -1
- data/spec/grape/validations/params_scope_spec.rb +26 -0
- data/spec/grape/validations/types/array_coercer_spec.rb +35 -0
- data/spec/grape/validations/types/primitive_coercer_spec.rb +135 -0
- 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 +329 -77
- data/spec/grape/validations/validators/default_spec.rb +170 -0
- data/spec/grape/validations/validators/exactly_one_of_spec.rb +12 -12
- 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 +30 -30
- data/spec/integration/eager_load/eager_load_spec.rb +15 -0
- data/spec/spec_helper.rb +3 -10
- data/spec/support/chunks.rb +14 -0
- data/spec/support/eager_load.rb +19 -0
- data/spec/support/versioned_helpers.rb +3 -5
- metadata +121 -105
- data/lib/grape/util/content_types.rb +0 -28
@@ -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,18 @@ 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
|
36
|
+
end
|
37
|
+
|
38
|
+
def key?(name)
|
39
|
+
inherited_values.key?(name) || new_values.key?(name)
|
33
40
|
end
|
34
41
|
end
|
35
42
|
end
|
@@ -0,0 +1,20 @@
|
|
1
|
+
# frozen_String_literal: true
|
2
|
+
|
3
|
+
require 'singleton'
|
4
|
+
require 'forwardable'
|
5
|
+
|
6
|
+
module Grape
|
7
|
+
module Util
|
8
|
+
class Cache
|
9
|
+
include Singleton
|
10
|
+
|
11
|
+
attr_reader :cache
|
12
|
+
|
13
|
+
class << self
|
14
|
+
extend Forwardable
|
15
|
+
def_delegators :cache, :[]
|
16
|
+
def_delegators :instance, :cache
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# Based on https://github.com/HornsAndHooves/lazy_object
|
4
|
+
|
5
|
+
module Grape
|
6
|
+
module Util
|
7
|
+
class LazyObject < BasicObject
|
8
|
+
attr_reader :callable
|
9
|
+
|
10
|
+
def initialize(&callable)
|
11
|
+
@callable = callable
|
12
|
+
end
|
13
|
+
|
14
|
+
def __target_object__
|
15
|
+
@__target_object__ ||= callable.call
|
16
|
+
end
|
17
|
+
|
18
|
+
def ==(other)
|
19
|
+
__target_object__ == other
|
20
|
+
end
|
21
|
+
|
22
|
+
def !=(other)
|
23
|
+
__target_object__ != other
|
24
|
+
end
|
25
|
+
|
26
|
+
def !
|
27
|
+
!__target_object__
|
28
|
+
end
|
29
|
+
|
30
|
+
def method_missing(method_name, *args, &block)
|
31
|
+
if __target_object__.respond_to?(method_name)
|
32
|
+
__target_object__.send(method_name, *args, &block)
|
33
|
+
else
|
34
|
+
super
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def respond_to_missing?(method_name, include_priv = false)
|
39
|
+
__target_object__.respond_to?(method_name, include_priv)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -5,30 +5,19 @@ require_relative 'base_inheritable'
|
|
5
5
|
module Grape
|
6
6
|
module Util
|
7
7
|
class StackableValues < BaseInheritable
|
8
|
-
attr_reader :frozen_values
|
9
|
-
|
10
|
-
def initialize(*_args)
|
11
|
-
super
|
12
|
-
|
13
|
-
@frozen_values = {}
|
14
|
-
end
|
15
|
-
|
16
8
|
# Even if there is no value, an empty array will be returned.
|
17
9
|
def [](name)
|
18
|
-
|
10
|
+
inherited_value = inherited_values[name]
|
11
|
+
new_value = new_values[name]
|
19
12
|
|
20
|
-
|
21
|
-
new_value = @new_values[name] || []
|
22
|
-
|
23
|
-
return new_value unless inherited_value
|
13
|
+
return new_value || [] unless inherited_value
|
24
14
|
|
25
15
|
concat_values(inherited_value, new_value)
|
26
16
|
end
|
27
17
|
|
28
18
|
def []=(name, value)
|
29
|
-
|
30
|
-
|
31
|
-
@new_values[name].push value
|
19
|
+
new_values[name] ||= []
|
20
|
+
new_values[name].push value
|
32
21
|
end
|
33
22
|
|
34
23
|
def to_hash
|
@@ -37,13 +26,11 @@ module Grape
|
|
37
26
|
end
|
38
27
|
end
|
39
28
|
|
40
|
-
def freeze_value(key)
|
41
|
-
@frozen_values[key] = self[key].freeze
|
42
|
-
end
|
43
|
-
|
44
29
|
protected
|
45
30
|
|
46
31
|
def concat_values(inherited_value, new_value)
|
32
|
+
return inherited_value unless new_value
|
33
|
+
|
47
34
|
[].tap do |value|
|
48
35
|
value.concat(inherited_value)
|
49
36
|
value.concat(new_value)
|
@@ -54,11 +54,12 @@ 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
|
return false unless params.respond_to?(:with_indifferent_access)
|
64
65
|
params = params.with_indifferent_access
|
@@ -237,14 +238,14 @@ module Grape
|
|
237
238
|
@parent.push_declared_params [element => @declared_params]
|
238
239
|
else
|
239
240
|
@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
241
|
end
|
242
|
+
|
243
|
+
# params were stored in settings, it can be cleaned from the params scope
|
244
|
+
@declared_params = nil
|
244
245
|
end
|
245
246
|
|
246
247
|
def validates(attrs, validations)
|
247
|
-
doc_attrs = { required: validations.
|
248
|
+
doc_attrs = { required: validations.key?(:presence) }
|
248
249
|
|
249
250
|
coerce_type = infer_coercion(validations)
|
250
251
|
|
@@ -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,20 +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
|
-
|
10
|
+
class << self
|
11
|
+
def parse(input)
|
12
|
+
return if input.nil?
|
13
|
+
return InvalidValue.new unless parsed?(input)
|
12
14
|
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
15
|
+
# Processing of multipart file objects
|
16
|
+
# is already taken care of by Rack::Request.
|
17
|
+
# Nothing to do here.
|
18
|
+
input
|
19
|
+
end
|
18
20
|
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
24
27
|
end
|
25
28
|
end
|
26
29
|
end
|