activemodel 6.1.4.6 → 7.0.0.alpha1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +27 -114
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +3 -3
  5. data/lib/active_model/api.rb +99 -0
  6. data/lib/active_model/attribute.rb +4 -0
  7. data/lib/active_model/attribute_methods.rb +99 -52
  8. data/lib/active_model/attribute_set/builder.rb +1 -1
  9. data/lib/active_model/attribute_set.rb +4 -1
  10. data/lib/active_model/attributes.rb +15 -12
  11. data/lib/active_model/callbacks.rb +1 -1
  12. data/lib/active_model/conversion.rb +2 -2
  13. data/lib/active_model/dirty.rb +6 -5
  14. data/lib/active_model/errors.rb +20 -6
  15. data/lib/active_model/gem_version.rb +4 -4
  16. data/lib/active_model/locale/en.yml +1 -0
  17. data/lib/active_model/model.rb +6 -59
  18. data/lib/active_model/naming.rb +15 -8
  19. data/lib/active_model/serialization.rb +7 -2
  20. data/lib/active_model/translation.rb +1 -1
  21. data/lib/active_model/type/helpers/numeric.rb +9 -1
  22. data/lib/active_model/type/helpers/time_value.rb +2 -2
  23. data/lib/active_model/type/integer.rb +4 -1
  24. data/lib/active_model/type/registry.rb +9 -41
  25. data/lib/active_model/type/time.rb +1 -1
  26. data/lib/active_model/type.rb +6 -5
  27. data/lib/active_model/validations/absence.rb +1 -1
  28. data/lib/active_model/validations/clusivity.rb +1 -1
  29. data/lib/active_model/validations/comparability.rb +29 -0
  30. data/lib/active_model/validations/comparison.rb +82 -0
  31. data/lib/active_model/validations/confirmation.rb +4 -4
  32. data/lib/active_model/validations/numericality.rb +27 -20
  33. data/lib/active_model/validations.rb +4 -4
  34. data/lib/active_model/validator.rb +2 -2
  35. data/lib/active_model.rb +2 -1
  36. metadata +17 -14
@@ -96,10 +96,10 @@ module ActiveModel
96
96
  self.class._to_partial_path
97
97
  end
98
98
 
99
- module ClassMethods #:nodoc:
99
+ module ClassMethods # :nodoc:
100
100
  # Provide a class level cache for #to_partial_path. This is an
101
101
  # internal method and should not be accessed directly.
102
- def _to_partial_path #:nodoc:
102
+ def _to_partial_path # :nodoc:
103
103
  @_to_partial_path ||= begin
104
104
  element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(name))
105
105
  collection = ActiveSupport::Inflector.tableize(name)
@@ -123,10 +123,11 @@ module ActiveModel
123
123
  include ActiveModel::AttributeMethods
124
124
 
125
125
  included do
126
- attribute_method_suffix "_changed?", "_change", "_will_change!", "_was"
127
- attribute_method_suffix "_previously_changed?", "_previous_change", "_previously_was"
128
- attribute_method_affix prefix: "restore_", suffix: "!"
129
- attribute_method_affix prefix: "clear_", suffix: "_change"
126
+ attribute_method_suffix "_previously_changed?", "_changed?", parameters: "**options"
127
+ attribute_method_suffix "_change", "_will_change!", "_was", parameters: false
128
+ attribute_method_suffix "_previous_change", "_previously_was", parameters: false
129
+ attribute_method_affix prefix: "restore_", suffix: "!", parameters: false
130
+ attribute_method_affix prefix: "clear_", suffix: "_change", parameters: false
130
131
  end
131
132
 
132
133
  def initialize_dup(other) # :nodoc:
@@ -140,7 +141,7 @@ module ActiveModel
140
141
  end
141
142
 
142
143
  def as_json(options = {}) # :nodoc:
143
- options[:except] = [options[:except], "mutations_from_database"].flatten
144
+ options[:except] = [*options[:except], "mutations_from_database", "mutations_before_last_save"]
144
145
  super(options)
145
146
  end
146
147
 
@@ -249,7 +249,7 @@ module ActiveModel
249
249
 
