activemodel 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
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 }