grape 3.2.1 → 3.3.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 +80 -0
- data/README.md +116 -43
- data/UPGRADING.md +336 -1
- data/grape.gemspec +5 -5
- data/lib/grape/api/instance.rb +7 -7
- data/lib/grape/api.rb +22 -25
- data/lib/grape/cookies.rb +2 -6
- data/lib/grape/declared_params_handler.rb +48 -50
- data/lib/grape/dsl/callbacks.rb +9 -3
- data/lib/grape/dsl/desc.rb +8 -2
- data/lib/grape/dsl/entity.rb +88 -0
- data/lib/grape/dsl/helpers.rb +27 -7
- data/lib/grape/dsl/inside_route.rb +38 -129
- data/lib/grape/dsl/logger.rb +3 -5
- data/lib/grape/dsl/parameters.rb +32 -38
- data/lib/grape/dsl/request_response.rb +53 -48
- data/lib/grape/dsl/rescue_options.rb +24 -0
- data/lib/grape/dsl/routing.rb +51 -35
- data/lib/grape/dsl/settings.rb +14 -8
- data/lib/grape/dsl/version_options.rb +23 -0
- data/lib/grape/endpoint/options.rb +19 -0
- data/lib/grape/endpoint.rb +96 -68
- data/lib/grape/env.rb +1 -3
- data/lib/grape/error_formatter/base.rb +23 -20
- data/lib/grape/error_formatter/json.rb +8 -4
- data/lib/grape/error_formatter/txt.rb +10 -10
- data/lib/grape/exceptions/base.rb +3 -1
- data/lib/grape/exceptions/error_response.rb +45 -0
- data/lib/grape/exceptions/internal_server_error.rb +16 -0
- data/lib/grape/exceptions/validation.rb +14 -0
- data/lib/grape/exceptions/validation_array_errors.rb +4 -0
- data/lib/grape/exceptions/validation_errors.rb +12 -20
- data/lib/grape/formatter/serializable_hash.rb +5 -9
- data/lib/grape/json.rb +38 -2
- data/lib/grape/locale/en.yml +2 -0
- data/lib/grape/middleware/auth/base.rb +2 -3
- data/lib/grape/middleware/auth/dsl.rb +23 -8
- data/lib/grape/middleware/base.rb +22 -33
- data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
- data/lib/grape/middleware/error.rb +152 -62
- data/lib/grape/middleware/formatter.rb +66 -50
- data/lib/grape/middleware/precomputed_content_types.rb +46 -0
- data/lib/grape/middleware/stack.rb +5 -6
- data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
- data/lib/grape/middleware/versioner/base.rb +34 -38
- data/lib/grape/middleware/versioner/header.rb +3 -5
- data/lib/grape/middleware/versioner/path.rb +8 -3
- data/lib/grape/namespace.rb +3 -3
- data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
- data/lib/grape/parser/json.rb +1 -1
- data/lib/grape/path.rb +14 -17
- data/lib/grape/request.rb +15 -8
- data/lib/grape/router/mustermann_pattern.rb +44 -0
- data/lib/grape/router/pattern.rb +6 -10
- data/lib/grape/router.rb +28 -42
- data/lib/grape/serve_stream/file_body.rb +1 -0
- data/lib/grape/serve_stream/sendfile_response.rb +3 -5
- data/lib/grape/serve_stream/stream_response.rb +1 -0
- data/lib/grape/testing.rb +33 -0
- data/lib/grape/util/base_inheritable.rb +13 -16
- data/lib/grape/util/inheritable_setting.rb +44 -27
- data/lib/grape/util/inheritable_values.rb +7 -3
- data/lib/grape/util/lazy/base.rb +16 -0
- data/lib/grape/util/lazy/block.rb +2 -9
- data/lib/grape/util/lazy/value.rb +2 -9
- data/lib/grape/util/lazy/value_enumerable.rb +13 -16
- data/lib/grape/util/media_type.rb +1 -4
- data/lib/grape/util/path_normalizer.rb +34 -0
- data/lib/grape/util/registry.rb +1 -1
- data/lib/grape/util/stackable_values.rb +11 -8
- data/lib/grape/validations/attributes_iterator.rb +13 -13
- data/lib/grape/validations/coerce_options.rb +21 -0
- data/lib/grape/validations/oneof_collector.rb +39 -0
- data/lib/grape/validations/param_scope_tracker.rb +14 -9
- data/lib/grape/validations/params_documentation.rb +25 -23
- data/lib/grape/validations/params_scope.rb +54 -172
- data/lib/grape/validations/shared_options.rb +19 -0
- data/lib/grape/validations/types/array_coercer.rb +2 -2
- data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
- data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
- data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
- data/lib/grape/validations/types/primitive_coercer.rb +10 -5
- data/lib/grape/validations/types/set_coercer.rb +1 -1
- data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
- data/lib/grape/validations/types.rb +23 -30
- data/lib/grape/validations/validations_spec.rb +149 -0
- data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
- data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
- data/lib/grape/validations/validators/base.rb +39 -22
- data/lib/grape/validations/validators/coerce_validator.rb +5 -3
- data/lib/grape/validations/validators/default_validator.rb +7 -8
- data/lib/grape/validations/validators/except_values_validator.rb +3 -2
- data/lib/grape/validations/validators/length_validator.rb +1 -1
- data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
- data/lib/grape/validations/validators/oneof_validator.rb +49 -0
- data/lib/grape/validations/validators/values_validator.rb +5 -5
- data/lib/grape/version.rb +1 -1
- data/lib/grape/xml.rb +8 -1
- data/lib/grape.rb +6 -6
- metadata +34 -18
- data/lib/grape/middleware/globals.rb +0 -14
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Validations
|
|
5
|
+
# Frozen value object holding everything {ParamsScope#validates} needs to
|
|
6
|
+
# know about a single +requires+/+optional+ declaration. Built once from
|
|
7
|
+
# the raw validations hash supplied by the DSL; the raw hash is never
|
|
8
|
+
# mutated.
|
|
9
|
+
#
|
|
10
|
+
# Splits the raw entries into three logical buckets:
|
|
11
|
+
#
|
|
12
|
+
# * Spec-consumed keys (type/types/coerce*, presence/message,
|
|
13
|
+
# default/fail_fast, doc keys) — exposed via named accessors and never
|
|
14
|
+
# handed to validator dispatch.
|
|
15
|
+
# * Shared opts (allow_blank, fail_fast) — read by every validator at
|
|
16
|
+
# construction time via {#shared_opts}.
|
|
17
|
+
# * Validator entries (everything else, e.g. +regexp+, +length+, +values+,
|
|
18
|
+
# +allow_blank+, custom validators) — exposed via {#validator_entries}
|
|
19
|
+
# for the dispatch loop.
|
|
20
|
+
#
|
|
21
|
+
# Same key can land in more than one bucket (e.g. +allow_blank+ is both a
|
|
22
|
+
# shared opt and a validator entry; +length+ is both a doc source and a
|
|
23
|
+
# validator entry).
|
|
24
|
+
class ValidationsSpec
|
|
25
|
+
# Keys consumed by the spec itself; must NOT be dispatched as validators
|
|
26
|
+
# by the caller. Documentation-only keys are filtered through a separate
|
|
27
|
+
# set so that dual-purpose keys (length, default, values, except_values)
|
|
28
|
+
# aren't accidentally swallowed.
|
|
29
|
+
SPEC_CONSUMED_KEYS = %i[
|
|
30
|
+
type types coerce coerce_with coerce_message
|
|
31
|
+
presence message
|
|
32
|
+
fail_fast
|
|
33
|
+
desc description documentation
|
|
34
|
+
].freeze
|
|
35
|
+
|
|
36
|
+
attr_reader :raw, :coerce_type, :coerce_method, :coerce_message, :presence_options, :values, :except_values, :default, :allow_blank, :fail_fast, :shared_opts, :validator_entries
|
|
37
|
+
|
|
38
|
+
def self.from(validations)
|
|
39
|
+
new(validations)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def initialize(raw)
|
|
43
|
+
raise ArgumentError, ':type may not be supplied with :types' if raw.key?(:type) && raw.key?(:types)
|
|
44
|
+
|
|
45
|
+
@raw = raw
|
|
46
|
+
@coerce_type, @coerce_message, @coerce_method = parse_coerce(raw)
|
|
47
|
+
@values = resolve_value(raw[:values])
|
|
48
|
+
@except_values = resolve_value(raw[:except_values])
|
|
49
|
+
@default = raw[:default]
|
|
50
|
+
@presence_options = raw[:presence]
|
|
51
|
+
@allow_blank = resolve_value(raw[:allow_blank])
|
|
52
|
+
@fail_fast = raw[:fail_fast] || false
|
|
53
|
+
|
|
54
|
+
@coerce_type = guess_coerce_type(@coerce_type, @values, @except_values)
|
|
55
|
+
@shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
|
|
56
|
+
@validator_entries = build_validator_entries(raw)
|
|
57
|
+
|
|
58
|
+
validate!
|
|
59
|
+
|
|
60
|
+
freeze
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def required?
|
|
64
|
+
!@presence_options.nil? && @presence_options != false
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def coerce_options
|
|
68
|
+
CoerceOptions.new(type: @coerce_type, coerce_method: @coerce_method, message: @coerce_message)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
private
|
|
72
|
+
|
|
73
|
+
# Cross-field consistency checks on the parsed declaration. Run at
|
|
74
|
+
# construction so an incoherent spec (e.g. a +default+ outside +values+,
|
|
75
|
+
# or +values+ whose elements don't match +type+) can never exist —
|
|
76
|
+
# callers no longer have to remember to invoke these separately.
|
|
77
|
+
def validate!
|
|
78
|
+
check_incompatible_option_values(@default, @values, @except_values)
|
|
79
|
+
validate_value_coercion(@coerce_type, @values, @except_values)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def check_incompatible_option_values(default, values, except_values)
|
|
83
|
+
return if default.nil? || default.is_a?(Proc)
|
|
84
|
+
|
|
85
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && Array(default).any? { |def_val| !values.include?(def_val) }
|
|
86
|
+
|
|
87
|
+
return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
|
|
88
|
+
|
|
89
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def validate_value_coercion(coerce_type, *values_list)
|
|
93
|
+
return unless coerce_type
|
|
94
|
+
|
|
95
|
+
coerce_type = coerce_type.first if coerce_type.is_a?(Enumerable)
|
|
96
|
+
values_list.each do |values|
|
|
97
|
+
next if !values || values.is_a?(Proc)
|
|
98
|
+
|
|
99
|
+
value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
|
|
100
|
+
value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if coerce_type == Grape::API::Boolean
|
|
101
|
+
raise Grape::Exceptions::IncompatibleOptionValues.new(:type, coerce_type, :values, values) unless value_types.all?(coerce_type)
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
def build_validator_entries(raw)
|
|
106
|
+
raw.reject do |k, _|
|
|
107
|
+
SPEC_CONSUMED_KEYS.include?(k) || ParamsScope::RESERVED_DOCUMENTATION_KEYWORDS.include?(k)
|
|
108
|
+
end.freeze
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def parse_coerce(raw)
|
|
112
|
+
if raw.key?(:type)
|
|
113
|
+
coerce, coerce_message = extract_value_and_message(raw[:type])
|
|
114
|
+
coerce_with = raw[:coerce_with]
|
|
115
|
+
return [Types::VariantCollectionCoercer.new(coerce, coerce_with), coerce_message, nil] if Types.multiple?(coerce)
|
|
116
|
+
elsif raw.key?(:types)
|
|
117
|
+
coerce, coerce_message = extract_value_and_message(raw[:types])
|
|
118
|
+
coerce_with = raw[:coerce_with]
|
|
119
|
+
else
|
|
120
|
+
coerce = raw[:coerce]
|
|
121
|
+
coerce_message = raw[:coerce_message]
|
|
122
|
+
coerce_with = raw[:coerce_with]
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
[coerce, coerce_message, coerce_with]
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
def extract_value_and_message(opt)
|
|
129
|
+
return [opt, nil] unless opt.is_a?(Hash)
|
|
130
|
+
|
|
131
|
+
[opt[:value], opt[:message]]
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def resolve_value(opt)
|
|
135
|
+
opt.is_a?(Hash) ? opt[:value] : opt
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def guess_coerce_type(coerce_type, *values_list)
|
|
139
|
+
return coerce_type unless coerce_type == Array
|
|
140
|
+
|
|
141
|
+
values_list.each do |values|
|
|
142
|
+
next if !values || values.is_a?(Proc)
|
|
143
|
+
return values.first.class if values.is_a?(Range) || !values.empty?
|
|
144
|
+
end
|
|
145
|
+
coerce_type
|
|
146
|
+
end
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
end
|
|
@@ -9,7 +9,7 @@ module Grape
|
|
|
9
9
|
def validate_params!(params)
|
|
10
10
|
known_keys = all_keys
|
|
11
11
|
keys = keys_in_common(params, known_keys)
|
|
12
|
-
return if keys.empty? || keys.length ==
|
|
12
|
+
return if keys.empty? || keys.length == attrs.length
|
|
13
13
|
|
|
14
14
|
validation_error!(known_keys)
|
|
15
15
|
end
|
|
@@ -8,7 +8,7 @@ module Grape
|
|
|
8
8
|
|
|
9
9
|
def validate_params!(params)
|
|
10
10
|
known_keys = all_keys
|
|
11
|
-
return if hash_like?(params) && known_keys.intersect?(params.keys.map { |attr|
|
|
11
|
+
return if hash_like?(params) && known_keys.intersect?(params.keys.map { |attr| scope.full_name(attr) })
|
|
12
12
|
|
|
13
13
|
validation_error!(known_keys)
|
|
14
14
|
end
|
|
@@ -12,10 +12,20 @@ module Grape
|
|
|
12
12
|
# from them are frozen by construction. Lazy ivar assignment
|
|
13
13
|
# (e.g. +memoize+, <tt>||=</tt>) will raise +FrozenError+ at request time.
|
|
14
14
|
class Base
|
|
15
|
+
extend Forwardable
|
|
15
16
|
include Grape::Util::Translation
|
|
16
17
|
|
|
17
18
|
attr_reader :attrs
|
|
18
19
|
|
|
20
|
+
# +allow_blank+ / +fail_fast+ are read straight off the internal
|
|
21
|
+
# {Grape::Validations::SharedOptions} value object; no per-validator
|
|
22
|
+
# ivars. The object is built from the +opts+ Hash in #initialize, so
|
|
23
|
+
# the public 5th-argument contract stays a plain Hash.
|
|
24
|
+
def_delegators :@opts, :allow_blank, :fail_fast
|
|
25
|
+
|
|
26
|
+
alias fail_fast? fail_fast
|
|
27
|
+
alias allow_blank? allow_blank
|
|
28
|
+
|
|
19
29
|
class << self
|
|
20
30
|
# Declares the default I18n message key used by +validation_error!+.
|
|
21
31
|
# Subclasses that only need a single fixed error message can declare it
|
|
@@ -52,15 +62,17 @@ module Grape
|
|
|
52
62
|
# @param options [Object] implementation-dependent Validator options; deep-frozen on assignment
|
|
53
63
|
# @param required [Boolean] attribute(s) are required or optional
|
|
54
64
|
# @param scope [ParamsScope] parent scope for this Validator
|
|
55
|
-
# @param opts [Hash]
|
|
65
|
+
# @param opts [Hash] shared validator options; only +:allow_blank+ and
|
|
66
|
+
# +:fail_fast+ are consulted (other keys ignored, as before)
|
|
56
67
|
def initialize(attrs, options, required, scope, opts)
|
|
57
68
|
@attrs = Array(attrs).freeze
|
|
58
69
|
@options = Grape::Util::DeepFreeze.deep_freeze(options)
|
|
59
70
|
@option = @options # TODO: remove in next major release
|
|
60
71
|
@required = required
|
|
61
72
|
@scope = scope
|
|
62
|
-
@
|
|
73
|
+
@opts = SharedOptions.new(**opts.slice(:allow_blank, :fail_fast))
|
|
63
74
|
@exception_message = message(self.class.default_message_key) if self.class.default_message_key
|
|
75
|
+
@iterator = iterator_class.new(@attrs, @scope).freeze
|
|
64
76
|
end
|
|
65
77
|
|
|
66
78
|
# Validates a given request.
|
|
@@ -69,15 +81,11 @@ module Grape
|
|
|
69
81
|
# @raise [Grape::Exceptions::Validation] if validation failed
|
|
70
82
|
# @return [void]
|
|
71
83
|
def validate(request)
|
|
72
|
-
return unless
|
|
84
|
+
return unless scope.should_validate?(request.params)
|
|
73
85
|
|
|
74
86
|
validate!(request.params)
|
|
75
87
|
end
|
|
76
88
|
|
|
77
|
-
def fail_fast?
|
|
78
|
-
@fail_fast
|
|
79
|
-
end
|
|
80
|
-
|
|
81
89
|
# Validates a given parameter hash.
|
|
82
90
|
# @note Override #validate_param! for per-parameter validation,
|
|
83
91
|
# or #validate if you need access to the entire request.
|
|
@@ -85,21 +93,20 @@ module Grape
|
|
|
85
93
|
# @raise [Grape::Exceptions::Validation] if validation failed
|
|
86
94
|
# @return [void]
|
|
87
95
|
def validate!(params)
|
|
88
|
-
attributes = SingleAttributeIterator.new(@attrs, @scope, params)
|
|
89
96
|
# we collect errors inside array because
|
|
90
97
|
# there may be more than one error per field
|
|
91
|
-
array_errors =
|
|
98
|
+
array_errors = nil
|
|
92
99
|
|
|
93
|
-
|
|
94
|
-
next if
|
|
95
|
-
next unless
|
|
100
|
+
@iterator.each(params) do |val, attr_name, empty_val|
|
|
101
|
+
next if !scope.required? && empty_val
|
|
102
|
+
next unless scope.meets_dependency?(val, params)
|
|
96
103
|
|
|
97
|
-
validate_param!(attr_name, val) if
|
|
104
|
+
validate_param!(attr_name, val) if required? || (hash_like?(val) && val.key?(attr_name))
|
|
98
105
|
rescue Grape::Exceptions::Validation => e
|
|
99
|
-
array_errors << e
|
|
106
|
+
(array_errors ||= []) << e
|
|
100
107
|
end
|
|
101
108
|
|
|
102
|
-
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
109
|
+
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
103
110
|
end
|
|
104
111
|
|
|
105
112
|
protected
|
|
@@ -115,8 +122,18 @@ module Grape
|
|
|
115
122
|
|
|
116
123
|
private
|
|
117
124
|
|
|
118
|
-
|
|
119
|
-
|
|
125
|
+
attr_reader :options, :scope, :required, :exception_message
|
|
126
|
+
|
|
127
|
+
alias required? required
|
|
128
|
+
|
|
129
|
+
# The AttributesIterator subclass used to walk this validator's
|
|
130
|
+
# attributes. Built once in #initialize and reused across requests.
|
|
131
|
+
def iterator_class
|
|
132
|
+
SingleAttributeIterator
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def validation_error!(attr_name_or_params, message = exception_message)
|
|
136
|
+
params = attr_name_or_params.is_a?(Array) ? attr_name_or_params : scope.full_name(attr_name_or_params)
|
|
120
137
|
raise Grape::Exceptions::Validation.new(params:, message:)
|
|
121
138
|
end
|
|
122
139
|
|
|
@@ -124,8 +141,8 @@ module Grape
|
|
|
124
141
|
obj.respond_to?(:key?)
|
|
125
142
|
end
|
|
126
143
|
|
|
127
|
-
def options_key?(key,
|
|
128
|
-
current_options =
|
|
144
|
+
def options_key?(key, given_options = nil)
|
|
145
|
+
current_options = given_options || options
|
|
129
146
|
hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
|
|
130
147
|
end
|
|
131
148
|
|
|
@@ -137,18 +154,18 @@ module Grape
|
|
|
137
154
|
# @exception_message = message(:presence) # symbol key or custom message
|
|
138
155
|
# @exception_message = message { build_hash_message } # computed fallback
|
|
139
156
|
def message(default_key = nil)
|
|
140
|
-
key = options_key?(:message) ?
|
|
157
|
+
key = options_key?(:message) ? options[:message] : default_key
|
|
141
158
|
return key unless key.nil?
|
|
142
159
|
|
|
143
160
|
yield if block_given?
|
|
144
161
|
end
|
|
145
162
|
|
|
146
163
|
def option_value
|
|
147
|
-
options_key?(:value) ?
|
|
164
|
+
options_key?(:value) ? options[:value] : options
|
|
148
165
|
end
|
|
149
166
|
|
|
150
167
|
def scrub(value)
|
|
151
|
-
return value
|
|
168
|
+
return value if !value.respond_to?(:valid_encoding?) || value.valid_encoding?
|
|
152
169
|
|
|
153
170
|
value.scrub
|
|
154
171
|
end
|
|
@@ -9,13 +9,15 @@ module Grape
|
|
|
9
9
|
def initialize(attrs, options, required, scope, opts)
|
|
10
10
|
super
|
|
11
11
|
|
|
12
|
-
|
|
12
|
+
@exception_message = options.message if options.message
|
|
13
|
+
|
|
14
|
+
raw_type = options.type
|
|
13
15
|
type = hash_like?(raw_type) ? raw_type[:value] : raw_type
|
|
14
16
|
@converter =
|
|
15
17
|
if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
|
|
16
18
|
type
|
|
17
19
|
else
|
|
18
|
-
Types.build_coercer(type, method:
|
|
20
|
+
Types.build_coercer(type, method: options.coerce_method)
|
|
19
21
|
end
|
|
20
22
|
end
|
|
21
23
|
|
|
@@ -24,7 +26,7 @@ module Grape
|
|
|
24
26
|
|
|
25
27
|
new_value = coerce_value(params[attr_name])
|
|
26
28
|
|
|
27
|
-
validation_error!(attr_name, new_value.message ||
|
|
29
|
+
validation_error!(attr_name, new_value.message || exception_message) if new_value.is_a?(Types::InvalidValue)
|
|
28
30
|
|
|
29
31
|
# Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
|
|
30
32
|
# which looses wrappers for hashes and arrays after reassigning values
|
|
@@ -8,19 +8,18 @@ module Grape
|
|
|
8
8
|
super
|
|
9
9
|
# !important, lazy call at runtime
|
|
10
10
|
@default_call =
|
|
11
|
-
if
|
|
12
|
-
|
|
13
|
-
elsif
|
|
14
|
-
proc {
|
|
11
|
+
if options.is_a?(Proc)
|
|
12
|
+
options.arity.zero? ? proc { options.call } : options
|
|
13
|
+
elsif options.duplicable?
|
|
14
|
+
proc { options.dup }
|
|
15
15
|
else
|
|
16
|
-
proc {
|
|
16
|
+
proc { options }
|
|
17
17
|
end
|
|
18
18
|
end
|
|
19
19
|
|
|
20
20
|
def validate!(params)
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
next unless @scope.meets_dependency?(resource_params, params)
|
|
21
|
+
@iterator.each(params) do |resource_params, attr_name|
|
|
22
|
+
next unless scope.meets_dependency?(resource_params, params)
|
|
24
23
|
|
|
25
24
|
resource_params[attr_name] = @default_call.call(resource_params) if hash_like?(resource_params) && resource_params[attr_name].nil?
|
|
26
25
|
end
|
|
@@ -9,11 +9,12 @@ module Grape
|
|
|
9
9
|
def initialize(attrs, options, required, scope, opts)
|
|
10
10
|
super
|
|
11
11
|
except = option_value
|
|
12
|
-
|
|
12
|
+
except_proc = except.is_a?(Proc)
|
|
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?
|
|
13
14
|
|
|
14
15
|
# Zero-arity procs (e.g. -> { User.pluck(:role) }) must be called per-request,
|
|
15
16
|
# not at definition time, so they are wrapped in a lambda to defer execution.
|
|
16
|
-
@excepts_call =
|
|
17
|
+
@excepts_call = except_proc ? except : -> { except }
|
|
17
18
|
end
|
|
18
19
|
|
|
19
20
|
def validate_param!(attr_name, params)
|
|
@@ -7,7 +7,7 @@ module Grape
|
|
|
7
7
|
def initialize(attrs, options, required, scope, opts)
|
|
8
8
|
super
|
|
9
9
|
|
|
10
|
-
@min, @max, @is =
|
|
10
|
+
@min, @max, @is = options.values_at(:min, :max, :is)
|
|
11
11
|
validate_boundary!(:min, @min)
|
|
12
12
|
validate_boundary!(:max, @max)
|
|
13
13
|
raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if @min && @max && @min > @max
|
|
@@ -5,28 +5,31 @@ module Grape
|
|
|
5
5
|
module Validators
|
|
6
6
|
class MultipleParamsBase < Base
|
|
7
7
|
def validate!(params)
|
|
8
|
-
|
|
9
|
-
array_errors = []
|
|
8
|
+
array_errors = nil
|
|
10
9
|
|
|
11
|
-
|
|
10
|
+
@iterator.each(params) do |resource_params|
|
|
12
11
|
validate_params!(resource_params)
|
|
13
12
|
rescue Grape::Exceptions::Validation => e
|
|
14
|
-
array_errors << e
|
|
13
|
+
(array_errors ||= []) << e
|
|
15
14
|
end
|
|
16
15
|
|
|
17
|
-
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
16
|
+
raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
|
|
18
17
|
end
|
|
19
18
|
|
|
20
19
|
private
|
|
21
20
|
|
|
21
|
+
def iterator_class
|
|
22
|
+
MultipleAttributesIterator
|
|
23
|
+
end
|
|
24
|
+
|
|
22
25
|
def keys_in_common(resource_params, known_keys = all_keys)
|
|
23
26
|
return [] unless hash_like?(resource_params)
|
|
24
27
|
|
|
25
|
-
known_keys & resource_params.keys.map! { |attr|
|
|
28
|
+
known_keys & resource_params.keys.map! { |attr| scope.full_name(attr) }
|
|
26
29
|
end
|
|
27
30
|
|
|
28
31
|
def all_keys
|
|
29
|
-
|
|
32
|
+
attrs.map { |attr| scope.full_name(attr) }
|
|
30
33
|
end
|
|
31
34
|
end
|
|
32
35
|
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Grape
|
|
4
|
+
module Validations
|
|
5
|
+
module Validators
|
|
6
|
+
# Validates that a Hash parameter matches at least one of a set of
|
|
7
|
+
# variant schemas. Each variant is a list of pre-built validators
|
|
8
|
+
# captured by evaluating the variant's block in a {Grape::Validations::OneofCollector}-backed
|
|
9
|
+
# {ParamsScope}. At request time we try each variant in order against
|
|
10
|
+
# a deep-dup of the value; the first variant that produces no errors
|
|
11
|
+
# wins and its (possibly coerced) hash replaces the original.
|
|
12
|
+
class OneofValidator < Base
|
|
13
|
+
default_message_key :oneof
|
|
14
|
+
|
|
15
|
+
def initialize(attrs, options, required, scope, opts)
|
|
16
|
+
super
|
|
17
|
+
@variants = Array(options)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def validate_param!(attr_name, params)
|
|
21
|
+
value = params[attr_name]
|
|
22
|
+
return if value.nil? && !required?
|
|
23
|
+
|
|
24
|
+
winning_candidate = nil
|
|
25
|
+
@variants.each do |variant_validators|
|
|
26
|
+
candidate = value.deep_dup
|
|
27
|
+
if variant_matches?(variant_validators, candidate)
|
|
28
|
+
winning_candidate = candidate
|
|
29
|
+
break
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
return params[attr_name] = winning_candidate if winning_candidate
|
|
34
|
+
|
|
35
|
+
validation_error!(attr_name)
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
private
|
|
39
|
+
|
|
40
|
+
def variant_matches?(variant_validators, candidate)
|
|
41
|
+
variant_validators.each { |v| v.validate!(candidate) }
|
|
42
|
+
true
|
|
43
|
+
rescue Grape::Exceptions::Validation, Grape::Exceptions::ValidationArrayErrors
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -28,7 +28,7 @@ module Grape
|
|
|
28
28
|
val = scrub(params[attr_name])
|
|
29
29
|
|
|
30
30
|
return if val.nil? && !required_for_root_scope?
|
|
31
|
-
return if val != false && val.blank? &&
|
|
31
|
+
return if val != false && val.blank? && allow_blank?
|
|
32
32
|
return if check_values?(val, attr_name)
|
|
33
33
|
|
|
34
34
|
validation_error!(attr_name)
|
|
@@ -55,12 +55,12 @@ module Grape
|
|
|
55
55
|
end
|
|
56
56
|
|
|
57
57
|
def required_for_root_scope?
|
|
58
|
-
return false unless
|
|
58
|
+
return false unless required?
|
|
59
59
|
|
|
60
|
-
|
|
61
|
-
|
|
60
|
+
current_scope = scope
|
|
61
|
+
current_scope = current_scope.parent while current_scope.lateral?
|
|
62
62
|
|
|
63
|
-
|
|
63
|
+
current_scope.root?
|
|
64
64
|
end
|
|
65
65
|
end
|
|
66
66
|
end
|
data/lib/grape/version.rb
CHANGED
data/lib/grape/xml.rb
CHANGED
|
@@ -1,7 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module Grape
|
|
4
|
-
|
|
4
|
+
# Since multi_xml 0.9.0 the canonical constant is MultiXML; MultiXml is a
|
|
5
|
+
# deprecated alias (removed in v1.0) that warns on use. Prefer MultiXML so
|
|
6
|
+
# Grape::Xml.parse doesn't trip the deprecation, falling back to the legacy
|
|
7
|
+
# constant and then ActiveSupport::XmlMini.
|
|
8
|
+
# https://github.com/sferik/multi_xml/blob/v0.9.1/CHANGELOG.md
|
|
9
|
+
if defined?(::MultiXML)
|
|
10
|
+
Xml = ::MultiXML
|
|
11
|
+
elsif defined?(::MultiXml)
|
|
5
12
|
Xml = ::MultiXml
|
|
6
13
|
else
|
|
7
14
|
Xml = ::ActiveSupport::XmlMini
|
data/lib/grape.rb
CHANGED
|
@@ -2,21 +2,20 @@
|
|
|
2
2
|
|
|
3
3
|
require 'logger'
|
|
4
4
|
require 'active_support'
|
|
5
|
-
require 'active_support/version'
|
|
6
5
|
require 'active_support/isolated_execution_state'
|
|
7
6
|
require 'active_support/core_ext/array/conversions' # to_xml
|
|
8
7
|
require 'active_support/core_ext/array/wrap'
|
|
9
8
|
require 'active_support/core_ext/hash/conversions' # to_xml
|
|
10
9
|
require 'active_support/core_ext/hash/deep_merge'
|
|
11
10
|
require 'active_support/core_ext/hash/deep_transform_values'
|
|
12
|
-
require 'active_support/core_ext/hash/indifferent_access'
|
|
13
|
-
require 'active_support/
|
|
11
|
+
require 'active_support/core_ext/hash/indifferent_access' # nested_under_indifferent_access, required by HashWithIndifferentAccess.new
|
|
12
|
+
require 'active_support/hash_with_indifferent_access'
|
|
14
13
|
require 'active_support/core_ext/module/delegation' # delegate_missing_to
|
|
15
14
|
require 'active_support/core_ext/object/blank'
|
|
16
15
|
require 'active_support/core_ext/object/deep_dup'
|
|
17
16
|
require 'active_support/core_ext/object/duplicable'
|
|
17
|
+
require 'active_support/core_ext/string/inflections' # demodulize, underscore
|
|
18
18
|
require 'active_support/deprecation'
|
|
19
|
-
require 'active_support/inflector'
|
|
20
19
|
require 'active_support/ordered_options'
|
|
21
20
|
require 'active_support/notifications'
|
|
22
21
|
|
|
@@ -27,8 +26,8 @@ require 'dry-types'
|
|
|
27
26
|
require 'dry-configurable'
|
|
28
27
|
require 'forwardable'
|
|
29
28
|
require 'json'
|
|
30
|
-
require 'mustermann
|
|
31
|
-
require '
|
|
29
|
+
require 'mustermann'
|
|
30
|
+
require 'mustermann/ast/pattern'
|
|
32
31
|
require 'rack'
|
|
33
32
|
require 'rack/auth/basic'
|
|
34
33
|
require 'rack/builder'
|
|
@@ -52,6 +51,7 @@ module Grape
|
|
|
52
51
|
|
|
53
52
|
setting :param_builder, default: :hash_with_indifferent_access
|
|
54
53
|
setting :lint, default: false
|
|
54
|
+
setting :warn_on_helper_overrides, default: false
|
|
55
55
|
|
|
56
56
|
HTTP_SUPPORTED_METHODS = [
|
|
57
57
|
Rack::GET,
|