250
250
  You are passing a block expecting two parameters,
251
251
  so the old hash behavior is simulated. As this is deprecated,
252
- this will result in an ArgumentError in Rails 6.2.
252
+ this will result in an ArgumentError in Rails 7.0.
253
253
  MSG
254
254
  @errors.
255
255
  sort { |a, b| a.attribute <=> b.attribute }.
@@ -325,7 +325,7 @@ module ActiveModel
325
325
 
326
326
  def to_h
327
327
  ActiveSupport::Deprecation.warn(<<~EOM)
328
- ActiveModel::Errors#to_h is deprecated and will be removed in Rails 6.2.
328
+ ActiveModel::Errors#to_h is deprecated and will be removed in Rails 7.0.
329
329
  Please use `ActiveModel::Errors.to_hash` instead. The values in the hash
330
330
  returned by `ActiveModel::Errors.to_hash` is an array of error messages.
331
331
  EOM
@@ -378,6 +378,14 @@ module ActiveModel
378
378
  # If +type+ is a symbol, it will be translated using the appropriate
379
379
  # scope (see +generate_message+).
380
380
  #
381
+ # person.errors.add(:name, :blank)
382
+ # person.errors.messages
383
+ # # => {:name=>["can't be blank"]}
384
+ #
385
+ # person.errors.add(:name, :too_long, { count: 25 })
386
+ # person.errors.messages
387
+ # # => ["is too long (maximum is 25 characters)"]
388
+ #
381
389
  # If +type+ is a proc, it will be called, allowing for things like
382
390
  # <tt>Time.now</tt> to be used within an error.
383
391
  #
@@ -563,6 +571,12 @@ module ActiveModel
563
571
  add_from_legacy_details_hash(data["details"]) if data.key?("details")
564
572
  end
565
573
 
574
+ def inspect # :nodoc:
575
+ inspection = @errors.inspect
576
+
577
+ "#<#{self.class.name} #{inspection}>"
578
+ end
579
+
566
580
  private
567
581
  def normalize_arguments(attribute, type, **options)
568
582
  # Evaluate proc first
@@ -583,7 +597,7 @@ module ActiveModel
583
597
  end
584
598
 
585
599
  def deprecation_removal_warning(method_name, alternative_message = nil)
586
- message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 6.2."
600
+ message = +"ActiveModel::Errors##{method_name} is deprecated and will be removed in Rails 7.0."
587
601
  if alternative_message
588
602
  message << "\n\nTo achieve the same use:\n\n "
589
603
  message << alternative_message
@@ -596,7 +610,7 @@ module ActiveModel
596
610
  end
597
611
  end
598
612
 
599
- class DeprecationHandlingMessageHash < SimpleDelegator
613
+ class DeprecationHandlingMessageHash < SimpleDelegator # :nodoc:
600
614
  def initialize(errors)
601
615
  @errors = errors
602
616
  super(prepare_content)
@@ -635,7 +649,7 @@ module ActiveModel
635
649
  end
636
650
  end
637
651
 
638
- class DeprecationHandlingMessageArray < SimpleDelegator
652
+ class DeprecationHandlingMessageArray < SimpleDelegator # :nodoc:
639
653
  def initialize(content, errors, attribute)
640
654
  @errors = errors
641
655
  @attribute = attribute
@@ -657,7 +671,7 @@ module ActiveModel
657
671
  end
658
672
  end
659
673
 
660
- class DeprecationHandlingDetailsHash < SimpleDelegator
674
+ class DeprecationHandlingDetailsHash < SimpleDelegator # :nodoc:
661
675
  def initialize(details)
662
676
  details.default = []
663
677
  details.freeze
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 6
11
- MINOR = 1
12
- TINY = 4
13
- PRE = "6"
10
+ MAJOR = 7
11
+ MINOR = 0
12
+ TINY = 0
13
+ PRE = "alpha1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -32,5 +32,6 @@ en:
32
32
  less_than: "must be less than %{count}"
33
33
  less_than_or_equal_to: "must be less than or equal to %{count}"
34
34
  other_than: "must be other than %{count}"
