grape 2.4.0 → 4.0.1

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 (170) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +351 -0
  3. data/CONTRIBUTING.md +18 -10
  4. data/README.md +457 -228
  5. data/UPGRADING.md +1240 -1
  6. data/grape.gemspec +7 -7
  7. data/lib/grape/api/instance.rb +65 -116
  8. data/lib/grape/api.rb +53 -62
  9. data/lib/grape/content_types.rb +58 -3
  10. data/lib/grape/cookies.rb +2 -6
  11. data/lib/grape/declared_params_handler.rb +116 -0
  12. data/lib/grape/dry_types.rb +48 -4
  13. data/lib/grape/dsl/callbacks.rb +8 -58
  14. data/lib/grape/dsl/declared.rb +35 -0
  15. data/lib/grape/dsl/desc.rb +24 -67
  16. data/lib/grape/dsl/entity.rb +106 -0
  17. data/lib/grape/dsl/helpers.rb +80 -65
  18. data/lib/grape/dsl/inside_route.rb +56 -296
  19. data/lib/grape/dsl/logger.rb +4 -9
  20. data/lib/grape/dsl/middleware.rb +22 -40
  21. data/lib/grape/dsl/parameters.rb +86 -93
  22. data/lib/grape/dsl/request_response.rb +139 -139
  23. data/lib/grape/dsl/rescue_options.rb +25 -0
  24. data/lib/grape/dsl/routing.rb +328 -203
  25. data/lib/grape/dsl/settings.rb +29 -135
  26. data/lib/grape/dsl/validations.rb +38 -44
  27. data/lib/grape/dsl/version_options.rb +23 -0
  28. data/lib/grape/endpoint/options.rb +25 -0
  29. data/lib/grape/endpoint.rb +285 -245
  30. data/lib/grape/env.rb +8 -3
  31. data/lib/grape/error_formatter/base.rb +27 -21
  32. data/lib/grape/error_formatter/json.rb +6 -0
  33. data/lib/grape/error_formatter/txt.rb +10 -10
  34. data/lib/grape/error_formatter.rb +6 -2
  35. data/lib/grape/exceptions/base.rb +20 -44
  36. data/lib/grape/exceptions/error_response.rb +48 -0
  37. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  38. data/lib/grape/exceptions/internal_server_error.rb +16 -0
  39. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  40. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  41. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  42. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  43. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  44. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  45. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  46. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  47. data/lib/grape/exceptions/request_error.rb +11 -0
  48. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  49. data/lib/grape/exceptions/unknown_error_formatter.rb +11 -0
  50. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  51. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  52. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  53. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  54. data/lib/grape/exceptions/validation.rb +25 -7
  55. data/lib/grape/exceptions/validation_array_errors.rb +4 -0
  56. data/lib/grape/exceptions/validation_errors.rb +21 -23
  57. data/lib/grape/formatter/json.rb +1 -0
  58. data/lib/grape/formatter/serializable_hash.rb +6 -9
  59. data/lib/grape/json.rb +38 -2
  60. data/lib/grape/locale/en.yml +3 -5
  61. data/lib/grape/middleware/auth/base.rb +4 -3
  62. data/lib/grape/middleware/auth/dsl.rb +9 -23
  63. data/lib/grape/middleware/base.rb +41 -36
  64. data/lib/grape/middleware/error.rb +271 -73
  65. data/lib/grape/middleware/formatter.rb +152 -63
  66. data/lib/grape/middleware/precomputed_content_types.rb +51 -0
  67. data/lib/grape/middleware/stack.rb +20 -11
  68. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  69. data/lib/grape/middleware/versioner/base.rb +40 -48
  70. data/lib/grape/middleware/versioner/header.rb +5 -23
  71. data/lib/grape/middleware/versioner/param.rb +2 -2
  72. data/lib/grape/middleware/versioner/path.rb +54 -7
  73. data/lib/grape/mountable.rb +22 -0
  74. data/lib/grape/namespace.rb +19 -11
  75. data/lib/grape/params_builder/base.rb +2 -0
  76. data/lib/grape/params_builder/hash_with_indifferent_access.rb +1 -1
  77. data/lib/grape/params_builder.rb +2 -19
  78. data/lib/grape/parser/json.rb +1 -1
  79. data/lib/grape/path.rb +8 -72
  80. data/lib/grape/precompiled_json.rb +50 -0
  81. data/lib/grape/request.rb +57 -20
  82. data/lib/grape/router/base_route.rb +53 -7
  83. data/lib/grape/router/greedy_route.rb +16 -6
  84. data/lib/grape/router/mustermann_pattern.rb +44 -0
  85. data/lib/grape/router/pattern/path.rb +78 -0
  86. data/lib/grape/router/pattern.rb +65 -37
  87. data/lib/grape/router/route.rb +66 -18
  88. data/lib/grape/router.rb +126 -115
  89. data/lib/grape/serve_stream/file_body.rb +7 -0
  90. data/lib/grape/serve_stream/sendfile_response.rb +3 -5
  91. data/lib/grape/serve_stream/stream_response.rb +7 -0
  92. data/lib/grape/testing.rb +33 -0
  93. data/lib/grape/util/api_description.rb +67 -0
  94. data/lib/grape/util/cache.rb +22 -2
  95. data/lib/grape/util/deep_freeze.rb +34 -0
  96. data/lib/grape/util/freeze_on_new.rb +20 -0
  97. data/lib/grape/util/inheritable_setting.rb +777 -39
  98. data/lib/grape/util/lazy/base.rb +16 -0
  99. data/lib/grape/util/lazy/block.rb +2 -9
  100. data/lib/grape/util/lazy/value.rb +2 -9
  101. data/lib/grape/util/lazy/value_enumerable.rb +13 -16
  102. data/lib/grape/util/media_type.rb +13 -9
  103. data/lib/grape/util/path_normalizer.rb +37 -0
  104. data/lib/grape/util/registry.rb +13 -3
  105. data/lib/grape/util/shadowed_rescue_handlers.rb +49 -0
  106. data/lib/grape/util/stackable_values.rb +39 -10
  107. data/lib/grape/util/translation.rb +42 -0
  108. data/lib/grape/validations/attributes_iterator.rb +61 -29
  109. data/lib/grape/validations/coerce_options.rb +21 -0
  110. data/lib/grape/validations/contract_scope.rb +8 -13
  111. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  112. data/lib/grape/validations/oneof_collector.rb +35 -0
  113. data/lib/grape/validations/param_scope_tracker.rb +62 -0
  114. data/lib/grape/validations/params_documentation.rb +52 -0
  115. data/lib/grape/validations/params_scope.rb +217 -304
  116. data/lib/grape/validations/shared_options.rb +19 -0
  117. data/lib/grape/validations/single_attribute_iterator.rb +6 -4
  118. data/lib/grape/validations/types/array_coercer.rb +7 -10
  119. data/lib/grape/validations/types/custom_type_coercer.rb +47 -85
  120. data/lib/grape/validations/types/custom_type_collection_coercer.rb +1 -1
  121. data/lib/grape/validations/types/dry_type_coercer.rb +10 -15
  122. data/lib/grape/validations/types/json.rb +1 -3
  123. data/lib/grape/validations/types/multiple_type_coercer.rb +5 -3
  124. data/lib/grape/validations/types/primitive_coercer.rb +14 -33
  125. data/lib/grape/validations/types/set_coercer.rb +1 -1
  126. data/lib/grape/validations/types/variant_collection_coercer.rb +16 -3
  127. data/lib/grape/validations/types.rb +29 -51
  128. data/lib/grape/validations/validations_spec.rb +164 -0
  129. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  130. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  131. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  132. data/lib/grape/validations/validators/base.rb +118 -34
  133. data/lib/grape/validations/validators/coerce_validator.rb +25 -37
  134. data/lib/grape/validations/validators/contract_scope_validator.rb +10 -5
  135. data/lib/grape/validations/validators/default_validator.rb +13 -20
  136. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  137. data/lib/grape/validations/validators/except_values_validator.rb +14 -4
  138. data/lib/grape/validations/validators/length_validator.rb +23 -22
  139. data/lib/grape/validations/validators/multiple_params_base.rb +12 -9
  140. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  141. data/lib/grape/validations/validators/oneof_validator.rb +51 -0
  142. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  143. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  144. data/lib/grape/validations/validators/same_as_validator.rb +7 -15
  145. data/lib/grape/validations/validators/values_validator.rb +34 -26
  146. data/lib/grape/version.rb +1 -2
  147. data/lib/grape/xml.rb +8 -1
  148. data/lib/grape.rb +58 -31
  149. metadata +62 -39
  150. data/lib/grape/api/helpers.rb +0 -9
  151. data/lib/grape/dsl/api.rb +0 -17
  152. data/lib/grape/dsl/configuration.rb +0 -15
  153. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  154. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  155. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  156. data/lib/grape/exceptions/missing_option.rb +0 -11
  157. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  158. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  159. data/lib/grape/exceptions/unknown_options.rb +0 -11
  160. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  161. data/lib/grape/extensions/hash.rb +0 -27
  162. data/lib/grape/extensions/hashie/mash.rb +0 -24
  163. data/lib/grape/middleware/globals.rb +0 -14
  164. data/lib/grape/types/invalid_value.rb +0 -8
  165. data/lib/grape/util/base_inheritable.rb +0 -43
  166. data/lib/grape/util/inheritable_values.rb +0 -29
  167. data/lib/grape/util/reverse_stackable_values.rb +0 -15
  168. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  169. data/lib/grape/validations/attributes_doc.rb +0 -60
  170. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -0,0 +1,164 @@
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
+ @guessed_coerce_type = guess_coerce_type(declared_coerce_type, @values, @except_values)
55
+ @shared_opts = { allow_blank: @allow_blank, fail_fast: @fail_fast }.freeze
56
+ @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
+ #
78
+ # NB. +@guessed_coerce_type+ is used only for this check, never for
79
+ # +coerce_options+ — a bare +type: Array+ combined with scalar
80
+ # +values:+ must still coerce to +Array+ at runtime (only the elements
81
+ # are scalar), so the real +@coerce_type+ is never overwritten by it.
82
+ def validate!
83
+ check_incompatible_option_values(@default, @values, @except_values)
84
+ validate_value_coercion(@guessed_coerce_type, @values, @except_values)
85
+ end
86
+
87
+ def check_incompatible_option_values(default, values, except_values)
88
+ return if default.nil? || default.is_a?(Proc)
89
+
90
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :values, values) if values && !values.is_a?(Proc) && Array(default).any? { |def_val| !values.include?(def_val) }
91
+
92
+ return unless except_values && !except_values.is_a?(Proc) && Array(default).any? { |def_val| except_values.include?(def_val) }
93
+
94
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:default, default, :except, except_values)
95
+ end
96
+
97
+ def validate_value_coercion(coerce_type, *values_list)
98
+ return unless coerce_type
99
+
100
+ element_type = coerce_type.is_a?(Enumerable) ? coerce_type.first : coerce_type
101
+ values_list.each do |values|
102
+ next if !values || values.is_a?(Proc)
103
+
104
+ value_types = values.is_a?(Range) ? [values.begin, values.end].compact : values
105
+ value_types = value_types.map { |type| Grape::API::Boolean.build(type) } if element_type == Grape::API::Boolean
106
+ raise Grape::Exceptions::IncompatibleOptionValues.new(:type, element_type, :values, values) unless value_types.all?(element_type)
107
+ end
108
+ end
109
+
110
+ def build_validator_entries(raw)
111
+ raw.reject do |k, _|
112
+ SPEC_CONSUMED_KEYS.include?(k) || ParamsScope::RESERVED_DOCUMENTATION_KEYWORDS.include?(k)
113
+ end.freeze
114
+ end
115
+
116
+ def parse_coerce(raw)
117
+ if raw.key?(:type)
118
+ coerce, coerce_message = extract_value_and_message(raw[:type])
119
+ coerce_with = raw[:coerce_with]
120
+ return [Types::VariantCollectionCoercer.new(coerce, coerce_with), coerce_message, nil] if Types.multiple?(coerce)
121
+ elsif raw.key?(:types)
122
+ coerce, coerce_message = extract_value_and_message(raw[:types])
123
+ coerce_with = raw[:coerce_with]
124
+ else
125
+ coerce = raw[:coerce]
126
+ coerce_message = raw[:coerce_message]
127
+ coerce_with = raw[:coerce_with]
128
+ end
129
+
130
+ [coerce, coerce_message, coerce_with]
131
+ end
132
+
133
+ def extract_value_and_message(opt)
134
+ return [opt, nil] unless opt.is_a?(Hash)
135
+
136
+ [opt[:value], opt[:message]]
137
+ end
138
+
139
+ def resolve_value(opt)
140
+ opt.is_a?(Hash) ? opt[:value] : opt
141
+ end
142
+
143
+ # The type as written in the DSL. Identical to +@coerce_type+ except for
144
+ # a multiple-type declaration (+type: [Integer, String]+), where
145
+ # {#parse_coerce} wraps the declared list in a
146
+ # {Types::VariantCollectionCoercer} for runtime coercion — the
147
+ # definition-time coherence checks need the list back, since the wrapper
148
+ # matches nothing under +===+.
149
+ def declared_coerce_type
150
+ @coerce_type.is_a?(Types::VariantCollectionCoercer) ? @coerce_type.types : @coerce_type
151
+ end
152
+
153
+ def guess_coerce_type(coerce_type, *values_list)
154
+ return coerce_type unless coerce_type == Array
155
+
156
+ values_list.each do |values|
157
+ next if !values || values.is_a?(Proc)
158
+ return values.first.class if values.is_a?(Range) || !values.empty?
159
+ end
160
+ coerce_type
161
+ end
162
+ end
163
+ end
164
+ end
@@ -4,11 +4,14 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AllOrNoneOfValidator < MultipleParamsBase
7
+ default_message_key :all_or_none
8
+
7
9
  def validate_params!(params)
