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.
Files changed (102) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +116 -43
  4. data/UPGRADING.md +336 -1
  5. data/grape.gemspec +5 -5
  6. data/lib/grape/api/instance.rb +7 -7
  7. data/lib/grape/api.rb +22 -25
  8. data/lib/grape/cookies.rb +2 -6
  9. data/lib/grape/declared_params_handler.rb +48 -50
  10. data/lib/grape/dsl/callbacks.rb +9 -3
  11. data/lib/grape/dsl/desc.rb +8 -2
  12. data/lib/grape/dsl/entity.rb +88 -0
  13. data/lib/grape/dsl/helpers.rb +27 -7
  14. data/lib/grape/dsl/inside_route.rb +38 -129
  15. data/lib/grape/dsl/logger.rb +3 -5
  16. data/lib/grape/dsl/parameters.rb +32 -38
  17. data/lib/grape/dsl/request_response.rb +53 -48
  18. data/lib/grape/dsl/rescue_options.rb +24 -0
  19. data/lib/grape/dsl/routing.rb +51 -35
  20. data/lib/grape/dsl/settings.rb +14 -8
  21. data/lib/grape/dsl/version_options.rb +23 -0
  22. data/lib/grape/endpoint/options.rb +19 -0
  23. data/lib/grape/endpoint.rb +96 -68
  24. data/lib/grape/env.rb +1 -3
  25. data/lib/grape/error_formatter/base.rb +23 -20
  26. data/lib/grape/error_formatter/json.rb +8 -4
  27. data/lib/grape/error_formatter/txt.rb +10 -10
  28. data/lib/grape/exceptions/base.rb +3 -1
  29. data/lib/grape/exceptions/error_response.rb +45 -0
  30. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  31. data/lib/grape/exceptions/validation.rb +14 -0
  32. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  33. data/lib/grape/exceptions/validation_errors.rb +12 -20
  34. data/lib/grape/formatter/serializable_hash.rb +5 -9
  35. data/lib/grape/json.rb +38 -2
  36. data/lib/grape/locale/en.yml +2 -0
  37. data/lib/grape/middleware/auth/base.rb +2 -3
  38. data/lib/grape/middleware/auth/dsl.rb +23 -8
  39. data/lib/grape/middleware/base.rb +22 -33
  40. data/lib/grape/middleware/deprecated_options_hash_access.rb +19 -0
  41. data/lib/grape/middleware/error.rb +152 -62
  42. data/lib/grape/middleware/formatter.rb +66 -50
  43. data/lib/grape/middleware/precomputed_content_types.rb +46 -0
  44. data/lib/grape/middleware/stack.rb +5 -6
  45. data/lib/grape/middleware/versioner/accept_version_header.rb +1 -1
  46. data/lib/grape/middleware/versioner/base.rb +34 -38
  47. data/lib/grape/middleware/versioner/header.rb +3 -5
  48. data/lib/grape/middleware/versioner/path.rb +8 -3
  49. data/lib/grape/namespace.rb +3 -3
  50. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  51. data/lib/grape/parser/json.rb +1 -1
  52. data/lib/grape/path.rb +14 -17
  53. data/lib/grape/request.rb +15 -8
  54. data/lib/grape/router/mustermann_pattern.rb +44 -0
  55. data/lib/grape/router/pattern.rb +6 -10
  56. data/lib/grape/router.rb +28 -42
  57. data/lib/grape/serve_stream/file_body.rb +1 -0
  58. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  59. data/lib/grape/serve_stream/stream_response.rb +1 -0
  60. data/lib/grape/testing.rb +33 -0
  61. data/lib/grape/util/base_inheritable.rb +13 -16
  62. data/lib/grape/util/inheritable_setting.rb +44 -27
  63. data/lib/grape/util/inheritable_values.rb +7 -3
  64. data/lib/grape/util/lazy/base.rb +16 -0
  65. data/lib/grape/util/lazy/block.rb +2 -9
  66. data/lib/grape/util/lazy/value.rb +2 -9
  67. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  68. data/lib/grape/util/media_type.rb +1 -4
  69. data/lib/grape/util/path_normalizer.rb +34 -0
  70. data/lib/grape/util/registry.rb +1 -1
  71. data/lib/grape/util/stackable_values.rb +11 -8
  72. data/lib/grape/validations/attributes_iterator.rb +13 -13
  73. data/lib/grape/validations/coerce_options.rb +21 -0
  74. data/lib/grape/validations/oneof_collector.rb +39 -0
  75. data/lib/grape/validations/param_scope_tracker.rb +14 -9
  76. data/lib/grape/validations/params_documentation.rb +25 -23
  77. data/lib/grape/validations/params_scope.rb +54 -172
  78. data/lib/grape/validations/shared_options.rb +19 -0
  79. data/lib/grape/validations/types/array_coercer.rb +2 -2
  80. data/lib/grape/validations/types/custom_type_coercer.rb +41 -85
  81. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  82. data/lib/grape/validations/types/dry_type_coercer.rb +3 -3
  83. data/lib/grape/validations/types/primitive_coercer.rb +10 -5
  84. data/lib/grape/validations/types/set_coercer.rb +1 -1
  85. data/lib/grape/validations/types/variant_collection_coercer.rb +8 -0
  86. data/lib/grape/validations/types.rb +23 -30
  87. data/lib/grape/validations/validations_spec.rb +149 -0
  88. data/lib/grape/validations/validators/all_or_none_of_validator.rb +1 -1
  89. data/lib/grape/validations/validators/at_least_one_of_validator.rb +1 -1
  90. data/lib/grape/validations/validators/base.rb +39 -22
  91. data/lib/grape/validations/validators/coerce_validator.rb +5 -3
  92. data/lib/grape/validations/validators/default_validator.rb +7 -8
  93. data/lib/grape/validations/validators/except_values_validator.rb +3 -2
  94. data/lib/grape/validations/validators/length_validator.rb +1 -1
  95. data/lib/grape/validations/validators/multiple_params_base.rb +10 -7
  96. data/lib/grape/validations/validators/oneof_validator.rb +49 -0
  97. data/lib/grape/validations/validators/values_validator.rb +5 -5
  98. data/lib/grape/version.rb +1 -1
  99. data/lib/grape/xml.rb +8 -1
  100. data/lib/grape.rb +6 -6
  101. metadata +34 -18
  102. 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 == @attrs.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| @scope.full_name(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] additional validation options
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
- @fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank)
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 @scope.should_validate?(request.params)
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
- attributes.each do |val, attr_name, empty_val|
94
- next if !@scope.required? && empty_val
95
- next unless @scope.meets_dependency?(val, params)
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 @required || (hash_like?(val) && val.key?(attr_name))
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.any?
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
- def validation_error!(attr_name_or_params, message = @exception_message)
119
- params = attr_name_or_params.is_a?(Array) ? attr_name_or_params : @scope.full_name(attr_name_or_params)
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, options = nil)
128
- current_options = options || @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) ? @options[:message] : default_key
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) ? @options[:value] : @options
164
+ options_key?(:value) ? options[:value] : options
148
165
  end