35
+ in: "must be in %{count}"
35
36
  odd: "must be odd"
36
37
  even: "must be even"
@@ -3,11 +3,10 @@
3
3
  module ActiveModel
4
4
  # == Active \Model \Basic \Model
5
5
  #
6
- # Includes the required interface for an object to interact with
7
- # Action Pack and Action View, using different Active Model modules.
8
- # It includes model name introspections, conversions, translations and
9
- # validations. Besides that, it allows you to initialize the object with a
10
- # hash of attributes, pretty much like Active Record does.
6
+ # Allows implementing models similar to <tt>ActiveRecord::Base</tt>.
7
+ # Includes <tt>ActiveModel::API</tt> for the required interface for an
8
+ # object to interact with Action Pack and Action View, but can be
9
+ # extended with other functionalities.
11
10
  #
12
11
  # A minimal implementation could be:
13
12
  #
@@ -20,23 +19,7 @@ module ActiveModel
20
19
  # person.name # => "bob"
21
20
  # person.age # => "18"
22
21
  #
23
- # Note that, by default, <tt>ActiveModel::Model</tt> implements <tt>persisted?</tt>
24
- # to return +false+, which is the most common case. You may want to override
25
- # it in your class to simulate a different scenario:
26
- #
27
- # class Person
28
- # include ActiveModel::Model
29
- # attr_accessor :id, :name
30
- #
31
- # def persisted?
32
- # self.id == 1
33
- # end
34
- # end
35
- #
36
- # person = Person.new(id: 1, name: 'bob')
37
- # person.persisted? # => true
38
- #
39
- # Also, if for some reason you need to run code on <tt>initialize</tt>, make
22
+ # If for some reason you need to run code on <tt>initialize</tt>, make
40
23
  # sure you call +super+ if you want the attributes hash initialization to
41
24
  # happen.
42
25
  #
@@ -58,42 +41,6 @@ module ActiveModel
58
41
  # (see below).
59
42
  module Model
60
43
  extend ActiveSupport::Concern
61
- include ActiveModel::AttributeAssignment
62
- include ActiveModel::Validations
63
- include ActiveModel::Conversion
64
-
65
- included do
66
- extend ActiveModel::Naming
67
- extend ActiveModel::Translation
68
- end
69
-
70
- # Initializes a new model with the given +params+.
71
- #
72
- # class Person
73
- # include ActiveModel::Model
74
- # attr_accessor :name, :age
75
- # end
76
- #
77
- # person = Person.new(name: 'bob', age: '18')
78
- # person.name # => "bob"
79
- # person.age # => "18"
80
- def initialize(attributes = {})
81
- assign_attributes(attributes) if attributes
82
-
83
- super()
84
- end
85
-
86
- # Indicates if the model is persisted. Default is +false+.
87
- #
88
- # class Person
89
- # include ActiveModel::Model
90
- # attr_accessor :id, :name
91
- # end
92
- #
93
- # person = Person.new(id: 1, name: 'bob')
94
- # person.persisted? # => false
95
- def persisted?
96
- false
97
- end
44
+ include ActiveModel::API
98
45
  end
99
46
  end
@@ -3,6 +3,7 @@
3
3
  require "active_support/core_ext/hash/except"
4
4
  require "active_support/core_ext/module/introspection"
5
5
  require "active_support/core_ext/module/redefine_method"
6
+ require "active_support/core_ext/module/delegation"
6
7
 
7
8
  module ActiveModel
8
9
  class Name
@@ -153,6 +154,7 @@ module ActiveModel
153
154
  # Returns a new ActiveModel::Name instance. By default, the +namespace+
154
155
  # and +name+ option will take the namespace and name of the given class
155
156
  # respectively.
157
+ # Use +locale+ argument for singularize and pluralize model name.
156
158
  #
157
159
  # module Foo
158
160
  # class Bar
@@ -161,7 +163,7 @@ module ActiveModel
161
163
  #
162
164
  # ActiveModel::Name.new(Foo::Bar).to_s
163
165
  # # => "Foo::Bar"