8
- keys = keys_in_common(params)
9
- return if keys.empty? || keys.length == all_keys.length
10
+ known_keys = all_keys
11
+ keys = keys_in_common(params, known_keys)
12
+ return if keys.empty? || keys.length == attrs.length
10
13
 
11
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:all_or_none))
14
+ validation_error!(known_keys)
12
15
  end
13
16
  end
14
17
  end
@@ -4,15 +4,20 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AllowBlankValidator < Base
7
- def validate_param!(attr_name, params)
8
- return if (options_key?(:value) ? @option[:value] : @option) || !params.is_a?(Hash)
7
+ default_message_key :blank
8
+
9
+ def initialize(attrs, options, required, scope, opts)
10
+ super
11
+ @value = option_value
12
+ end
9
13
 
10
- value = params[attr_name]
11
- value = value.scrub if value.respond_to?(:scrub)
14
+ def validate_param!(attr_name, params)
15
+ return if @value || !hash_like?(params)
12
16
 
17
+ value = scrub(params[attr_name])
13
18
  return if value == false || value.present?
14
19
 
15
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:blank))
20
+ validation_error!(attr_name)
16
21
  end
17
22
  end
18
23
  end
@@ -4,10 +4,13 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class AtLeastOneOfValidator < MultipleParamsBase
7
+ default_message_key :at_least_one
8
+
7
9
  def validate_params!(params)
