activemodel 7.0.8 → 7.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +146 -178
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +9 -9
  5. data/lib/active_model/access.rb +16 -0
  6. data/lib/active_model/api.rb +5 -5
  7. data/lib/active_model/attribute/user_provided_default.rb +4 -0
  8. data/lib/active_model/attribute.rb +26 -1
  9. data/lib/active_model/attribute_assignment.rb +1 -1
  10. data/lib/active_model/attribute_methods.rb +102 -63
  11. data/lib/active_model/attribute_registration.rb +77 -0
  12. data/lib/active_model/attribute_set.rb +9 -0
  13. data/lib/active_model/attributes.rb +62 -45
  14. data/lib/active_model/callbacks.rb +5 -5
  15. data/lib/active_model/conversion.rb +14 -4
  16. data/lib/active_model/deprecator.rb +7 -0
  17. data/lib/active_model/dirty.rb +134 -13
  18. data/lib/active_model/error.rb +4 -3
  19. data/lib/active_model/errors.rb +11 -6
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +3 -3
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +1 -0
  24. data/lib/active_model/model.rb +26 -2
  25. data/lib/active_model/naming.rb +29 -10
  26. data/lib/active_model/railtie.rb +4 -0
  27. data/lib/active_model/secure_password.rb +61 -23
  28. data/lib/active_model/serialization.rb +3 -3
  29. data/lib/active_model/serializers/json.rb +1 -1
  30. data/lib/active_model/translation.rb +18 -16
  31. data/lib/active_model/type/big_integer.rb +23 -1
  32. data/lib/active_model/type/binary.rb +7 -1
  33. data/lib/active_model/type/boolean.rb +11 -9
  34. data/lib/active_model/type/date.rb +28 -2
  35. data/lib/active_model/type/date_time.rb +45 -3
  36. data/lib/active_model/type/decimal.rb +39 -1
  37. data/lib/active_model/type/float.rb +30 -1
  38. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
  39. data/lib/active_model/type/helpers/numeric.rb +4 -0
  40. data/lib/active_model/type/helpers/time_value.rb +28 -12
  41. data/lib/active_model/type/immutable_string.rb +37 -1
  42. data/lib/active_model/type/integer.rb +44 -1
  43. data/lib/active_model/type/serialize_cast_value.rb +47 -0
  44. data/lib/active_model/type/string.rb +9 -1
  45. data/lib/active_model/type/time.rb +48 -7
  46. data/lib/active_model/type/value.rb +17 -1
  47. data/lib/active_model/type.rb +1 -0
  48. data/lib/active_model/validations/absence.rb +1 -1
  49. data/lib/active_model/validations/acceptance.rb +1 -1
  50. data/lib/active_model/validations/callbacks.rb +4 -4
  51. data/lib/active_model/validations/clusivity.rb +5 -8
  52. data/lib/active_model/validations/comparability.rb +0 -11
  53. data/lib/active_model/validations/comparison.rb +15 -7
  54. data/lib/active_model/validations/format.rb +6 -7
  55. data/lib/active_model/validations/length.rb +10 -8
  56. data/lib/active_model/validations/numericality.rb +35 -23
  57. data/lib/active_model/validations/presence.rb +1 -1
  58. data/lib/active_model/validations/resolve_value.rb +26 -0
  59. data/lib/active_model/validations/validates.rb +3 -3
  60. data/lib/active_model/validations/with.rb +9 -2
  61. data/lib/active_model/validations.rb +44 -9
  62. data/lib/active_model/validator.rb +7 -5
  63. data/lib/active_model/version.rb +1 -1
  64. data/lib/active_model.rb +5 -1
  65. metadata +12 -7
@@ -3,7 +3,7 @@
3
3
  require "active_support/core_ext/enumerable"
4
4
 
5
5
  module ActiveModel
6
- # == Active \Model \Serialization
6
+ # = Active \Model \Serialization
7
7
  #
8
8
  # Provides a basic serialization to a serializable_hash for your objects.
9
9
  #
@@ -33,8 +33,8 @@ module ActiveModel
33
33
  # at the private method +read_attribute_for_serialization+.
34
34
  #
35
35
  # ActiveModel::Serializers::JSON module automatically includes
