grape 2.4.0 → 3.2.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 (119) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +109 -0
  3. data/CONTRIBUTING.md +2 -10
  4. data/README.md +137 -181
  5. data/UPGRADING.md +154 -0
  6. data/grape.gemspec +4 -4
  7. data/lib/grape/api/instance.rb +51 -104
  8. data/lib/grape/api.rb +29 -42
  9. data/lib/grape/content_types.rb +1 -4
  10. data/lib/grape/declared_params_handler.rb +118 -0
  11. data/lib/grape/dry_types.rb +48 -4
  12. data/lib/grape/dsl/callbacks.rb +8 -58
  13. data/lib/grape/dsl/declared.rb +35 -0
  14. data/lib/grape/dsl/desc.rb +8 -67
  15. data/lib/grape/dsl/helpers.rb +59 -64
  16. data/lib/grape/dsl/inside_route.rb +28 -189
  17. data/lib/grape/dsl/logger.rb +3 -6
  18. data/lib/grape/dsl/middleware.rb +22 -40
  19. data/lib/grape/dsl/parameters.rb +24 -51
  20. data/lib/grape/dsl/request_response.rb +136 -139
  21. data/lib/grape/dsl/routing.rb +240 -200
  22. data/lib/grape/dsl/settings.rb +23 -135
  23. data/lib/grape/dsl/validations.rb +38 -44
  24. data/lib/grape/endpoint.rb +169 -205
  25. data/lib/grape/error_formatter/base.rb +4 -2
  26. data/lib/grape/exceptions/base.rb +19 -45
  27. data/lib/grape/exceptions/incompatible_option_values.rb +1 -1
  28. data/lib/grape/exceptions/invalid_accept_header.rb +1 -1
  29. data/lib/grape/exceptions/invalid_formatter.rb +1 -1
  30. data/lib/grape/exceptions/invalid_message_body.rb +1 -1
  31. data/lib/grape/exceptions/invalid_version_header.rb +1 -1
  32. data/lib/grape/exceptions/invalid_versioner_option.rb +1 -1
  33. data/lib/grape/exceptions/method_not_allowed.rb +1 -1
  34. data/lib/grape/exceptions/missing_group_type.rb +0 -2
  35. data/lib/grape/exceptions/missing_mime_type.rb +1 -1
  36. data/lib/grape/exceptions/request_error.rb +11 -0
  37. data/lib/grape/exceptions/unknown_auth_strategy.rb +1 -1
  38. data/lib/grape/exceptions/unknown_parameter.rb +1 -1
  39. data/lib/grape/exceptions/unknown_params_builder.rb +1 -1
  40. data/lib/grape/exceptions/unknown_validator.rb +1 -1
  41. data/lib/grape/exceptions/unsupported_group_type.rb +0 -2
  42. data/lib/grape/exceptions/validation.rb +7 -4
  43. data/lib/grape/exceptions/validation_errors.rb +13 -7
  44. data/lib/grape/locale/en.yml +0 -5
  45. data/lib/grape/middleware/auth/base.rb +2 -0
  46. data/lib/grape/middleware/auth/dsl.rb +9 -10
  47. data/lib/grape/middleware/base.rb +6 -4
  48. data/lib/grape/middleware/error.rb +4 -14
  49. data/lib/grape/middleware/formatter.rb +11 -7
  50. data/lib/grape/middleware/stack.rb +3 -2
  51. data/lib/grape/middleware/versioner/accept_version_header.rb +3 -3
  52. data/lib/grape/middleware/versioner/base.rb +42 -40
  53. data/lib/grape/middleware/versioner/header.rb +2 -18
  54. data/lib/grape/middleware/versioner/param.rb +2 -2
  55. data/lib/grape/middleware/versioner/path.rb +2 -2
  56. data/lib/grape/namespace.rb +15 -8
  57. data/lib/grape/params_builder/base.rb +2 -0
  58. data/lib/grape/params_builder.rb +2 -19
  59. data/lib/grape/request.rb +2 -10
  60. data/lib/grape/router/base_route.rb +14 -5
  61. data/lib/grape/router/greedy_route.rb +11 -5
  62. data/lib/grape/router/pattern.rb +6 -20
  63. data/lib/grape/router/route.rb +7 -11
  64. data/lib/grape/router.rb +42 -65
  65. data/lib/grape/util/api_description.rb +58 -0
  66. data/lib/grape/util/base_inheritable.rb +5 -2
  67. data/lib/grape/util/cache.rb +1 -0
  68. data/lib/grape/util/deep_freeze.rb +35 -0
  69. data/lib/grape/util/inheritable_setting.rb +8 -1
  70. data/lib/grape/util/media_type.rb +2 -2
  71. data/lib/grape/util/registry.rb +1 -1
  72. data/lib/grape/util/translation.rb +42 -0
  73. data/lib/grape/validations/attributes_iterator.rb +35 -20
  74. data/lib/grape/validations/contract_scope.rb +2 -8
  75. data/lib/grape/validations/multiple_attributes_iterator.rb +1 -1
  76. data/lib/grape/validations/param_scope_tracker.rb +57 -0
  77. data/lib/grape/validations/params_documentation.rb +50 -0
  78. data/lib/grape/validations/params_scope.rb +147 -156
  79. data/lib/grape/validations/single_attribute_iterator.rb +2 -2
  80. data/lib/grape/validations/types/array_coercer.rb +2 -3
  81. data/lib/grape/validations/types/dry_type_coercer.rb +4 -11
  82. data/lib/grape/validations/types/primitive_coercer.rb +1 -28
  83. data/lib/grape/validations/types.rb +10 -25
  84. data/lib/grape/validations/validators/all_or_none_of_validator.rb +6 -3
  85. data/lib/grape/validations/validators/allow_blank_validator.rb +10 -5
  86. data/lib/grape/validations/validators/at_least_one_of_validator.rb +5 -2
  87. data/lib/grape/validations/validators/base.rb +95 -25
  88. data/lib/grape/validations/validators/coerce_validator.rb +15 -35
  89. data/lib/grape/validations/validators/contract_scope_validator.rb +9 -5
  90. data/lib/grape/validations/validators/default_validator.rb +12 -18
  91. data/lib/grape/validations/validators/exactly_one_of_validator.rb +10 -3
  92. data/lib/grape/validations/validators/except_values_validator.rb +13 -4
  93. data/lib/grape/validations/validators/length_validator.rb +21 -22
  94. data/lib/grape/validations/validators/multiple_params_base.rb +5 -5
  95. data/lib/grape/validations/validators/{mutual_exclusion_validator.rb → mutually_exclusive_validator.rb} +4 -2
  96. data/lib/grape/validations/validators/presence_validator.rb +4 -2
  97. data/lib/grape/validations/validators/regexp_validator.rb +11 -3
  98. data/lib/grape/validations/validators/same_as_validator.rb +6 -15
  99. data/lib/grape/validations/validators/values_validator.rb +29 -21
  100. data/lib/grape/version.rb +1 -1
  101. data/lib/grape.rb +29 -23
  102. metadata +31 -26
  103. data/lib/grape/api/helpers.rb +0 -9
  104. data/lib/grape/dsl/api.rb +0 -17
  105. data/lib/grape/dsl/configuration.rb +0 -15
  106. data/lib/grape/exceptions/conflicting_types.rb +0 -11
  107. data/lib/grape/exceptions/empty_message_body.rb +0 -11
  108. data/lib/grape/exceptions/invalid_parameters.rb +0 -11
  109. data/lib/grape/exceptions/missing_option.rb +0 -11
  110. data/lib/grape/exceptions/too_deep_parameters.rb +0 -11
  111. data/lib/grape/exceptions/too_many_multipart_files.rb +0 -11
  112. data/lib/grape/exceptions/unknown_options.rb +0 -11
  113. data/lib/grape/extensions/active_support/hash_with_indifferent_access.rb +0 -24
  114. data/lib/grape/extensions/hash.rb +0 -27
  115. data/lib/grape/extensions/hashie/mash.rb +0 -24
  116. data/lib/grape/types/invalid_value.rb +0 -8
  117. data/lib/grape/util/strict_hash_configuration.rb +0 -108
  118. data/lib/grape/validations/attributes_doc.rb +0 -60
  119. data/lib/grape/validations/validator_factory.rb +0 -15