8
- return unless keys_in_common(params).empty?
10
+ known_keys = all_keys
11
+ return if hash_like?(params) && known_keys.intersect?(params.keys.map { |attr| scope.full_name(attr) })
9
12
 
10
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:at_least_one))
13
+ validation_error!(known_keys)
11
14
  end
12
15
  end
13
16
  end
@@ -3,24 +3,72 @@
3
3
  module Grape
4
4
  module Validations
5
5
  module Validators
6
+ # Base class for all parameter validators.
7
+ #
8
+ # == Freeze contract
9
+ # Validator instances are shared across requests and are frozen after
10
+ # initialization (via +.new+). All inputs (+options+, +opts+, +attrs+)
11
+ # arrive pre-frozen from the DSL boundary, so subclass ivars derived
12
+ # from them are frozen by construction. Lazy ivar assignment
13
+ # (e.g. +memoize+, <tt>||=</tt>) will raise +FrozenError+ at request time.
6
14
  class Base
15
+ extend Forwardable
16
+ extend Grape::Util::FreezeOnNew
17
+ include Grape::Util::Translation
18
+
7
19
  attr_reader :attrs
8
20
 
21
+ # +allow_blank+ / +fail_fast+ are read straight off the internal
22
+ # {Grape::Validations::SharedOptions} value object; no per-validator
23
+ # ivars. The object is built from the +opts+ Hash in #initialize, so
24
+ # the public 5th-argument contract stays a plain Hash.
25
+ def_delegators :@opts, :allow_blank, :fail_fast
26
+
27
+ alias fail_fast? fail_fast
28
+ alias allow_blank? allow_blank
29
+
30
+ class << self
31
+ # Declares the default I18n message key used by +validation_error!+.
32
+ # Subclasses that only need a single fixed error message can declare it
33
+ # at the class level instead of overriding +initialize+:
34
+ #
35
+ # class MyValidator < Grape::Validations::Validators::Base
36
+ # default_message_key :my_error
37
+ # end
38
+ #
39
+ # The key is resolved through +message+, so a per-option +:message+
40
+ # override still takes precedence.
41
+ def default_message_key(key = nil)
42
+ if key
43
+ @default_message_key = key
44
+ else
45
+ @default_message_key || (superclass.respond_to?(:default_message_key) ? superclass.default_message_key : nil)
46
+ end
47
+ end
48
+
49
+ def inherited(klass)
50
+ super
51
+ Validations.register(klass)
52
+ end
53
+ end
54
+
9
55
  # Creates a new Validator from options specified