164
- def initialize(klass, namespace = nil, name = nil)
166
+ def initialize(klass, namespace = nil, name = nil, locale = :en)
165
167
  @name = name || klass.name
166
168
 
167
169
  raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
@@ -169,16 +171,17 @@ module ActiveModel
169
171
  @unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
170
172
  @klass = klass
171
173
  @singular = _singularize(@name)
172
- @plural = ActiveSupport::Inflector.pluralize(@singular)
174
+ @plural = ActiveSupport::Inflector.pluralize(@singular, locale)
175
+ @uncountable = @plural == @singular
173
176
  @element = ActiveSupport::Inflector.underscore(ActiveSupport::Inflector.demodulize(@name))
174
177
  @human = ActiveSupport::Inflector.humanize(@element)
175
178
  @collection = ActiveSupport::Inflector.tableize(@name)
176
179
  @param_key = (namespace ? _singularize(@unnamespaced) : @singular)
177
180
  @i18n_key = @name.underscore.to_sym
178
181
 
179
- @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key) : @plural.dup)
180
- @singular_route_key = ActiveSupport::Inflector.singularize(@route_key)
181
- @route_key << "_index" if @plural == @singular
182
+ @route_key = (namespace ? ActiveSupport::Inflector.pluralize(@param_key, locale) : @plural.dup)
183
+ @singular_route_key = ActiveSupport::Inflector.singularize(@route_key, locale)
184
+ @route_key << "_index" if @uncountable
182
185
  end
183
186
 
184
187
  # Transform the model name into a more human format, using I18n. By default,
@@ -206,6 +209,10 @@ module ActiveModel
206
209
  I18n.translate(defaults.shift, **options)
207
210
  end
208
211
 
212
+ def uncountable?
213
+ @uncountable
214
+ end
215
+
209
216
  private
210
217
  def _singularize(string)
211
218
  ActiveSupport::Inflector.underscore(string).tr("/", "_")
@@ -232,7 +239,7 @@ module ActiveModel
232
239
  # is required to pass the \Active \Model Lint test. So either extending the
233
240
  # provided method below, or rolling your own is required.
234
241
  module Naming
235
- def self.extended(base) #:nodoc:
242
+ def self.extended(base) # :nodoc:
236
243
  base.silence_redefinition_of_method :model_name
237
244
  base.delegate :model_name, to: :class
238
245
  end
@@ -279,7 +286,7 @@ module ActiveModel
279
286
  # ActiveModel::Naming.uncountable?(Sheep) # => true
280
287
  # ActiveModel::Naming.uncountable?(Post) # => false
281
288
  def self.uncountable?(record_or_class)
282
- plural(record_or_class) == singular(record_or_class)
289
+ model_name_from_record_or_class(record_or_class).uncountable?
283
290
  end
284
291
 
285
292
  # Returns string to use while generating route names. It differs for
@@ -321,7 +328,7 @@ module ActiveModel
321
328
  model_name_from_record_or_class(record_or_class).param_key
322
329
  end
323
330
 
324
- def self.model_name_from_record_or_class(record_or_class) #:nodoc:
331
+ def self.model_name_from_record_or_class(record_or_class) # :nodoc:
325
332
  if record_or_class.respond_to?(:to_model)
326
333
  record_or_class.to_model.model_name
327
334
  else
@@ -123,7 +123,7 @@ module ActiveModel
123
123
  # user.serializable_hash(include: { notes: { only: 'title' }})
124
124
  # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
125
125
  def serializable_hash(options = nil)
126
- attribute_names = attributes.keys
126
+ attribute_names = self.attribute_names
127
127
 
128
128
  return serializable_attributes(attribute_names) if options.blank?
129
129
 
@@ -148,6 +148,11 @@ module ActiveModel
148
148
  hash
149
149
  end
150
150
 
151
+ # Returns an array of attribute names as strings
152
+ def attribute_names # :nodoc:
153
+ attributes.keys
154
+ end
155
+
151
156
  private
152
157
  # Hook method defining how an attribute value should be retrieved for
153
158
  # serialization. By default this is assumed to be an instance named after