@@ -155,9 +155,10 @@ module Grape
155
155
  # @return [Object] object to be used
156
156
  # for coercion and type validation
157
157
  def build_coercer(type, method: nil, strict: false)
158
- cache_instance(type, method, strict) do
159
- create_coercer_instance(type, method, strict)
160
- end
158
+ # no cache since unique
159
+ return create_coercer_instance(type, method, strict) if method.respond_to?(:call)
160
+
161
+ CoercerCache[[type, method, strict]]
161
162
  end
162
163
 
163
164
  def create_coercer_instance(type, method, strict)
@@ -184,30 +185,14 @@ module Grape
184
185
  end
185
186
  end
186
187
 
187
- def cache_instance(type, method, strict, &_block)
188
- key = cache_key(type, method, strict)
189
-
190
- return @__cache[key] if @__cache.key?(key)
191
-
192
- instance = yield
193
-
194
- @__cache_write_lock.synchronize do
195
- @__cache[key] = instance
188
+ class CoercerCache < Grape::Util::Cache
189
+ def initialize
190
+ super
191
+ @cache = Hash.new do |h, (type, method, strict)|
192
+ h[[type, method, strict]] = Grape::Validations::Types.create_coercer_instance(type, method, strict)
193
+ end
196
194
  end
197
-
198
- instance
199
195
  end