10
56
  # by a +requires+ or +optional+ directive during
11
57
  # parameter definition.
12
58
  # @param attrs [Array] names of attributes to which the Validator applies
13
- # @param options [Object] implementation-dependent Validator options
59
+ # @param options [Object] implementation-dependent Validator options; deep-frozen on assignment
14
60
  # @param required [Boolean] attribute(s) are required or optional
15
61
  # @param scope [ParamsScope] parent scope for this Validator
16
- # @param opts [Hash] additional validation options
62
+ # @param opts [Hash] shared validator options; only +:allow_blank+ and
63
+ # +:fail_fast+ are consulted (other keys ignored, as before)
17
64
  def initialize(attrs, options, required, scope, opts)
18
- @attrs = Array(attrs)
19
- @option = options
65
+ @attrs = Array(attrs).freeze
66
+ @options = Grape::Util::DeepFreeze.deep_freeze(options)
20
67
  @required = required
21
68
  @scope = scope
22
- @fail_fast = opts[:fail_fast]
23
- @allow_blank = opts[:allow_blank]
69
+ @opts = SharedOptions.new(**opts.slice(:allow_blank, :fail_fast))
70
+ @exception_message = message(self.class.default_message_key) if self.class.default_message_key
71
+ @iterator = iterator_class.new(@attrs, @scope).freeze
24
72
  end
