activemodel 7.0.8.1 → 7.1.0.beta1

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 (66) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +132 -196
  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 +17 -12
  20. data/lib/active_model/forbidden_attributes_protection.rb +2 -0
  21. data/lib/active_model/gem_version.rb +4 -4
  22. data/lib/active_model/lint.rb +1 -1
  23. data/lib/active_model/locale/en.yml +4 -3
  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/confirmation.rb +1 -1
  55. data/lib/active_model/validations/format.rb +6 -7
  56. data/lib/active_model/validations/length.rb +10 -8
  57. data/lib/active_model/validations/numericality.rb +35 -23
  58. data/lib/active_model/validations/presence.rb +2 -2
  59. data/lib/active_model/validations/resolve_value.rb +26 -0
  60. data/lib/active_model/validations/validates.rb +4 -4
  61. data/lib/active_model/validations/with.rb +9 -2
  62. data/lib/active_model/validations.rb +45 -10
  63. data/lib/active_model/validator.rb +7 -5
  64. data/lib/active_model/version.rb +1 -1
  65. data/lib/active_model.rb +5 -1
  66. metadata +15 -10
@@ -16,8 +16,8 @@ module ActiveModel
16
16
 
17
17
  module ClassMethods
18
18
  # Adds methods to set and authenticate against a BCrypt password.
19
- # This mechanism requires you to have a +XXX_digest+ attribute.
20
- # Where +XXX+ is the attribute name of your desired password.
19
+ # This mechanism requires you to have a +XXX_digest+ attribute,
20
+ # where +XXX+ is the attribute name of your desired password.
21
21
  #
22
22
  # The following validations are added automatically:
23
23
  # * Password must be present on creation
@@ -29,10 +29,17 @@ module ActiveModel
29
29
  # it). When this attribute has a +nil+ value, the validation will not be
30
30
  # triggered.
31
31
  #
32
- # For further customizability, it is possible to suppress the default
33
- # validations by passing <tt>validations: false</tt> as an argument.
32
+ # Additionally, a +XXX_challenge+ attribute is created. When set to a
33
+ # value other than +nil+, it will validate against the currently persisted
34
+ # password. This validation relies on dirty tracking, as provided by
35
+ # ActiveModel::Dirty; if dirty tracking methods are not defined, this
36
+ # validation will fail.
34
37
  #
35
- # Add bcrypt (~> 3.1.7) to Gemfile to use #has_secure_password:
38
+ # All of the above validations can be omitted by passing
39
+ # <tt>validations: false</tt> as an argument. This allows complete
40
+ # customizability of validation behavior.
41
+ #
42
+ # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
36
43
  #
37
44
  # gem 'bcrypt', '~> 3.1.7'
38
45
  #
@@ -46,20 +53,30 @@ module ActiveModel
46
53
  # has_secure_password :recovery_password, validations: false
47
54
  # end
48
55
  #
49
- # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
50
- # user.save # => false, password required
51
- # user.password = 'mUc3m00RsqyRe'
52
- # user.save # => false, confirmation doesn't match
53
- # user.password_confirmation = 'mUc3m00RsqyRe'
54
- # user.save # => true
56
+ # user = User.new(name: "david", password: "", password_confirmation: "nomatch")
57
+ #
58
+ # user.save # => false, password required
59
+ # user.password = "vr00m"
60
+ # user.save # => false, confirmation doesn’t match
61
+ # user.password_confirmation = "vr00m"
62
+ # user.save # => true
63
+ #
64
+ # user.authenticate("notright") # => false
65
+ # user.authenticate("vr00m") # => user
66
+ # User.find_by(name: "david")&.authenticate("notright") # => false
67
+ # User.find_by(name: "david")&.authenticate("vr00m") # => user
68
+ #
55
69
  # user.recovery_password = "42password"
56
- # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
57
- # user.save # => true
58
- # user.authenticate('notright') # => false
59
- # user.authenticate('mUc3m00RsqyRe') # => user
60
- # user.authenticate_recovery_password('42password') # => user
61
- # User.find_by(name: 'david')&.authenticate('notright') # => false
62
- # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
70
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
71
+ # user.save # => true
72
+ #
73
+ # user.authenticate_recovery_password("42password") # => user
74
+ #
75
+ # user.update(password: "pwn3d", password_challenge: "") # => false, challenge doesn't authenticate
76
+ # user.update(password: "nohack4u", password_challenge: "vr00m") # => true
77
+ #
78
+ # user.authenticate("vr00m") # => false, old password
79
+ # user.authenticate("nohack4u") # => user
63
80
  #
64
81
  # ===== Conditionally requiring a password
65
82
  #
@@ -88,7 +105,7 @@ module ActiveModel
88
105
  begin
89
106
  require "bcrypt"
90
107
  rescue LoadError
91
- $stderr.puts "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install"
108
+ warn "You don't have bcrypt installed in your application. Please add it to your Gemfile and run bundle install."
92
109
  raise
93
110
  end
94
111
 
@@ -105,7 +122,24 @@ module ActiveModel
105
122
  record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
106
123
  end
107
124
 
108
- validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
125
+ validate do |record|
126
+ if challenge = record.public_send(:"#{attribute}_challenge")
127
+ digest_was = record.public_send(:"#{attribute}_digest_was") if record.respond_to?(:"#{attribute}_digest_was")
128
+
129
+ unless digest_was.present? && BCrypt::Password.new(digest_was).is_password?(challenge)
130
+ record.errors.add(:"#{attribute}_challenge")
131
+ end
132
+ end
133
+ end
134
+
135
+ # Validates that the password does not exceed the maximum allowed bytes for BCrypt (72 bytes).
136
+ validate do |record|
137
+ password_value = record.public_send(attribute)
138
+ if password_value.present? && password_value.bytesize > ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
139
+ record.errors.add(attribute, :password_too_long)
140
+ end
141
+ end
142
+
109
143
  validates_confirmation_of attribute, allow_blank: true
110
144
  end
111
145
  end
@@ -126,9 +160,7 @@ module ActiveModel
126
160
  end
127
161
  end
128
162
 
129
- define_method("#{attribute}_confirmation=") do |unencrypted_password|
130
- instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
131
- end
163
+ attr_accessor :"#{attribute}_confirmation", :"#{attribute}_challenge"
132
164
 
133
165
  # Returns +self+ if the password is correct, otherwise +false+.
134
166
  #
@@ -145,6 +177,12 @@ module ActiveModel
145
177
  attribute_digest.present? && BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
146
178
  end
147
179
 
180
+ # Returns the salt, a small chunk of random data added to the password before it's hashed.
181
+ define_method("#{attribute}_salt") do
182
+ attribute_digest = public_send("#{attribute}_digest")
183
+ attribute_digest.present? ? BCrypt::Password.new(attribute_digest).salt : nil
184
+ end
185
+
148
186
  alias_method :authenticate, :authenticate_password if attribute == :password
149
187
  end
150
188
  end
@@ -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