200
-
201
- def cache_key(type, method, strict)
202
- [type, method, strict].each_with_object(+'_') do |val, memo|
203
- next if val.nil?
204
-
205
- memo << '_' << val.to_s
206
- end
207
- end
208
-
209
- instance_variable_set(:@__cache, {})
210
- instance_variable_set(:@__cache_write_lock, Mutex.new)
211
196
  end
212
197
  end
213
198
  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,64 @@
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
+ include Grape::Util::Translation
16
+
7
17
  attr_reader :attrs
8
18
 
19
+ class << self
20
+ # Declares the default I18n message key used by +validation_error!+.
21
+ # Subclasses that only need a single fixed error message can declare it
22
+ # at the class level instead of overriding +initialize+:
23
+ #
24
+ # class MyValidator < Grape::Validations::Validators::Base
25
+ # default_message_key :my_error
26
+ # end
27
+ #
28
+ # The key is resolved through +message+, so a per-option +:message+
29
+ # override still takes precedence.
30
+ def default_message_key(key = nil)
31
+ if key
32
+ @default_message_key = key
33
+ else
34
+ @default_message_key || (superclass.respond_to?(:default_message_key) ? superclass.default_message_key : nil)
35
+ end
36
+ end
37
+
38
+ def new(...)
39
+ super.freeze
40
+ end
41
+
42
+ def inherited(klass)
43
+ super
44
+ Validations.register(klass)
45
+ end
46
+ end
47
+
9
48
  # Creates a new Validator from options specified
10
49
  # by a +requires+ or +optional+ directive during
11
50
  # parameter definition.
12
51
  # @param attrs [Array] names of attributes to which the Validator applies
13
- # @param options [Object] implementation-dependent Validator options
52
+ # @param options [Object] implementation-dependent Validator options; deep-frozen on assignment
14
53
  # @param required [Boolean] attribute(s) are required or optional
15
54
  # @param scope [ParamsScope] parent scope for this Validator
16
55
  # @param opts [Hash] additional validation options
17
56
  def initialize(attrs, options, required, scope, opts)
18
- @attrs = Array(attrs)
19
- @option = options
57
+ @attrs = Array(attrs).freeze
58
+ @options = Grape::Util::DeepFreeze.deep_freeze(options)
59
+ @option = @options # TODO: remove in next major release
20
60
  @required = required
21
61
  @scope = scope
22
- @fail_fast = opts[:fail_fast]
23
- @allow_blank = opts[:allow_blank]
62
+ @fail_fast, @allow_blank = opts.values_at(:fail_fast, :allow_blank)
63
+ @exception_message = message(self.class.default_message_key) if self.class.default_message_key
24
64
  end
25
65
 
26
66
  # Validates a given request.
@@ -34,13 +74,18 @@ module Grape
34
74
  validate!(request.params)
35
75
  end
36
76
 
77
+ def fail_fast?
78
+ @fail_fast
79
+ end
80
+
37
81
  # Validates a given parameter hash.
38
- # @note Override #validate if you need to access the entire request.
82
+ # @note Override #validate_param! for per-parameter validation,
83
+ # or #validate if you need access to the entire request.
39
84
  # @param params [Hash] parameters to validate
40
85
  # @raise [Grape::Exceptions::Validation] if validation failed
41
86
  # @return [void]
42
87
  def validate!(params)
43
- attributes = SingleAttributeIterator.new(self, @scope, params)
88
+ attributes = SingleAttributeIterator.new(@attrs, @scope, params)
44
89
  # we collect errors inside array because
45
90
  # there may be more than one error per field
46
91
  array_errors = []
@@ -49,7 +94,7 @@ module Grape
49
94
  next if !@scope.required? && empty_val