25
73
 
26
74
  # Validates a given request.
@@ -29,60 +77,96 @@ module Grape
29
77
  # @raise [Grape::Exceptions::Validation] if validation failed
30
78
  # @return [void]
31
79
  def validate(request)
32
- return unless @scope.should_validate?(request.params)
80
+ params = request.params
81
+ return unless scope.should_validate?(params)
33
82
 
34
- validate!(request.params)
83
+ validate!(params)
35
84
  end
36
85
 
37
86
  # Validates a given parameter hash.
38
- # @note Override #validate if you need to access the entire request.
87
+ # @note Override #validate_param! for per-parameter validation,
88
+ # or #validate if you need access to the entire request.
39
89
  # @param params [Hash] parameters to validate
40
90
  # @raise [Grape::Exceptions::Validation] if validation failed
41
91
  # @return [void]
42
92
  def validate!(params)
43
- attributes = SingleAttributeIterator.new(self, @scope, params)
44
93
  # we collect errors inside array because
45
94
  # there may be more than one error per field
46
- array_errors = []
95
+ array_errors = nil
47
96
 
48
- attributes.each do |val, attr_name, empty_val|
49
- next if !@scope.required? && empty_val
50
- next unless @scope.meets_dependency?(val, params)
97
+ @iterator.each(params) do |val, attr_name, empty_val|
98
+ next if !scope.required? && empty_val
99
+ next unless scope.meets_dependency?(val, params)
51
100
 
