activemodel 7.0.8.1 → 7.1.3.2

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 +155 -170
  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 +10 -1
  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 +37 -6
  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 +1 -0
  24. data/lib/active_model/model.rb +34 -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 +6 -1
  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 +4 -4
  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
@@ -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.
@@ -38,7 +42,8 @@ module ActiveModel
38
42
  end
39
43
 
40
44
  def number_to_non_number?(old_value, new_value_before_type_cast)
41
- old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
45
+ old_value != nil && !new_value_before_type_cast.is_a?(::Numeric) &&
46
+ non_numeric_string?(new_value_before_type_cast.to_s)
42
47
  end
43
48
 
44
49
  def non_numeric_string?(value)
@@ -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