50
95
  next unless @scope.meets_dependency?(val, params)
51
96
 
52
- validate_param!(attr_name, val) if @required || val.try(:key?, attr_name)
97
+ validate_param!(attr_name, val) if @required || (hash_like?(val) && val.key?(attr_name))
53
98
  rescue Grape::Exceptions::Validation => e
54
99
  array_errors << e
55
100
  end
@@ -57,32 +102,57 @@ module Grape
57
102
  raise Grape::Exceptions::ValidationArrayErrors.new(array_errors) if array_errors.any?
58
103
  end
59
104
 
60
- def self.inherited(klass)
61
- super
62
- Validations.register(klass)
105
+ protected
106
+
107
+ # Validates a single attribute. Override in subclasses.
108
+ # @param attr_name [Symbol, String] the attribute name
109
+ # @param params [Hash] the parameter hash containing the attribute
110
+ # @raise [Grape::Exceptions::Validation] if validation failed
111
+ # @return [void]
112
+ def validate_param!(attr_name, params)
113
+ raise NotImplementedError
63
114
  end
64
115
 
65
- def message(default_key = nil)
66
- options = instance_variable_get(:@option)
67
- options_key?(:message) ? options[:message] : default_key
116
+ private
117
+
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)
120
+ raise Grape::Exceptions::Validation.new(params:, message:)
121
+ end
122
+
123
+ def hash_like?(obj)
124
+ obj.respond_to?(:key?)
68
125
  end
69
126
 
70
127
  def options_key?(key, options = nil)
71
- options = instance_variable_get(:@option) if options.nil?
72
- options.try(:key?, key) && !options[key].nil?
128
+ current_options = options || @options
129
+ hash_like?(current_options) && current_options.key?(key) && !current_options[key].nil?
73
130
  end
74
131
 
75
- def fail_fast?
76
- @fail_fast
132
+ # Returns the effective message for a validation error.
133
+ # Prefers an explicit +:message+ option, then +default_key+.
134
+ # If both are nil, the block (if given) is called to compute a fallback —
135
+ # useful for validators that build a message Hash for deferred i18n interpolation.
136
+ # @example
137
+ # @exception_message = message(:presence) # symbol key or custom message
138
+ # @exception_message = message { build_hash_message } # computed fallback
139
+ def message(default_key = nil)
140
+ key = options_key?(:message) ? @options[:message] : default_key
141
+ return key unless key.nil?
142
+
143
+ yield if block_given?
144
+ end
145
+
146
+ def option_value
147
+ options_key?(:value) ? @options[:value] : @options
148
+ end
149
+
150
+ def scrub(value)
151
+ return value unless value.respond_to?(:valid_encoding?) && !value.valid_encoding?
152
+
153
+ value.scrub
77
154
  end
78
155
  end
79
156
  end
80
157
  end
81
158
  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,27 @@ 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
+ raw_type = @options[:type]
13
+ type = hash_like?(raw_type) ? raw_type[:value] : raw_type
14
+ @converter =
15
+ if type.is_a?(Grape::Validations::Types::VariantCollectionCoercer)
16
+ type
17
+ else
18
+ Types.build_coercer(type, method: @options[:method])
19
+ end
15
20
  end
16
21
 
17
22
  def validate_param!(attr_name, params)
18
- raise validation_exception(attr_name) unless params.is_a? Hash
23
+ validation_error!(attr_name) unless hash_like?(params)
19
24
 
20
25
  new_value = coerce_value(params[attr_name])
21
26
 
22
- raise validation_exception(attr_name, new_value.message) unless valid_type?(new_value)
27
+ validation_error!(attr_name, new_value.message || @exception_message) if new_value.is_a?(Types::InvalidValue)
23
28
 
24
29
  # Don't assign a value if it is identical. It fixes a problem with Hashie::Mash
25
30
  # which looses wrappers for hashes and arrays after reassigning values
@@ -37,38 +42,13 @@ module Grape
37
42
 
38
43
  private
39
44
 
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
-
45
+ # Calls the converter built at definition time.
46
+ # Custom coercers may raise; any StandardError is treated as an invalid value.
52
47
  def coerce_value(val)
53
- converter.call(val)
54
- # Some custom types might fail, so it should be treated as an invalid value
48
+ @converter.call(val)
55
49
  rescue StandardError
56
50
  Types::InvalidValue.new
57
51
  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
52
  end
73
53
  end
74
54
  end
@@ -3,12 +3,12 @@
3
3
  module Grape
