activemodel 6.1.4.6 → 7.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 (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)