@@ -177,7 +182,7 @@ module ActiveModel
177
182
  # +association+ - name of the association
178
183
  # +records+ - the association record(s) to be serialized
179
184
  # +opts+ - options for the association records
180
- def serializable_add_includes(options = {}) #:nodoc:
185
+ def serializable_add_includes(options = {}) # :nodoc:
181
186
  return unless includes = options[:include]
182
187
 
183
188
  unless includes.is_a?(Hash)
@@ -17,7 +17,7 @@ module ActiveModel
17
17
  #
18
18
  # This also provides the required class methods for hooking into the
19
19
  # Rails internationalization API, including being able to define a
20
- # class based +i18n_scope+ and +lookup_ancestors+ to find translations in
20
+ # class-based +i18n_scope+ and +lookup_ancestors+ to find translations in
21
21
  # parent classes.
22
22
  module Translation
23
23
  include ActiveModel::Naming
@@ -25,10 +25,18 @@ module ActiveModel
25
25
  end
26
26
 
27
27
  def changed?(old_value, _new_value, new_value_before_type_cast) # :nodoc:
28
- super || number_to_non_number?(old_value, new_value_before_type_cast)
28
+ (super || number_to_non_number?(old_value, new_value_before_type_cast)) &&
29
+ !equal_nan?(old_value, new_value_before_type_cast)
29
30
  end
30
31
 
31
32
  private
33
+ def equal_nan?(old_value, new_value)
34
+ (old_value.is_a?(::Float) || old_value.is_a?(BigDecimal)) &&
35
+ old_value.nan? &&
36
+ old_value.instance_of?(new_value.class) &&
37
+ new_value.nan?
38
+ end
39
+
32
40
  def number_to_non_number?(old_value, new_value_before_type_cast)
33
41
  old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
34
42
  end
@@ -12,9 +12,9 @@ module ActiveModel
12
12
 
13
13
  if value.acts_like?(:time)
14
14
  if is_utc?
15
- value = value.getutc if value.respond_to?(:getutc) && !value.utc?
15
+ value = value.getutc if !value.utc?
16
16
  else
17
- value = value.getlocal if value.respond_to?(:getlocal)
17
+ value = value.getlocal
18
18
  end
19
19
  end
20
20
 
@@ -30,7 +30,10 @@ module ActiveModel
30
30
 
31
31
  def serializable?(value)
32
32
  cast_value = cast(value)
33
- in_range?(cast_value) && super
33
+ in_range?(cast_value) || begin
34
+ yield cast_value if block_given?
35
+ false
36
+ end
34
37
  end
35
38
 
36
39
  private
@@ -1,70 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # :stopdoc:
5
4
  module Type
6
- class Registry
5
+ class Registry # :nodoc:
7
6
  def initialize
8
- @registrations = []
7
+ @registrations = {}
9
8
  end
10
9
 
11
- def initialize_dup(other)
10
+ def initialize_copy(other)
12
11
  @registrations = @registrations.dup
13
12
  super
14
13
  end
15
14
 
16
- def register(type_name, klass = nil, **options, &block)
15
+ def register(type_name, klass = nil, &block)
17
16
  unless block_given?
18
17
  block = proc { |_, *args| klass.new(*args) }
19
18
  block.ruby2_keywords if block.respond_to?(:ruby2_keywords)
20
19
  end
21
- registrations << registration_klass.new(type_name, block, **options)
20
+ registrations[type_name] = block
22
21
  end
23
22
 
24
- def lookup(symbol, *args, **kwargs)
25
- registration = find_registration(symbol, *args, **kwargs)
23
+ def lookup(symbol, *args)
24
+ registration = registrations[symbol]
26
25
 
27
26
  if registration
28
- registration.call(self, symbol, *args, **kwargs)
27
+ registration.call(symbol, *args)
29
28
  else
30
29
  raise ArgumentError, "Unknown type #{symbol.inspect}"
31
30
  end
32
31
  end
32
+ ruby2_keywords(:lookup)
33
33
 
34
34
  private
35
35
  attr_reader :registrations