4
4
  module Validations
5
5
  module Validators
6
- class ContractScopeValidator < Base
6
+ class ContractScopeValidator
7
7
  attr_reader :schema
8
8
 
9
- def initialize(_attrs, _options, _required, _scope, opts)
10
- super
11
- @schema = opts.fetch(:schema)
9
+ def initialize(schema:)
10
+ @schema = schema
11
+ freeze
12
12
  end
13
13
 
14
14
  # Validates a given request.
@@ -26,13 +26,17 @@ module Grape
26
26
  raise Grape::Exceptions::ValidationArrayErrors.new(build_errors_from_messages(res.errors.messages))
27
27
  end
28
28
 
29
+ def fail_fast?
30
+ false
31
+ end
32
+
29
33
  private
30
34
 
31
35
  def build_errors_from_messages(messages)
32
36
  messages.map do |message|
33
37
  full_name = message.path.first.to_s
34
38
  full_name << "[#{message.path[1..].join('][')}]" if message.path.size > 1
35
- Grape::Exceptions::Validation.new(params: [full_name], message: message.text)
39
+ Grape::Exceptions::Validation.new(params: full_name, message: message.text)
36
40
  end
37
41
  end
38
42
  end
@@ -4,31 +4,25 @@ 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)
21
+ attrs = SingleAttributeIterator.new(@attrs, @scope, params)
28
22
  attrs.each do |resource_params, attr_name|
29
23
  next unless @scope.meets_dependency?(resource_params, params)
30
24
 
31
- validate_param!(attr_name, resource_params) if resource_params.is_a?(Hash) && resource_params[attr_name].nil?
25
+ resource_params[attr_name] = @default_call.call(resource_params) if hash_like?(resource_params) && resource_params[attr_name].nil?
32
26
  end
33
27
  end
34
28
  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,28 @@ 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
+ 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?
13
+
14
+ # Zero-arity procs (e.g. -> { User.pluck(:role) }) must be called per-request,
15
+ # not at definition time, so they are wrapped in a lambda to defer execution.
16
+ @excepts_call = except.is_a?(Proc) ? except : -> { except }
10
17
  end
11
18
 
12
19
  def validate_param!(attr_name, params)
13
- return unless params.try(:key?, attr_name)
20
+ return unless hash_like?(params) && params.key?(attr_name)
14
21
 
15
- excepts = @except.is_a?(Proc) ? @except.call : @except
22
+ excepts = @excepts_call.call
16
23
  return if excepts.nil?
17
24
 
18
25
  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) }
26
+ return if param_array.none? { |param| excepts.include?(param) }
27
+
28
+ validation_error!(attr_name)
20
29
  end
21
30
  end
22
31
  end
@@ -5,19 +5,16 @@ module Grape
5
5
  module Validators
6
6
  class LengthValidator < Base
7
7
  def initialize(attrs, options, required, scope, opts)
8
- @min = options[:min]
9
- @max = options[:max]
10
- @is = options[:is]
11
-
12
8
  super
13
9
 
14
- raise ArgumentError, 'min must be an integer greater than or equal to zero' if !@min.nil? && (!@min.is_a?(Integer) || @min.negative?)
15
- raise ArgumentError, 'max must be an integer greater than or equal to zero' if !@max.nil? && (!@max.is_a?(Integer) || @max.negative?)
16
- raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if !@min.nil? && !@max.nil? && @min > @max
10
+ @min, @max, @is = @options.values_at(:min, :max, :is)
11
+ validate_boundary!(:min, @min)
12
+ validate_boundary!(:max, @max)
13
+ raise ArgumentError, "min #{@min} cannot be greater than max #{@max}" if @min && @max && @min > @max
17
14
 
18
15
  return if @is.nil?
19
- raise ArgumentError, 'is must be an integer greater than zero' if !@is.is_a?(Integer) || !@is.positive?
20
- raise ArgumentError, 'is cannot be combined with min or max' if !@min.nil? || !@max.nil?
16
+ raise ArgumentError, 'is must be an integer greater than zero' unless @is.is_a?(Integer) && @is.positive?
17
+ raise ArgumentError, 'is cannot be combined with min or max' unless @min.nil? && @max.nil?
21
18
  end
22
19
 
23
20
  def validate_param!(attr_name, params)
@@ -27,21 +24,23 @@ module Grape
27
24
 
28
25
  return unless (!@min.nil? && param.length < @min) || (!@max.nil? && param.length > @max) || (!@is.nil? && param.length != @is)