52
- validate_param!(attr_name, val) if @required || val.try(:key?, attr_name)
101
+ validate_param!(attr_name, val) if required? || (hash_like?(val) && val.key?(attr_name))
53
102
  rescue Grape::Exceptions::Validation => e
54
- array_errors << e
103
+ (array_errors ||= []) << e
55
104
  end
56
105
 
57
- raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
106
+ raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors
58
107
  end
59
108
 
60
- def self.inherited(klass)
61
- super
62
- Validations.register(klass)
109
+ protected
110
+
111
+ # Validates a single attribute. Override in subclasses.
112
+ # @param attr_name [Symbol, String] the attribute name
113
+ # @param params [Hash] the parameter hash containing the attribute
114
+ # @raise [Grape::Exceptions::Validation] if validation failed
115
+ # @return [void]
116
+ def validate_param!(attr_name, params)
117
+ raise NotImplementedError
63
118
  end
64
119
 
120
+ private
121
+
122
+ attr_reader :options, :scope, :required, :exception_message
123
+
124
+ alias required? required
125
+
126
+ # The AttributesIterator subclass used to walk this validator's
127
+ # attributes. Built once in #initialize and reused across requests.
128
+ def iterator_class
129
+ SingleAttributeIterator
130
+ end
131
+
132
+ def validation_error!(attr_name_or_params, message = exception_message)
133
+ params = attr_name_or_params.is_a?(Array) ? attr_name_or_params : scope.full_name(attr_name_or_params)
134
+ raise Grape::Exceptions::Validation.new(params:, message:)
135
+ end
136
+
137
+ def hash_like?(obj)
138
+ obj.respond_to?(:key?)
139
+ end
140
+
141
+ def options_key?(key, given_options = nil)
142
+ current_options = given_options || options
143
+ hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
144
+ end
145
+
146
+ # Returns the effective message for a validation error.
147
+ # Prefers an explicit +:message+ option, then +default_key+.
148
+ # If both are nil, the block (if given) is called to compute a fallback —
149
+ # useful for validators that build a message Hash for deferred i18n interpolation.
150
+ # @example
151
+ # @exception_message = message(:presence) # symbol key or custom message
152
+ # @exception_message = message { build_hash_message } # computed fallback
65
153
  def message(default_key = nil)
66
- options = instance_variable_get(:@option)
67
- options_key?(:message) ? options[:message] : default_key
154
+ key = options_key?(:message) ? options[:message] : default_key
155
+ return key unless key.nil?
156
+
157
+ yield if block_given?
68
158
  end
69
159
 
70
- def options_key?(key, options = nil)
71
- options = instance_variable_get(:@option) if options.nil?
72
- options.try(:key?, key) && !options[key].nil?
160
+ def option_value
161
+ options_key?(:value) ? options[:value] : options
73
162
  end
74
163
 
75
- def fail_fast?
76
- @fail_fast
164
+ def scrub(value)
165
+ return value if !value.respond_to?(:valid_encoding?) || value.valid_encoding?
166
+
167
+ value.scrub
77
168
  end
78
169
  end
79
170
  end
80
171
  end
81
172
  end