36
-
37
- def registration_klass
38
- Registration
39
- end
40
-
41
- def find_registration(symbol, *args, **kwargs)
42
- registrations.find { |r| r.matches?(symbol, *args, **kwargs) }
43
- end
44
- end
45
-
46
- class Registration
47
- # Options must be taken because of https://bugs.ruby-lang.org/issues/10856
48
- def initialize(name, block, **)
49
- @name = name
50
- @block = block
51
- end
52
-
53
- def call(_registry, *args, **kwargs)
54
- if kwargs.any? # https://bugs.ruby-lang.org/issues/10856
55
- block.call(*args, **kwargs)
56
- else
57
- block.call(*args)
58
- end
59
- end
60
-
61
- def matches?(type_name, *args, **kwargs)
62
- type_name == name
63
- end
64
-
65
- private
66
- attr_reader :name, :block
67
36
  end
68
37
  end
69
- # :startdoc:
70
38
  end
@@ -33,7 +33,7 @@ module ActiveModel
33
33
  return apply_seconds_precision(value) unless value.is_a?(::String)
34
34
  return if value.empty?
35
35
 
36
- dummy_time_value = value.sub(/\A(\d\d\d\d-\d\d-\d\d |)/, "2000-01-01 ")
36
+ dummy_time_value = value.sub(/\A\d{4}-\d\d-\d\d(?:T|\s)|/, "2000-01-01 ")
37
37
 
38
38
  fast_string_to_time(dummy_time_value) || begin
39
39
  time_hash = ::Date._parse(dummy_time_value)
@@ -24,13 +24,14 @@ module ActiveModel
24
24
  class << self
25
25
  attr_accessor :registry # :nodoc:
26
26
 
27
- # Add a new type to the registry, allowing it to be gotten through ActiveModel::Type#lookup
28
- def register(type_name, klass = nil, **options, &block)
29
- registry.register(type_name, klass, **options, &block)
27
+ # Add a new type to the registry, allowing it to be referenced as a
28
+ # symbol by {attribute}[rdoc-ref:Attributes::ClassMethods#attribute].
29
+ def register(type_name, klass = nil, &block)
30
+ registry.register(type_name, klass, &block)
30
31
  end
31
32
 
32
- def lookup(*args, **kwargs) # :nodoc:
33
- registry.lookup(*args, **kwargs)
33
+ def lookup(...) # :nodoc:
34
+ registry.lookup(...)
34
35
  end
35
36
 
36
37
  def default_value # :nodoc:
@@ -3,7 +3,7 @@
3
3
  module ActiveModel
4
4
  module Validations
5
5
  # == \Active \Model Absence Validator
6
- class AbsenceValidator < EachValidator #:nodoc:
6
+ class AbsenceValidator < EachValidator # :nodoc:
7
7
  def validate_each(record, attr_name, value)
8
8
  record.errors.add(attr_name, :present, **options) if value.present?
9
9
  end
@@ -4,7 +4,7 @@ require "active_support/core_ext/range"
4
4
 
5
5
  module ActiveModel
6
6
  module Validations
7
- module Clusivity #:nodoc:
7
+ module Clusivity # :nodoc:
8
8
  ERROR_MESSAGE = "An object with the method #include? or a proc, lambda or symbol is required, " \
9
9
  "and must be supplied as the :in (or :within) option of the configuration hash"
