activemodel 7.0.8 → 7.1.0
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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +145 -182
- data/MIT-LICENSE +1 -1
- data/README.rdoc +9 -9
- data/lib/active_model/access.rb +16 -0
- data/lib/active_model/api.rb +5 -5
- data/lib/active_model/attribute/user_provided_default.rb +4 -0
- data/lib/active_model/attribute.rb +26 -1
- data/lib/active_model/attribute_assignment.rb +1 -1
- data/lib/active_model/attribute_methods.rb +102 -63
- data/lib/active_model/attribute_registration.rb +77 -0
- data/lib/active_model/attribute_set.rb +9 -0
- data/lib/active_model/attributes.rb +62 -45
- data/lib/active_model/callbacks.rb +5 -5
- data/lib/active_model/conversion.rb +14 -4
- data/lib/active_model/deprecator.rb +7 -0
- data/lib/active_model/dirty.rb +134 -13
- data/lib/active_model/error.rb +4 -3
- data/lib/active_model/errors.rb +11 -6
- data/lib/active_model/forbidden_attributes_protection.rb +2 -0
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +1 -1
- data/lib/active_model/locale/en.yml +1 -0
- data/lib/active_model/model.rb +26 -2
- data/lib/active_model/naming.rb +29 -10
- data/lib/active_model/railtie.rb +4 -0
- data/lib/active_model/secure_password.rb +61 -23
- data/lib/active_model/serialization.rb +3 -3
- data/lib/active_model/serializers/json.rb +1 -1
- data/lib/active_model/translation.rb +18 -16
- data/lib/active_model/type/big_integer.rb +23 -1
- data/lib/active_model/type/binary.rb +7 -1
- data/lib/active_model/type/boolean.rb +11 -9
- data/lib/active_model/type/date.rb +28 -2
- data/lib/active_model/type/date_time.rb +45 -3
- data/lib/active_model/type/decimal.rb +39 -1
- data/lib/active_model/type/float.rb +30 -1
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +5 -1
- data/lib/active_model/type/helpers/numeric.rb +4 -0
- data/lib/active_model/type/helpers/time_value.rb +28 -12
- data/lib/active_model/type/immutable_string.rb +37 -1
- data/lib/active_model/type/integer.rb +44 -1
- data/lib/active_model/type/serialize_cast_value.rb +47 -0
- data/lib/active_model/type/string.rb +9 -1
- data/lib/active_model/type/time.rb +48 -7
- data/lib/active_model/type/value.rb +17 -1
- data/lib/active_model/type.rb +1 -0
- data/lib/active_model/validations/absence.rb +1 -1
- data/lib/active_model/validations/acceptance.rb +1 -1
- data/lib/active_model/validations/callbacks.rb +4 -4
- data/lib/active_model/validations/clusivity.rb +5 -8
- data/lib/active_model/validations/comparability.rb +0 -11
- data/lib/active_model/validations/comparison.rb +15 -7
- data/lib/active_model/validations/format.rb +6 -7
- data/lib/active_model/validations/length.rb +10 -8
- data/lib/active_model/validations/numericality.rb +35 -23
- data/lib/active_model/validations/presence.rb +1 -1
- data/lib/active_model/validations/resolve_value.rb +26 -0
- data/lib/active_model/validations/validates.rb +3 -3
- data/lib/active_model/validations/with.rb +9 -2
- data/lib/active_model/validations.rb +44 -9
- data/lib/active_model/validator.rb +7 -5
- data/lib/active_model/version.rb +1 -1
- data/lib/active_model.rb +5 -1
- metadata +12 -7
@@ -3,7 +3,7 @@
|
|
3
3
|
require "active_support/core_ext/enumerable"
|
4
4
|
|
5
5
|
module ActiveModel
|
6
|
-
#
|
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
|
37
|
-
# explicitly include
|
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
|
#
|
@@ -1,9 +1,9 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveModel
|
4
|
-
#
|
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
|
-
|
46
|
-
|
47
|
-
attribute
|
48
|
-
|
49
|
-
|
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
|
-
:"#{
|
54
|
+
:"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
|
54
55
|
end
|
55
|
-
defaults << :"#{
|
56
|
+
defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
|
56
57
|
else
|
57
58
|
defaults = lookup_ancestors.map do |klass|
|
58
|
-
:"#{
|
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
|
64
|
-
defaults <<
|
64
|
+
defaults << options[:default] if options[:default]
|
65
|
+
defaults << MISSING_TRANSLATION
|
65
66
|
|
66
|
-
|
67
|
-
|
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
|
-
|
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
|
-
|
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
|
-
#
|
5
|
+
# = Active Model \Boolean \Type
|
6
6
|
#
|
7
|
-
# A class that behaves like a boolean type, including rules for coercion of
|
7
|
+
# A class that behaves like a boolean type, including rules for coercion of
|
8
|
+
# user input.
|
8
9
|
#
|
9
|
-
#
|
10
|
-
#
|
11
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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 =
|
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
|
-
|
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
|
-
|
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
|
@@ -7,7 +7,7 @@ module ActiveModel
|
|
7
7
|
module Type
|
8
8
|
module Helpers # :nodoc: all
|
9
9
|
module TimeValue
|
10
|
-
def
|
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
|
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
|
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
|
-
|
82
|
-
|
83
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|