grape 3.3.4 → 4.0.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 +128 -0
- data/CONTRIBUTING.md +16 -0
- data/README.md +208 -8
- data/UPGRADING.md +756 -6
- data/lib/grape/api/instance.rb +40 -33
- data/lib/grape/content_types.rb +47 -1
- data/lib/grape/dry_types.rb +1 -1
- data/lib/grape/dsl/callbacks.rb +3 -9
- data/lib/grape/dsl/declared.rb +4 -4
- data/lib/grape/dsl/desc.rb +18 -8
- data/lib/grape/dsl/entity.rb +37 -19
- data/lib/grape/dsl/helpers.rb +3 -3
- data/lib/grape/dsl/inside_route.rb +20 -8
- data/lib/grape/dsl/middleware.rb +3 -3
- data/lib/grape/dsl/parameters.rb +54 -28
- data/lib/grape/dsl/request_response.rb +37 -39
- data/lib/grape/dsl/rescue_options.rb +6 -5
- data/lib/grape/dsl/routing.rb +116 -49
- data/lib/grape/dsl/settings.rb +1 -1
- data/lib/grape/dsl/validations.rb +3 -3
- data/lib/grape/dsl/version_options.rb +3 -3
- data/lib/grape/endpoint/options.rb +13 -7
- data/lib/grape/endpoint.rb +141 -118
- data/lib/grape/env.rb +7 -0
- data/lib/grape/error_formatter/base.rb +6 -5
- data/lib/grape/error_formatter.rb +6 -2
- data/lib/grape/exceptions/error_response.rb +4 -1
- data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
- data/lib/grape/exceptions/validation.rb +9 -8
- data/lib/grape/formatter/json.rb +1 -0
- data/lib/grape/formatter/serializable_hash.rb +1 -0
- data/lib/grape/locale/en.yml +1 -0
- data/lib/grape/middleware/auth/dsl.rb +5 -33
- data/lib/grape/middleware/error.rb +153 -46
- data/lib/grape/middleware/formatter.rb +95 -26
- data/lib/grape/middleware/precomputed_content_types.rb +12 -7
- data/lib/grape/middleware/stack.rb +13 -4
- data/lib/grape/middleware/versioner/base.rb +3 -9
- data/lib/grape/middleware/versioner/path.rb +49 -7
- data/lib/grape/namespace.rb +3 -2
- data/lib/grape/path.rb +8 -69
- data/lib/grape/precompiled_json.rb +50 -0
- data/lib/grape/request.rb +36 -4
- data/lib/grape/router/base_route.rb +47 -10
- data/lib/grape/router/greedy_route.rb +5 -1
- data/lib/grape/router/pattern/path.rb +78 -0
- data/lib/grape/router/pattern.rb +60 -14
- data/lib/grape/router/route.rb +62 -10
- data/lib/grape/router.rb +100 -57
- data/lib/grape/serve_stream/file_body.rb +6 -0
- data/lib/grape/serve_stream/stream_response.rb +6 -0
- data/lib/grape/util/api_description.rb +10 -1
- data/lib/grape/util/cache.rb +21 -2
- data/lib/grape/util/deep_freeze.rb +1 -2
- data/lib/grape/util/freeze_on_new.rb +20 -0
- data/lib/grape/util/inheritable_setting.rb +751 -37
- data/lib/grape/util/media_type.rb +10 -3
- data/lib/grape/util/path_normalizer.rb +12 -9
- data/lib/grape/util/registry.rb +12 -2
- data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
- data/lib/grape/util/stackable_values.rb +32 -6
- data/lib/grape/validations/attributes_iterator.rb +21 -4
- data/lib/grape/validations/contract_scope.rb +8 -7
- data/lib/grape/validations/oneof_collector.rb +6 -10
- data/lib/grape/validations/params_documentation.rb +2 -2
- data/lib/grape/validations/params_scope.rb +66 -26
- data/lib/grape/validations/single_attribute_iterator.rb +5 -3
- data/lib/grape/validations/types/array_coercer.rb +4 -6
- data/lib/grape/validations/types/custom_type_coercer.rb +7 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -1
- data/lib/grape/validations/types/json.rb +1 -3
- data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
- data/lib/grape/validations/types/primitive_coercer.rb +12 -9
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -3
- data/lib/grape/validations/types.rb +5 -5
- data/lib/grape/validations/validations_spec.rb +14 -4
- data/lib/grape/validations/validators/base.rb +4 -7
- data/lib/grape/validations/validators/coerce_validator.rb +8 -2
- data/lib/grape/validations/validators/contract_scope_validator.rb +2 -1
- data/lib/grape/validations/validators/length_validator.rb +4 -2
- data/lib/grape/validations/validators/oneof_validator.rb +2 -0
- data/lib/grape/validations/validators/same_as_validator.rb +1 -0
- data/lib/grape/version.rb +2 -1
- data/lib/grape.rb +23 -2
- metadata +10 -9
- data/lib/grape/middleware/deprecated_options_hash_access.rb +0 -19
- data/lib/grape/util/base_inheritable.rb +0 -43
- data/lib/grape/util/inheritable_values.rb +0 -33
- data/lib/grape/util/reverse_stackable_values.rb +0 -15
|
@@ -7,6 +7,10 @@ module Grape
|
|
|
7
7
|
|
|
8
8
|
# based on the HTTP Accept header with the pattern:
|
|
9
9
|
# application/vnd.:vendor-:version+:format
|
|
10
|
+
#
|
|
11
|
+
# Matched against a down-cased media type: they are case-insensitive
|
|
12
|
+
# (RFC 9110 §8.3.1), while a vendor and version are declared in the DSL
|
|
13
|
+
# in the case they will be compared in.
|
|
10
14
|
VENDOR_VERSION_HEADER_REGEX = /\Avnd\.(?<vendor>[a-z0-9.\-_!^]+?)(?:-(?<version>[a-z0-9*.]+))?(?:\+(?<format>[a-z0-9*\-.]+))?\z/
|
|
11
15
|
|
|
12
16
|
def initialize(type:, subtype:)
|
|
@@ -41,7 +45,7 @@ module Grape
|
|
|
41
45
|
def parse(media_type)
|
|
42
46
|
return if media_type.blank?
|
|
43
47
|
|
|
44
|
-
type, subtype = media_type.split('/', 2)
|
|
48
|
+
type, subtype = media_type.downcase.split('/', 2)
|
|
45
49
|
return if type.blank? || subtype.blank?
|
|
46
50
|
|
|
47
51
|
new(type:, subtype:)
|
|
@@ -50,14 +54,17 @@ module Grape
|
|
|
50
54
|
def match?(media_type)
|
|
51
55
|
return false if media_type.blank?
|
|
52
56
|
|
|
53
|
-
subtype = media_type.split('/', 2).last
|
|
57
|
+
subtype = media_type.downcase.split('/', 2).last
|
|
54
58
|
return false if subtype.blank?
|
|
55
59
|
|
|
56
60
|
VENDOR_VERSION_HEADER_REGEX.match?(subtype)
|
|
57
61
|
end
|
|
58
62
|
|
|
63
|
+
# The available types are registered in lower case and Rack matches them
|
|
64
|
+
# literally, so the header has to be down-cased to be compared against
|
|
65
|
+
# them at all.
|
|
59
66
|
def best_quality_media_type(header, available_media_types)
|
|
60
|
-
header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header, available_media_types)
|
|
67
|
+
header.blank? ? available_media_types.first : Rack::Utils.best_q_match(header.downcase, available_media_types)
|
|
61
68
|
end
|
|
62
69
|
end
|
|
63
70
|
|
|
@@ -14,20 +14,23 @@ module Grape
|
|
|
14
14
|
return '/' unless path
|
|
15
15
|
return path if path == '/'
|
|
16
16
|
|
|
17
|
-
# Fast path for the overwhelming majority of paths that don't need to be
|
|
18
|
-
|
|
17
|
+
# Fast path for the overwhelming majority of paths that don't need to be
|
|
18
|
+
# normalized. Two String#include? calls rather than one `%|//` regexp:
|
|
19
|
+
# same predicate, and the scan stays in C without building a match.
|
|
20
|
+
return path if path.start_with?('/') && !(path.end_with?('/') || path.include?('%') || path.include?('//'))
|
|
19
21
|
|
|
20
|
-
# Slow path
|
|
22
|
+
# Slow path. The bangs below are safe because +normalized+ is a fresh
|
|
23
|
+
# String built here, never the one the caller passed in.
|
|
21
24
|
encoding = path.encoding
|
|
22
|
-
|
|
23
|
-
|
|
25
|
+
normalized = "/#{path}"
|
|
26
|
+
normalized.squeeze!('/')
|
|
24
27
|
|
|
25
|
-
unless
|
|
26
|
-
|
|
27
|
-
|
|
28
|
+
unless normalized == '/'
|
|
29
|
+
normalized.delete_suffix!('/')
|
|
30
|
+
normalized.gsub!(/(%[a-f0-9]{2})/) { ::Regexp.last_match(1).upcase }
|
|
28
31
|
end
|
|
29
32
|
|
|
30
|
-
|
|
33
|
+
normalized.force_encoding(encoding)
|
|
31
34
|
end
|
|
32
35
|
end
|
|
33
36
|
end
|
data/lib/grape/util/registry.rb
CHANGED
|
@@ -8,7 +8,7 @@ module Grape
|
|
|
8
8
|
return if short_name.nil?
|
|
9
9
|
|
|
10
10
|
warn "#{short_name} is already registered with class #{registry[short_name]}. It will be overridden globally with the following: #{klass.name}" if registry.key?(short_name)
|
|
11
|
-
registry[short_name] = klass
|
|
11
|
+
registry[short_name] = registry[short_name.to_sym] = klass
|
|
12
12
|
end
|
|
13
13
|
|
|
14
14
|
private
|
|
@@ -19,8 +19,18 @@ module Grape
|
|
|
19
19
|
klass.name.demodulize.underscore
|
|
20
20
|
end
|
|
21
21
|
|
|
22
|
+
# Every registration under both spellings in one plain Hash, so a lookup
|
|
23
|
+
# is a single +Hash#[]+. A +HashWithIndifferentAccess+ converted the key
|
|
24
|
+
# on every read instead -- and these registries are read on the request
|
|
25
|
+
# path, by +Grape::Formatter+ once per response, by +Grape::Parser+ once
|
|
26
|
+
# per parsed body and by +Grape::ParamsBuilder+ once per params build,
|
|
27
|
+
# always with a Symbol, which is the spelling +convert_key+ allocates a
|
|
28
|
+
# String for.
|
|
29
|
+
#
|
|
30
|
+
# +register+ derives the short name as a String, so the Symbol is the
|
|
31
|
+
# alias.
|
|
22
32
|
def registry
|
|
23
|
-
@registry ||=
|
|
33
|
+
@registry ||= {}
|
|
24
34
|
end
|
|
25
35
|
end
|
|
26
36
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Util
|
|
5
|
+
# Diagnostics for +rescue_from+ registrations that can never run.
|
|
6
|
+
#
|
|
7
|
+
# Middleware::Error resolves with +find+, so within a scope the first
|
|
8
|
+
# matching class wins and one registered for a class an earlier handler
|
|
9
|
+
# already covers is dead code — silently, before this warned:
|
|
10
|
+
#
|
|
11
|
+
# rescue_from StandardError do ... end # wins
|
|
12
|
+
# rescue_from ArgumentError do ... end # never runs
|
|
13
|
+
#
|
|
14
|
+
# Warn rather than reorder: which should win is the author's call, and
|
|
15
|
+
# +rescue_from :all+ (consulted only after the registered handlers) already
|
|
16
|
+
# offers "broad first, specific still wins" to anyone who wants it.
|
|
17
|
+
module ShadowedRescueHandlers
|
|
18
|
+
module_function
|
|
19
|
+
|
|
20
|
+
# @param registered [Hash] the scope's own handlers, in registration order
|
|
21
|
+
# @param mapping [Hash] the handlers being registered now
|
|
22
|
+
# @return [void]
|
|
23
|
+
#
|
|
24
|
+
# Only a scope's own registrations are compared: across scopes the nearest
|
|
25
|
+
# one deliberately wins, so an inner +rescue_from StandardError+ shadowing
|
|
26
|
+
# an outer +rescue_from ArgumentError+ is the documented behaviour rather
|
|
27
|
+
# than a mistake. Classes sharing a handler object are skipped too, since
|
|
28
|
+
# +rescue_from A, B+ registers one handler for both and the entry that
|
|
29
|
+
# loses to the other changes nothing.
|
|
30
|
+
def warn_about(registered, mapping)
|
|
31
|
+
return if registered.empty?
|
|
32
|
+
|
|
33
|
+
mapping.each do |klass, handler|
|
|
34
|
+
covered_by, = registered.find { |already, existing| klass <= already && !existing.equal?(handler) }
|
|
35
|
+
next unless covered_by
|
|
36
|
+
|
|
37
|
+
warn(message_for(klass, covered_by))
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def message_for(klass, covered_by)
|
|
42
|
+
return "Grape: rescue_from #{klass} was already registered in this scope; the first handler is kept and this one will never run." if klass == covered_by
|
|
43
|
+
|
|
44
|
+
"Grape: rescue_from #{klass} will never run — #{covered_by} was registered earlier in the same scope " \
|
|
45
|
+
'and is matched first. Register the more specific class before the broader one.'
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -2,10 +2,36 @@
|
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Util
|
|
5
|
-
|
|
5
|
+
# A read-only view of one settings scope's stackable registrations.
|
|
6
|
+
#
|
|
7
|
+
# Grape stores nothing here anymore: the registrations live on
|
|
8
|
+
# Grape::Util::InheritableSetting, one Array per key per scope, and are
|
|
9
|
+
# reached through its semantic accessors (+helpers+, +middleware+,
|
|
10
|
+
# +namespaces+, ...). This class survives only because grape-swagger
|
|
11
|
+
# reads InheritableSetting#namespace_stackable directly — including
|
|
12
|
+
# walking the #inherited_values chain and reading each level's own
|
|
13
|
+
# #new_values to recover per-scope registrations — and is expected to be
|
|
14
|
+
# removed once grape-swagger reads those accessors instead.
|
|
15
|
+
#
|
|
16
|
+
# Instances are built on demand by InheritableSetting#namespace_stackable
|
|
17
|
+
# and are not the backing store: writing to #new_values does not register
|
|
18
|
+
# anything.
|
|
19
|
+
class StackableValues
|
|
6
20
|
EMPTY = [].freeze
|
|
7
21
|
|
|
8
|
-
|
|
22
|
+
attr_reader :inherited_values, :new_values
|
|
23
|
+
|
|
24
|
+
# @param new_values [Hash, nil] the scope's own registrations, one Array
|
|
25
|
+
# per key; nil when the scope never registered anything.
|
|
26
|
+
# @param inherited_values [StackableValues, Hash] the enclosing scope's
|
|
27
|
+
# view, or an empty Hash at the root of the chain.
|
|
28
|
+
def initialize(new_values, inherited_values)
|
|
29
|
+
@new_values = new_values
|
|
30
|
+
@inherited_values = inherited_values
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Outermost scope first. Even if there is no value, an empty (frozen)
|
|
34
|
+
# array will be returned.
|
|
9
35
|
def [](name)
|
|
10
36
|
inherited_value = @inherited_values[name]
|
|
11
37
|
new_value = @new_values && @new_values[name]
|
|
@@ -15,10 +41,10 @@ module Grape
|
|
|
15
41
|
concat_values(inherited_value, new_value)
|
|
16
42
|
end
|
|
17
43
|
|
|
18
|
-
def
|
|
19
|
-
@new_values
|
|
20
|
-
|
|
21
|
-
@new_values
|
|
44
|
+
def keys
|
|
45
|
+
return @inherited_values.keys if @new_values.nil? || @new_values.empty?
|
|
46
|
+
|
|
47
|
+
(@inherited_values.keys + @new_values.keys).uniq
|
|
22
48
|
end
|
|
23
49
|
|
|
24
50
|
def to_hash
|
|
@@ -10,10 +10,22 @@ module Grape
|
|
|
10
10
|
def initialize(attrs, scope)
|
|
11
11
|
@attrs = attrs
|
|
12
12
|
@scope = scope
|
|
13
|
+
# How many times #do_each may descend into a nested array. The
|
|
14
|
+
# declaration allows one level per Array-typed scope on the chain, less
|
|
15
|
+
# the one +Array.wrap+ already consumes in #each. Anything deeper was
|
|
16
|
+
# put there by the request, not by the declaration.
|
|
17
|
+
@max_nesting = [scope.array_depth - 1, 0].max
|
|
13
18
|
end
|
|
14
19
|
|
|
15
20
|
def each(params, &)
|
|
16
21
|
original_params = @scope.params(params)
|
|
22
|
+
# A scope resolves to a Hash unless the declaration nests arrays, and
|
|
23
|
+
# then #do_each has nothing to do but hand it straight back: Array.wrap
|
|
24
|
+
# boxes it, the loop unboxes it on its only iteration, and with no Array
|
|
25
|
+
# anywhere neither the nesting descent nor the index bookkeeping
|
|
26
|
+
# applies. Every validator on a flat +params+ block comes through here.
|
|
27
|
+
return yield_attributes(original_params, &) if original_params.is_a?(Hash) && !@scope.iterates_elements?
|
|
28
|
+
|
|
17
29
|
# because we need recursion for nested arrays
|
|
18
30
|
do_each(Array.wrap(original_params), original_params, &)
|
|
19
31
|
end
|
|
@@ -24,19 +36,24 @@ module Grape
|
|
|
24
36
|
params_to_process.each_with_index do |resource_params, index|
|
|
25
37
|
# when we get arrays of arrays it means that target element located inside array
|
|
26
38
|
# we need this because we want to know parent arrays indices
|
|
27
|
-
|
|
39
|
+
#
|
|
40
|
+
# Only descend as far as the declaration nests. A request that wraps
|
|
41
|
+
# its elements deeper than that is yielded as-is, so the attribute
|
|
42
|
+
# validators see a non-hash and fail it the same way any other
|
|
43
|
+
# unexpected element type does.
|
|
44
|
+
if resource_params.is_a?(Array) && parent_indices.size < @max_nesting
|
|
28
45
|
do_each(resource_params, original_params, [index] + parent_indices, &block)
|
|
29
46
|
next
|
|
30
47
|
end
|
|
31
48
|
|
|
32
|
-
if @scope.
|
|
49
|
+
if @scope.iterates_elements?
|
|
33
50
|
next unless original_params.is_a?(Array) # do not validate content of array if it isn't array
|
|
34
51
|
|
|
35
52
|
store_indices(@scope, index, parent_indices)
|
|
36
53
|
elsif original_params.is_a?(Array)
|
|
37
54
|
# Lateral scope (no @element) whose params resolved to an array —
|
|
38
|
-
# delegate index tracking to the nearest
|
|
39
|
-
# that full_name produces the correct bracketed index.
|
|
55
|
+
# delegate index tracking to the nearest element-iterating ancestor
|
|
56
|
+
# so that full_name produces the correct bracketed index.
|
|
40
57
|
target = @scope.nearest_array_ancestor
|
|
41
58
|
store_indices(target, index, parent_indices) if target
|
|
42
59
|
end
|
|
@@ -9,19 +9,20 @@ module Grape
|
|
|
9
9
|
# @yield a block yielding a new schema class. Optional.
|
|
10
10
|
def initialize(api, contract = nil, &block)
|
|
11
11
|
# When block is passed, the first arg is either schema or nil.
|
|
12
|
-
|
|
12
|
+
declared = block ? Dry::Schema.Params(parent: contract, &block) : contract
|
|
13
13
|
|
|
14
|
-
if
|
|
14
|
+
if declared.respond_to?(:schema)
|
|
15
15
|
# It's a Dry::Validation::Contract, then.
|
|
16
|
-
|
|
17
|
-
key_map =
|
|
16
|
+
schema = declared.new
|
|
17
|
+
key_map = schema.schema.key_map
|
|
18
18
|
else
|
|
19
19
|
# Dry::Schema::Processor, hopefully.
|
|
20
|
-
|
|
20
|
+
schema = declared
|
|
21
|
+
key_map = declared.key_map
|
|
21
22
|
end
|
|
22
23
|
|
|
23
|
-
api.inheritable_setting.
|
|
24
|
-
api.inheritable_setting.
|
|
24
|
+
api.inheritable_setting.add_contract_key_map(key_map)
|
|
25
|
+
api.inheritable_setting.add_validation(Validators::ContractScopeValidator.new(schema:))
|
|
25
26
|
end
|
|
26
27
|
end
|
|
27
28
|
end
|
|
@@ -8,31 +8,27 @@ module Grape
|
|
|
8
8
|
# the real API. Exposes only the slice of the API surface that
|
|
9
9
|
# ParamsScope and its helpers touch during definition.
|
|
10
10
|
class OneofCollector
|
|
11
|
+
extend Forwardable
|
|
12
|
+
|
|
11
13
|
attr_reader :inheritable_setting
|
|
12
14
|
|
|
15
|
+
def_delegator :@inheritable_setting, :validations
|
|
16
|
+
|
|
13
17
|
def initialize
|
|
14
18
|
@inheritable_setting = Grape::Util::InheritableSetting.new
|
|
15
|
-
@inheritable_setting.
|
|
19
|
+
@inheritable_setting.do_not_document!
|
|
16
20
|
end
|
|
17
21
|
|
|
18
22
|
def configuration
|
|
19
23
|
nil
|
|
20
24
|
end
|
|
21
25
|
|
|
22
|
-
def validators
|
|
23
|
-
inheritable_setting.namespace_stackable[:validations]
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
def declared_params
|
|
27
|
-
inheritable_setting.namespace_stackable[:declared_params]
|
|
28
|
-
end
|
|
29
|
-
|
|
30
26
|
# Evaluate +variant_block+ in a fresh +ParamsScope+ backed by a new
|
|
31
27
|
# collector and return the validators that the block registered.
|
|
32
28
|
def self.collect(variant_block)
|
|
33
29
|
collector = new
|
|
34
30
|
ParamsScope.new(api: collector, type: Hash, &variant_block)
|
|
35
|
-
collector.
|
|
31
|
+
collector.validations
|
|
36
32
|
end
|
|
37
33
|
end
|
|
38
34
|
end
|
|
@@ -6,12 +6,12 @@ module Grape
|
|
|
6
6
|
# +ValidationsSpec+; never mutates the user's validations hash.
|
|
7
7
|
module ParamsDocumentation
|
|
8
8
|
def document_params(attrs, spec)
|
|
9
|
-
return if @api.inheritable_setting.
|
|
9
|
+
return if @api.inheritable_setting.do_not_document?
|
|
10
10
|
|
|
11
11
|
documented_attrs = attrs.to_h do |name|
|
|
12
12
|
[full_name(name), extract_details(spec)]
|
|
13
13
|
end
|
|
14
|
-
@api.inheritable_setting.
|
|
14
|
+
@api.inheritable_setting.add_params_documentation(documented_attrs)
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
private
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Grape
|
|
4
4
|
module Validations
|
|
5
5
|
class ParamsScope
|
|
6
|
-
attr_reader :parent, :type, :nearest_array_ancestor, :full_path
|
|
6
|
+
attr_reader :parent, :type, :nearest_array_ancestor, :array_depth, :full_path
|
|
7
7
|
|
|
8
8
|
def qualifying_params
|
|
9
9
|
ParamScopeTracker.current&.qualifying_params(self)
|
|
@@ -78,6 +78,9 @@ module Grape
|
|
|
78
78
|
# configure_declared_params consumes it and clears @declared_params to nil.
|
|
79
79
|
@declared_params = []
|
|
80
80
|
@full_path = build_full_path
|
|
81
|
+
# Read by the validators instantiated from the block below, so it has to
|
|
82
|
+
# be settled before the instance_eval.
|
|
83
|
+
@array_depth = find_array_depth
|
|
81
84
|
|
|
82
85
|
instance_eval(&block) if block
|
|
83
86
|
|
|
@@ -169,6 +172,17 @@ module Grape
|
|
|
169
172
|
@parent && @element
|
|
170
173
|
end
|
|
171
174
|
|
|
175
|
+
# Whether this scope's params resolve to one entry per element, which is
|
|
176
|
+
# what makes both an element index and a nesting level meaningful.
|
|
177
|
+
#
|
|
178
|
+
# +type: Array[JSON]+ counts as much as +type: Array+ does. It is easy to
|
|
179
|
+
# miss because it evaluates to the Array *instance* +[JSON]+ rather than
|
|
180
|
+
# the Array class, so an +== Array+ test quietly excluded it.
|
|
181
|
+
# @return [Boolean]
|
|
182
|
+
def iterates_elements?
|
|
183
|
+
@type == Array || @type == SPECIAL_JSON.last
|
|
184
|
+
end
|
|
185
|
+
|
|
172
186
|
# A lateral scope is subordinate to its parent, but its keys are at the
|
|
173
187
|
# same level as its parent and thus is not contained within an element.
|
|
174
188
|
# @return [Boolean] whether or not this scope is lateral
|
|
@@ -186,12 +200,16 @@ module Grape
|
|
|
186
200
|
|
|
187
201
|
# Adds a parameter declaration to our list of validations.
|
|
188
202
|
# @param attrs [Array] (see Grape::DSL::Parameters#requires)
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
203
|
+
# +declared_params_scope+ is the scope an attribute is recorded against.
|
|
204
|
+
# It defaults to the receiver and is forwarded unchanged when a lateral
|
|
205
|
+
# scope hands the push to its parent, so an attribute declared inside a
|
|
206
|
+
# +given+ block stays attributed to the lateral scope that declared it
|
|
207
|
+
# rather than to the parent that stores it.
|
|
208
|
+
def push_declared_params(attrs, as: nil, declared_params_scope: self)
|
|
209
|
+
return @parent.push_declared_params(attrs, as:, declared_params_scope:) if lateral?
|
|
192
210
|
|
|
193
|
-
push_renamed_param(full_path + [attrs.first],
|
|
194
|
-
@declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr,
|
|
211
|
+
push_renamed_param(full_path + [attrs.first], as) if as
|
|
212
|
+
@declared_params.concat(attrs.map { |attr| ::Grape::Validations::ParamsScope::Attr.new(attr, declared_params_scope) })
|
|
195
213
|
end
|
|
196
214
|
|
|
197
215
|
private
|
|
@@ -211,10 +229,7 @@ module Grape
|
|
|
211
229
|
# @param new_name [String, Symbol] the new name of the parameter (the
|
|
212
230
|
# renamed name, with the +as: ...+ semantic)
|
|
213
231
|
def push_renamed_param(path, new_name)
|
|
214
|
-
|
|
215
|
-
base = api_route_setting[:renamed_params] || {}
|
|
216
|
-
base[Array(path).map(&:to_s)] = new_name.to_s
|
|
217
|
-
api_route_setting[:renamed_params] = base
|
|
232
|
+
@api.inheritable_setting.add_route_renamed_param(Array(path).map(&:to_s), new_name.to_s)
|
|
218
233
|
end
|
|
219
234
|
|
|
220
235
|
def require_required_and_optional_fields(context, using:, except: nil)
|
|
@@ -230,7 +245,7 @@ module Grape
|
|
|
230
245
|
end
|
|
231
246
|
required_fields.each do |field|
|
|
232
247
|
field_opts = using[field]
|
|
233
|
-
raise ArgumentError, "required field not exist:
|
|
248
|
+
raise ArgumentError, "required field not exist: `#{field}`" unless field_opts
|
|
234
249
|
|
|
235
250
|
requires(field, **field_opts)
|
|
236
251
|
end
|
|
@@ -252,11 +267,6 @@ module Grape
|
|
|
252
267
|
end
|
|
253
268
|
end
|
|
254
269
|
|
|
255
|
-
def validate_attributes(attrs, **opts, &block)
|
|
256
|
-
opts[:type] ||= Array if block
|
|
257
|
-
validates(attrs, opts)
|
|
258
|
-
end
|
|
259
|
-
|
|
260
270
|
# Returns a new parameter scope, subordinate to the current one and nested
|
|
261
271
|
# under the given element.
|
|
262
272
|
# @param element [Symbol] the parameter name under which this scope is nested
|
|
@@ -266,8 +276,11 @@ module Grape
|
|
|
266
276
|
# is optional or not (and hence, whether this block's params will be).
|
|
267
277
|
# @yield parameter scope
|
|
268
278
|
def new_scope(element, type:, as:, optional: false, &)
|
|
269
|
-
#
|
|
270
|
-
|
|
279
|
+
# A group needs a type: it says whether the nested params sit under one
|
|
280
|
+
# object or repeat in a list, and without it the `type || Array` below
|
|
281
|
+
# would quietly pick one. Checked here for `requires` and `optional`
|
|
282
|
+
# alike — `new_scope` is only reached from their block branch.
|
|
283
|
+
if element
|
|
271
284
|
raise Grape::Exceptions::MissingGroupType if type.nil?
|
|
272
285
|
raise Grape::Exceptions::UnsupportedGroupType unless Grape::Validations::Types.group?(type)
|
|
273
286
|
end
|
|
@@ -316,20 +329,44 @@ module Grape
|
|
|
316
329
|
push_renamed_param(full_path, @element_renamed) if @element_renamed
|
|
317
330
|
return @parent.push_declared_params [{ @element => @declared_params }] if nested?
|
|
318
331
|
|
|
319
|
-
@api.inheritable_setting.
|
|
332
|
+
@api.inheritable_setting.add_declared_params(@declared_params)
|
|
320
333
|
ensure
|
|
321
334
|
@declared_params = nil
|
|
322
335
|
end
|
|
323
336
|
|
|
324
337
|
def find_nearest_array_ancestor
|
|
325
338
|
scope = @parent
|
|
326
|
-
scope = scope.parent while scope && scope.
|
|
339
|
+
scope = scope.parent while scope && !scope.iterates_elements?
|
|
327
340
|
scope
|
|
328
341
|
end
|
|
329
342
|
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
343
|
+
# Every element-iterating scope on the chain adds one level of nesting to
|
|
344
|
+
# what {#params} returns, because +map_params+ maps over the array it
|
|
345
|
+
# resolved from the parent. Counting them tells {AttributesIterator} how
|
|
346
|
+
# deep the declaration says the params for this scope may legitimately be.
|
|
347
|
+
def find_array_depth
|
|
348
|
+
(iterates_elements? ? 1 : 0) + (@parent&.array_depth || 0)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
# +required+ is the DSL's own signal — +requires+ passes true, +optional+
|
|
352
|
+
# false. It used to travel as a +:presence+ key that +requires+ wrote into
|
|
353
|
+
# the caller's option Hash, which is why a user-supplied +presence:+ was
|
|
354
|
+
# silently overwritten there and silently honoured by +optional+, where
|
|
355
|
+
# nothing overwrote it. The key is built here from the flag instead, after
|
|
356
|
+
# the caller has merged in any enclosing +with+ attributes, so a
|
|
357
|
+
# group-level +message:+ reaches the presence validator.
|
|
358
|
+
#
|
|
359
|
+
# A +presence:+ supplied by an API still decides the outcome exactly as it
|
|
360
|
+
# used to — deprecated rather than dropped, so nothing changes under an
|
|
361
|
+
# API that relies on it until the key is ignored outright.
|
|
362
|
+
def validates(attrs, validations, required: false)
|
|
363
|
+
if validations.key?(:presence)
|
|
364
|
+
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
|
+
end
|
|
366
|
+
|
|
367
|
+
declared = required ? validations.merge(presence: { value: true, message: validations[:message] }) : validations
|
|
368
|
+
declared = declared.merge(oneof: collected_oneof(declared)) if declared.key?(:oneof)
|
|
369
|
+
spec = ValidationsSpec.from(declared)
|
|
333
370
|
|
|
334
371
|
document_params(attrs, spec)
|
|
335
372
|
|
|
@@ -381,14 +418,17 @@ module Grape
|
|
|
381
418
|
# {OneofCollector} so the full params DSL is available inside variants
|
|
382
419
|
# and the resulting validators are kept out of the real API's
|
|
383
420
|
# registration list.
|
|
384
|
-
|
|
421
|
+
# Returns the collected variants rather than writing them back into
|
|
422
|
+
# +validations+, which is the options Hash the +requires+/+optional+ call
|
|
423
|
+
# site built.
|
|
424
|
+
def collected_oneof(validations)
|
|
385
425
|
raise ArgumentError, 'oneof: requires type: Hash' unless validations[:type] == Hash
|
|
386
426
|
|
|
387
427
|
variants = validations[:oneof]
|
|
388
428
|
raise ArgumentError, 'oneof: must be a non-empty Array of blocks' unless variants.is_a?(Array) && variants.any?
|
|
389
429
|
raise ArgumentError, 'oneof: each variant must be a Proc' unless variants.all?(Proc)
|
|
390
430
|
|
|
391
|
-
|
|
431
|
+
variants.map { |block| OneofCollector.collect(block) }
|
|
392
432
|
end
|
|
393
433
|
|
|
394
434
|
def validate(type, options, attrs, required, opts)
|
|
@@ -400,7 +440,7 @@ module Grape
|
|
|
400
440
|
self,
|
|
401
441
|
opts
|
|
402
442
|
)
|
|
403
|
-
@api.inheritable_setting.
|
|
443
|
+
@api.inheritable_setting.add_validation(validator_instance)
|
|
404
444
|
end
|
|
405
445
|
|
|
406
446
|
def all_element_blank?(scoped_params)
|
|
@@ -8,9 +8,11 @@ module Grape
|
|
|
8
8
|
def yield_attributes(val)
|
|
9
9
|
return if skip?(val)
|
|
10
10
|
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
11
|
+
# The emptiness of the scope's params is the same answer for every
|
|
12
|
+
# attribute in it, and answering it costs a +respond_to?+, so it is
|
|
13
|
+
# asked once for the whole list rather than once per attribute.
|
|
14
|
+
empty_val = empty?(val)
|
|
15
|
+
@attrs.each { |attr_name| yield val, attr_name, empty_val }
|
|
14
16
|
end
|
|
15
17
|
|
|
16
18
|
# Primitives like Integers and Booleans don't respond to +empty?+.
|
|
@@ -15,7 +15,9 @@ module Grape
|
|
|
15
15
|
def initialize(type, strict: false)
|
|
16
16
|
super
|
|
17
17
|
@coercer = strict ? DryTypes::Strict::Array : DryTypes::Params::Array
|
|
18
|
-
|
|
18
|
+
# Built eagerly: instances are shared across requests (and cached in
|
|
19
|
+
# Types::CoercerCache), so no lazy state may be created at call time.
|
|
20
|
+
@elem_coercer = DryTypeCoercer.coercer_instance_for(type.first, strict:)
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def call(_val)
|
|
@@ -27,7 +29,7 @@ module Grape
|
|
|
27
29
|
|
|
28
30
|
protected
|
|
29
31
|
|
|
30
|
-
attr_reader :
|
|
32
|
+
attr_reader :elem_coercer
|
|
31
33
|
|
|
32
34
|
def coerce_elements(collection)
|
|
33
35
|
return if collection.nil?
|
|
@@ -50,10 +52,6 @@ module Grape
|
|
|
50
52
|
def reject?(val)
|
|
51
53
|
val.nil?
|
|
52
54
|
end
|
|
53
|
-
|
|
54
|
-
def elem_coercer
|
|
55
|
-
@elem_coercer ||= DryTypeCoercer.coercer_instance_for(subtype, strict:)
|
|
56
|
-
end
|
|
57
55
|
end
|
|
58
56
|
end
|
|
59
57
|
end
|
|
@@ -33,6 +33,8 @@ module Grape
|
|
|
33
33
|
# contract as +coerced?+, and must be supplied with a coercion
|
|
34
34
|
# +method+.
|
|
35
35
|
class CustomTypeCoercer
|
|
36
|
+
extend Grape::Util::FreezeOnNew
|
|
37
|
+
|
|
36
38
|
TYPE_CHECK_METHODS = %i[coerced? parsed?].freeze
|
|
37
39
|
COLLECTION_TYPES = [Array, Set].freeze
|
|
38
40
|
private_constant :TYPE_CHECK_METHODS, :COLLECTION_TYPES
|
|
@@ -88,8 +90,12 @@ module Grape
|
|
|
88
90
|
->(val) { method.call(val).deep_symbolize_keys }
|
|
89
91
|
end
|
|
90
92
|
|
|
93
|
+
# +dup+ before +map!+: the collection is whatever the user's coercion
|
|
94
|
+
# method returned — possibly frozen, possibly the input itself — so we
|
|
95
|
+
# must not mutate it in place. +dup+ also preserves the class, keeping a
|
|
96
|
+
# +Set+ a +Set+ (unlike +map+, which would return an +Array+).
|
|
91
97
|
def collection_symbolizer(method)
|
|
92
|
-
->(val) { method.call(val).map! { |item| symbolize_if_hash(item) } }
|
|
98
|
+
->(val) { method.call(val).dup.map! { |item| symbolize_if_hash(item) } }
|
|
93
99
|
end
|
|
94
100
|
|
|
95
101
|
def symbolize_if_hash(item)
|
|
@@ -8,6 +8,8 @@ module Grape
|
|
|
8
8
|
# but check its type. More information there
|
|
9
9
|
# https://dry-rb.org/gems/dry-types/main/built-in-types/
|
|
10
10
|
class DryTypeCoercer
|
|
11
|
+
extend Grape::Util::FreezeOnNew
|
|
12
|
+
|
|
11
13
|
class << self
|
|
12
14
|
# Returns a collection coercer which corresponds to a given type.
|
|
13
15
|
# Example:
|
|
@@ -21,7 +23,7 @@ module Grape
|
|
|
21
23
|
when Set
|
|
22
24
|
SetCoercer
|
|
23
25
|
else
|
|
24
|
-
raise ArgumentError, "
|
|
26
|
+
raise ArgumentError, "unknown type: `#{type}`"
|
|
25
27
|
end
|
|
26
28
|
end
|
|
27
29
|
|
|
@@ -17,9 +17,7 @@ module Grape
|
|
|
17
17
|
# @return [Hash,Array<Hash>,nil]
|
|
18
18
|
def parse(input)
|
|
19
19
|
return input if parsed?(input)
|
|
20
|
-
|
|
21
|
-
# Allow nulls and blank strings
|
|
22
|
-
return if input.nil? || input.match?(/^\s*$/)
|
|
20
|
+
return if input.blank?
|
|
23
21
|
|
|
24
22
|
JSON.parse(input, symbolize_names: true)
|
|
25
23
|
end
|
|
@@ -13,6 +13,8 @@ module Grape
|
|
|
13
13
|
# an allowed type it should be declared last, since it will always
|
|
14
14
|
# successfully "coerce" the value.
|
|
15
15
|
class MultipleTypeCoercer
|
|
16
|
+
extend Grape::Util::FreezeOnNew
|
|
17
|
+
|
|
16
18
|
# Construct a new coercer that will attempt to coerce
|
|
17
19
|
# values to the given list of types in the given order.
|
|
18
20
|
#
|
|
@@ -28,7 +30,7 @@ module Grape
|
|
|
28
30
|
else
|
|
29
31
|
Types.build_coercer type, strict: !@method.nil?
|
|
30
32
|
end
|
|
31
|
-
end
|
|
33
|
+
end.freeze
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Coerces the given value.
|
|
@@ -39,12 +41,12 @@ module Grape
|
|
|
39
41
|
# of {InvalidValue} if the value could not be coerced.
|
|
40
42
|
def call(val)
|
|
41
43
|
# once the value is coerced by the custom method, its type should be checked
|
|
42
|
-
|
|
44
|
+
candidate = @method ? @method.call(val) : val
|
|
43
45
|
|
|
44
46
|
coerced_val = InvalidValue.new
|
|
45
47
|
|
|
46
48
|
@type_coercers.each do |coercer|
|
|
47
|
-
coerced_val = coercer.call(
|
|
49
|
+
coerced_val = coercer.call(candidate)
|
|
48
50
|
|
|
49
51
|
return coerced_val unless coerced_val.is_a?(InvalidValue)
|
|
50
52
|
end
|