activemodel 6.1.3.1 → 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 +29 -72
  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 +10 -4
  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 +14 -11
@@ -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)
@@ -1,27 +1,34 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_model/validations/comparability"
3
4
  require "bigdecimal/util"
4
5
 
5
6
  module ActiveModel
6
7
  module Validations
7
8
  class NumericalityValidator < EachValidator # :nodoc:
8
- CHECKS = { greater_than: :>, greater_than_or_equal_to: :>=,
9
- equal_to: :==, less_than: :<, less_than_or_equal_to: :<=,
10
- odd: :odd?, even: :even?, other_than: :!= }.freeze
9
+ include Comparability
11
10
 
12
- RESERVED_OPTIONS = CHECKS.keys + [:only_integer]
11
+ RANGE_CHECKS = { in: :in? }
12
+ NUMBER_CHECKS = { odd: :odd?, even: :even? }
13
+
14
+ RESERVED_OPTIONS = COMPARE_CHECKS.keys + NUMBER_CHECKS.keys + RANGE_CHECKS.keys + [:only_integer]
13
15
 
14
16
  INTEGER_REGEX = /\A[+-]?\d+\z/
15
17
 
16
18
  HEXADECIMAL_REGEX = /\A[+-]?0[xX]/
17
19
 
18
20
  def check_validity!
19
- keys = CHECKS.keys - [:odd, :even]
20
- options.slice(*keys).each do |option, value|
21
+ options.slice(*COMPARE_CHECKS.keys).each do |option, value|
21
22
  unless value.is_a?(Numeric) || value.is_a?(Proc) || value.is_a?(Symbol)
22
23
  raise ArgumentError, ":#{option} must be a number, a symbol or a proc"
23
24
  end
24
25
  end
26
+
27
+ options.slice(*RANGE_CHECKS.keys).each do |option, value|
28
+ unless value.is_a?(Range)
29
+ raise ArgumentError, ":#{option} must be a range"
30
+ end
31
+ end
25
32
  end
26
33
 
27
34
  def validate_each(record, attr_name, value, precision: Float::DIG, scale: nil)
@@ -37,23 +44,18 @@ module ActiveModel
37
44
 
38
45
  value = parse_as_number(value, precision, scale)
39
46
 
40
- options.slice(*CHECKS.keys).each do |option, option_value|
41
- case option
42
- when :odd, :even
43
- unless value.to_i.public_send(CHECKS[option])
47
+ options.slice(*RESERVED_OPTIONS).each do |option, option_value|
48
+ if NUMBER_CHECKS.include?(option)
49
+ unless value.to_i.public_send(NUMBER_CHECKS[option])
44
50
  record.errors.add(attr_name, option, **filtered_options(value))
45
51
  end
46
- else
47
- case option_value
48
- when Proc
49
- option_value = option_value.call(record)
50
- when Symbol
51
- option_value = record.send(option_value)
52
+ elsif RANGE_CHECKS.include?(option)
53
+ unless value.public_send(RANGE_CHECKS[option], option_value)
54
+ record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
52
55
  end
53
-
54
- option_value = parse_as_number(option_value, precision, scale)
55
-
56
- unless value.public_send(CHECKS[option], option_value)
56
+ elsif COMPARE_CHECKS.include?(option)
57
+ option_value = option_as_number(record, option_value, precision, scale)
58
+ unless value.public_send(COMPARE_CHECKS[option], option_value)
57
59
  record.errors.add(attr_name, option, **filtered_options(value).merge!(count: option_value))
58
60
  end
59
61
  end
@@ -61,6 +63,10 @@ module ActiveModel
61
63
  end
62
64
 
63
65
  private
66
+ def option_as_number(record, option_value, precision, scale)
67
+ parse_as_number(option_value(record, option_value), precision, scale)
68
+ end
69
+
64
70
  def parse_as_number(raw_value, precision, scale)
65
71
  if raw_value.is_a?(Float)
66
72
  parse_float(raw_value, precision, scale)
@@ -173,6 +179,7 @@ module ActiveModel
173
179
  # supplied value.
174
180
  # * <tt>:odd</tt> - Specifies the value must be an odd number.
175
181
  # * <tt>:even</tt> - Specifies the value must be an even number.
182
+ # * <tt>:in</tt> - Check that the value is within a range.
176
183
  #
177
184
  # There is also a list of default options supported by every validator:
178
185
  # +:if+, +:unless+, +:on+, +:allow_nil+, +:allow_blank+, and +:strict+ .
@@ -118,7 +118,7 @@ module ActiveModel
118
118
  # end
119
119
  # end
120
120
  #
121
- # Or with a block where self points to the current record to be validated:
121
+ # Or with a block where +self+ points to the current record to be validated:
122
122
  #
123
123
  # class Comment
124
124
  # include ActiveModel::Validations
@@ -152,7 +152,7 @@ module ActiveModel
152
152
  def validate(*args, &block)
153
153
  options = args.extract_options!
154
154
 
155
- if args.all? { |arg| arg.is_a?(Symbol) }
155
+ if args.all?(Symbol)
156
156
  options.each_key do |k|
157
157
  unless VALID_OPTIONS_FOR_VALIDATE.include?(k)
158
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`?")
@@ -272,7 +272,7 @@ module ActiveModel
272
272
  end
273
273
 
274
274
  # Copy validators on inheritance.
275
- def inherited(base) #:nodoc:
275
+ def inherited(base) # :nodoc:
276
276
  dup = _validators.dup
277
277
  base._validators = dup.each { |k, v| dup[k] = v.dup }
278
278
  super
@@ -280,7 +280,7 @@ module ActiveModel
280
280
  end
281
281
 
282
282
  # Clean the +Errors+ object if instance is duped.
283
- def initialize_dup(other) #:nodoc:
283
+ def initialize_dup(other) # :nodoc:
284
284
  @errors = nil
285
285
  super
286
286
  end
@@ -129,7 +129,7 @@ module ActiveModel
129
129
  # record, attribute and value.
130
130
  #
131
131
  # All \Active \Model validations are built on top of this validator.
132
- class EachValidator < Validator #:nodoc:
132
+ class EachValidator < Validator
133
133
  attr_reader :attributes
134
134
 
135
135
  # Returns a new validator instance. All options will be available via the
@@ -174,7 +174,7 @@ module ActiveModel
174
174
 
175
175
  # +BlockValidator+ is a special +EachValidator+ which receives a block on initialization
176
176
  # and call this block for each attribute being validated. +validates_each+ uses this validator.
177
- class BlockValidator < EachValidator #:nodoc:
177
+ class BlockValidator < EachValidator # :nodoc:
178
178
  def initialize(options, &block)
179
179
  @block = block
180
180
  super
data/lib/active_model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2004-2020 David Heinemeier Hansson
4
+ # Copyright (c) 2004-2021 David Heinemeier Hansson
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -30,6 +30,7 @@ require "active_model/version"
30
30
  module ActiveModel
31
31
  extend ActiveSupport::Autoload
32
32
 
33
+ autoload :API
33
34
  autoload :Attribute
34
35
  autoload :Attributes
35
36
  autoload :AttributeAssignment