omg-activemodel 8.0.0.alpha1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +67 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +266 -0
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +99 -0
- data/lib/active_model/attribute/user_provided_default.rb +55 -0
- data/lib/active_model/attribute.rb +277 -0
- data/lib/active_model/attribute_assignment.rb +78 -0
- data/lib/active_model/attribute_methods.rb +592 -0
- data/lib/active_model/attribute_mutation_tracker.rb +189 -0
- data/lib/active_model/attribute_registration.rb +117 -0
- data/lib/active_model/attribute_set/builder.rb +182 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attribute_set.rb +118 -0
- data/lib/active_model/attributes.rb +165 -0
- data/lib/active_model/callbacks.rb +155 -0
- data/lib/active_model/conversion.rb +121 -0
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +416 -0
- data/lib/active_model/error.rb +208 -0
- data/lib/active_model/errors.rb +547 -0
- data/lib/active_model/forbidden_attributes_protection.rb +33 -0
- data/lib/active_model/gem_version.rb +17 -0
- data/lib/active_model/lint.rb +118 -0
- data/lib/active_model/locale/en.yml +38 -0
- data/lib/active_model/model.rb +78 -0
- data/lib/active_model/naming.rb +359 -0
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +24 -0
- data/lib/active_model/secure_password.rb +231 -0
- data/lib/active_model/serialization.rb +198 -0
- data/lib/active_model/serializers/json.rb +154 -0
- data/lib/active_model/translation.rb +78 -0
- data/lib/active_model/type/big_integer.rb +36 -0
- data/lib/active_model/type/binary.rb +62 -0
- data/lib/active_model/type/boolean.rb +48 -0
- data/lib/active_model/type/date.rb +78 -0
- data/lib/active_model/type/date_time.rb +88 -0
- data/lib/active_model/type/decimal.rb +107 -0
- data/lib/active_model/type/float.rb +64 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
- data/lib/active_model/type/helpers/mutable.rb +24 -0
- data/lib/active_model/type/helpers/numeric.rb +61 -0
- data/lib/active_model/type/helpers/time_value.rb +127 -0
- data/lib/active_model/type/helpers/timezone.rb +23 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/immutable_string.rb +71 -0
- data/lib/active_model/type/integer.rb +113 -0
- data/lib/active_model/type/registry.rb +37 -0
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +43 -0
- data/lib/active_model/type/time.rb +87 -0
- data/lib/active_model/type/value.rb +157 -0
- data/lib/active_model/type.rb +55 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +113 -0
- data/lib/active_model/validations/callbacks.rb +119 -0
- data/lib/active_model/validations/clusivity.rb +54 -0
- data/lib/active_model/validations/comparability.rb +18 -0
- data/lib/active_model/validations/comparison.rb +90 -0
- data/lib/active_model/validations/confirmation.rb +80 -0
- data/lib/active_model/validations/exclusion.rb +49 -0
- data/lib/active_model/validations/format.rb +112 -0
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +47 -0
- data/lib/active_model/validations/length.rb +130 -0
- data/lib/active_model/validations/numericality.rb +222 -0
- data/lib/active_model/validations/presence.rb +39 -0
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +175 -0
- data/lib/active_model/validations/with.rb +154 -0
- data/lib/active_model/validations.rb +489 -0
- data/lib/active_model/validator.rb +190 -0
- data/lib/active_model/version.rb +10 -0
- data/lib/active_model.rb +84 -0
- metadata +139 -0
@@ -0,0 +1,489 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/array/extract_options"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
# = Active \Model \Validations
|
7
|
+
#
|
8
|
+
# Provides a full validation framework to your objects.
|
9
|
+
#
|
10
|
+
# A minimal implementation could be:
|
11
|
+
#
|
12
|
+
# class Person
|
13
|
+
# include ActiveModel::Validations
|
14
|
+
#
|
15
|
+
# attr_accessor :first_name, :last_name
|
16
|
+
#
|
17
|
+
# validates_each :first_name, :last_name do |record, attr, value|
|
18
|
+
# record.errors.add attr, "starts with z." if value.start_with?("z")
|
19
|
+
# end
|
20
|
+
# end
|
21
|
+
#
|
22
|
+
# Which provides you with the full standard validation stack that you
|
23
|
+
# know from Active Record:
|
24
|
+
#
|
25
|
+
# person = Person.new
|
26
|
+
# person.valid? # => true
|
27
|
+
# person.invalid? # => false
|
28
|
+
#
|
29
|
+
# person.first_name = 'zoolander'
|
30
|
+
# person.valid? # => false
|
31
|
+
# person.invalid? # => true
|
32
|
+
# person.errors.messages # => {first_name:["starts with z."]}
|
33
|
+
#
|
34
|
+
# Note that +ActiveModel::Validations+ automatically adds an +errors+
|
35
|
+
# method to your instances initialized with a new ActiveModel::Errors
|
36
|
+
# object, so there is no need for you to do this manually.
|
37
|
+
module Validations
|
38
|
+
extend ActiveSupport::Concern
|
39
|
+
|
40
|
+
included do
|
41
|
+
extend ActiveModel::Naming
|
42
|
+
extend ActiveModel::Callbacks
|
43
|
+
extend ActiveModel::Translation
|
44
|
+
|
45
|
+
extend HelperMethods
|
46
|
+
include HelperMethods
|
47
|
+
|
48
|
+
define_callbacks :validate, scope: :name
|
49
|
+
|
50
|
+
class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
|
51
|
+
end
|
52
|
+
|
53
|
+
module ClassMethods
|
54
|
+
# Validates each attribute against a block.
|
55
|
+
#
|
56
|
+
# class Person
|
57
|
+
# include ActiveModel::Validations
|
58
|
+
#
|
59
|
+
# attr_accessor :first_name, :last_name
|
60
|
+
#
|
61
|
+
# validates_each :first_name, :last_name, allow_blank: true do |record, attr, value|
|
62
|
+
# record.errors.add attr, "starts with z." if value.start_with?("z")
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
#
|
66
|
+
# Options:
|
67
|
+
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
|
68
|
+
# Runs in all validation contexts by default +nil+. You can pass a symbol
|
69
|
+
# or an array of symbols. (e.g. <tt>on: :create</tt> or
|
70
|
+
# <tt>on: :custom_validation_context</tt> or
|
71
|
+
# <tt>on: [:create, :custom_validation_context]</tt>)
|
72
|
+
# * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
|
73
|
+
# * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
|
74
|
+
# * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
|
75
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
76
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
77
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
78
|
+
# * <tt>:unless</tt> - Specifies a method, proc, or string to call to
|
79
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
80
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
81
|
+
# method, proc, or string should return or evaluate to a +true+ or +false+
|
82
|
+
# value.
|
83
|
+
def validates_each(*attr_names, &block)
|
84
|
+
validates_with BlockValidator, _merge_attributes(attr_names), &block
|
85
|
+
end
|
86
|
+
|
87
|
+
VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
|
88
|
+
|
89
|
+
# Adds a validation method or block to the class. This is useful when
|
90
|
+
# overriding the +validate+ instance method becomes too unwieldy and
|
91
|
+
# you're looking for more descriptive declaration of your validations.
|
92
|
+
#
|
93
|
+
# This can be done with a symbol pointing to a method:
|
94
|
+
#
|
95
|
+
# class Comment
|
96
|
+
# include ActiveModel::Validations
|
97
|
+
#
|
98
|
+
# validate :must_be_friends
|
99
|
+
#
|
100
|
+
# def must_be_friends
|
101
|
+
# errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
|
102
|
+
# end
|
103
|
+
# end
|
104
|
+
#
|
105
|
+
# With a block which is passed with the current record to be validated:
|
106
|
+
#
|
107
|
+
# class Comment
|
108
|
+
# include ActiveModel::Validations
|
109
|
+
#
|
110
|
+
# validate do |comment|
|
111
|
+
# comment.must_be_friends
|
112
|
+
# end
|
113
|
+
#
|
114
|
+
# def must_be_friends
|
115
|
+
# errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
|
116
|
+
# end
|
117
|
+
# end
|
118
|
+
#
|
119
|
+
# Or with a block where +self+ points to the current record to be validated:
|
120
|
+
#
|
121
|
+
# class Comment
|
122
|
+
# include ActiveModel::Validations
|
123
|
+
#
|
124
|
+
# validate do
|
125
|
+
# errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
|
126
|
+
# end
|
127
|
+
# end
|
128
|
+
#
|
129
|
+
# Note that the return value of validation methods is not relevant.
|
130
|
+
# It's not possible to halt the validate callback chain.
|
131
|
+
#
|
132
|
+
# Options:
|
133
|
+
# * <tt>:on</tt> - Specifies the contexts where this validation is active.
|
134
|
+
# Runs in all validation contexts by default +nil+. You can pass a symbol
|
135
|
+
# or an array of symbols. (e.g. <tt>on: :create</tt> or
|
136
|
+
# <tt>on: :custom_validation_context</tt> or
|
137
|
+
# <tt>on: [:create, :custom_validation_context]</tt>)
|
138
|
+
# * <tt>:if</tt> - Specifies a method, proc, or string to call to determine
|
139
|
+
# if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
|
140
|
+
# or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
|
141
|
+
# proc or string should return or evaluate to a +true+ or +false+ value.
|
142
|
+
# * <tt>:unless</tt> - Specifies a method, proc, or string to call to
|
143
|
+
# determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
|
144
|
+
# or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
|
145
|
+
# method, proc, or string should return or evaluate to a +true+ or +false+
|
146
|
+
# value.
|
147
|
+
#
|
148
|
+
# NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
|
149
|
+
#
|
150
|
+
def validate(*args, &block)
|
151
|
+
options = args.extract_options!
|
152
|
+
|
153
|
+
if args.all?(Symbol)
|
154
|
+
options.each_key do |k|
|
155
|
+
unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
|
156
|
+
raise ArgumentError.new("Unknown key: #{k.inspect}. Valid keys are: #{VALID_OPTIONS_FOR_VALIDATE.map(&:inspect).join(', ')}. Perhaps you meant to call `validates` instead of `validate`?")
|
157
|
+
end
|
158
|
+
end
|
159
|
+
end
|
160
|
+
|
161
|
+
if options.key?(:on)
|
162
|
+
options = options.merge(if: [predicate_for_validation_context(options[:on]), *options[:if]])
|
163
|
+
end
|
164
|
+
|
165
|
+
set_callback(:validate, *args, options, &block)
|
166
|
+
end
|
167
|
+
|
168
|
+
# List all validators that are being used to validate the model using
|
169
|
+
# +validates_with+ method.
|
170
|
+
#
|
171
|
+
# class Person
|
172
|
+
# include ActiveModel::Validations
|
173
|
+
#
|
174
|
+
# validates_with MyValidator
|
175
|
+
# validates_with OtherValidator, on: :create
|
176
|
+
# validates_with StrictValidator, strict: true
|
177
|
+
# end
|
178
|
+
#
|
179
|
+
# Person.validators
|
180
|
+
# # => [
|
181
|
+
# # #<MyValidator:0x007fbff403e808 @options={}>,
|
182
|
+
# # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
|
183
|
+
# # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
|
184
|
+
# # ]
|
185
|
+
def validators
|
186
|
+
_validators.values.flatten.uniq
|
187
|
+
end
|
188
|
+
|
189
|
+
# Clears all of the validators and validations.
|
190
|
+
#
|
191
|
+
# Note that this will clear anything that is being used to validate
|
192
|
+
# the model for both the +validates_with+ and +validate+ methods.
|
193
|
+
# It clears the validators that are created with an invocation of
|
194
|
+
# +validates_with+ and the callbacks that are set by an invocation
|
195
|
+
# of +validate+.
|
196
|
+
#
|
197
|
+
# class Person
|
198
|
+
# include ActiveModel::Validations
|
199
|
+
#
|
200
|
+
# validates_with MyValidator
|
201
|
+
# validates_with OtherValidator, on: :create
|
202
|
+
# validates_with StrictValidator, strict: true
|
203
|
+
# validate :cannot_be_robot
|
204
|
+
#
|
205
|
+
# def cannot_be_robot
|
206
|
+
# errors.add(:base, 'A person cannot be a robot') if person_is_robot
|
207
|
+
# end
|
208
|
+
# end
|
209
|
+
#
|
210
|
+
# Person.validators
|
211
|
+
# # => [
|
212
|
+
# # #<MyValidator:0x007fbff403e808 @options={}>,
|
213
|
+
# # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
|
214
|
+
# # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
|
215
|
+
# # ]
|
216
|
+
#
|
217
|
+
# If one runs <tt>Person.clear_validators!</tt> and then checks to see what
|
218
|
+
# validators this class has, you would obtain:
|
219
|
+
#
|
220
|
+
# Person.validators # => []
|
221
|
+
#
|
222
|
+
# Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased
|
223
|
+
# so that:
|
224
|
+
#
|
225
|
+
# Person._validate_callbacks.empty? # => true
|
226
|
+
#
|
227
|
+
def clear_validators!
|
228
|
+
reset_callbacks(:validate)
|
229
|
+
_validators.clear
|
230
|
+
end
|
231
|
+
|
232
|
+
# List all validators that are being used to validate a specific attribute.
|
233
|
+
#
|
234
|
+
# class Person
|
235
|
+
# include ActiveModel::Validations
|
236
|
+
#
|
237
|
+
# attr_accessor :name, :age
|
238
|
+
#
|
239
|
+
# validates_presence_of :name
|
240
|
+
# validates_inclusion_of :age, in: 0..99
|
241
|
+
# end
|
242
|
+
#
|
243
|
+
# Person.validators_on(:name)
|
244
|
+
# # => [
|
245
|
+
# # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>,
|
246
|
+
# # ]
|
247
|
+
def validators_on(*attributes)
|
248
|
+
attributes.flat_map do |attribute|
|
249
|
+
_validators[attribute.to_sym]
|
250
|
+
end
|
251
|
+
end
|
252
|
+
|
253
|
+
# Returns +true+ if +attribute+ is an attribute method, +false+ otherwise.
|
254
|
+
#
|
255
|
+
# class Person
|
256
|
+
# include ActiveModel::Validations
|
257
|
+
#
|
258
|
+
# attr_accessor :name
|
259
|
+
# end
|
260
|
+
#
|
261
|
+
# User.attribute_method?(:name) # => true
|
262
|
+
# User.attribute_method?(:age) # => false
|
263
|
+
def attribute_method?(attribute)
|
264
|
+
method_defined?(attribute)
|
265
|
+
end
|
266
|
+
|
267
|
+
# Copy validators on inheritance.
|
268
|
+
def inherited(base) # :nodoc:
|
269
|
+
dup = _validators.dup
|
270
|
+
base._validators = dup.each { |k, v| dup[k] = v.dup }
|
271
|
+
super
|
272
|
+
end
|
273
|
+
|
274
|
+
private
|
275
|
+
@@predicates_for_validation_contexts = {}
|
276
|
+
|
277
|
+
def predicate_for_validation_context(context)
|
278
|
+
context = context.is_a?(Array) ? context.sort : Array(context)
|
279
|
+
|
280
|
+
@@predicates_for_validation_contexts[context] ||= -> (model) do
|
281
|
+
if model.validation_context.is_a?(Array)
|
282
|
+
model.validation_context.any? { |model_context| context.include?(model_context) }
|
283
|
+
else
|
284
|
+
context.include?(model.validation_context)
|
285
|
+
end
|
286
|
+
end
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
# Clean the +Errors+ object if instance is duped.
|
291
|
+
def initialize_dup(other) # :nodoc:
|
292
|
+
@errors = nil
|
293
|
+
super
|
294
|
+
end
|
295
|
+
|
296
|
+
# Returns the +Errors+ object that holds all information about attribute
|
297
|
+
# error messages.
|
298
|
+
#
|
299
|
+
# class Person
|
300
|
+
# include ActiveModel::Validations
|
301
|
+
#
|
302
|
+
# attr_accessor :name
|
303
|
+
# validates_presence_of :name
|
304
|
+
# end
|
305
|
+
#
|
306
|
+
# person = Person.new
|
307
|
+
# person.valid? # => false
|
308
|
+
# person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}>
|
309
|
+
def errors
|
310
|
+
@errors ||= Errors.new(self)
|
311
|
+
end
|
312
|
+
|
313
|
+
# Runs all the specified validations and returns +true+ if no errors were
|
314
|
+
# added otherwise +false+.
|
315
|
+
#
|
316
|
+
# class Person
|
317
|
+
# include ActiveModel::Validations
|
318
|
+
#
|
319
|
+
# attr_accessor :name
|
320
|
+
# validates_presence_of :name
|
321
|
+
# end
|
322
|
+
#
|
323
|
+
# person = Person.new
|
324
|
+
# person.name = ''
|
325
|
+
# person.valid? # => false
|
326
|
+
# person.name = 'david'
|
327
|
+
# person.valid? # => true
|
328
|
+
#
|
329
|
+
# Context can optionally be supplied to define which callbacks to test
|
330
|
+
# against (the context is defined on the validations using <tt>:on</tt>).
|
331
|
+
#
|
332
|
+
# class Person
|
333
|
+
# include ActiveModel::Validations
|
334
|
+
#
|
335
|
+
# attr_accessor :name
|
336
|
+
# validates_presence_of :name, on: :new
|
337
|
+
# end
|
338
|
+
#
|
339
|
+
# person = Person.new
|
340
|
+
# person.valid? # => true
|
341
|
+
# person.valid?(:new) # => false
|
342
|
+
def valid?(context = nil)
|
343
|
+
current_context = validation_context
|
344
|
+
context_for_validation.context = context
|
345
|
+
errors.clear
|
346
|
+
run_validations!
|
347
|
+
ensure
|
348
|
+
context_for_validation.context = current_context
|
349
|
+
end
|
350
|
+
|
351
|
+
alias_method :validate, :valid?
|
352
|
+
|
353
|
+
def freeze
|
354
|
+
errors
|
355
|
+
context_for_validation
|
356
|
+
|
357
|
+
super
|
358
|
+
end
|
359
|
+
|
360
|
+
# Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
|
361
|
+
# added, +false+ otherwise.
|
362
|
+
#
|
363
|
+
# class Person
|
364
|
+
# include ActiveModel::Validations
|
365
|
+
#
|
366
|
+
# attr_accessor :name
|
367
|
+
# validates_presence_of :name
|
368
|
+
# end
|
369
|
+
#
|
370
|
+
# person = Person.new
|
371
|
+
# person.name = ''
|
372
|
+
# person.invalid? # => true
|
373
|
+
# person.name = 'david'
|
374
|
+
# person.invalid? # => false
|
375
|
+
#
|
376
|
+
# Context can optionally be supplied to define which callbacks to test
|
377
|
+
# against (the context is defined on the validations using <tt>:on</tt>).
|
378
|
+
#
|
379
|
+
# class Person
|
380
|
+
# include ActiveModel::Validations
|
381
|
+
#
|
382
|
+
# attr_accessor :name
|
383
|
+
# validates_presence_of :name, on: :new
|
384
|
+
# end
|
385
|
+
#
|
386
|
+
# person = Person.new
|
387
|
+
# person.invalid? # => false
|
388
|
+
# person.invalid?(:new) # => true
|
389
|
+
def invalid?(context = nil)
|
390
|
+
!valid?(context)
|
391
|
+
end
|
392
|
+
|
393
|
+
# Runs all the validations within the specified context. Returns +true+ if
|
394
|
+
# no errors are found, raises +ValidationError+ otherwise.
|
395
|
+
#
|
396
|
+
# Validations with no <tt>:on</tt> option will run no matter the context. Validations with
|
397
|
+
# some <tt>:on</tt> option will only run in the specified context.
|
398
|
+
def validate!(context = nil)
|
399
|
+
valid?(context) || raise_validation_error
|
400
|
+
end
|
401
|
+
|
402
|
+
# Hook method defining how an attribute value should be retrieved. By default
|
403
|
+
# this is assumed to be an instance named after the attribute. Override this
|
404
|
+
# method in subclasses should you need to retrieve the value for a given
|
405
|
+
# attribute differently:
|
406
|
+
#
|
407
|
+
# class MyClass
|
408
|
+
# include ActiveModel::Validations
|
409
|
+
#
|
410
|
+
# def initialize(data = {})
|
411
|
+
# @data = data
|
412
|
+
# end
|
413
|
+
#
|
414
|
+
# def read_attribute_for_validation(key)
|
415
|
+
# @data[key]
|
416
|
+
# end
|
417
|
+
# end
|
418
|
+
alias :read_attribute_for_validation :send
|
419
|
+
|
420
|
+
# Returns the context when running validations.
|
421
|
+
#
|
422
|
+
# This is useful when running validations except a certain context (opposite to the +on+ option).
|
423
|
+
#
|
424
|
+
# class Person
|
425
|
+
# include ActiveModel::Validations
|
426
|
+
#
|
427
|
+
# attr_accessor :name
|
428
|
+
# validates :name, presence: true, if: -> { validation_context != :custom }
|
429
|
+
# end
|
430
|
+
#
|
431
|
+
# person = Person.new
|
432
|
+
# person.valid? #=> false
|
433
|
+
# person.valid?(:new) #=> false
|
434
|
+
# person.valid?(:custom) #=> true
|
435
|
+
def validation_context
|
436
|
+
context_for_validation.context
|
437
|
+
end
|
438
|
+
|
439
|
+
private
|
440
|
+
def validation_context=(context)
|
441
|
+
context_for_validation.context = context
|
442
|
+
end
|
443
|
+
|
444
|
+
def context_for_validation
|
445
|
+
@context_for_validation ||= ValidationContext.new
|
446
|
+
end
|
447
|
+
|
448
|
+
def init_internals
|
449
|
+
super
|
450
|
+
@errors = nil
|
451
|
+
@context_for_validation = nil
|
452
|
+
end
|
453
|
+
|
454
|
+
def run_validations!
|
455
|
+
_run_validate_callbacks
|
456
|
+
errors.empty?
|
457
|
+
end
|
458
|
+
|
459
|
+
def raise_validation_error # :doc:
|
460
|
+
raise(ValidationError.new(self))
|
461
|
+
end
|
462
|
+
end
|
463
|
+
|
464
|
+
# = Active \Model \ValidationError
|
465
|
+
#
|
466
|
+
# Raised by <tt>validate!</tt> when the model is invalid. Use the
|
467
|
+
# +model+ method to retrieve the record which did not validate.
|
468
|
+
#
|
469
|
+
# begin
|
470
|
+
# complex_operation_that_internally_calls_validate!
|
471
|
+
# rescue ActiveModel::ValidationError => invalid
|
472
|
+
# puts invalid.model.errors
|
473
|
+
# end
|
474
|
+
class ValidationError < StandardError
|
475
|
+
attr_reader :model
|
476
|
+
|
477
|
+
def initialize(model)
|
478
|
+
@model = model
|
479
|
+
errors = @model.errors.full_messages.join(", ")
|
480
|
+
super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
|
481
|
+
end
|
482
|
+
end
|
483
|
+
|
484
|
+
class ValidationContext # :nodoc:
|
485
|
+
attr_accessor :context
|
486
|
+
end
|
487
|
+
end
|
488
|
+
|
489
|
+
Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }
|
@@ -0,0 +1,190 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/module/anonymous"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
# = Active \Model \Validator
|
7
|
+
#
|
8
|
+
# A simple base class that can be used along with
|
9
|
+
# ActiveModel::Validations::ClassMethods.validates_with
|
10
|
+
#
|
11
|
+
# class Person
|
12
|
+
# include ActiveModel::Validations
|
13
|
+
# validates_with MyValidator
|
14
|
+
# end
|
15
|
+
#
|
16
|
+
# class MyValidator < ActiveModel::Validator
|
17
|
+
# def validate(record)
|
18
|
+
# if some_complex_logic
|
19
|
+
# record.errors.add(:base, "This record is invalid")
|
20
|
+
# end
|
21
|
+
# end
|
22
|
+
#
|
23
|
+
# private
|
24
|
+
# def some_complex_logic
|
25
|
+
# # ...
|
26
|
+
# end
|
27
|
+
# end
|
28
|
+
#
|
29
|
+
# Any class that inherits from \ActiveModel::Validator must implement a method
|
30
|
+
# called +validate+ which accepts a +record+.
|
31
|
+
#
|
32
|
+
# class Person
|
33
|
+
# include ActiveModel::Validations
|
34
|
+
# validates_with MyValidator
|
35
|
+
# end
|
36
|
+
#
|
37
|
+
# class MyValidator < ActiveModel::Validator
|
38
|
+
# def validate(record)
|
39
|
+
# record # => The person instance being validated
|
40
|
+
# options # => Any non-standard options passed to validates_with
|
41
|
+
# end
|
42
|
+
# end
|
43
|
+
#
|
44
|
+
# To cause a validation error, you must add to the +record+'s errors directly
|
45
|
+
# from within the validators message.
|
46
|
+
#
|
47
|
+
# class MyValidator < ActiveModel::Validator
|
48
|
+
# def validate(record)
|
49
|
+
# record.errors.add :base, "This is some custom error message"
|
50
|
+
# record.errors.add :first_name, "This is some complex validation"
|
51
|
+
# # etc...
|
52
|
+
# end
|
53
|
+
# end
|
54
|
+
#
|
55
|
+
# To add behavior to the initialize method, use the following signature:
|
56
|
+
#
|
57
|
+
# class MyValidator < ActiveModel::Validator
|
58
|
+
# def initialize(options)
|
59
|
+
# super
|
60
|
+
# @my_custom_field = options[:field_name] || :first_name
|
61
|
+
# end
|
62
|
+
# end
|
63
|
+
#
|
64
|
+
# Note that the validator is initialized only once for the whole application
|
65
|
+
# life cycle, and not on each validation run.
|
66
|
+
#
|
67
|
+
# The easiest way to add custom validators for validating individual attributes
|
68
|
+
# is with the convenient ActiveModel::EachValidator class.
|
69
|
+
#
|
70
|
+
# class TitleValidator < ActiveModel::EachValidator
|
71
|
+
# def validate_each(record, attribute, value)
|
72
|
+
# record.errors.add attribute, 'must be Mr., Mrs., or Dr.' unless %w(Mr. Mrs. Dr.).include?(value)
|
73
|
+
# end
|
74
|
+
# end
|
75
|
+
#
|
76
|
+
# This can now be used in combination with the +validates+ method.
|
77
|
+
# See ActiveModel::Validations::ClassMethods#validates for more on this.
|
78
|
+
#
|
79
|
+
# class Person
|
80
|
+
# include ActiveModel::Validations
|
81
|
+
# attr_accessor :title
|
82
|
+
#
|
83
|
+
# validates :title, presence: true, title: true
|
84
|
+
# end
|
85
|
+
#
|
86
|
+
# It can be useful to access the class that is using that validator when there are prerequisites such
|
87
|
+
# as an +attr_accessor+ being present. This class is accessible via <tt>options[:class]</tt> in the constructor.
|
88
|
+
# To set up your validator override the constructor.
|
89
|
+
#
|
90
|
+
# class MyValidator < ActiveModel::Validator
|
91
|
+
# def initialize(options={})
|
92
|
+
# super
|
93
|
+
# options[:class].attr_accessor :custom_attribute
|
94
|
+
# end
|
95
|
+
# end
|
96
|
+
class Validator
|
97
|
+
attr_reader :options
|
98
|
+
|
99
|
+
# Returns the kind of the validator.
|
100
|
+
#
|
101
|
+
# PresenceValidator.kind # => :presence
|
102
|
+
# AcceptanceValidator.kind # => :acceptance
|
103
|
+
def self.kind
|
104
|
+
@kind ||= name.split("::").last.underscore.chomp("_validator").to_sym unless anonymous?
|
105
|
+
end
|
106
|
+
|
107
|
+
# Accepts options that will be made available through the +options+ reader.
|
108
|
+
def initialize(options = {})
|
109
|
+
@options = options.except(:class).freeze
|
110
|
+
end
|
111
|
+
|
112
|
+
# Returns the kind for this validator.
|
113
|
+
#
|
114
|
+
# PresenceValidator.new(attributes: [:username]).kind # => :presence
|
115
|
+
# AcceptanceValidator.new(attributes: [:terms]).kind # => :acceptance
|
116
|
+
def kind
|
117
|
+
self.class.kind
|
118
|
+
end
|
119
|
+
|
120
|
+
# Override this method in subclasses with validation logic, adding errors
|
121
|
+
# to the records +errors+ array where necessary.
|
122
|
+
def validate(record)
|
123
|
+
raise NotImplementedError, "Subclasses must implement a validate(record) method."
|
124
|
+
end
|
125
|
+
end
|
126
|
+
|
127
|
+
# = Active \Model \EachValidator
|
128
|
+
#
|
129
|
+
# +EachValidator+ is a validator which iterates through the attributes given
|
130
|
+
# in the options hash invoking the <tt>validate_each</tt> method passing in the
|
131
|
+
# record, attribute, and value.
|
132
|
+
#
|
133
|
+
# All \Active \Model validations are built on top of this validator.
|
134
|
+
class EachValidator < Validator
|
135
|
+
attr_reader :attributes
|
136
|
+
|
137
|
+
# Returns a new validator instance. All options will be available via the
|
138
|
+
# +options+ reader, however the <tt>:attributes</tt> option will be removed
|
139
|
+
# and instead be made available through the +attributes+ reader.
|
140
|
+
def initialize(options)
|
141
|
+
@attributes = Array(options.delete(:attributes))
|
142
|
+
raise ArgumentError, ":attributes cannot be blank" if @attributes.empty?
|
143
|
+
super
|
144
|
+
check_validity!
|
145
|
+
end
|
146
|
+
|
147
|
+
# Performs validation on the supplied record. By default this will call
|
148
|
+
# +validate_each+ to determine validity therefore subclasses should
|
149
|
+
# override +validate_each+ with validation logic.
|
150
|
+
def validate(record)
|
151
|
+
attributes.each do |attribute|
|
152
|
+
value = record.read_attribute_for_validation(attribute)
|
153
|
+
next if (value.nil? && options[:allow_nil]) || (value.blank? && options[:allow_blank])
|
154
|
+
value = prepare_value_for_validation(value, record, attribute)
|
155
|
+
validate_each(record, attribute, value)
|
156
|
+
end
|
157
|
+
end
|
158
|
+
|
159
|
+
# Override this method in subclasses with the validation logic, adding
|
160
|
+
# errors to the records +errors+ array where necessary.
|
161
|
+
def validate_each(record, attribute, value)
|
162
|
+
raise NotImplementedError, "Subclasses must implement a validate_each(record, attribute, value) method"
|
163
|
+
end
|
164
|
+
|
165
|
+
# Hook method that gets called by the initializer allowing verification
|
166
|
+
# that the arguments supplied are valid. You could for example raise an
|
167
|
+
# +ArgumentError+ when invalid options are supplied.
|
168
|
+
def check_validity!
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
def prepare_value_for_validation(value, record, attr_name)
|
173
|
+
value
|
174
|
+
end
|
175
|
+
end
|
176
|
+
|
177
|
+
# +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
|
178
|
+
# and call this block for each attribute being validated. +validates_each+ uses this validator.
|
179
|
+
class BlockValidator < EachValidator # :nodoc:
|
180
|
+
def initialize(options, &block)
|
181
|
+
@block = block
|
182
|
+
super
|
183
|
+
end
|
184
|
+
|
185
|
+
private
|
186
|
+
def validate_each(record, attribute, value)
|
187
|
+
@block.call(record, attribute, value)
|
188
|
+
end
|
189
|
+
end
|
190
|
+
end
|