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
|
@@ -2,37 +2,76 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Validations
|
|
5
|
+
# Yields each value a scope's attributes are read from: the scope's params,
|
|
6
|
+
# or each element of them when the scope sits in an Array. Walking the
|
|
7
|
+
# attributes themselves is left to the validator's block.
|
|
8
|
+
#
|
|
9
|
+
# Each value comes with whether it is an element a request sent in the
|
|
10
|
+
# scope's own Array. Otherwise it is the scope's value in an element of an
|
|
11
|
+
# Array further up, where an empty one is an optional scope left out of
|
|
12
|
+
# that element, or an element of an optional scope's Array whose elements
|
|
13
|
+
# are all empty, which is passed over the same way.
|
|
5
14
|
class AttributesIterator
|
|
6
|
-
# +
|
|
7
|
-
#
|
|
8
|
-
#
|
|
9
|
-
#
|
|
10
|
-
def initialize(
|
|
11
|
-
@attrs = attrs
|
|
15
|
+
# +scope+ is static per validator; only +params+ varies per request, so
|
|
16
|
+
# an instance can be built once and reused (it keeps no request-derived
|
|
17
|
+
# state). Reused instances are shared across threads, so +each+ must
|
|
18
|
+
# stay free of mutable instance state.
|
|
19
|
+
def initialize(scope)
|
|
12
20
|
@scope = scope
|
|
13
21
|
# How many times #do_each may descend into a nested array. The
|
|
14
22
|
# declaration allows one level per Array-typed scope on the chain, less
|
|
15
23
|
# the one +Array.wrap+ already consumes in #each. Anything deeper was
|
|
16
24
|
# put there by the request, not by the declaration.
|
|
17
25
|
@max_nesting = [scope.array_depth - 1, 0].max
|
|
26
|
+
@iterates_elements = scope.iterates_elements?
|
|
27
|
+
@optional = !scope.required?
|
|
18
28
|
end
|
|
19
29
|
|
|
20
30
|
def each(params, &)
|
|
21
31
|
original_params = @scope.params(params)
|
|
32
|
+
iterates_elements = @scope.iterates_elements?
|
|
22
33
|
# A scope resolves to a Hash unless the declaration nests arrays, and
|
|
23
34
|
# then #do_each has nothing to do but hand it straight back: Array.wrap
|
|
24
35
|
# boxes it, the loop unboxes it on its only iteration, and with no Array
|
|
25
36
|
# anywhere neither the nesting descent nor the index bookkeeping
|
|
26
37
|
# applies. Every validator on a flat +params+ block comes through here.
|
|
27
|
-
return
|
|
38
|
+
return yield(original_params) if original_params.is_a?(Hash) && !iterates_elements
|
|
39
|
+
|
|
40
|
+
array_params = original_params.is_a?(Array)
|
|
41
|
+
# Do not validate the content of an array scope that did not get one.
|
|
42
|
+
return if iterates_elements && !array_params
|
|
43
|
+
|
|
44
|
+
index_scope = index_scope_for(iterates_elements, array_params)
|
|
45
|
+
# No tracker means we're outside a ParamScopeTracker.track block (e.g.
|
|
46
|
+
# a unit test that invokes a validator directly). Index tracking is
|
|
47
|
+
# skipped — full_name will produce bracket-less names — but validation
|
|
48
|
+
# continues rather than crashing.
|
|
49
|
+
tracker = ParamScopeTracker.current if index_scope
|
|
28
50
|
|
|
29
51
|
# because we need recursion for nested arrays
|
|
30
|
-
|
|
52
|
+
params_to_process = Array.wrap(original_params)
|
|
53
|
+
do_each(params_to_process, tracker, index_scope, NO_PARENT_INDICES, sent_elements?(params_to_process, NO_PARENT_INDICES), &)
|
|
31
54
|
end
|
|
32
55
|
|
|
33
56
|
private
|
|
34
57
|
|
|
35
|
-
|
|
58
|
+
# The top-level call's parent indices; only ever read.
|
|
59
|
+
NO_PARENT_INDICES = [].freeze
|
|
60
|
+
private_constant :NO_PARENT_INDICES
|
|
61
|
+
|
|
62
|
+
# Where each element's index is recorded depends on the scope and on
|
|
63
|
+
# whether its params are an Array, never on the element, so it is
|
|
64
|
+
# settled once per #each rather than per element. A lateral scope (no
|
|
65
|
+
# @element) whose params resolved to an array hands its index to the
|
|
66
|
+
# nearest element-iterating ancestor, so full_name still produces the
|
|
67
|
+
# right bracketed index.
|
|
68
|
+
def index_scope_for(iterates_elements, array_params)
|
|
69
|
+
return @scope if iterates_elements
|
|
70
|
+
|
|
71
|
+
@scope.nearest_array_ancestor if array_params
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def do_each(params_to_process, tracker, index_scope, parent_indices, sent_elements, &)
|
|
36
75
|
params_to_process.each_with_index do |resource_params, index|
|
|
37
76
|
# when we get arrays of arrays it means that target element located inside array
|
|
38
77
|
# we need this because we want to know parent arrays indices
|
|
@@ -42,52 +81,49 @@ module Grape
|
|
|
42
81
|
# validators see a non-hash and fail it the same way any other
|
|
43
82
|
# unexpected element type does.
|
|
44
83
|
if resource_params.is_a?(Array) && parent_indices.size < @max_nesting
|
|
45
|
-
|
|
84
|
+
nested_indices = [index] + parent_indices
|
|
85
|
+
do_each(resource_params, tracker, index_scope, nested_indices, sent_elements?(resource_params, nested_indices), &)
|
|
46
86
|
next
|
|
47
87
|
end
|
|
48
88
|
|
|
49
|
-
if
|
|
50
|
-
|
|
89
|
+
store_indices(tracker, index_scope, index, parent_indices) if tracker
|
|
90
|
+
yield resource_params, sent_elements unless skip?(resource_params)
|
|
91
|
+
end
|
|
92
|
+
end
|
|
51
93
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
94
|
+
# Whether +values+, reached through +parent_indices+, are elements of the
|
|
95
|
+
# scope's own Array that were sent. An optional scope's Array whose
|
|
96
|
+
# elements are all empty is passed over, as the scope itself is when it
|
|
97
|
+
# is left out (see ParamsScope#should_validate?, which answers that for
|
|
98
|
+
# the Array at the root). Checked once per Array rather than per element.
|
|
99
|
+
def sent_elements?(values, parent_indices)
|
|
100
|
+
@iterates_elements && parent_indices.size == @max_nesting && !(@optional && values.all? { |value| empty?(value) })
|
|
101
|
+
end
|
|
60
102
|
|
|
61
|
-
|
|
62
|
-
|
|
103
|
+
# Primitives like Integers and Booleans don't respond to +empty?+.
|
|
104
|
+
def empty?(value)
|
|
105
|
+
value.respond_to?(:empty?) ? value.empty? : value.nil?
|
|
63
106
|
end
|
|
64
107
|
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
parent_scope = target_scope.parent
|
|
108
|
+
# Each parent index belongs to the next element-iterating scope up the
|
|
109
|
+
# chain, which a Hash scope may sit below. There is always one:
|
|
110
|
+
# +parent_indices+ holds at most +array_depth - 1+ entries, and
|
|
111
|
+
# +target_scope+ has that many iterating ancestors.
|
|
112
|
+
def store_indices(tracker, target_scope, index, parent_indices)
|
|
113
|
+
parent_scope = target_scope.nearest_array_ancestor
|
|
72
114
|
parent_indices.each do |parent_index|
|
|
73
|
-
break unless parent_scope
|
|
74
|
-
|
|
75
115
|
tracker.store_index(parent_scope, parent_index)
|
|
76
|
-
parent_scope = parent_scope.
|
|
116
|
+
parent_scope = parent_scope.nearest_array_ancestor
|
|
77
117
|
end
|
|
78
118
|
tracker.store_index(target_scope, index)
|
|
79
119
|
end
|
|
80
120
|
|
|
81
|
-
def yield_attributes(_resource_params)
|
|
82
|
-
raise NotImplementedError
|
|
83
|
-
end
|
|
84
|
-
|
|
85
121
|
# This is a special case so that we can ignore trees where option
|
|
86
122
|
# values are missing lower down. Unfortunately we can't remove this
|
|
87
123
|
# at the parameter parsing stage as they are required to ensure
|
|
88
124
|
# the correct indexing is maintained
|
|
89
125
|
def skip?(val)
|
|
90
|
-
|
|
126
|
+
ParamsScope::EmptyOptionalValue.equal?(val)
|
|
91
127
|
end
|
|
92
128
|
end
|
|
93
129
|
end
|
|
@@ -6,7 +6,7 @@ module Grape
|
|
|
6
6
|
# by {ValidationsSpec#coerce_options} from the parsed +type+/+coerce_with+/
|
|
7
7
|
# +coerce_message+ declaration — never written by the user — and consumed
|
|
8
8
|
# by {ParamsScope#check_coerce_with} / {ParamsScope#validate_coerce} and by
|
|
9
|
-
# {Validators::
|
|
9
|
+
# {Validators::CoerceValidator} (which receives it as its
|
|
10
10
|
# +options+ argument).
|
|
11
11
|
#
|
|
12
12
|
# All three fields may be +nil+ (e.g. a remountable API evaluated on its
|
|
@@ -17,26 +17,34 @@ module Grape
|
|
|
17
17
|
private
|
|
18
18
|
|
|
19
19
|
def extract_details(spec)
|
|
20
|
-
details = {}
|
|
21
|
-
details[:required] = spec.required?
|
|
20
|
+
details = { required: spec.required? }
|
|
22
21
|
details[:type] = TypeCache[spec.coerce_type] if spec.coerce_type
|
|
22
|
+
add_value_details(details, spec)
|
|
23
|
+
add_raw_details(details, spec.raw)
|
|
24
|
+
details
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
# The values the declaration accepts, rejects and defaults to.
|
|
28
|
+
def add_value_details(details, spec)
|
|
23
29
|
details[:values] = spec.values if spec.values
|
|
24
30
|
details[:except_values] = spec.except_values if spec.except_values
|
|
25
31
|
details[:default] = spec.default unless spec.default.nil?
|
|
32
|
+
end
|
|
26
33
|
|
|
27
|
-
|
|
34
|
+
# What the declaration's own options document beyond its validations:
|
|
35
|
+
# the +length+ bounds, the description and the +documentation+ Hash.
|
|
36
|
+
def add_raw_details(details, raw)
|
|
37
|
+
length = raw[:length]
|
|
28
38
|
if length.is_a?(Hash)
|
|
29
39
|
details[:min_length] = length[:min] if length.key?(:min)
|
|
30
40
|
details[:max_length] = length[:max] if length.key?(:max)
|
|
31
41
|
end
|
|
32
42
|
|
|
33
|
-
desc =
|
|
43
|
+
desc = raw[:desc] || raw[:description]
|
|
34
44
|
details[:desc] = desc if desc
|
|
35
45
|
|
|
36
|
-
documentation =
|
|
46
|
+
documentation = raw[:documentation]
|
|
37
47
|
details[:documentation] = documentation if documentation
|
|
38
|
-
|
|
39
|
-
details
|
|
40
48
|
end
|
|
41
49
|
|
|
42
50
|
class TypeCache < Grape::Util::Cache
|
|
@@ -5,10 +5,35 @@ module Grape
|
|
|
5
5
|
class ParamsScope
|
|
6
6
|
attr_reader :parent, :type, :nearest_array_ancestor, :array_depth, :full_path
|
|
7
7
|
|
|
8
|
+
# The elements a +given+ scope narrowed its Array params down to during
|
|
9
|
+
# this request (see #meets_dependency?). Only a scope with a dependency
|
|
10
|
+
# ever stores any, so every other scope answers without the fiber-storage
|
|
11
|
+
# and tracker lookups -- which #params pays on each nested resolution,
|
|
12
|
+
# twice per validator, on every request.
|
|
8
13
|
def qualifying_params
|
|
14
|
+
return unless @dependent_on
|
|
15
|
+
|
|
9
16
|
ParamScopeTracker.current&.qualifying_params(self)
|
|
10
17
|
end
|
|
11
18
|
|
|
19
|
+
# @param params [Hash] initial hash of parameters
|
|
20
|
+
# @return hash of parameters relevant for the current scope
|
|
21
|
+
# @api private
|
|
22
|
+
# Only a +given+ scope ever stores qualifying params, and a scope on an
|
|
23
|
+
# independent chain has none above it (see #independent?), so the
|
|
24
|
+
# lookup and the +presence+ test that follows it are skipped -- both of
|
|
25
|
+
# them, at every level, on every call.
|
|
26
|
+
def params(params)
|
|
27
|
+
scoped = if @parent.nil?
|
|
28
|
+
params
|
|
29
|
+
elsif @independent
|
|
30
|
+
@parent.params(params)
|
|
31
|
+
else
|
|
32
|
+
@parent.qualifying_params.presence || @parent.params(params)
|
|
33
|
+
end
|
|
34
|
+
@element ? map_params(scoped, @element) : scoped
|
|
35
|
+
end
|
|
36
|
+
|
|
12
37
|
include Grape::DSL::Parameters
|
|
13
38
|
include Grape::Validations::ParamsDocumentation
|
|
14
39
|
|
|
@@ -19,6 +44,19 @@ module Grape
|
|
|
19
44
|
|
|
20
45
|
SPECIAL_JSON = [JSON, Array[JSON]].freeze
|
|
21
46
|
|
|
47
|
+
# What {#validated_params} returns for a scope that should not be
|
|
48
|
+
# validated. A plain +false+ would be indistinguishable from params of
|
|
49
|
+
# that value, which +map_params+ can hand back for an element a request
|
|
50
|
+
# set to +false+.
|
|
51
|
+
NOT_VALIDATED = Object.new.freeze
|
|
52
|
+
private_constant :NOT_VALIDATED
|
|
53
|
+
|
|
54
|
+
# What {#params} hands back for an element of an Array that left an
|
|
55
|
+
# optional scope out, in place of that scope's params. Dropping the
|
|
56
|
+
# element instead would shift the indices the errors name, so it stays,
|
|
57
|
+
# and AttributesIterator and the validators pass over it.
|
|
58
|
+
EmptyOptionalValue = Object.new.freeze
|
|
59
|
+
|
|
22
60
|
class Attr
|
|
23
61
|
attr_reader :key, :scope
|
|
24
62
|
|
|
@@ -50,7 +88,7 @@ module Grape
|
|
|
50
88
|
end
|
|
51
89
|
|
|
52
90
|
# Open up a new ParamsScope, allowing parameter definitions per
|
|
53
|
-
# Grape::DSL::
|
|
91
|
+
# Grape::DSL::Parameters.
|
|
54
92
|
# @param api [API] the API endpoint to modify
|
|
55
93
|
# @param element [Symbol] the element that contains this scope; for
|
|
56
94
|
# this to be relevant, parent must be set
|
|
@@ -59,10 +97,10 @@ module Grape
|
|
|
59
97
|
# @param parent [ParamsScope] the scope containing this scope
|
|
60
98
|
# @param optional [Boolean] whether or not this scope needs to have
|
|
61
99
|
# any parameters set or not
|
|
62
|
-
# @param type [Class] a type meant to govern this scope
|
|
63
|
-
# @param
|
|
64
|
-
# @param dependent_on [Symbol] if present, this scope should
|
|
65
|
-
# validate if
|
|
100
|
+
# @param type [Class] a type meant to govern this scope
|
|
101
|
+
# @param group [Hash] group options for this scope
|
|
102
|
+
# @param dependent_on [Array<Symbol, Hash>] if present, this scope should
|
|
103
|
+
# only validate if these params are present in the parent scope
|
|
66
104
|
# @yield the instance context, open for parameter definitions
|
|
67
105
|
def initialize(api:, element: nil, element_renamed: nil, parent: nil, optional: false, type: nil, group: nil, dependent_on: nil, &block)
|
|
68
106
|
@element = element
|
|
@@ -73,6 +111,9 @@ module Grape
|
|
|
73
111
|
@type = type
|
|
74
112
|
@group = group
|
|
75
113
|
@dependent_on = dependent_on
|
|
114
|
+
# See #independent?. Settled here because a scope's block may declare
|
|
115
|
+
# children, and each of them asks its parent.
|
|
116
|
+
@independent = !dependent_on && (parent.nil? || parent.independent?)
|
|
76
117
|
# Must be an ivar: push_declared_params is dispatched on self during
|
|
77
118
|
# instance_eval, so local variables from initialize are unreachable.
|
|
78
119
|
# configure_declared_params consumes it and clears @declared_params to nil.
|
|
@@ -97,23 +138,35 @@ module Grape
|
|
|
97
138
|
# @return [Boolean] whether or not this entire scope needs to be
|
|
98
139
|
# validated
|
|
99
140
|
def should_validate?(parameters)
|
|
141
|
+
return !NOT_VALIDATED.equal?(validated_params(parameters)) if @independent
|
|
142
|
+
|
|
100
143
|
scoped_params = params(parameters)
|
|
101
144
|
|
|
102
|
-
return false if @optional && (
|
|
145
|
+
return false if @optional && (Grape::Util::Scrub.blank?(scoped_params) || all_element_blank?(scoped_params))
|
|
103
146
|
return false unless meets_dependency?(scoped_params, parameters)
|
|
104
|
-
return true if @parent.nil?
|
|
105
147
|
|
|
148
|
+
# Past the +@independent+ return, this scope or one above it is a
|
|
149
|
+
# +given+ scope, and a +given+ scope is always nested: there is a parent.
|
|
106
150
|
@parent.should_validate?(parameters)
|
|
107
151
|
end
|
|
108
152
|
|
|
153
|
+
# Whether this scope and every scope above it declare no dependency, so
|
|
154
|
+
# #should_validate? has nothing to ask but whether each of them was given
|
|
155
|
+
# params -- and +qualifying_params+, which only a +given+ scope ever
|
|
156
|
+
# stores, cannot stand in for any of them.
|
|
157
|
+
# @return [Boolean]
|
|
158
|
+
def independent?
|
|
159
|
+
@independent
|
|
160
|
+
end
|
|
161
|
+
|
|
109
162
|
def meets_dependency?(params, request_params)
|
|
110
163
|
return true unless @dependent_on
|
|
111
|
-
return false if @parent
|
|
164
|
+
return false if @parent && !@parent.meets_dependency?(@parent.params(request_params), request_params)
|
|
112
165
|
|
|
113
166
|
if params.is_a?(Array)
|
|
114
167
|
filtered = params.flatten.filter { |param| meets_dependency?(param, request_params) }
|
|
115
168
|
ParamScopeTracker.current&.store_qualifying_params(self, filtered)
|
|
116
|
-
return filtered.
|
|
169
|
+
return !filtered.empty?
|
|
117
170
|
end
|
|
118
171
|
|
|
119
172
|
meets_hash_dependency?(params)
|
|
@@ -121,7 +174,7 @@ module Grape
|
|
|
121
174
|
|
|
122
175
|
def attr_meets_dependency?(params)
|
|
123
176
|
return true unless @dependent_on
|
|
124
|
-
return false if @parent
|
|
177
|
+
return false if @parent && !@parent.attr_meets_dependency?(params)
|
|
125
178
|
|
|
126
179
|
meets_hash_dependency?(params)
|
|
127
180
|
end
|
|
@@ -135,7 +188,7 @@ module Grape
|
|
|
135
188
|
key, callable = dependency.first
|
|
136
189
|
callable.call(params[key])
|
|
137
190
|
else
|
|
138
|
-
params[dependency]
|
|
191
|
+
!Grape::Util::Scrub.blank?(params[dependency])
|
|
139
192
|
end
|
|
140
193
|
end
|
|
141
194
|
end
|
|
@@ -166,6 +219,23 @@ module Grape
|
|
|
166
219
|
!@parent
|
|
167
220
|
end
|
|
168
221
|
|
|
222
|
+
# Whether #should_validate? answers true for every request: this scope
|
|
223
|
+
# and each of its ancestors is required and depends on nothing, so
|
|
224
|
+
# neither the params nor the parent chain have anything to say.
|
|
225
|
+
# @return [Boolean]
|
|
226
|
+
def always_validated?
|
|
227
|
+
!@optional && validated_when_given?
|
|
228
|
+
end
|
|
229
|
+
|
|
230
|
+
# Whether #should_validate? has nothing to ask but whether this scope's
|
|
231
|
+
# own params were given: it depends on no other param, and every scope
|
|
232
|
+
# above it is always validated. A required scope like that is then
|
|
233
|
+
# always validated, an optional one whenever its params are there.
|
|
234
|
+
# @return [Boolean]
|
|
235
|
+
def validated_when_given?
|
|
236
|
+
!@dependent_on && (@parent.nil? || @parent.always_validated?)
|
|
237
|
+
end
|
|
238
|
+
|
|
169
239
|
# A nested scope is contained in one of its parent's elements.
|
|
170
240
|
# @return [Boolean] whether or not this scope is nested
|
|
171
241
|
def nested?
|
|
@@ -198,6 +268,29 @@ module Grape
|
|
|
198
268
|
|
|
199
269
|
protected
|
|
200
270
|
|
|
271
|
+
# This scope's params, once it and every scope above it has been found
|
|
272
|
+
# worth validating; NOT_VALIDATED as soon as one of them is not.
|
|
273
|
+
#
|
|
274
|
+
# Only for an independent chain (see #independent?), where every scope
|
|
275
|
+
# answers from its own params alone: each one is then resolved from the
|
|
276
|
+
# one above rather than from the root, so the walk costs one +map_params+
|
|
277
|
+
# per scope instead of one per scope per level. Nothing here has a side
|
|
278
|
+
# effect, so checking from the root down rather than from here up reaches
|
|
279
|
+
# the same answer.
|
|
280
|
+
def validated_params(parameters)
|
|
281
|
+
if @parent
|
|
282
|
+
scoped = @parent.validated_params(parameters)
|
|
283
|
+
return scoped if NOT_VALIDATED.equal?(scoped)
|
|
284
|
+
|
|
285
|
+
scoped = map_params(scoped, @element) if @element
|
|
286
|
+
else
|
|
287
|
+
scoped = parameters
|
|
288
|
+
end
|
|
289
|
+
return NOT_VALIDATED if @optional && (Grape::Util::Scrub.blank?(scoped) || all_element_blank?(scoped))
|
|
290
|
+
|
|
291
|
+
scoped
|
|
292
|
+
end
|
|
293
|
+
|
|
201
294
|
# Adds a parameter declaration to our list of validations.
|
|
202
295
|
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
|
|
203
296
|
# +declared_params_scope+ is the scope an attribute is recorded against.
|
|
@@ -214,6 +307,28 @@ module Grape
|
|
|
214
307
|
|
|
215
308
|
private
|
|
216
309
|
|
|
310
|
+
def map_params(params, element, is_array: false)
|
|
311
|
+
case params
|
|
312
|
+
when Array
|
|
313
|
+
params.map do |el|
|
|
314
|
+
map_params(el, element, is_array: true)
|
|
315
|
+
end
|
|
316
|
+
when Hash
|
|
317
|
+
params[element] || missing_element(is_array)
|
|
318
|
+
when EmptyOptionalValue
|
|
319
|
+
EmptyOptionalValue
|
|
320
|
+
else
|
|
321
|
+
{}
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# What a Hash without the element maps to: EmptyOptionalValue for an
|
|
326
|
+
# optional scope's element of an Array, which the validators pass over,
|
|
327
|
+
# and an empty Hash otherwise.
|
|
328
|
+
def missing_element(is_array)
|
|
329
|
+
@optional && is_array ? EmptyOptionalValue : {}
|
|
330
|
+
end
|
|
331
|
+
|
|
217
332
|
def build_full_path
|
|
218
333
|
return @parent.full_path + [@element] if nested?
|
|
219
334
|
return @parent.full_path if lateral?
|
|
@@ -249,10 +364,7 @@ module Grape
|
|
|
249
364
|
|
|
250
365
|
requires(field, **field_opts)
|
|
251
366
|
end
|
|
252
|
-
optional_fields
|
|
253
|
-
field_opts = using[field]
|
|
254
|
-
optional(field, **field_opts) if field_opts
|
|
255
|
-
end
|
|
367
|
+
declare_optional_fields(optional_fields, using)
|
|
256
368
|
end
|
|
257
369
|
|
|
258
370
|
def require_optional_fields(context, using:, except: nil)
|
|
@@ -261,7 +373,13 @@ module Grape
|
|
|
261
373
|
except_fields = Array.wrap(except)
|
|
262
374
|
optional_fields.delete_if { |f| except_fields.include?(f) }
|
|
263
375
|
end
|
|
264
|
-
optional_fields
|
|
376
|
+
declare_optional_fields(optional_fields, using)
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# Each of +fields+ as optional, with its options in +using+; one +using+
|
|
380
|
+
# does not hold is left out.
|
|
381
|
+
def declare_optional_fields(fields, using)
|
|
382
|
+
fields.each do |field|
|
|
265
383
|
field_opts = using[field]
|
|
266
384
|
optional(field, **field_opts) if field_opts
|
|
267
385
|
end
|
|
@@ -277,13 +395,15 @@ module Grape
|
|
|
277
395
|
# @yield parameter scope
|
|
278
396
|
def new_scope(element, type:, as:, optional: false, &)
|
|
279
397
|
# A group needs a type: it says whether the nested params sit under one
|
|
280
|
-
# object or repeat in a list, and
|
|
281
|
-
#
|
|
282
|
-
#
|
|
283
|
-
if
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
398
|
+
# object or repeat in a list, and nothing else could decide it. Checked
|
|
399
|
+
# here for `requires` and `optional` alike — `new_scope` is only reached
|
|
400
|
+
# from their block branch.
|
|
401
|
+
raise Grape::Exceptions::MissingGroupType if type.nil?
|
|
402
|
+
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
|
|
403
|
+
|
|
404
|
+
# The block declares the keys of each element, so an element has to be
|
|
405
|
+
# a Hash. +Array[JSON]+ checks that when it parses the elements.
|
|
406
|
+
validate('hash_elements', {}, [element], false, {}) if type == Array
|
|
287
407
|
|
|
288
408
|
self.class.new(
|
|
289
409
|
api: @api,
|
|
@@ -291,7 +411,7 @@ module Grape
|
|
|
291
411
|
element_renamed: as,
|
|
292
412
|
parent: self,
|
|
293
413
|
optional:,
|
|
294
|
-
type
|
|
414
|
+
type:,
|
|
295
415
|
group: @group,
|
|
296
416
|
&
|
|
297
417
|
)
|
|
@@ -299,8 +419,8 @@ module Grape
|
|
|
299
419
|
|
|
300
420
|
# Returns a new parameter scope, not nested under any current-level param
|
|
301
421
|
# but instead at the same level as the current scope.
|
|
302
|
-
# @param dependent_on [Symbol] if given,
|
|
303
|
-
#
|
|
422
|
+
# @param dependent_on [Array<Symbol, Hash>] if given, this scope only
|
|
423
|
+
# validates if these parameters from the above scope are present
|
|
304
424
|
# @yield parameter scope
|
|
305
425
|
def new_lateral_scope(dependent_on:, &)
|
|
306
426
|
self.class.new(
|
|
@@ -348,6 +468,23 @@ module Grape
|
|
|
348
468
|
(iterates_elements? ? 1 : 0) + (@parent&.array_depth || 0)
|
|
349
469
|
end
|
|
350
470
|
|
|
471
|
+
def validates(attrs, validations, required: false)
|
|
472
|
+
spec = ValidationsSpec.from(declared_validations(validations, required))
|
|
473
|
+
|
|
474
|
+
document_params(attrs, spec)
|
|
475
|
+
|
|
476
|
+
# Presence runs first — `required` is forwarded to every subsequent
|
|
477
|
+
# validator (some short-circuit on it).
|
|
478
|
+
validate_presence(spec, attrs)
|
|
479
|
+
|
|
480
|
+
# Coerce runs second — later validators see the typed value.
|
|
481
|
+
validate_coerce(spec, attrs)
|
|
482
|
+
|
|
483
|
+
spec.validator_entries.each do |type, options|
|
|
484
|
+
validate(type, options, attrs, spec.required?, spec.shared_opts)
|
|
485
|
+
end
|
|
486
|
+
end
|
|
487
|
+
|
|
351
488
|
# +required+ is the DSL's own signal — +requires+ passes true, +optional+
|
|
352
489
|
# false. It used to travel as a +:presence+ key that +requires+ wrote into
|
|
353
490
|
# the caller's option Hash, which is why a user-supplied +presence:+ was
|
|
@@ -359,27 +496,15 @@ module Grape
|
|
|
359
496
|
# A +presence:+ supplied by an API still decides the outcome exactly as it
|
|
360
497
|
# used to — deprecated rather than dropped, so nothing changes under an
|
|
361
498
|
# API that relies on it until the key is ignored outright.
|
|
362
|
-
def
|
|
499
|
+
def declared_validations(validations, required)
|
|
363
500
|
if validations.key?(:presence)
|
|
364
501
|
Grape.deprecator.warn('Passing a `presence` option is deprecated and it will be ignored in a future release. Declare the parameter with `requires` to make it required, `optional` to make it optional.')
|
|
365
502
|
end
|
|
366
503
|
|
|
367
504
|
declared = required ? validations.merge(presence: { value: true, message: validations[:message] }) : validations
|
|
368
|
-
|
|
369
|
-
spec = ValidationsSpec.from(declared)
|
|
370
|
-
|
|
371
|
-
document_params(attrs, spec)
|
|
372
|
-
|
|
373
|
-
# Presence runs first — `required` is forwarded to every subsequent
|
|
374
|
-
# validator (some short-circuit on it).
|
|
375
|
-
validate_presence(spec, attrs)
|
|
376
|
-
|
|
377
|
-
# Coerce runs second — later validators see the typed value.
|
|
378
|
-
validate_coerce(spec, attrs)
|
|
505
|
+
return declared unless declared.key?(:oneof)
|
|
379
506
|
|
|
380
|
-
|
|
381
|
-
validate(type, options, attrs, spec.required?, spec.shared_opts)
|
|
382
|
-
end
|
|
507
|
+
declared.merge(oneof: collected_oneof(declared))
|
|
383
508
|
end
|
|
384
509
|
|
|
385
510
|
# Enforce correct usage of :coerce_with on a CoerceOptions.
|
|
@@ -425,7 +550,7 @@ module Grape
|
|
|
425
550
|
raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
|
|
426
551
|
|
|
427
552
|
variants = validations[:oneof]
|
|
428
|
-
raise ArgumentError, 'oneof: must be a non-empty Array of blocks'
|
|
553
|
+
raise ArgumentError, 'oneof: must be a non-empty Array of blocks' if !variants.is_a?(Array) || variants.empty?
|
|
429
554
|
raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
|
|
430
555
|
|
|
431
556
|
variants.map { |block| OneofCollector.collect(block) }
|
|
@@ -443,8 +568,16 @@ module Grape
|
|
|
443
568
|
@api.inheritable_setting.add_validation(validator_instance)
|
|
444
569
|
end
|
|
445
570
|
|
|
571
|
+
# Only ever asked about params that are not blank (see #should_validate?
|
|
572
|
+
# and #validated_params), and a Hash that is not blank is never all
|
|
573
|
+
# blank: what +all?+ yields for it is a [key, value] pair, an Array of
|
|
574
|
+
# two, which +blank?+ is false for. Asking anyway walked the Hash and
|
|
575
|
+
# built one of those Arrays per entry, for every optional Hash scope of
|
|
576
|
+
# every request.
|
|
446
577
|
def all_element_blank?(scoped_params)
|
|
447
|
-
|
|
578
|
+
return false if scoped_params.is_a?(::Hash)
|
|
579
|
+
|
|
580
|
+
scoped_params.respond_to?(:all?) && scoped_params.all? { |element| Grape::Util::Scrub.blank?(element) }
|
|
448
581
|
end
|
|
449
582
|
end
|
|
450
583
|
end
|
|
@@ -10,7 +10,7 @@ module Grape
|
|
|
10
10
|
# there are any problems parsing the string.
|
|
11
11
|
#
|
|
12
12
|
# Alternately an optional +method+ may be supplied (see the
|
|
13
|
-
# +coerce_with+ option of {Grape::
|
|
13
|
+
# +coerce_with+ option of {Grape::DSL::Parameters#requires}).
|
|
14
14
|
# This may be any class or object implementing +parse+ or +call+,
|
|
15
15
|
# with the same contract as described above.
|
|
16
16
|
#
|
|
@@ -53,8 +53,7 @@ module Grape
|
|
|
53
53
|
|
|
54
54
|
# Coerces the given value.
|
|
55
55
|
#
|
|
56
|
-
# @param
|
|
57
|
-
# this should always be a string.
|
|
56
|
+
# @param val [Object] value to be coerced
|
|
58
57
|
# @return [Object] the coerced result
|
|
59
58
|
def call(val)
|
|
60
59
|
coerced_val = @method.call(val)
|
|
@@ -4,7 +4,7 @@ module Grape
|
|
|
4
4
|
module Validations
|
|
5
5
|
module Types
|
|
6
6
|
# See {CustomTypeCoercer} for details on types
|
|
7
|
-
# that will be supported by this
|
|
7
|
+
# that will be supported by this coercer.
|
|
8
8
|
# This coercer works in the same way as +CustomTypeCoercer+
|
|
9
9
|
# except that it expects to receive an array of strings to
|
|
10
10
|
# coerce and will return an array (or optionally, a set)
|