36
- # the <tt>ActiveModel::Serialization</tt> module, so there is no need to
37
- # explicitly include <tt>ActiveModel::Serialization</tt>.
36
+ # the +ActiveModel::Serialization+ module, so there is no need to
37
+ # explicitly include +ActiveModel::Serialization+.
38
38
  #
39
39
  # A minimal implementation including JSON would be:
40
40
  #
@@ -4,7 +4,7 @@ require "active_support/json"
4
4
 
5
5
  module ActiveModel
6
6
  module Serializers
7
- # == Active \Model \JSON \Serializer
7
+ # = Active \Model \JSON \Serializer
8
8
  module JSON
9
9
  extend ActiveSupport::Concern
10
10
  include ActiveModel::Serialization
@@ -1,9 +1,9 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveModel
4
- # == Active \Model \Translation
4
+ # = Active \Model \Translation
5
5
  #
6
- # Provides integration between your object and the Rails internationalization
6
+ # Provides integration between your object and the \Rails internationalization
7
7
  # (i18n) framework.
8
8
  #
9
9
  # A minimal implementation could be:
@@ -16,7 +16,7 @@ module ActiveModel
16
16
  # # => "My attribute"
17
17
  #
18
18
  # This also provides the required class methods for hooking into the
19
- # Rails internationalization API, including being able to define a
19
+ # \Rails internationalization API, including being able to define a
20
20
  # class-based +i18n_scope+ and +lookup_ancestors+ to find translations in
21
21
  # parent classes.
22
22
  module Translation
@@ -35,6 +35,8 @@ module ActiveModel
35
35
  ancestors.select { |x| x.respond_to?(:model_name) }
36
36
  end
37
37
 
38
+ MISSING_TRANSLATION = -(2**60) # :nodoc:
39
+
38
40
  # Transforms attribute names into a more human format, such as "First name"
39
41
  # instead of "first_name".
40
42
  #
@@ -42,29 +44,29 @@ module ActiveModel
42
44
  #
43
45
  # Specify +options+ with additional translating options.
44
46
  def human_attribute_name(attribute, options = {})
45
- options = { count: 1 }.merge!(options)
46
- parts = attribute.to_s.split(".")
47
- attribute = parts.pop
48
- namespace = parts.join("/") unless parts.empty?
49
- attributes_scope = "#{i18n_scope}.attributes"
47
+ attribute = attribute.to_s
48
+
49
+ if attribute.include?(".")
50
+ namespace, _, attribute = attribute.rpartition(".")
51
+ namespace.tr!(".", "/")
50
52
 
51
- if namespace
52
53
  defaults = lookup_ancestors.map do |klass|
53
- :"#{attributes_scope}.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
54
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
54
55
  end
55
- defaults << :"#{attributes_scope}.#{namespace}.#{attribute}"
56
+ defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
56
57
  else
57
58
  defaults = lookup_ancestors.map do |klass|
58
- :"#{attributes_scope}.#{klass.model_name.i18n_key}.#{attribute}"
59
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
59
60
  end
60
61
  end
61
62
 
62
63
  defaults << :"attributes.#{attribute}"
63
- defaults << options.delete(:default) if options[:default]
64
- defaults << attribute.humanize
64
+ defaults << options[:default] if options[:default]
65
+ defaults << MISSING_TRANSLATION
65
66
 
66
- options[:default] = defaults
67
- I18n.translate(defaults.shift, **options)
67
+ translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
68
+ translation = attribute.humanize if translation == MISSING_TRANSLATION
69
+ translation
68
70
  end
69
71
  end
70
72
  end
@@ -4,7 +4,29 @@ require "active_model/type/integer"
4
4
 
5
5
  module ActiveModel
6
6
  module Type
7
- class BigInteger < Integer # :nodoc:
7
+ # = Active Model \BigInteger \Type
8
+ #
9
+ # Attribute type for integers that can be serialized to an unlimited number
10
+ # of bytes. This type is registered under the +:big_integer+ key.
11
+ #
12
+ # class Person
13
+ # include ActiveModel::Attributes
14
+ #
15
+ # attribute :id, :big_integer
16
+ # end
17
+ #
18
+ # person = Person.new
19
+ # person.id = "18_000_000_000"
20
+ #
21
+ # person.id # => 18000000000
22
+ #
23
+ # All casting and serialization are performed in the same way as the
24
+ # standard ActiveModel::Type::Integer type.
25
+ class BigInteger < Integer
26
+ def serialize_cast_value(value) # :nodoc:
27
+ value
28
+ end
29
+
8
30
  private
