activemodel 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +172 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +247 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +517 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +178 -0
  11. data/lib/active_model/attribute_set.rb +106 -0
  12. data/lib/active_model/attribute_set/builder.rb +124 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  14. data/lib/active_model/attributes.rb +138 -0
  15. data/lib/active_model/callbacks.rb +156 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +280 -0
  18. data/lib/active_model/errors.rb +601 -0
  19. data/lib/active_model/forbidden_attributes_protection.rb +31 -0
  20. data/lib/active_model/gem_version.rb +17 -0
  21. data/lib/active_model/lint.rb +118 -0
  22. data/lib/active_model/locale/en.yml +36 -0
  23. data/lib/active_model/model.rb +99 -0
  24. data/lib/active_model/naming.rb +334 -0
  25. data/lib/active_model/railtie.rb +20 -0
  26. data/lib/active_model/secure_password.rb +128 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +147 -0
  29. data/lib/active_model/translation.rb +70 -0
  30. data/lib/active_model/type.rb +53 -0
  31. data/lib/active_model/type/big_integer.rb +15 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +47 -0
  34. data/lib/active_model/type/date.rb +53 -0
  35. data/lib/active_model/type/date_time.rb +47 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +34 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +45 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +44 -0
  42. data/lib/active_model/type/helpers/time_value.rb +81 -0
  43. data/lib/active_model/type/helpers/timezone.rb +19 -0
  44. data/lib/active_model/type/immutable_string.rb +32 -0
  45. data/lib/active_model/type/integer.rb +58 -0
  46. data/lib/active_model/type/registry.rb +62 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +47 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +437 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +102 -0
  53. data/lib/active_model/validations/callbacks.rb +122 -0
  54. data/lib/active_model/validations/clusivity.rb +54 -0
  55. data/lib/active_model/validations/confirmation.rb +80 -0
  56. data/lib/active_model/validations/exclusion.rb +49 -0
  57. data/lib/active_model/validations/format.rb +114 -0
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +47 -0
  60. data/lib/active_model/validations/length.rb +129 -0
  61. data/lib/active_model/validations/numericality.rb +189 -0
  62. data/lib/active_model/validations/presence.rb +39 -0
  63. data/lib/active_model/validations/validates.rb +174 -0
  64. data/lib/active_model/validations/with.rb +147 -0
  65. data/lib/active_model/validator.rb +183 -0
  66. data/lib/active_model/version.rb +10 -0
  67. metadata +125 -0
