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.
Files changed (77) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +67 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +99 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +55 -0
  8. data/lib/active_model/attribute.rb +277 -0
  9. data/lib/active_model/attribute_assignment.rb +78 -0
  10. data/lib/active_model/attribute_methods.rb +592 -0
  11. data/lib/active_model/attribute_mutation_tracker.rb +189 -0
  12. data/lib/active_model/attribute_registration.rb +117 -0
  13. data/lib/active_model/attribute_set/builder.rb +182 -0
  14. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  15. data/lib/active_model/attribute_set.rb +118 -0
  16. data/lib/active_model/attributes.rb +165 -0
  17. data/lib/active_model/callbacks.rb +155 -0
  18. data/lib/active_model/conversion.rb +121 -0
  19. data/lib/active_model/deprecator.rb +7 -0
  20. data/lib/active_model/dirty.rb +416 -0
  21. data/lib/active_model/error.rb +208 -0
  22. data/lib/active_model/errors.rb +547 -0
  23. data/lib/active_model/forbidden_attributes_protection.rb +33 -0
  24. data/lib/active_model/gem_version.rb +17 -0
  25. data/lib/active_model/lint.rb +118 -0
  26. data/lib/active_model/locale/en.yml +38 -0
  27. data/lib/active_model/model.rb +78 -0
  28. data/lib/active_model/naming.rb +359 -0
  29. data/lib/active_model/nested_error.rb +22 -0
  30. data/lib/active_model/railtie.rb +24 -0
  31. data/lib/active_model/secure_password.rb +231 -0
  32. data/lib/active_model/serialization.rb +198 -0
  33. data/lib/active_model/serializers/json.rb +154 -0
  34. data/lib/active_model/translation.rb +78 -0
  35. data/lib/active_model/type/big_integer.rb +36 -0
  36. data/lib/active_model/type/binary.rb +62 -0
  37. data/lib/active_model/type/boolean.rb +48 -0
  38. data/lib/active_model/type/date.rb +78 -0
  39. data/lib/active_model/type/date_time.rb +88 -0
  40. data/lib/active_model/type/decimal.rb +107 -0
  41. data/lib/active_model/type/float.rb +64 -0
  42. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +53 -0
  43. data/lib/active_model/type/helpers/mutable.rb +24 -0
  44. data/lib/active_model/type/helpers/numeric.rb +61 -0
  45. data/lib/active_model/type/helpers/time_value.rb +127 -0
  46. data/lib/active_model/type/helpers/timezone.rb +23 -0
  47. data/lib/active_model/type/helpers.rb +7 -0
  48. data/lib/active_model/type/immutable_string.rb +71 -0
  49. data/lib/active_model/type/integer.rb +113 -0
  50. data/lib/active_model/type/registry.rb +37 -0
  51. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  52. data/lib/active_model/type/string.rb +43 -0
  53. data/lib/active_model/type/time.rb +87 -0
  54. data/lib/active_model/type/value.rb +157 -0
  55. data/lib/active_model/type.rb +55 -0
  56. data/lib/active_model/validations/absence.rb +33 -0
  57. data/lib/active_model/validations/acceptance.rb +113 -0
  58. data/lib/active_model/validations/callbacks.rb +119 -0
  59. data/lib/active_model/validations/clusivity.rb +54 -0
  60. data/lib/active_model/validations/comparability.rb +18 -0
  61. data/lib/active_model/validations/comparison.rb +90 -0
  62. data/lib/active_model/validations/confirmation.rb +80 -0
  63. data/lib/active_model/validations/exclusion.rb +49 -0
  64. data/lib/active_model/validations/format.rb +112 -0
  65. data/lib/active_model/validations/helper_methods.rb +15 -0
  66. data/lib/active_model/validations/inclusion.rb +47 -0
  67. data/lib/active_model/validations/length.rb +130 -0
  68. data/lib/active_model/validations/numericality.rb +222 -0
  69. data/lib/active_model/validations/presence.rb +39 -0
  70. data/lib/active_model/validations/resolve_value.rb +26 -0
  71. data/lib/active_model/validations/validates.rb +175 -0
  72. data/lib/active_model/validations/with.rb +154 -0
  73. data/lib/active_model/validations.rb +489 -0
  74. data/lib/active_model/validator.rb +190 -0
  75. data/lib/active_model/version.rb +10 -0
  76. data/lib/active_model.rb +84 -0
  77. 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
@@ -0,0 +1,10 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "gem_version"
4
+
5
+ module ActiveModel
6
+ # Returns the currently loaded version of \Active \Model as a +Gem::Version+.
7
+ def self.version
8
+ gem_version
9
+ end
10
+ end