29
26
 
30
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: build_message)
27
+ validation_error!(attr_name, message do
28
+ if @min && @max
29
+ translate(:length, min: @min, max: @max)
30
+ elsif @min
31
+ translate(:length_min, min: @min)
32
+ elsif @max
33
+ translate(:length_max, max: @max)
34
+ else
35
+ translate(:length_is, is: @is)
36
+ end
37
+ end)
31
38
  end
32
39
 
33
- def build_message
34
- if options_key?(:message)
35
- @option[:message]
36
- elsif @min && @max
37
- format I18n.t(:length, scope: 'grape.errors.messages'), min: @min, max: @max
38
- elsif @min
39
- format I18n.t(:length_min, scope: 'grape.errors.messages'), min: @min
40
- elsif @max
41
- format I18n.t(:length_max, scope: 'grape.errors.messages'), max: @max
42
- else
43
- format I18n.t(:length_is, scope: 'grape.errors.messages'), is: @is
44
- end
40
+ private
41
+
42
+ def validate_boundary!(name, val)
43
+ raise ArgumentError, "#{name} must be an integer greater than or equal to zero" if !val.nil? && (!val.is_a?(Integer) || val.negative?)
45
44
  end
46
45
  end
47
46
  end
@@ -5,7 +5,7 @@ module Grape
5
5
  module Validators
6
6
  class MultipleParamsBase < Base
7
7
  def validate!(params)
8
- attributes = MultipleAttributesIterator.new(self, @scope, params)
8
+ attributes = MultipleAttributesIterator.new(@attrs, @scope, params)
9
9
  array_errors = []
10
10
 
11
11
  attributes.each do |resource_params|
@@ -19,14 +19,14 @@ module Grape
19
19
 
20
20
  private
21
21
 
22
- def keys_in_common(resource_params)
23
- return [] unless resource_params.is_a?(Hash)
22
+ def keys_in_common(resource_params, known_keys = all_keys)
23
+ return [] unless hash_like?(resource_params)
24
24
 
25
- all_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) }
25
+ known_keys & resource_params.keys.map! { |attr| @scope.full_name(attr) }
26
26
  end
27
27
 
28
28
  def all_keys
29
- attrs.map { |attr| @scope.full_name(attr) }
29
+ @attrs.map { |attr| @scope.full_name(attr) }
30
30
  end
31
31
  end
32
32
  end
@@ -3,12 +3,14 @@
3
3
  module Grape
4
4
  module Validations
5
5
  module Validators
6
- class MutualExclusionValidator < MultipleParamsBase
6
+ class MutuallyExclusiveValidator < MultipleParamsBase
7
+ default_message_key :mutual_exclusion
8
+
7
9
  def validate_params!(params)
8
10
  keys = keys_in_common(params)
9
11
  return if keys.length <= 1
10
12
 
11
- raise Grape::Exceptions::Validation.new(params: keys, message: message(:mutual_exclusion))
13
+ validation_error!(keys)
12
14
  end
13
15
  end
14
16
  end
@@ -4,10 +4,12 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class PresenceValidator < Base
7
+ default_message_key :presence
8
+
7
9
  def validate_param!(attr_name, params)
8
- return if params.try(:key?, attr_name)
10
+ return if hash_like?(params) && params.key?(attr_name)
9
11
 
10
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:presence))
12
+ validation_error!(attr_name)
11
13
  end
12
14
  end
13
15
  end
@@ -4,11 +4,19 @@ module Grape
4
4
  module Validations
5
5
  module Validators
6
6
  class RegexpValidator < Base
7
+ default_message_key :regexp
8
+
9
+ def initialize(attrs, options, required, scope, opts)
10
+ super
11
+ @value = option_value
12
+ end
13
+
7
14
  def validate_param!(attr_name, params)
8
- return unless params.try(:key?, attr_name)
9
- return if Array.wrap(params[attr_name]).all? { |param| param.nil? || param.to_s.scrub.match?((options_key?(:value) ? @option[:value] : @option)) }
15
+ return unless hash_like?(params) && params.key?(attr_name)
16
+
17
+ return if Array.wrap(params[attr_name]).all? { |param| param.nil? || scrub(param.to_s).match?(@value) }
10
18
 
11
- raise Grape::Exceptions::Validation.new(params: [@scope.full_name(attr_name)], message: message(:regexp))
19
+ validation_error!(attr_name)
12
20
  end
13
21
  end
14
22
  end