82
-
83
- Grape::Validations::Base = Class.new(Grape::Validations::Validators::Base) do
84
- def self.inherited(*)
85
- Grape.deprecator.warn 'Grape::Validations::Base is deprecated! Use Grape::Validations::Validators::Base instead.'
86
- super
87
- end
88
- end
@@ -4,22 +4,35 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class CoerceValidator < Base
7
+ default_message_key :coerce
8
+
7
9
  def initialize(attrs, options, required, scope, opts)
8
10
  super
9
11
 
10
- @converter = if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
11
- type
12
- else
13
- Types.build_coercer(type, method: @option[:method])
14
- end
12
+ @exception_message = options.message if options.message
13
+
14
+ raw_type = options.type
15
+ type = hash_like?(raw_type) ? raw_type[:value] : raw_type
16
+ @converter =
17
+ if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
18
+ type
19
+ else
20
+ Types.build_coercer(type, method: options.coerce_method)
21
+ end
15
22
  end
16
23
 
24
+ # The attribute is read once and held: +coerce_value+ only ever sees the
25
+ # value, never the Hash, so the two further reads the identity check
26
+ # used to make came back with the same object -- through a
27
+ # HashWithIndifferentAccess key conversion each time, for every coerced
28
+ # attribute of every request.
17
29
  def validate_param!(attr_name, params)
18
- raise validation_exception(attr_name) unless params.is_a? Hash
30
+ validation_error!(attr_name) unless hash_like?(params)
19
31
 
20
- new_value = coerce_value(params[attr_name])
32
+ value = params[attr_name]
33
+ new_value = coerce_value(value)
21
34
 
22
- raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value)
35
+ validation_error!(attr_name, new_value.message || exception_message) if new_value.is_a?(Types::InvalidValue)
23
36
 
24
37
  # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
25
38
  # which looses wrappers for hashes and arrays after reassigning values
@@ -30,45 +43,20 @@ module Grape
30
43
  # h[:list] = list
31
44
  # h
32
45
  # => #<Hashie::Mash list=[1, 2, 3, 4]>
33
- return if params[attr_name].instance_of?(new_value.class) && params[attr_name] == new_value
46
+ return if value.instance_of?(new_value.class) && value == new_value
34
47
 
35
48
  params[attr_name] = new_value
36
49
  end
37
50
 
38
51
  private
39
52
 
40
- # @!attribute [r] converter
41
- # Object that will be used for parameter coercion and type checking.
42
- #
43
- # See {Types.build_coercer}
44
- #
45
- # @return [Object]
46
- attr_reader :converter
47
-
48
- def valid_type?(val)
49
- !val.is_a?(Types::InvalidValue)
50
- end
51
-
53
+ # Calls the converter built at definition time.
54
+ # Custom coercers may raise; any StandardError is treated as an invalid value.
52
55
  def coerce_value(val)
53
- converter.call(val)
54
- # Some custom types might fail, so it should be treated as an invalid value
56
+ @converter.call(val)
55
57
  rescue StandardError
56
58
  Types::InvalidValue.new
57
59
  end
58
-
59
- # Type to which the parameter will be coerced.
60
- #
61
- # @return [Class]
62
- def type
63
- @option[:type].is_a?(Hash) ? @option[:type][:value] : @option[:type]
64
- end
65
-
66
- def validation_exception(attr_name, custom_msg = nil)
67
- Grape::Exceptions::Validation.new(
68
- params: [@scope.full_name(attr_name)],
69
- message: custom_msg || message(:coerce)
70
- )
71
- end
72
60
  end
73
61
  end
74
62
  end
@@ -3,12 +3,13 @@
3
3
  module Grape
4
4
  module Validations
5
5
  module Validators
6
- class ContractScopeValidator < Base
6
+ class ContractScopeValidator
7
+ extend Grape::Util::FreezeOnNew
8
+
7
9
  attr_reader :schema
8
10
 
9
- def initialize(_attrs, _options, _required, _scope, opts)
10
- super
11
- @schema = opts.fetch(:schema)
11
+ def initialize(schema:)
12
+ @schema = schema
12
13
  end
