grape 4.0.0 → 4.1.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 +108 -1
- data/README.md +195 -95
- data/UPGRADING.md +364 -0
- data/grape.gemspec +1 -1
- data/lib/grape/api/instance.rb +57 -30
- data/lib/grape/api.rb +52 -18
- data/lib/grape/content_types.rb +26 -9
- data/lib/grape/cookies.rb +1 -1
- data/lib/grape/declared_params_handler.rb +35 -7
- data/lib/grape/dsl/declared.rb +5 -4
- data/lib/grape/dsl/desc.rb +1 -2
- data/lib/grape/dsl/entity.rb +35 -19
- data/lib/grape/dsl/headers.rb +3 -3
- data/lib/grape/dsl/helpers.rb +5 -8
- data/lib/grape/dsl/inside_route.rb +33 -27
- data/lib/grape/dsl/logger.rb +10 -4
- data/lib/grape/dsl/middleware.rb +4 -9
- data/lib/grape/dsl/mounting.rb +149 -0
- data/lib/grape/dsl/parameters.rb +78 -104
- data/lib/grape/dsl/request_response.rb +35 -22
- data/lib/grape/dsl/routing.rb +17 -191
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/version_options.rb +2 -2
- data/lib/grape/dsl/versioning.rb +98 -0
- data/lib/grape/endpoint/state.rb +15 -0
- data/lib/grape/endpoint.rb +229 -137
- data/lib/grape/error_formatter/base.rb +28 -13
- data/lib/grape/error_formatter/json.rb +3 -0
- data/lib/grape/error_formatter/jsonl.rb +15 -0
- data/lib/grape/error_formatter/xml.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +23 -7
- data/lib/grape/exceptions/halt.rb +39 -0
- data/lib/grape/exceptions/validation.rb +9 -9
- data/lib/grape/exceptions/validation_errors.rb +12 -2
- data/lib/grape/formatter/jsonl.rb +34 -0
- data/lib/grape/formatter/markdown.rb +17 -0
- data/lib/grape/formatter/md.rb +11 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/base.rb +16 -1
- data/lib/grape/middleware/auth/strategy_info.rb +2 -2
- data/lib/grape/middleware/base.rb +19 -13
- data/lib/grape/middleware/error.rb +133 -35
- data/lib/grape/middleware/formatter.rb +235 -94
- data/lib/grape/middleware/precomputed_content_types.rb +14 -2
- data/lib/grape/middleware/stack.rb +7 -8
- data/lib/grape/middleware/versioner/base.rb +26 -10
- data/lib/grape/middleware/versioner/header.rb +70 -15
- data/lib/grape/middleware/versioner/param.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +20 -11
- data/lib/grape/middleware/versioner.rb +1 -1
- data/lib/grape/mountable.rb +2 -4
- data/lib/grape/namespace.rb +2 -3
- data/lib/grape/parser/jsonl.rb +19 -0
- data/lib/grape/request.rb +46 -8
- data/lib/grape/router/base_route.rb +41 -0
- data/lib/grape/router/greedy_route.rb +1 -1
- data/lib/grape/router/mustermann_pattern.rb +1 -1
- data/lib/grape/router/pattern/path.rb +1 -1
- data/lib/grape/router/pattern.rb +11 -5
- data/lib/grape/router/route.rb +42 -11
- data/lib/grape/router/route_buckets.rb +246 -0
- data/lib/grape/router.rb +262 -32
- data/lib/grape/util/endpoint_configuration.rb +7 -1
- data/lib/grape/util/inheritable_setting.rb +80 -161
- data/lib/grape/util/lazy/value.rb +52 -11
- data/lib/grape/util/media_type.rb +38 -15
- data/lib/grape/util/path_normalizer.rb +7 -2
- data/lib/grape/util/registry.rb +9 -2
- data/lib/grape/util/scrub.rb +33 -0
- data/lib/grape/util/translation.rb +6 -6
- data/lib/grape/validations/attributes_iterator.rb +73 -37
- data/lib/grape/validations/coerce_options.rb +1 -1
- data/lib/grape/validations/params_documentation.rb +15 -7
- data/lib/grape/validations/params_scope.rb +176 -43
- data/lib/grape/validations/types/custom_type_coercer.rb +2 -3
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +20 -12
- data/lib/grape/validations/types/multiple_type_coercer.rb +1 -2
- data/lib/grape/validations/types/primitive_coercer.rb +17 -0
- data/lib/grape/validations/types/variant_collection_coercer.rb +24 -10
- data/lib/grape/validations/types.rb +12 -18
- data/lib/grape/validations/validations_spec.rb +53 -22
- data/lib/grape/validations/validators/allow_blank_validator.rb +2 -2
- data/lib/grape/validations/validators/as_validator.rb +2 -3
- data/lib/grape/validations/validators/base.rb +124 -25
- data/lib/grape/validations/validators/coerce_validator.rb +2 -3
- data/lib/grape/validations/validators/default_validator.rb +18 -2
- data/lib/grape/validations/validators/except_values_validator.rb +16 -3
- data/lib/grape/validations/validators/hash_elements_validator.rb +34 -0
- data/lib/grape/validations/validators/length_validator.rb +24 -16
- data/lib/grape/validations/validators/multiple_params_base.rb +0 -4
- data/lib/grape/validations/validators/values_validator.rb +18 -24
- data/lib/grape/version.rb +1 -2
- data/lib/grape.rb +3 -0
- metadata +18 -11
- data/lib/grape/util/lazy/value_array.rb +0 -21
- data/lib/grape/util/lazy/value_enumerable.rb +0 -31
- data/lib/grape/util/lazy/value_hash.rb +0 -21
- data/lib/grape/validations/multiple_attributes_iterator.rb +0 -13
- data/lib/grape/validations/single_attribute_iterator.rb +0 -28
|
@@ -14,17 +14,13 @@ module Grape
|
|
|
14
14
|
# Returns a collection coercer which corresponds to a given type.
|
|
15
15
|
# Example:
|
|
16
16
|
#
|
|
17
|
-
# collection_coercer_for(
|
|
17
|
+
# collection_coercer_for([Integer])
|
|
18
18
|
# #=> Grape::Validations::Types::ArrayCoercer
|
|
19
19
|
def collection_coercer_for(type)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
SetCoercer
|
|
25
|
-
else
|
|
26
|
-
raise ArgumentError, "unknown type: `#{type}`"
|
|
27
|
-
end
|
|
20
|
+
return ArrayCoercer if type.is_a?(Array)
|
|
21
|
+
return SetCoercer if type.is_a?(Set)
|
|
22
|
+
|
|
23
|
+
raise ArgumentError, "unknown type: `#{type}`"
|
|
28
24
|
end
|
|
29
25
|
|
|
30
26
|
# Returns an instance of a coercer for a given type
|
|
@@ -43,13 +39,25 @@ module Grape
|
|
|
43
39
|
# Coerces the given value to a type which was specified during
|
|
44
40
|
# initialization as a type argument.
|
|
45
41
|
#
|
|
42
|
+
# Given a block, dry-types reports a value it cannot coerce by calling
|
|
43
|
+
# the block instead of raising. Raising is what cost: its CoercionError
|
|
44
|
+
# is re-raised with the backtrace of the error underneath, and building
|
|
45
|
+
# that backtrace as strings at request depth took about 25 µs for every
|
|
46
|
+
# rejected value -- every `types: [Integer, String]` param given a
|
|
47
|
+
# string, every 400 for a mistyped value.
|
|
48
|
+
#
|
|
49
|
+
# Every coercion dry-types runs takes that block, so none of them
|
|
50
|
+
# raises a CoercionError here. Anything that raises something else --
|
|
51
|
+
# +Kernel#String+, which +Coercible::String+ is built from, ignores the
|
|
52
|
+
# block and raises TypeError -- is answered by
|
|
53
|
+
# +CoerceValidator#coerce_value+, which rescues StandardError with the
|
|
54
|
+
# same InvalidValue.
|
|
55
|
+
#
|
|
46
56
|
# @param val [Object]
|
|
47
57
|
def call(val)
|
|
48
58
|
return if val.nil?
|
|
49
59
|
|
|
50
|
-
@coercer
|
|
51
|
-
rescue Dry::Types::CoercionError
|
|
52
|
-
InvalidValue.new
|
|
60
|
+
@coercer.call(val) { InvalidValue.new }
|
|
53
61
|
end
|
|
54
62
|
|
|
55
63
|
protected
|
|
@@ -35,8 +35,7 @@ module Grape
|
|
|
35
35
|
|
|
36
36
|
# Coerces the given value.
|
|
37
37
|
#
|
|
38
|
-
# @param val [
|
|
39
|
-
# this should always be a string.
|
|
38
|
+
# @param val [Object] value to be coerced
|
|
40
39
|
# @return [Object,InvalidValue] the coerced result, or an instance
|
|
41
40
|
# of {InvalidValue} if the value could not be coerced.
|
|
42
41
|
def call(val)
|
|
@@ -19,9 +19,16 @@ module Grape
|
|
|
19
19
|
@coercer = cache_coercer[type]
|
|
20
20
|
@rejected_inputs = REJECTED_INPUTS[type]
|
|
21
21
|
@treat_empty_as_nil = type != String
|
|
22
|
+
@integer = type == Integer
|
|
22
23
|
end
|
|
23
24
|
|
|
24
25
|
def call(val)
|
|
26
|
+
# A value already of the declared type is what the dry-types coercer
|
|
27
|
+
# hands back for it -- the very same object, in both the params and
|
|
28
|
+
# the strict cache -- so the round trip through it only costs time.
|
|
29
|
+
# +instance_of?+ rather than +is_a?+: a subclass is not covered by
|
|
30
|
+
# that, and Coercible::String does rebuild one.
|
|
31
|
+
return val if val.instance_of?(@type)
|
|
25
32
|
return InvalidValue.new if reject?(val)
|
|
26
33
|
return if val.nil? || treat_as_nil?(val)
|
|
27
34
|
|
|
@@ -37,11 +44,21 @@ module Grape
|
|
|
37
44
|
# but Virtus wouldn't accept it. So, this method only exists to not introduce
|
|
38
45
|
# breaking changes.
|
|
39
46
|
def reject?(val)
|
|
47
|
+
return fractional?(val) if @integer
|
|
40
48
|
return false unless @rejected_inputs
|
|
41
49
|
|
|
42
50
|
@rejected_inputs.any? { |klass| val.is_a?(klass) }
|
|
43
51
|
end
|
|
44
52
|
|
|
53
|
+
# dry-types turns a number into an Integer with Kernel#Integer, which
|
|
54
|
+
# truncates, so a JSON body's 2.99 became 2 while the same value in a
|
|
55
|
+
# query string, "2.99", was rejected. A number with a fractional part
|
|
56
|
+
# is refused instead; a whole one such as 1.0 still becomes 1. Complex
|
|
57
|
+
# numbers have no fractional part to test and are left to dry-types.
|
|
58
|
+
def fractional?(val)
|
|
59
|
+
val.is_a?(Numeric) && val.real? && !(val % 1).zero?
|
|
60
|
+
end
|
|
61
|
+
|
|
45
62
|
# Dry-Types treats an empty string as invalid. However, Grape considers an empty string as
|
|
46
63
|
# absence of a value and coerces it into nil. See a discussion there
|
|
47
64
|
# https://github.com/ruby-grape/grape/pull/2045
|
|
@@ -41,23 +41,37 @@ module Grape
|
|
|
41
41
|
|
|
42
42
|
# Coerce the given value.
|
|
43
43
|
#
|
|
44
|
+
# A value that is not an Array, and an Array holding a member none of
|
|
45
|
+
# the types accepts, are invalid, as they are for a collection of one
|
|
46
|
+
# type. A coercion method is handed the value whatever it is, as it is
|
|
47
|
+
# for +types:+, so it can build the collection out of a String.
|
|
48
|
+
#
|
|
44
49
|
# @param value [Array<String>] collection of values to be coerced
|
|
45
|
-
# @return [Array<Object>,Set<Object>,InvalidValue]
|
|
46
|
-
# the coerced result, or an
|
|
47
|
-
# of {InvalidValue} if the value could not be coerced.
|
|
50
|
+
# @return [Array<Object>,Set<Object>,InvalidValue,nil]
|
|
51
|
+
# the coerced result, nil when the value is nil or an empty String,
|
|
52
|
+
# or an instance of {InvalidValue} if the value could not be coerced.
|
|
48
53
|
def call(value)
|
|
49
|
-
return
|
|
54
|
+
return if value.nil? || (value.is_a?(String) && value.empty?)
|
|
50
55
|
|
|
51
|
-
coerced =
|
|
52
|
-
|
|
53
|
-
@method.call(value)
|
|
54
|
-
else
|
|
55
|
-
value.map { |v| @member_coercer.call(v) }
|
|
56
|
-
end
|
|
56
|
+
coerced = @method ? @method.call(value) : coerce_members(value)
|
|
57
|
+
return coerced if coerced.is_a?(InvalidValue)
|
|
57
58
|
return Set.new coerced if @types.is_a? Set
|
|
58
59
|
|
|
59
60
|
coerced
|
|
60
61
|
end
|
|
62
|
+
|
|
63
|
+
private
|
|
64
|
+
|
|
65
|
+
def coerce_members(value)
|
|
66
|
+
return InvalidValue.new unless value.is_a?(Array)
|
|
67
|
+
|
|
68
|
+
value.map do |member|
|
|
69
|
+
coerced = @member_coercer.call(member)
|
|
70
|
+
return coerced if coerced.is_a?(InvalidValue)
|
|
71
|
+
|
|
72
|
+
coerced
|
|
73
|
+
end
|
|
74
|
+
end
|
|
61
75
|
end
|
|
62
76
|
end
|
|
63
77
|
end
|
|
@@ -9,8 +9,8 @@ module Grape
|
|
|
9
9
|
# Grape uses a number of tests and assertions to
|
|
10
10
|
# work out exactly how a parameter should be handled,
|
|
11
11
|
# based on the +type+ and +coerce_with+ options that
|
|
12
|
-
# may be supplied to {Grape::
|
|
13
|
-
# and {Grape::
|
|
12
|
+
# may be supplied to {Grape::DSL::Parameters#requires}
|
|
13
|
+
# and {Grape::DSL::Parameters#optional}. The main
|
|
14
14
|
# entry point for this process is {Types.build_coercer}.
|
|
15
15
|
module Types
|
|
16
16
|
module_function
|
|
@@ -112,11 +112,10 @@ module Grape
|
|
|
112
112
|
type.respond_to?(:parse) && type.method(:parse).arity == 1
|
|
113
113
|
end
|
|
114
114
|
|
|
115
|
-
# Is the declared type an +Array+ or +Set+ of a {#custom?} type?
|
|
115
|
+
# Is the declared type an +Array+ or +Set+ of a {#custom?} or {#special?} type?
|
|
116
116
|
#
|
|
117
117
|
# @param type [Array<Class>,Class] type to check
|
|
118
|
-
# @return [Boolean] true if +type+ is a collection of a type
|
|
119
|
-
# its own +#parse+ method.
|
|
118
|
+
# @return [Boolean] true if +type+ is a one-element collection of such a type
|
|
120
119
|
def collection_of_custom?(type)
|
|
121
120
|
array_or_set?(type) && type.length == 1 && (custom?(type.first) || special?(type.first))
|
|
122
121
|
end
|
|
@@ -131,26 +130,21 @@ module Grape
|
|
|
131
130
|
#
|
|
132
131
|
# There are a few very special coercers which might be returned.
|
|
133
132
|
#
|
|
134
|
-
# +
|
|
135
|
-
#
|
|
136
|
-
# For example, +[Integer, String]+ allows integer and string values in
|
|
137
|
-
# an array.
|
|
133
|
+
# +MultipleTypeCoercer+ is returned for a value that may be one of
|
|
134
|
+
# several types, e.g. +types: [Integer, String]+.
|
|
138
135
|
#
|
|
139
|
-
# +
|
|
140
|
-
#
|
|
141
|
-
# specifies a custom type which implements requirments of
|
|
142
|
-
# +Grape::Types::CustomTypeCoercer+.
|
|
136
|
+
# +CustomTypeCoercer+ is returned for a +coerce_with+ method or a type
|
|
137
|
+
# with its own +parse+ (see {#custom?}).
|
|
143
138
|
#
|
|
144
|
-
# +
|
|
145
|
-
#
|
|
146
|
-
# type implemented by the user.
|
|
139
|
+
# +CustomTypeCollectionCoercer+ is returned for an array or set of such
|
|
140
|
+
# a type (see {#collection_of_custom?}).
|
|
147
141
|
#
|
|
148
142
|
# There is also a group of custom types implemented by Grape, check
|
|
149
143
|
# +Grape::Validations::Types::SPECIAL+ to get the full list.
|
|
150
144
|
#
|
|
151
|
-
# @param type [Class] the type to which input
|
|
152
|
-
# should be coerced
|
|
145
|
+
# @param type [Class] the type to which input values should be coerced
|
|
153
146
|
# @param method [Class,#call] the coercion method to use
|
|
147
|
+
# @param strict [Boolean] check the type rather than coerce
|
|
154
148
|
# @return [Object] object to be used
|
|
155
149
|
# for coercion and type validation
|
|
156
150
|
def build_coercer(type, method: nil, strict: false)
|
|
@@ -9,9 +9,8 @@ module Grape
|
|
|
9
9
|
#
|
|
10
10
|
# Splits the raw entries into three logical buckets:
|
|
11
11
|
#
|
|
12
|
-
# * Spec-consumed keys (type/types/coerce*, presence/message,
|
|
13
|
-
#
|
|
14
|
-
# handed to validator dispatch.
|
|
12
|
+
# * Spec-consumed keys (type/types/coerce*, presence/message, fail_fast,
|
|
13
|
+
# doc keys) — never handed to validator dispatch.
|
|
15
14
|
# * Shared opts (allow_blank, fail_fast) — read by every validator at
|
|
16
15
|
# construction time via {#shared_opts}.
|
|
17
16
|
# * Validator entries (everything else, e.g. +regexp+, +length+, +values+,
|
|
@@ -33,6 +32,9 @@ module Grape
|
|
|
33
32
|
desc description documentation
|
|
34
33
|
].freeze
|
|
35
34
|
|
|
35
|
+
BOOLEAN_VALUES = [true, false].freeze
|
|
36
|
+
private_constant :BOOLEAN_VALUES
|
|
37
|
+
|
|
36
38
|
attr_reader :raw, :coerce_type, :coerce_method, :coerce_message, :presence_options, :values, :except_values, :default, :allow_blank, :fail_fast, :shared_opts, :validator_entries
|
|
37
39
|
|
|
38
40
|
def self.from(validations)
|
|
@@ -44,15 +46,11 @@ module Grape
|
|
|
44
46
|
|
|
45
47
|
@raw = raw
|
|
46
48
|
@coerce_type, @coerce_message, @coerce_method = parse_coerce(raw)
|
|
47
|
-
|
|
48
|
-
@except_values = resolve_value(raw[:except_values])
|
|
49
|
-
@default = raw[:default]
|
|
49
|
+
read_values(raw)
|
|
50
50
|
@presence_options = raw[:presence]
|
|
51
|
-
|
|
52
|
-
@fail_fast = raw[:fail_fast] || false
|
|
51
|
+
read_shared_opts(raw)
|
|
53
52
|
|
|
54
53
|
@guessed_coerce_type = guess_coerce_type(declared_coerce_type, @values, @except_values)
|
|
55
|
-
@shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
|
|
56
54
|
@validator_entries = build_validator_entries(raw)
|
|
57
55
|
|
|
58
56
|
validate!
|
|
@@ -70,6 +68,20 @@ module Grape
|
|
|
70
68
|
|
|
71
69
|
private
|
|
72
70
|
|
|
71
|
+
# The values the declaration accepts, rejects and defaults to.
|
|
72
|
+
def read_values(raw)
|
|
73
|
+
@values = resolve_value(raw[:values])
|
|
74
|
+
@except_values = resolve_value(raw[:except_values])
|
|
75
|
+
@default = raw[:default]
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# What every validator of the declaration reads (see #shared_opts).
|
|
79
|
+
def read_shared_opts(raw)
|
|
80
|
+
@allow_blank = resolve_value(raw[:allow_blank])
|
|
81
|
+
@fail_fast = raw[:fail_fast] || false
|
|
82
|
+
@shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
|
|
83
|
+
end
|
|
84
|
+
|
|
73
85
|
# Cross-field consistency checks on the parsed declaration. Run at
|
|
74
86
|
# construction so an incoherent spec (e.g. a +default+ outside +values+,
|
|
75
87
|
# or +values+ whose elements don't match +type+) can never exist —
|
|
@@ -87,11 +99,25 @@ module Grape
|
|
|
87
99
|
def check_incompatible_option_values(default, values, except_values)
|
|
88
100
|
return if default.nil? || default.is_a?(Proc)
|
|
89
101
|
|
|
90
|
-
|
|
102
|
+
defaults = Array(default)
|
|
103
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if any_outside?(defaults, values)
|
|
104
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values) if any_among?(defaults, except_values)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
# Whether one of +defaults+ is outside +values+, known now (see #fixed_values?).
|
|
108
|
+
def any_outside?(defaults, values)
|
|
109
|
+
fixed_values?(values) && defaults.any? { |default| !values.include?(default) }
|
|
110
|
+
end
|
|
91
111
|
|
|
92
|
-
|
|
112
|
+
# Whether one of +defaults+ is among +values+, known now (see #fixed_values?).
|
|
113
|
+
def any_among?(defaults, values)
|
|
114
|
+
fixed_values?(values) && defaults.any? { |default| values.include?(default) }
|
|
115
|
+
end
|
|
93
116
|
|
|
94
|
-
|
|
117
|
+
# A +values+ or +except_values+ list known at definition time: given,
|
|
118
|
+
# and not a Proc called on every request.
|
|
119
|
+
def fixed_values?(values)
|
|
120
|
+
values && !values.is_a?(Proc)
|
|
95
121
|
end
|
|
96
122
|
|
|
97
123
|
def validate_value_coercion(coerce_type, *values_list)
|
|
@@ -99,14 +125,19 @@ module Grape
|
|
|
99
125
|
|
|
100
126
|
element_type = coerce_type.is_a?(Enumerable) ? coerce_type.first : coerce_type
|
|
101
127
|
values_list.each do |values|
|
|
102
|
-
next
|
|
128
|
+
next unless fixed_values?(values)
|
|
103
129
|
|
|
104
130
|
value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
|
|
105
|
-
|
|
106
|
-
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, element_type, :values, values) unless value_types.all?(element_type)
|
|
131
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, element_type, :values, values) unless all_of_type?(value_types, element_type)
|
|
107
132
|
end
|
|
108
133
|
end
|
|
109
134
|
|
|
135
|
+
def all_of_type?(values, type)
|
|
136
|
+
return values.all? { |value| BOOLEAN_VALUES.include?(value) } if type == Grape::API::Boolean
|
|
137
|
+
|
|
138
|
+
values.all?(type)
|
|
139
|
+
end
|
|
140
|
+
|
|
110
141
|
def build_validator_entries(raw)
|
|
111
142
|
raw.reject do |k, _|
|
|
112
143
|
SPEC_CONSUMED_KEYS.include?(k) || ParamsScope::RESERVED_DOCUMENTATION_KEYWORDS.include?(k)
|
|
@@ -114,17 +145,15 @@ module Grape
|
|
|
114
145
|
end
|
|
115
146
|
|
|
116
147
|
def parse_coerce(raw)
|
|
148
|
+
coerce_with = raw[:coerce_with]
|
|
117
149
|
if raw.key?(:type)
|
|
118
150
|
coerce, coerce_message = extract_value_and_message(raw[:type])
|
|
119
|
-
coerce_with = raw[:coerce_with]
|
|
120
151
|
return [Types::VariantCollectionCoercer.new(coerce, coerce_with), coerce_message, nil] if Types.multiple?(coerce)
|
|
121
152
|
elsif raw.key?(:types)
|
|
122
153
|
coerce, coerce_message = extract_value_and_message(raw[:types])
|
|
123
|
-
coerce_with = raw[:coerce_with]
|
|
124
154
|
else
|
|
125
|
-
coerce = raw[:coerce]
|
|
126
|
-
coerce_message = raw[:coerce_message]
|
|
127
|
-
coerce_with = raw[:coerce_with]
|
|
155
|
+
coerce, message = extract_value_and_message(raw[:coerce])
|
|
156
|
+
coerce_message = raw[:coerce_message] || message
|
|
128
157
|
end
|
|
129
158
|
|
|
130
159
|
[coerce, coerce_message, coerce_with]
|
|
@@ -154,8 +183,10 @@ module Grape
|
|
|
154
183
|
return coerce_type unless coerce_type == Array
|
|
155
184
|
|
|
156
185
|
values_list.each do |values|
|
|
157
|
-
next
|
|
158
|
-
|
|
186
|
+
next unless fixed_values?(values)
|
|
187
|
+
# A beginless Range has no first element to ask.
|
|
188
|
+
return (values.begin || values.end).class if values.is_a?(Range)
|
|
189
|
+
return values.first.class unless values.empty?
|
|
159
190
|
end
|
|
160
191
|
coerce_type
|
|
161
192
|
end
|
|
@@ -14,8 +14,8 @@ module Grape
|
|
|
14
14
|
def validate_param!(attr_name, params)
|
|
15
15
|
return if @value || !hash_like?(params)
|
|
16
16
|
|
|
17
|
-
value =
|
|
18
|
-
return if value == false ||
|
|
17
|
+
value = params[attr_name]
|
|
18
|
+
return if value == false || !Grape::Util::Scrub.blank?(value)
|
|
19
19
|
|
|
20
20
|
validation_error!(attr_name)
|
|
21
21
|
end
|
|
@@ -4,9 +4,8 @@ module Grape
|
|
|
4
4
|
module Validations
|
|
5
5
|
module Validators
|
|
6
6
|
class AsValidator < Base
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
# happens here.
|
|
7
|
+
# Never dispatched: +as:+ renaming is handled by ParamsScope. No
|
|
8
|
+
# validation happens here.
|
|
10
9
|
def validate_param!(*); end
|
|
11
10
|
end
|
|
12
11
|
end
|
|
@@ -7,10 +7,10 @@ module Grape
|
|
|
7
7
|
#
|
|
8
8
|
# == Freeze contract
|
|
9
9
|
# Validator instances are shared across requests and are frozen after
|
|
10
|
-
# initialization (via +.new+).
|
|
11
|
-
#
|
|
12
|
-
#
|
|
13
|
-
#
|
|
10
|
+
# initialization (via +.new+). #initialize freezes +attrs+ and
|
|
11
|
+
# deep-freezes +options+, so subclass ivars derived from them are frozen
|
|
12
|
+
# by construction. Lazy ivar assignment (e.g. +memoize+, <tt>||=</tt>)
|
|
13
|
+
# will raise +FrozenError+ at request time.
|
|
14
14
|
class Base
|
|
15
15
|
extend Forwardable
|
|
16
16
|
extend Grape::Util::FreezeOnNew
|
|
@@ -68,7 +68,8 @@ module Grape
|
|
|
68
68
|
@scope = scope
|
|
69
69
|
@opts = SharedOptions.new(**opts.slice(:allow_blank, :fail_fast))
|
|
70
70
|
@exception_message = message(self.class.default_message_key) if self.class.default_message_key
|
|
71
|
-
@iterator =
|
|
71
|
+
@iterator = AttributesIterator.new(@scope).freeze
|
|
72
|
+
choose_validation_path(scope)
|
|
72
73
|
end
|
|
73
74
|
|
|
74
75
|
# Validates a given request.
|
|
@@ -78,7 +79,7 @@ module Grape
|
|
|
78
79
|
# @return [void]
|
|
79
80
|
def validate(request)
|
|
80
81
|
params = request.params
|
|
81
|
-
return unless scope.should_validate?(params)
|
|
82
|
+
return unless @always_validated || scope.should_validate?(params)
|
|
82
83
|
|
|
83
84
|
validate!(params)
|
|
84
85
|
end
|
|
@@ -90,20 +91,12 @@ module Grape
|
|
|
90
91
|
# @raise [Grape::Exceptions::Validation] if validation failed
|
|
91
92
|
# @return [void]
|
|
92
93
|
def validate!(params)
|
|
93
|
-
|
|
94
|
-
# there may be more than one error per field
|
|
95
|
-
array_errors = nil
|
|
94
|
+
return validate_elements!(scope.params(params)) if @direct_elements
|
|
96
95
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
next unless scope.meets_dependency?(val, params)
|
|
96
|
+
scoped = scope.params(params) if @direct
|
|
97
|
+
return validate_attributes!(scoped) if scoped.is_a?(Hash)
|
|
100
98
|
|
|
101
|
-
|
|
102
|
-
rescue Grape::Exceptions::Validation => e
|
|
103
|
-
(array_errors ||= []) << e
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
99
|
+
validate_iterated!(params)
|
|
107
100
|
end
|
|
108
101
|
|
|
109
102
|
protected
|
|
@@ -123,10 +116,117 @@ module Grape
|
|
|
123
116
|
|
|
124
117
|
alias required? required
|
|
125
118
|
|
|
126
|
-
#
|
|
127
|
-
#
|
|
128
|
-
|
|
129
|
-
|
|
119
|
+
# A scope's parent, optionality, dependency and type are all set
|
|
120
|
+
# before its block declares anything, so these are settled by the
|
|
121
|
+
# time a validator is built. See #validate and #validate!.
|
|
122
|
+
def choose_validation_path(scope)
|
|
123
|
+
@always_validated = scope.always_validated?
|
|
124
|
+
@direct = @always_validated && !scope.iterates_elements?
|
|
125
|
+
@direct_elements = scope.validated_when_given? && scope.iterates_elements? && scope.array_depth == 1
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# #validate! on a scope that always validates and does not iterate
|
|
129
|
+
# elements, once its params resolved to a Hash: the root scope and the
|
|
130
|
+
# required Hash scopes under it, which is most validators of most
|
|
131
|
+
# endpoints. There the iterator hands back that Hash alone, and every
|
|
132
|
+
# scope on the chain is required with no dependency, so the checks
|
|
133
|
+
# around the attribute loop drop out. The machinery cost more than
|
|
134
|
+
# the validation itself.
|
|
135
|
+
def validate_attributes!(params)
|
|
136
|
+
array_errors = nil
|
|
137
|
+
|
|
138
|
+
@attrs.each do |attr_name|
|
|
139
|
+
validate_param!(attr_name, params) if required? || params.key?(attr_name)
|
|
140
|
+
rescue Grape::Exceptions::Validation => e
|
|
141
|
+
(array_errors ||= []) << e
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
# #validate_attributes! for each element of an Array scope, such as
|
|
148
|
+
# +requires :items, type: Array do+ at the root, when it is the only
|
|
149
|
+
# scope on the chain that iterates elements: its params are then the
|
|
150
|
+
# request's Array as it came in, with no nesting for the iterator to
|
|
151
|
+
# descend into. It depends on no other param and every scope above it
|
|
152
|
+
# is always validated, so the iterator's per-element checks come down
|
|
153
|
+
# to the index it records for the error names and passing over the
|
|
154
|
+
# placeholder of an optional scope that was left out. An optional
|
|
155
|
+
# scope whose elements are all empty is passed over too, as the
|
|
156
|
+
# iterator does; otherwise an empty element was sent, and is validated
|
|
157
|
+
# like any other. An element that is not a Hash goes through the same
|
|
158
|
+
# +hash_like?+ test as on the iterator path, and the members of a scope
|
|
159
|
+
# that did not get an Array are left alone, as they are there: the
|
|
160
|
+
# scope's own type check reports it.
|
|
161
|
+
def validate_elements!(elements)
|
|
162
|
+
return unless elements.is_a?(Array)
|
|
163
|
+
return if passed_over_elements?(elements)
|
|
164
|
+
|
|
165
|
+
tracker = ParamScopeTracker.current
|
|
166
|
+
array_errors = nil
|
|
167
|
+
|
|
168
|
+
elements.each_with_index do |element, index|
|
|
169
|
+
tracker&.store_index(scope, index)
|
|
170
|
+
next if ParamsScope::EmptyOptionalValue.equal?(element)
|
|
171
|
+
|
|
172
|
+
array_errors = validate_element_attributes(element, array_errors)
|
|
173
|
+
end
|
|
174
|
+
|
|
175
|
+
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
176
|
+
end
|
|
177
|
+
|
|
178
|
+
# #validate! on every other scope: each value the iterator yields is
|
|
179
|
+
# validated, unless the scope passes it over or depends on a param it
|
|
180
|
+
# does not meet.
|
|
181
|
+
def validate_iterated!(params)
|
|
182
|
+
# we collect errors inside array because
|
|
183
|
+
# there may be more than one error per field
|
|
184
|
+
array_errors = nil
|
|
185
|
+
|
|
186
|
+
@iterator.each(params) do |val, element|
|
|
187
|
+
next if passed_over?(val, element)
|
|
188
|
+
next unless scope.meets_dependency?(val, params)
|
|
189
|
+
|
|
190
|
+
array_errors = validate_element_attributes(val, array_errors)
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
# Runs #validate_param! for each attribute +element+ holds, or for every
|
|
197
|
+
# one when they are required, and returns +array_errors+ with what
|
|
198
|
+
# failed appended, allocating it on the first failure.
|
|
199
|
+
def validate_element_attributes(element, array_errors)
|
|
200
|
+
@attrs.each do |attr_name|
|
|
201
|
+
validate_param!(attr_name, element) if required? || (hash_like?(element) && element.key?(attr_name))
|
|
202
|
+
rescue Grape::Exceptions::Validation => e
|
|
203
|
+
(array_errors ||= []) << e
|
|
204
|
+
end
|
|
205
|
+
array_errors
|
|
206
|
+
end
|
|
207
|
+
|
|
208
|
+
# Whether #validate! leaves +val+ alone: in an optional scope, an empty
|
|
209
|
+
# value that is not an element the request sent (see AttributesIterator).
|
|
210
|
+
def passed_over?(val, element)
|
|
211
|
+
!scope.required? && !element && empty_element?(val)
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
# Whether #validate_elements! leaves +elements+ alone: an optional
|
|
215
|
+
# scope's Array whose elements are all empty, as the iterator does.
|
|
216
|
+
def passed_over_elements?(elements)
|
|
217
|
+
!scope.required? && elements.all? { |element| empty_element?(element) }
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
# What an optional scope passes over: its params given empty in an
|
|
221
|
+
# element of an Array further up, or the placeholder +map_params+ puts
|
|
222
|
+
# where they were not given at all. The iterator drops that placeholder
|
|
223
|
+
# itself; #validate_elements! meets it when a scope above was handed an
|
|
224
|
+
# Array instead of a Hash. An empty element of the scope's own Array is
|
|
225
|
+
# passed over only when every element of that Array is empty.
|
|
226
|
+
def empty_element?(element)
|
|
227
|
+
return true if ParamsScope::EmptyOptionalValue.equal?(element)
|
|
228
|
+
|
|
229
|
+
element.respond_to?(:empty?) ? element.empty? : element.nil?
|
|
130
230
|
end
|
|
131
231
|
|
|
132
232
|
def validation_error!(attr_name_or_params, message = exception_message)
|
|
@@ -161,10 +261,9 @@ module Grape
|
|
|
161
261
|
options_key?(:value) ? options[:value] : options
|
|
162
262
|
end
|
|
163
263
|
|
|
264
|
+
# Listed among the helpers a custom validator can call (see README).
|
|
164
265
|
def scrub(value)
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
value.scrub
|
|
266
|
+
Grape::Util::Scrub.scrub(value)
|
|
168
267
|
end
|
|
169
268
|
end
|
|
170
269
|
end
|
|
@@ -11,8 +11,7 @@ module Grape
|
|
|
11
11
|
|
|
12
12
|
@exception_message = options.message if options.message
|
|
13
13
|
|
|
14
|
-
|
|
15
|
-
type = hash_like?(raw_type) ? raw_type[:value] : raw_type
|
|
14
|
+
type = options.type
|
|
16
15
|
@converter =
|
|
17
16
|
if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
|
|
18
17
|
type
|
|
@@ -35,7 +34,7 @@ module Grape
|
|
|
35
34
|
validation_error!(attr_name, new_value.message || exception_message) if new_value.is_a?(Types::InvalidValue)
|
|
36
35
|
|
|
37
36
|
# Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
|
|
38
|
-
# which
|
|
37
|
+
# which loses wrappers for hashes and arrays after reassigning values
|
|
39
38
|
#
|
|
40
39
|
# h = Hashie::Mash.new(list: [1, 2, 3, 4])
|
|
41
40
|
# => #<Hashie::Mash list=#<Hashie::Array [1, 2, 3, 4]>>
|
|
@@ -18,10 +18,26 @@ module Grape
|
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def validate!(params)
|
|
21
|
-
|
|
21
|
+
scoped = scope.params(params) if @direct
|
|
22
|
+
return apply_defaults(scoped) if scoped.is_a?(Hash)
|
|
23
|
+
|
|
24
|
+
@iterator.each(params) do |resource_params|
|
|
22
25
|
next unless scope.meets_dependency?(resource_params, params)
|
|
23
26
|
|
|
24
|
-
|
|
27
|
+
apply_defaults(resource_params) if hash_like?(resource_params)
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
private
|
|
32
|
+
|
|
33
|
+
# Also #validate!'s whole job where Base#validate_attributes! applies:
|
|
34
|
+
# a scope that is always validated and does not iterate elements, once
|
|
35
|
+
# its params resolved to a Hash -- the root scope of every +params+
|
|
36
|
+
# block with a default in it. A scope like that depends on no other
|
|
37
|
+
# param, so the iterator and its dependency check have nothing to add.
|
|
38
|
+
def apply_defaults(params)
|
|
39
|
+
@attrs.each do |attr_name|
|
|
40
|
+
params[attr_name] = @default_call.call(params) if params[attr_name].nil?
|
|
25
41
|
end
|
|
26
42
|
end
|
|
27
43
|
end
|
|
@@ -13,14 +13,19 @@ module Grape
|
|
|
13
13
|
raise ArgumentError, 'except_values Proc must have arity of zero (use values: with a one-arity predicate for per-element checks)' if except_proc && !except.arity.zero?
|
|
14
14
|
|
|
15
15
|
# Zero-arity procs (e.g. -> { User.pluck(:role) }) must be called per-request,
|
|
16
|
-
# not at definition time
|
|
17
|
-
|
|
16
|
+
# not at definition time. Anything else is the collection itself, held
|
|
17
|
+
# as it came rather than behind a lambda that would only hand it back.
|
|
18
|
+
if except_proc
|
|
19
|
+
@excepts_call = except
|
|
20
|
+
else
|
|
21
|
+
@excepts = except
|
|
22
|
+
end
|
|
18
23
|
end
|
|
19
24
|
|
|
20
25
|
def validate_param!(attr_name, params)
|
|
21
26
|
return unless hash_like?(params) && params.key?(attr_name)
|
|
22
27
|
|
|
23
|
-
excepts =
|
|
28
|
+
excepts = current_excepts
|
|
24
29
|
return if excepts.nil?
|
|
25
30
|
|
|
26
31
|
param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
|
|
@@ -28,6 +33,14 @@ module Grape
|
|
|
28
33
|
|
|
29
34
|
validation_error!(attr_name)
|
|
30
35
|
end
|
|
36
|
+
|
|
37
|
+
private
|
|
38
|
+
|
|
39
|
+
# The values ruled out, asked of the Proc on every request when they
|
|
40
|
+
# come from one.
|
|
41
|
+
def current_excepts
|
|
42
|
+
@excepts_call ? @excepts_call.call : @excepts
|
|
43
|
+
end
|
|
31
44
|
end
|
|
32
45
|
end
|
|
33
46
|
end
|