10
10
 
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Validations
5
+ module Comparability # :nodoc:
6
+ COMPARE_CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
7
+ equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
8
+ other_than: :!= }.freeze
9
+
10
+ def option_value(record, option_value)
11
+ case option_value
12
+ when Proc
13
+ option_value.call(record)
14
+ when Symbol
15
+ record.send(option_value)
16
+ else
17
+ option_value
18
+ end
19
+ end
20
+
21
+ def error_options(value, option_value)
22
+ options.except(*COMPARE_CHECKS.keys).merge!(
23
+ count: option_value,
24
+ value: value
25
+ )
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,82 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/validations/comparability"
4
+
5
+ module ActiveModel
6
+ module Validations
7
+ class ComparisonValidator < EachValidator # :nodoc:
8
+ include Comparability
9
+
10
+ def check_validity!
11
+ unless (options.keys & COMPARE_CHECKS.keys).any?
12
+ raise ArgumentError, "Expected one of :greater_than, :greater_than_or_equal_to, "\
13
+ ":equal_to, :less_than, :less_than_or_equal_to, or :other_than option to be supplied."
14
+ end
15
+ end
16
+
17
+ def validate_each(record, attr_name, value)
18
+ options.slice(*COMPARE_CHECKS.keys).each do |option, raw_option_value|
19
+ option_value = option_value(record, raw_option_value)
20
+
21
+ if value.nil? || value.blank?
22
+ return record.errors.add(attr_name, :blank, **error_options(value, option_value))
23
+ end
24
+
25
+ unless value.public_send(COMPARE_CHECKS[option], option_value)
26
+ record.errors.add(attr_name, option, **error_options(value, option_value))
27
+ end
28
+ rescue ArgumentError => e
29
+ record.errors.add(attr_name, e.message)
30
+ end
31
+ end
32
+ end
33
+
34
+ module HelperMethods
35
+ # Validates the value of a specified attribute fulfills all
36
+ # defined comparisons with another value, proc, or attribute.
37
+ #
38
+ # class Person < ActiveRecord::Base
39
+ # validates_comparison_of :value, greater_than: 'the sum of its parts'
40
+ # end
41
+ #
42
+ # Configuration options:
43
+ # * <tt>:message</tt> - A custom error message (default is: "failed comparison").
44
+ # * <tt>:greater_than</tt> - Specifies the value must be greater than the
45
+ # supplied value.
46
+ # * <tt>:greater_than_or_equal_to</tt> - Specifies the value must be
47
+ # greater than or equal to the supplied value.
48
+ # * <tt>:equal_to</tt> - Specifies the value must be equal to the supplied
49
+ # value.
50
+ # * <tt>:less_than</tt> - Specifies the value must be less than the
51
+ # supplied value.
52
+ # * <tt>:less_than_or_equal_to</tt> - Specifies the value must be less
53
+ # than or equal to the supplied value.
54
+ # * <tt>:other_than</tt> - Specifies the value must not be equal to the
55
+ # supplied value.
56
+ #
57
+ # There is also a list of default options supported by every validator:
58
+ # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
59
+ # See <tt>ActiveModel::Validations#validates</tt> for more information
60
+ #
61
+ # The validator requires at least one of the following checks to be supplied.
62
+ # Each will accept a proc, value, or a symbol which corresponds to a method:
63
+ #
64
+ # * <tt>:greater_than</tt>
65
+ # * <tt>:greater_than_or_equal_to</tt>
66
+ # * <tt>:equal_to</tt>
67
+ # * <tt>:less_than</tt>
68
+ # * <tt>:less_than_or_equal_to</tt>
69
+ # * <tt>:other_than</tt>
70
+ #
71
+ # For example:
72
+ #
73
+ # class Person < ActiveRecord::Base
74
+ # validates_comparison_of :birth_date, less_than_or_equal_to: -> { Date.today }
75
+ # validates_comparison_of :preferred_name, other_than: :given_name, allow_nil: true
76
+ # end
77
+ def validates_comparison_of(*attr_names)
78
+ validates_with ComparisonValidator, _merge_attributes(attr_names)
79
+ end
80
+ end
81
+ end
82
+ end
@@ -19,13 +19,13 @@ module ActiveModel
19
19
 
20
20
  private
21
21
  def setup!(klass)
22
- klass.attr_reader(*attributes.map do |attribute|
22
+ klass.attr_reader(*attributes.filter_map do |attribute|
23
23
  :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation")
24
- end.compact)
24
+ end)
25
25
 
26
- klass.attr_writer(*attributes.map do |attribute|
26
+ klass.attr_writer(*attributes.filter_map do |attribute|
27
27
  :"#{attribute}_confirmation" unless klass.method_defined?(:"#{attribute}_confirmation=")
28
- end.compact)
28
+ end)
29
29
  end
30
30
 
31
31
  def confirmation_value_equal?(record, attribute, value, confirmed)