13
14
 
14
15
  # Validates a given request.
@@ -26,13 +27,17 @@ module Grape
26
27
  raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages))
27
28
  end
28
29
 
30
+ def fail_fast?
31
+ false
32
+ end
33
+
29
34
  private
30
35
 
31
36
  def build_errors_from_messages(messages)
32
37
  messages.map do |message|
33
38
  full_name = message.path.first.to_s
34
39
  full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1
35
- Grape::Exceptions::Validation.new(params: [full_name], message: message.text)
40
+ Grape::Exceptions::Validation.new(params: full_name, message: message.text)
36
41
  end
37
42
  end
38
43
  end
@@ -4,31 +4,24 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class DefaultValidator < Base
7
- def initialize(attrs, options, required, scope, opts = {})
8
- @default = options
7
+ def initialize(attrs, options, required, scope, opts)
9
8
  super
10
- end
11
-
12
- def validate_param!(attr_name, params)
13
- params[attr_name] = if @default.is_a? Proc
14
- if @default.parameters.empty?
15
- @default.call
16
- else
17
- @default.call(params)
18
- end
19
- elsif @default.frozen? || !@default.duplicable?
20
- @default
21
- else
22
- @default.dup
23
- end
9
+ # !important, lazy call at runtime
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 }
15
+ else
16
+ proc { options }
17
+ end
24
18
  end
25
19
 
26
20
  def validate!(params)
27
- attrs = SingleAttributeIterator.new(self, @scope, params)
28
- attrs.each do |resource_params, attr_name|
29
- 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)
30
23
 
31
- validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
24
+ resource_params[attr_name] = @default_call.call(resource_params) if hash_like?(resource_params) && resource_params[attr_name].nil?
32
25
  end
33
26
  end
34
27
  end
@@ -4,12 +4,19 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class ExactlyOneOfValidator < MultipleParamsBase
7
+ def initialize(attrs, options, required, scope, opts)
8
+ super
9
+ @exactly_one_exception_message = message(:exactly_one)
10
+ @mutual_exclusion_exception_message = message(:mutual_exclusion)
11
+ end
12
+
7
13
  def validate_params!(params)
8
- keys = keys_in_common(params)
14
+ known_keys = all_keys
15
+ keys = keys_in_common(params, known_keys)
9
16
  return if keys.length == 1
10
- raise Grape::Exceptions::Validation.new(params: all_keys, message: message(:exactly_one)) if keys.empty?
11
17
 
12
- raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
18
+ validation_error!(known_keys, @exactly_one_exception_message) if keys.empty?
19
+ validation_error!(keys, @mutual_exclusion_exception_message)
13
20
  end
14
21
  end
15
22
  end
@@ -4,19 +4,29 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class ExceptValuesValidator < Base
7
+ default_message_key :except_values
8
+
7
9
  def initialize(attrs, options, required, scope, opts)
8
- @except = options.is_a?(Hash) ? options[:value] : options
9
10
  super
11
+ except = option_value
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?
14
+
15
+ # Zero-arity procs (e.g. -> { User.pluck(:role) }) must be called per-request,
16
+ # not at definition time, so they are wrapped in a lambda to defer execution.
17
+ @excepts_call = except_proc ? except : -> { except }
10
18
  end
11
19
 
12
20
  def validate_param!(attr_name, params)
13
- return unless params.try(:key?, attr_name)
21
+ return unless hash_like?(params) && params.key?(attr_name)
14
22
 
15
- excepts = @except.is_a?(Proc) ? @except.call : @except
23
+ excepts = @excepts_call.call
16
24
  return if excepts.nil?
17
25
 
18
26
  param_array = params[attr_name].nil? ? [nil] : Array.wrap(params[attr_name])
19
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:except_values)) if param_array.any? { |param| excepts.include?(param) }
27
+ return if param_array.none? { |param| excepts.include?(param) }
28
+
29
+ validation_error!(attr_name)
20
30
  end
21
31
  end
22
32
  end