9
31
  def max_value
10
32
  ::Float::INFINITY
@@ -2,7 +2,13 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class Binary < Value # :nodoc:
5
+ # = Active Model \Binary \Type
6
+ #
7
+ # Attribute type for representation of binary data. This type is registered
8
+ # under the +:binary+ key.
9
+ #
10
+ # Non-string values are coerced to strings using their +to_s+ method.
11
+ class Binary < Value
6
12
  def type
7
13
  :binary
8
14
  end
@@ -2,17 +2,15 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- # == Active \Model \Type \Boolean
5
+ # = Active Model \Boolean \Type
6
6
  #
7
- # A class that behaves like a boolean type, including rules for coercion of user input.
7
+ # A class that behaves like a boolean type, including rules for coercion of
8
+ # user input.
8
9
  #
9
- # === Coercion
10
- # Values set from user input will first be coerced into the appropriate ruby type.
11
- # Coercion behavior is roughly mapped to Ruby's boolean semantics.
12
- #
13
- # - "false", "f" , "0", +0+ or any other value in +FALSE_VALUES+ will be coerced to +false+
14
- # - Empty strings are coerced to +nil+
15
- # - All other values will be coerced to +true+
10
+ # - <tt>"false"</tt>, <tt>"f"</tt>, <tt>"0"</tt>, +0+ or any other value in
11
+ # +FALSE_VALUES+ will be coerced to +false+.
12
+ # - Empty strings are coerced to +nil+.
13
+ # - All other values will be coerced to +true+.
16
14
  class Boolean < Value