149
166
 
150
167
  def scrub(value)
151
- return value unless value.respond_to?(:valid_encoding?) && !value.valid_encoding?
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
- raw_type = @options[:type]
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: @options[: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 || @exception_message) if new_value.is_a?(Types::InvalidValue)
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 @options.is_a?(Proc)
12
- @options.arity.zero? ? proc { @options.call } : @options
13
- elsif @options.duplicable?
14
- proc { @options.dup }
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 { @options }
16
+ proc { options }
17
17
  end
18
18
  end
19
19
 
20
20
  def validate!(params)
21
- attrs = SingleAttributeIterator.new(@attrs, @scope, params)
22
- attrs.each do |resource_params, attr_name|
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
- raise ArgumentError, 'except_values Proc must have arity of zero (use values: with a one-arity predicate for per-element checks)' if except.is_a?(Proc) && !except.arity.zero?
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 = except.is_a?(Proc) ? except : -> { except }
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 = @options.values_at(: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
- attributes = MultipleAttributesIterator.new(@attrs, @scope, params)
9
- array_errors = []
8
+ array_errors = nil
10
9
 
11
- attributes.each do |resource_params|
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.any?
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| @scope.full_name(attr) }
28
+ known_keys & resource_params.keys.map! { |attr| scope.full_name(attr) }
26
29
  end
27
30
 
28
31
  def all_keys
29
- @attrs.map { |attr| @scope.full_name(attr) }
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? && @allow_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 @required
58
+ return false unless required?
59
59
 
60
- scope = @scope
61
- scope = scope.parent while scope.lateral?
60
+ current_scope = scope
61
+ current_scope = current_scope.parent while current_scope.lateral?
62
62
 
63
- scope.root?
63
+ current_scope.root?
64
64
  end
65
65
  end
66
66
  end
data/lib/grape/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Grape
4
4
  # The current version of Grape.
5
- VERSION = '3.2.1'
5
+ VERSION = '3.3.0'
6
6
  end
data/lib/grape/xml.rb CHANGED
@@ -1,7 +1,14 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Grape
4
- if defined?(::MultiXml)
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/core_ext/hash/reverse_merge'
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/grape'
31
- require 'pathname'
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,