@@ -0,0 +1,126 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ class Value
6
+ attr_reader :precision, :scale, :limit
7
+
8
+ def initialize(precision: nil, limit: nil, scale: nil)
9
+ @precision = precision
10
+ @scale = scale
11
+ @limit = limit
12
+ end
13
+
14
+ def type # :nodoc:
15
+ end
16
+
17
+ # Converts a value from database input to the appropriate ruby type. The
18
+ # return value of this method will be returned from
19
+ # ActiveRecord::AttributeMethods::Read#read_attribute. The default
20
+ # implementation just calls Value#cast.
21
+ #
22
+ # +value+ The raw input, as provided from the database.
23
+ def deserialize(value)
24
+ cast(value)
25
+ end
26
+
27
+ # Type casts a value from user input (e.g. from a setter). This value may
28
+ # be a string from the form builder, or a ruby object passed to a setter.
29
+ # There is currently no way to differentiate between which source it came
30
+ # from.
31
+ #
32
+ # The return value of this method will be returned from
33
+ # ActiveRecord::AttributeMethods::Read#read_attribute. See also:
34
+ # Value#cast_value.
35
+ #
36
+ # +value+ The raw input, as provided to the attribute setter.
37
+ def cast(value)
38
+ cast_value(value) unless value.nil?
39
+ end
40
+
41
+ # Casts a value from the ruby type to a type that the database knows how
42
+ # to understand. The returned value from this method should be a
43
+ # +String+, +Numeric+, +Date+, +Time+, +Symbol+, +true+, +false+, or
44
+ # +nil+.
45
+ def serialize(value)
46
+ value
47
+ end
48
+
49
+ # Type casts a value for schema dumping. This method is private, as we are
50
+ # hoping to remove it entirely.
51
+ def type_cast_for_schema(value) # :nodoc:
52
+ value.inspect
53
+ end
54
+
55
+ # These predicates are not documented, as I need to look further into
56
+ # their use, and see if they can be removed entirely.
57
+ def binary? # :nodoc:
58
+ false
59
+ end
60
+
61
+ # Determines whether a value has changed for dirty checking. +old_value+
62
+ # and +new_value+ will always be type-cast. Types should not need to
63
+ # override this method.
64
+ def changed?(old_value, new_value, _new_value_before_type_cast)
65
+ old_value != new_value
66
+ end
67
+
68
+ # Determines whether the mutable value has been modified since it was
69
+ # read. Returns +false+ by default. If your type returns an object
70
+ # which could be mutated, you should override this method. You will need
71
+ # to either:
72
+ #
73
+ # - pass +new_value+ to Value#serialize and compare it to
74
+ # +raw_old_value+
75
+ #
76
+ # or
77
+ #
78
+ # - pass +raw_old_value+ to Value#deserialize and compare it to
79
+ # +new_value+
80
+ #
81
+ # +raw_old_value+ The original value, before being passed to
82
+ # +deserialize+.
83
+ #
84
+ # +new_value+ The current value, after type casting.
85
+ def changed_in_place?(raw_old_value, new_value)
86
+ false
87
+ end
88
+
89
+ def value_constructed_by_mass_assignment?(_value) # :nodoc:
90
+ false
91
+ end
92
+
93
+ def force_equality?(_value) # :nodoc:
94
+ false
95
+ end
96
+
97
+ def map(value) # :nodoc:
98
+ yield value
99
+ end
100
+
101
+ def ==(other)
102
+ self.class == other.class &&
103
+ precision == other.precision &&
104
+ scale == other.scale &&
105
+ limit == other.limit
106
+ end
107
+ alias eql? ==
108
+
109
+ def hash
110
+ [self.class, precision, scale, limit].hash
111
+ end
112
+
113
+ def assert_valid_value(*)
114
+ end
115
+
116
+ private
117
+
118
+ # Convenience method for types which do not need separate type casting
119
+ # behavior for user and database inputs. Called by Value#cast for
120
+ # values except +nil+.
121
+ def cast_value(value) # :doc:
122
+ value
123
+ end
124
+ end
125
+ end
126
+ end
@@ -0,0 +1,437 @@
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.to_s[0] == ?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 <tt>ActiveModel::Validations</tt> automatically adds an +errors+
35
+ # method to your instances initialized with a new <tt>ActiveModel::Errors</tt>
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
+ attr_accessor :validation_context
49
+ private :validation_context=
50
+ define_callbacks :validate, scope: :name
51
+
52
+ class_attribute :_validators, instance_writer: false, default: Hash.new { |h, k| h[k] = [] }
53
+ end
54
+
55
+ module ClassMethods
56
+ # Validates each attribute against a block.
57
+ #
58
+ # class Person
59
+ # include ActiveModel::Validations
60
+ #
61
+ # attr_accessor :first_name, :last_name
62
+ #
63
+ # validates_each :first_name, :last_name, allow_blank: true do |record, attr, value|
64
+ # record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
65
+ # end
66
+ # end
67
+ #
68
+ # Options:
69
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
70
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
71
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
72
+ # <tt>on: :custom_validation_context</tt> or
73
+ # <tt>on: [:create, :custom_validation_context]</tt>)
74
+ # * <tt>:allow_nil</tt> - Skip validation if attribute is +nil+.
75
+ # * <tt>:allow_blank</tt> - Skip validation if attribute is blank.
76
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
77
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
78
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
79
+ # proc or string should return or evaluate to a +true+ or +false+ value.
80
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
81
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
82
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
83
+ # method, proc or string should return or evaluate to a +true+ or +false+
84
+ # value.
85
+ def validates_each(*attr_names, &block)
86
+ validates_with BlockValidator, _merge_attributes(attr_names), &block
87
+ end
88
+
89
+ VALID_OPTIONS_FOR_VALIDATE = [:on, :if, :unless, :prepend].freeze # :nodoc:
90
+
91
+ # Adds a validation method or block to the class. This is useful when
92
+ # overriding the +validate+ instance method becomes too unwieldy and
93
+ # you're looking for more descriptive declaration of your validations.
94
+ #
95
+ # This can be done with a symbol pointing to a method:
96
+ #
97
+ # class Comment
98
+ # include ActiveModel::Validations
99
+ #
100
+ # validate :must_be_friends
101
+ #
102
+ # def must_be_friends
103
+ # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
104
+ # end
105
+ # end
106
+ #
107
+ # With a block which is passed with the current record to be validated:
108
+ #
109
+ # class Comment
110
+ # include ActiveModel::Validations
111
+ #
112
+ # validate do |comment|
113
+ # comment.must_be_friends
114
+ # end
115
+ #
116
+ # def must_be_friends
117
+ # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
118
+ # end
119
+ # end
120
+ #
121
+ # Or with a block where self points to the current record to be validated:
122
+ #
123
+ # class Comment
124
+ # include ActiveModel::Validations
125
+ #
126
+ # validate do
127
+ # errors.add(:base, 'Must be friends to leave a comment') unless commenter.friend_of?(commentee)
128
+ # end
129
+ # end
130
+ #
131
+ # Note that the return value of validation methods is not relevant.
132
+ # It's not possible to halt the validate callback chain.
133
+ #
134
+ # Options:
135
+ # * <tt>:on</tt> - Specifies the contexts where this validation is active.
136
+ # Runs in all validation contexts by default +nil+. You can pass a symbol
137
+ # or an array of symbols. (e.g. <tt>on: :create</tt> or
138
+ # <tt>on: :custom_validation_context</tt> or
139
+ # <tt>on: [:create, :custom_validation_context]</tt>)
140
+ # * <tt>:if</tt> - Specifies a method, proc or string to call to determine
141
+ # if the validation should occur (e.g. <tt>if: :allow_validation</tt>,
142
+ # or <tt>if: Proc.new { |user| user.signup_step > 2 }</tt>). The method,
143
+ # proc or string should return or evaluate to a +true+ or +false+ value.
144
+ # * <tt>:unless</tt> - Specifies a method, proc or string to call to
145
+ # determine if the validation should not occur (e.g. <tt>unless: :skip_validation</tt>,
146
+ # or <tt>unless: Proc.new { |user| user.signup_step <= 2 }</tt>). The
147
+ # method, proc or string should return or evaluate to a +true+ or +false+
148
+ # value.
149
+ #
150
+ # NOTE: Calling +validate+ multiple times on the same method will overwrite previous definitions.
151
+ #
152
+ def validate(*args, &block)
153
+ options = args.extract_options!
154
+
155
+ if args.all? { |arg| arg.is_a?(Symbol) }
156
+ options.each_key do |k|
157
+ unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
158
+ 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`?")
159
+ end
160
+ end
161
+ end
162
+
163
+ if options.key?(:on)
164
+ options = options.dup
165
+ options[:on] = Array(options[:on])
166
+ options[:if] = Array(options[:if])
167
+ options[:if].unshift ->(o) {
168
+ !(options[:on] & Array(o.validation_context)).empty?
169
+ }
170
+ end
171
+
172
+ set_callback(:validate, *args, options, &block)
173
+ end
174
+
175
+ # List all validators that are being used to validate the model using
176
+ # +validates_with+ method.
177
+ #
178
+ # class Person
179
+ # include ActiveModel::Validations
180
+ #
181
+ # validates_with MyValidator
182
+ # validates_with OtherValidator, on: :create
183
+ # validates_with StrictValidator, strict: true
184
+ # end
185
+ #
186
+ # Person.validators
187
+ # # => [
188
+ # # #<MyValidator:0x007fbff403e808 @options={}>,
189
+ # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
190
+ # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
191
+ # # ]
192
+ def validators
193
+ _validators.values.flatten.uniq
194
+ end
195
+
196
+ # Clears all of the validators and validations.
197
+ #
198
+ # Note that this will clear anything that is being used to validate
199
+ # the model for both the +validates_with+ and +validate+ methods.
200
+ # It clears the validators that are created with an invocation of
201
+ # +validates_with+ and the callbacks that are set by an invocation
202
+ # of +validate+.
203
+ #
204
+ # class Person
205
+ # include ActiveModel::Validations
206
+ #
207
+ # validates_with MyValidator
208
+ # validates_with OtherValidator, on: :create
209
+ # validates_with StrictValidator, strict: true
210
+ # validate :cannot_be_robot
211
+ #
212
+ # def cannot_be_robot
213
+ # errors.add(:base, 'A person cannot be a robot') if person_is_robot
214
+ # end
215
+ # end
216
+ #
217
+ # Person.validators
218
+ # # => [
219
+ # # #<MyValidator:0x007fbff403e808 @options={}>,
220
+ # # #<OtherValidator:0x007fbff403d930 @options={on: :create}>,
221
+ # # #<StrictValidator:0x007fbff3204a30 @options={strict:true}>
222
+ # # ]
223
+ #
224
+ # If one runs <tt>Person.clear_validators!</tt> and then checks to see what
225
+ # validators this class has, you would obtain:
226
+ #
227
+ # Person.validators # => []
228
+ #
229
+ # Also, the callback set by <tt>validate :cannot_be_robot</tt> will be erased
230
+ # so that:
231
+ #
232
+ # Person._validate_callbacks.empty? # => true
233
+ #
234
+ def clear_validators!
235
+ reset_callbacks(:validate)
236
+ _validators.clear
237
+ end
238
+
239
+ # List all validators that are being used to validate a specific attribute.
240
+ #
241
+ # class Person
242
+ # include ActiveModel::Validations
243
+ #
244
+ # attr_accessor :name , :age
245
+ #
246
+ # validates_presence_of :name
247
+ # validates_inclusion_of :age, in: 0..99
248
+ # end
249
+ #
250
+ # Person.validators_on(:name)
251
+ # # => [
252
+ # # #<ActiveModel::Validations::PresenceValidator:0x007fe604914e60 @attributes=[:name], @options={}>,
253
+ # # ]
254
+ def validators_on(*attributes)
255
+ attributes.flat_map do |attribute|
256
+ _validators[attribute.to_sym]
257
+ end
258
+ end
259
+
260
+ # Returns +true+ if +attribute+ is an attribute method, +false+ otherwise.
261
+ #
262
+ # class Person
263
+ # include ActiveModel::Validations
264
+ #
265
+ # attr_accessor :name
266
+ # end
267
+ #
268
+ # User.attribute_method?(:name) # => true
269
+ # User.attribute_method?(:age) # => false
270
+ def attribute_method?(attribute)
271
+ method_defined?(attribute)
272
+ end
273
+
274
+ # Copy validators on inheritance.
275
+ def inherited(base) #:nodoc:
276
+ dup = _validators.dup
277
+ base._validators = dup.each { |k, v| dup[k] = v.dup }
278
+ super
279
+ end
280
+ end
281
+
282
+ # Clean the +Errors+ object if instance is duped.
283
+ def initialize_dup(other) #:nodoc:
284
+ @errors = nil
285
+ super
286
+ end
287
+
288
+ # Returns the +Errors+ object that holds all information about attribute
289
+ # error messages.
290
+ #
291
+ # class Person
292
+ # include ActiveModel::Validations
293
+ #
294
+ # attr_accessor :name
295
+ # validates_presence_of :name
296
+ # end
297
+ #
298
+ # person = Person.new
299
+ # person.valid? # => false
300
+ # person.errors # => #<ActiveModel::Errors:0x007fe603816640 @messages={name:["can't be blank"]}>
301
+ def errors
302
+ @errors ||= Errors.new(self)
303
+ end
304
+
305
+ # Runs all the specified validations and returns +true+ if no errors were
306
+ # added otherwise +false+.
307
+ #
308
+ # class Person
309
+ # include ActiveModel::Validations
310
+ #
311
+ # attr_accessor :name
312
+ # validates_presence_of :name
313
+ # end
314
+ #
315
+ # person = Person.new
316
+ # person.name = ''
317
+ # person.valid? # => false
318
+ # person.name = 'david'
319
+ # person.valid? # => true
320
+ #
321
+ # Context can optionally be supplied to define which callbacks to test
322
+ # against (the context is defined on the validations using <tt>:on</tt>).
323
+ #
324
+ # class Person
325
+ # include ActiveModel::Validations
326
+ #
327
+ # attr_accessor :name
328
+ # validates_presence_of :name, on: :new
329
+ # end
330
+ #
331
+ # person = Person.new
332
+ # person.valid? # => true
333
+ # person.valid?(:new) # => false
334
+ def valid?(context = nil)
335
+ current_context, self.validation_context = validation_context, context
336
+ errors.clear
337
+ run_validations!
338
+ ensure
339
+ self.validation_context = current_context
340
+ end
341
+
342
+ alias_method :validate, :valid?
343
+
344
+ # Performs the opposite of <tt>valid?</tt>. Returns +true+ if errors were
345
+ # added, +false+ otherwise.
346
+ #
347
+ # class Person
348
+ # include ActiveModel::Validations
349
+ #
350
+ # attr_accessor :name
351
+ # validates_presence_of :name
352
+ # end
353
+ #
354
+ # person = Person.new
355
+ # person.name = ''
356
+ # person.invalid? # => true
357
+ # person.name = 'david'
358
+ # person.invalid? # => false
359
+ #
360
+ # Context can optionally be supplied to define which callbacks to test
361
+ # against (the context is defined on the validations using <tt>:on</tt>).
362
+ #
363
+ # class Person
364
+ # include ActiveModel::Validations
365
+ #
366
+ # attr_accessor :name
367
+ # validates_presence_of :name, on: :new
368
+ # end
369
+ #
370
+ # person = Person.new
371
+ # person.invalid? # => false
372
+ # person.invalid?(:new) # => true
373
+ def invalid?(context = nil)
374
+ !valid?(context)
375
+ end
376
+
377
+ # Runs all the validations within the specified context. Returns +true+ if
378
+ # no errors are found, raises +ValidationError+ otherwise.
379
+ #
380
+ # Validations with no <tt>:on</tt> option will run no matter the context. Validations with
381
+ # some <tt>:on</tt> option will only run in the specified context.
382
+ def validate!(context = nil)
383
+ valid?(context) || raise_validation_error
384
+ end
385
+
386
+ # Hook method defining how an attribute value should be retrieved. By default
387
+ # this is assumed to be an instance named after the attribute. Override this
388
+ # method in subclasses should you need to retrieve the value for a given
389
+ # attribute differently:
390
+ #
391
+ # class MyClass
392
+ # include ActiveModel::Validations
393
+ #
394
+ # def initialize(data = {})
395
+ # @data = data
396
+ # end
397
+ #
398
+ # def read_attribute_for_validation(key)
399
+ # @data[key]
400
+ # end
401
+ # end
402
+ alias :read_attribute_for_validation :send
403
+
404
+ private
405
+
406
+ def run_validations!
407
+ _run_validate_callbacks
408
+ errors.empty?
409
+ end
410
+
411
+ def raise_validation_error # :doc:
412
+ raise(ValidationError.new(self))
413
+ end
414
+ end
415
+
416
+ # = Active Model ValidationError
417
+ #
418
+ # Raised by <tt>validate!</tt> when the model is invalid. Use the
419
+ # +model+ method to retrieve the record which did not validate.
420
+ #
421
+ # begin
422
+ # complex_operation_that_internally_calls_validate!
423
+ # rescue ActiveModel::ValidationError => invalid
424
+ # puts invalid.model.errors
425
+ # end
426
+ class ValidationError < StandardError
427
+ attr_reader :model
428
+
429
+ def initialize(model)
430
+ @model = model
431
+ errors = @model.errors.full_messages.join(", ")
432
+ super(I18n.t(:"#{@model.class.i18n_scope}.errors.messages.model_invalid", errors: errors, default: :"errors.messages.model_invalid"))
433
+ end
434
+ end
435
+ end
436
+
437
+ Dir[File.expand_path("validations/*.rb", __dir__)].each { |file| require file }