17
15
  FALSE_VALUES = [
18
16
  false, 0,
@@ -33,6 +31,10 @@ module ActiveModel
33
31
  cast(value)
34
32
  end
35
33
 
34
+ def serialize_cast_value(value) # :nodoc:
35
+ value
36
+ end
37
+
36
38
  private
37
39
  def cast_value(value)
38
40
  if value == ""
@@ -2,7 +2,28 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class Date < Value # :nodoc:
5
+ # = Active Model \Date \Type
6
+ #
7
+ # Attribute type for date representation. It is registered under the
8
+ # +:date+ key.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :birthday, :date
14
+ # end
15
+ #
16
+ # person = Person.new
17
+ # person.birthday = "1989-07-13"
18
+ #
19
+ # person.birthday.class # => Date
20
+ # person.birthday.year # => 1989
21
+ # person.birthday.month # => 7
22
+ # person.birthday.day # => 13
23
+ #
24
+ # String values are parsed using the ISO 8601 date format. Any other values
25
+ # are cast using their +to_date+ method, if it exists.
26
+ class Date < Value
6
27
  include Helpers::Timezone
7
28
  include Helpers::AcceptsMultiparameterTime.new
8
29
 
@@ -34,7 +55,12 @@ module ActiveModel
34
55
  end
35
56
 
36
57
  def fallback_string_to_date(string)
37
- new_date(*::Date._parse(string, false).values_at(:year, :mon, :mday))
58
+ parts = begin
59
+ ::Date._parse(string, false)
60
+ rescue ArgumentError
61
+ end
62
+
63
+ new_date(*parts.values_at(:year, :mon, :mday)) if parts
38
64
  end
39
65
 
40
66
  def new_date(year, mon, mday)
@@ -2,12 +2,49 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class DateTime < Value # :nodoc:
5
+ # = Active Model \DateTime \Type
6
+ #
7
+ # Attribute type to represent dates and times. It is registered under the
8
+ # +:datetime+ key.
9
+ #
10
+ # class Event
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :start, :datetime
14
+ # end
15
+ #
16
+ # event = Event.new
17
+ # event.start = "Wed, 04 Sep 2013 03:00:00 EAT"
18
+ #
19
+ # event.start.class # => Time
20
+ # event.start.year # => 2013
21
+ # event.start.month # => 9
22
+ # event.start.day # => 4
23
+ # event.start.hour # => 3
24
+ # event.start.min # => 0
25
+ # event.start.sec # => 0
26
+ # event.start.zone # => "EAT"
27
+ #
28
+ # String values are parsed using the ISO 8601 datetime format. Partial
29
+ # time-only formats are also accepted.
30
+ #
31
+ # event.start = "06:07:08+09:00"
32
+ # event.start.utc # => 1999-12-31 21:07:08 UTC
33
+ #
34
+ # The degree of sub-second precision can be customized when declaring an
35
+ # attribute:
36
+ #
37
+ # class Event
38
+ # include ActiveModel::Attributes
39
+ #
40
+ # attribute :start, :datetime, precision: 4
41
+ # end
42
+ class DateTime < Value
6
43
  include Helpers::Timezone
7
- include Helpers::TimeValue
8
44
  include Helpers::AcceptsMultiparameterTime.new(
9
45
  defaults: { 4 => 0, 5 => 0 }
10
46
  )
47
+ include Helpers::TimeValue
11
48
 
12
49
  def type
13
50
  :datetime
@@ -28,7 +65,12 @@ module ActiveModel
28
65
  end
29
66
 
30
67
  def fallback_string_to_time(string)
31
- time_hash = ::Date._parse(string)
68
+ time_hash = begin
69
+ ::Date._parse(string)
70
+ rescue ArgumentError
71
+ end
72
+ return unless time_hash
73
+
32
74
  time_hash[:sec_fraction] = microseconds(time_hash)
33
75
 
34
76
  new_time(*time_hash.values_at(:year, :mon, :mday, :hour, :min, :sec, :sec_fraction, :offset))
@@ -4,7 +4,45 @@ require "bigdecimal/util"
4
4
 
5
5
  module ActiveModel
6
6
  module Type
7
- class Decimal < Value # :nodoc:
7
+ # = Active Model \Decimal \Type
8
+ #
9
+ # Attribute type for decimal, high-precision floating point numeric
10
+ # representation. It is registered under the +:decimal+ key.
11
+ #
12
+ # class BagOfCoffee
13
+ # include ActiveModel::Attributes
14
+ #
15
+ # attribute :weight, :decimal
16
+ # end
17
+ #
18
+ # Numeric instances are converted to BigDecimal instances. Any other objects
19
+ # are cast using their +to_d+ method, except for blank strings, which are
20
+ # cast to +nil+. If a +to_d+ method is not defined, the object is converted
21
+ # to a string using +to_s+, which is then cast using +to_d+.
22
+ #
23
+ # bag = BagOfCoffee.new
24
+ #
25
+ # bag.weight = 0.01
26
+ # bag.weight # => 0.1e-1
27
+ #
28
+ # bag.weight = "0.01"
29
+ # bag.weight # => 0.1e-1
30
+ #
31
+ # bag.weight = ""
32
+ # bag.weight # => nil
33
+ #
34
+ # bag.weight = :arbitrary
35
+ # bag.weight # => nil (the result of `.to_s.to_d`)
36
+ #
37
+ # Decimal precision defaults to 18, and can be customized when declaring an
38
+ # attribute:
39
+ #
40
+ # class BagOfCoffee
41
+ # include ActiveModel::Attributes
42
+ #
43
+ # attribute :weight, :decimal, precision: 24
44
+ # end
45
+ class Decimal < Value
8
46
  include Helpers::Numeric
9
47
  BIGDECIMAL_PRECISION = 18
10
48
 
@@ -4,7 +4,36 @@ require "active_support/core_ext/object/try"
4
4
 
5
5
  module ActiveModel
6
6
  module Type
7
- class Float < Value # :nodoc:
7
+ # = Active Model \Float \Type
8
+ #
9
+ # Attribute type for floating point numeric values. It is registered under
10
+ # the +:float+ key.
11
+ #
12
+ # class BagOfCoffee
13
+ # include ActiveModel::Attributes
14
+ #
15
+ # attribute :weight, :float
16
+ # end
17
+ #
18
+ # Values are cast using their +to_f+ method, except for the following
19
+ # strings:
20
+ #
21
+ # - Blank strings are cast to +nil+.
22
+ # - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
23
+ # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
24
+ # - <tt>"NaN"</tt> is cast to +Float::NAN+.
25
+ #
26
+ # bag = BagOfCoffee.new
27
+ #
28
+ # bag.weight = "0.25"
29
+ # bag.weight # => 0.25
30
+ #
31
+ # bag.weight = ""
32
+ # bag.weight # => nil
33
+ #
34
+ # bag.weight = "NaN"
35
+ # bag.weight # => Float::NAN
36
+ class Float < Value
8
37
  include Helpers::Numeric
9
38
 
10
39
  def type
@@ -6,7 +6,11 @@ module ActiveModel
6
6
  class AcceptsMultiparameterTime < Module
7
7
  module InstanceMethods
8
8
  def serialize(value)
9
- super(cast(value))
9
+ serialize_cast_value(cast(value))
10
+ end
11
+
12
+ def serialize_cast_value(value)
13
+ value
10
14
  end
11
15
 
12
16
  def cast(value)
@@ -8,6 +8,10 @@ module ActiveModel
8
8
  cast(value)
9
9
  end
10
10
 
11
+ def serialize_cast_value(value)
12
+ value
13
+ end
14
+
11
15
  def cast(value)
12
16
  # Checks whether the value is numeric. Spaceship operator
13
17
  # will return nil if value is not numeric.
@@ -7,7 +7,7 @@ module ActiveModel
7
7
  module Type
8
8
  module Helpers # :nodoc: all
9
9
  module TimeValue
10
- def serialize(value)
10
+ def serialize_cast_value(value)
11
11
  value = apply_seconds_precision(value)
12
12
 
13
13
  if value.acts_like?(:time)
@@ -69,20 +69,36 @@ module ActiveModel
69
69
  \z
70
70
  /x
71
71
 
72
- def fast_string_to_time(string)
73
- return unless ISO_DATETIME =~ string
74
-
75
- usec = $7.to_i
76
- usec_len = $7&.length
77
- if usec_len&.< 6
78
- usec *= 10**(6 - usec_len)
72
+ if RUBY_VERSION >= "3.2"
73
+ def fast_string_to_time(string)
74
+ return unless ISO_DATETIME.match?(string)
75
+
76
+ if is_utc?
77
+ # XXX: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0 used to return an invalid Time object
78
+ # see: https://bugs.ruby-lang.org/issues/19292
79
+ ::Time.at(::Time.new(string, in: "UTC"))
80
+ else
81
+ ::Time.new(string)
82
+ end
83
+ rescue ArgumentError
84
+ nil
79
85
  end
86
+ else
87
+ def fast_string_to_time(string)
88
+ return unless ISO_DATETIME =~ string
80
89
 
81
- if $8
82
- offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
83
- end
90
+ usec = $7.to_i
91
+ usec_len = $7&.length
92
+ if usec_len&.< 6
93
+ usec *= 10**(6 - usec_len)
94
+ end
84
95
 
85
- new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
96
+ if $8
97
+ offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
98
+ end
99
+
100
+ new_time($1.to_i, $2.to_i, $3.to_i, $4.to_i, $5.to_i, $6.to_i, usec, offset)
101
+ end
86
102
  end
87
103
  end
88
104
  end
@@ -2,7 +2,39 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class ImmutableString < Value # :nodoc:
5
+ # = Active Model \ImmutableString \Type
6
+ #
7
+ # Attribute type to represent immutable strings. It casts incoming values to
8
+ # frozen strings.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :name, :immutable_string
14
+ # end
15
+ #
16
+ # person = Person.new
17
+ # person.name = 1
18
+ #
19
+ # person.name # => "1"
20
+ # person.name.frozen? # => true
21
+ #
22
+ # Values are coerced to strings using their +to_s+ method. Boolean values
23
+ # are treated differently, however: +true+ will be cast to <tt>"t"</tt> and
24
+ # +false+ will be cast to <tt>"f"</tt>. These strings can be customized when
25
+ # declaring an attribute:
26
+ #
27
+ # class Person
28
+ # include ActiveModel::Attributes
29
+ #
30
+ # attribute :active, :immutable_string, true: "aye", false: "nay"
31
+ # end
32
+ #
33
+ # person = Person.new
34
+ # person.active = true
35
+ #
36
+ # person.active # => "aye"
37
+ class ImmutableString < Value
6
38
  def initialize(**args)
7
39
  @true = -(args.delete(:true)&.to_s || "t")
8
40
  @false = -(args.delete(:false)&.to_s || "f")
@@ -22,6 +54,10 @@ module ActiveModel
22
54
  end
23
55
  end
24
56
 
57
+ def serialize_cast_value(value) # :nodoc:
58
+ value
59
+ end
60
+
25
61
  private
26
62
  def cast_value(value)
27
63
  case value
@@ -2,7 +2,46 @@
2
2
 
3
3
  module ActiveModel
4
4
  module Type
5
- class Integer < Value # :nodoc:
5
+ # = Active Model \Integer \Type
6
+ #
7
+ # Attribute type for integer representation. This type is registered under
8
+ # the +:integer+ key.
9
+ #
10
+ # class Person
11
+ # include ActiveModel::Attributes
12
+ #
13
+ # attribute :age, :integer
14
+ # end
15
+ #
16
+ # Values are cast using their +to_i+ method, except for blank strings, which
17
+ # are cast to +nil+. If a +to_i+ method is not defined or raises an error,
18
+ # the value will be cast to +nil+.
19
+ #
20
+ # person = Person.new
21
+ #
22
+ # person.age = "18"
23
+ # person.age # => 18
24
+ #
25
+ # person.age = ""
26
+ # person.age # => nil
27
+ #
28
+ # person.age = :not_an_integer
29
+ # person.age # => nil (because Symbol does not define #to_i)
30
+ #
31
+ # Serialization also works under the same principle. Non-numeric strings are
32
+ # serialized as +nil+, for example.
33
+ #
34
+ # Serialization also validates that the integer can be stored using a
35
+ # limited number of bytes. If it cannot, an ActiveModel::RangeError will be
36
+ # raised. The default limit is 4 bytes, and can be customized when declaring
37
+ # an attribute:
38
+ #
39
+ # class Person
40
+ # include ActiveModel::Attributes
41
+ #
42
+ # attribute :age, :integer, limit: 6
43
+ # end
44
+ class Integer < Value
6
45
  include Helpers::Numeric
7
46
 
8
47
  # Column storage size in bytes.
@@ -28,6 +67,10 @@ module ActiveModel
28
67
  ensure_in_range(super)
29
68
  end
30
69
 
70
+ def serialize_cast_value(value) # :nodoc:
71
+ ensure_in_range(value)
72
+ end
73
+
31
74
  def serializable?(value)
32
75
  cast_value = cast(value)
33
76
  in_range?(cast_value) || begin
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module SerializeCastValue # :nodoc:
6
+ extend ActiveSupport::Concern
7
+
8
+ module ClassMethods
9
+ def serialize_cast_value_compatible?
10
+ return @serialize_cast_value_compatible if defined?(@serialize_cast_value_compatible)
11
+ @serialize_cast_value_compatible = ancestors.index(instance_method(:serialize_cast_value).owner) <= ancestors.index(instance_method(:serialize).owner)
12
+ end
13
+ end
14
+
15
+ module DefaultImplementation
16
+ def serialize_cast_value(value)
17
+ value
18
+ end
19
+ end
20
+
21
+ def self.included(klass)
22
+ klass.include DefaultImplementation unless klass.method_defined?(:serialize_cast_value)
23
+ end
24
+
25
+ def self.serialize(type, value)
26
+ # Using `type.equal?(type.itself_if_...)` is a performant way to also
27
+ # ensure that `type` is not just a DelegateClass instance (e.g.
28
+ # ActiveRecord::Type::Serialized) unintentionally delegating
29
+ # SerializeCastValue methods.
30
+ if type.equal?((type.itself_if_serialize_cast_value_compatible rescue nil))
31
+ type.serialize_cast_value(value)
32
+ else
33
+ type.serialize(value)
34
+ end
35
+ end
36
+
37
+ def itself_if_serialize_cast_value_compatible
38
+ self if self.class.serialize_cast_value_compatible?
39
+ end
40
+
41
+ def initialize(...)
42
+ super
43
+ self.class.serialize_cast_value_compatible? # eagerly compute
44
+ end
45
+ end
46
+ end
47
+ end
@@ -4,7 +4,15 @@ require "active_model/type/immutable_string"
4
4
 
5
5
  module ActiveModel
6
6
  module Type
7
- class String < ImmutableString # :nodoc:
7
+ # = Active Model \String \Type
8
+ #
9
+ # Attribute type for strings. It is registered under the +:string+ key.
10
+ #
11
+ # This class is a specialization of ActiveModel::Type::ImmutableString. It
12
+ # performs coercion in the same way, and can be configured in the same way.
13
+ # However, it accounts for mutable strings, so dirty tracking can properly
14
+ # check if a string has changed.
15
+ class String < ImmutableString
8
16
  def changed_in_place?(raw_old_value, new_value)
9
17
  if new_value.is_a?(::String)
10
18
  raw_old_value != new_value