activemodel 7.1.5.2 → 8.1.2.1

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +21 -281
  3. data/README.rdoc +10 -10
  4. data/lib/active_model/attribute/user_provided_default.rb +10 -0
  5. data/lib/active_model/attribute.rb +10 -2
  6. data/lib/active_model/attribute_assignment.rb +26 -3
  7. data/lib/active_model/attribute_methods.rb +42 -43
  8. data/lib/active_model/attribute_mutation_tracker.rb +5 -5
  9. data/lib/active_model/attribute_registration.rb +62 -22
  10. data/lib/active_model/attribute_set.rb +1 -1
  11. data/lib/active_model/attributes/normalization.rb +192 -0
  12. data/lib/active_model/attributes.rb +13 -0
  13. data/lib/active_model/callbacks.rb +1 -1
  14. data/lib/active_model/conversion.rb +1 -1
  15. data/lib/active_model/dirty.rb +16 -9
  16. data/lib/active_model/error.rb +3 -2
  17. data/lib/active_model/errors.rb +1 -4
  18. data/lib/active_model/gem_version.rb +3 -3
  19. data/lib/active_model/lint.rb +7 -3
  20. data/lib/active_model/model.rb +2 -2
  21. data/lib/active_model/naming.rb +0 -1
  22. data/lib/active_model/nested_error.rb +1 -3
  23. data/lib/active_model/railtie.rb +8 -4
  24. data/lib/active_model/secure_password.rb +62 -5
  25. data/lib/active_model/serialization.rb +21 -21
  26. data/lib/active_model/translation.rb +22 -5
  27. data/lib/active_model/type/big_integer.rb +21 -0
  28. data/lib/active_model/type/boolean.rb +1 -0
  29. data/lib/active_model/type/date.rb +1 -0
  30. data/lib/active_model/type/date_time.rb +8 -0
  31. data/lib/active_model/type/decimal.rb +1 -0
  32. data/lib/active_model/type/float.rb +9 -8
  33. data/lib/active_model/type/helpers/immutable.rb +13 -0
  34. data/lib/active_model/type/helpers/time_value.rb +12 -15
  35. data/lib/active_model/type/helpers/timezone.rb +5 -1
  36. data/lib/active_model/type/helpers.rb +1 -0
  37. data/lib/active_model/type/immutable_string.rb +2 -0
  38. data/lib/active_model/type/integer.rb +31 -19
  39. data/lib/active_model/type/registry.rb +2 -3
  40. data/lib/active_model/type/string.rb +4 -0
  41. data/lib/active_model/type/value.rb +4 -4
  42. data/lib/active_model/validations/acceptance.rb +1 -1
  43. data/lib/active_model/validations/callbacks.rb +11 -1
  44. data/lib/active_model/validations/comparison.rb +1 -1
  45. data/lib/active_model/validations/validates.rb +8 -3
  46. data/lib/active_model/validations.rb +57 -32
  47. data/lib/active_model.rb +6 -0
  48. metadata +10 -8
@@ -5,7 +5,6 @@ require "active_support/core_ext/string/inflections"
5
5
  require "active_support/core_ext/object/deep_dup"
6
6
  require "active_model/error"
7
7
  require "active_model/nested_error"
8
- require "forwardable"
9
8
 
10
9
  module ActiveModel
11
10
  # = Active \Model \Errors
@@ -61,8 +60,6 @@ module ActiveModel
61
60
  class Errors
62
61
  include Enumerable
63
62
 
64
- extend Forwardable
65
-
66
63
  ##
67
64
  # :method: each
68
65
  #
@@ -100,7 +97,7 @@ module ActiveModel
100
97
  #
101
98
  # Returns number of errors.
102
99
 
103
- def_delegators :@errors, :each, :clear, :empty?, :size, :uniq!
100
+ delegate :each, :clear, :empty?, :size, :uniq!, to: :@errors
104
101
 
105
102
  # The actual array of +Error+ objects
106
103
  # This method is aliased to <tt>objects</tt>.
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 7
10
+ MAJOR = 8
11
11
  MINOR = 1
12
- TINY = 5
13
- PRE = "2"
12
+ TINY = 2
13
+ PRE = "1"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -30,7 +30,7 @@ module ActiveModel
30
30
  # of the model, and is used to a generate unique DOM id for the object.
31
31
  def test_to_key
32
32
  assert_respond_to model, :to_key
33
- def model.persisted?() false end
33
+ def_method(model, :persisted?) { false }
34
34
  assert model.to_key.nil?, "to_key should return nil when `persisted?` returns false"
35
35
  end
36
36
 
@@ -45,8 +45,8 @@ module ActiveModel
45
45
  # any of the possible implementation strategies on the implementer.
46
46
  def test_to_param
47
47
  assert_respond_to model, :to_param
48
- def model.to_key() [1] end
49
- def model.persisted?() false end
48
+ def_method(model, :to_key) { [1] }
49
+ def_method(model, :persisted?) { false }
50
50
  assert model.to_param.nil?, "to_param should return nil when `persisted?` returns false"
51
51
  end
52
52
 
@@ -105,6 +105,10 @@ module ActiveModel
105
105
  end
106
106
 
107
107
  private
108
+ def def_method(receiver, name, &block)
109
+ ::Object.instance_method(:define_singleton_method).bind_call(receiver, name, &block)
110
+ end
111
+
108
112
  def model
109
113
  assert_respond_to @model, :to_model
110
114
  @model.to_model
@@ -54,7 +54,7 @@ module ActiveModel
54
54
  #
55
55
  # person = Person.new(id: 1, name: "bob")
56
56
  # person.slice(:id, :name)
57
- # => { "id" => 1, "name" => "bob" }
57
+ # # => { "id" => 1, "name" => "bob" }
58
58
  #
59
59
  #--
60
60
  # Implemented by ActiveModel::Access#slice.
@@ -68,7 +68,7 @@ module ActiveModel
68
68
  #
69
69
  # person = Person.new(id: 1, name: "bob")
70
70
  # person.values_at(:id, :name)
71
- # => [1, "bob"]
71
+ # # => [1, "bob"]
72
72
  #
73
73
  #--
74
74
  # Implemented by ActiveModel::Access#values_at.
@@ -3,7 +3,6 @@
3
3
  require "active_support/core_ext/hash/except"
4
4
  require "active_support/core_ext/module/introspection"
5
5
  require "active_support/core_ext/module/redefine_method"
6
- require "active_support/core_ext/module/delegation"
7
6
 
8
7
  module ActiveModel
9
8
  class Name
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "active_model/error"
4
- require "forwardable"
5
4
 
6
5
  module ActiveModel
7
6
  class NestedError < Error
@@ -16,7 +15,6 @@ module ActiveModel
16
15
 
17
16
  attr_reader :inner_error
18
17
 
19
- extend Forwardable
20
- def_delegators :@inner_error, :message
18
+ delegate :message, to: :@inner_error
21
19
  end
22
20
  end
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_model"
4
3
  require "rails"
4
+ require "active_model"
5
5
 
6
6
  module ActiveModel
7
7
  class Railtie < Rails::Railtie # :nodoc:
@@ -14,11 +14,15 @@ module ActiveModel
14
14
  end
15
15
 
16
16
  initializer "active_model.secure_password" do
17
- ActiveModel::SecurePassword.min_cost = Rails.env.test?
17
+ ActiveSupport.on_load(:active_model_secure_password) do
18
+ ActiveModel::SecurePassword.min_cost = Rails.env.test?
19
+ end
18
20
  end
19
21
 
20
- initializer "active_model.i18n_customize_full_message" do
21
- ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
22
+ initializer "active_model.i18n_customize_full_message" do |app|
23
+ ActiveSupport.on_load(:active_model_error) do
24
+ ActiveModel::Error.i18n_customize_full_message = app.config.active_model.i18n_customize_full_message || false
25
+ end
22
26
  end
23
27
  end
24
28
  end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/numeric/time"
4
+
3
5
  module ActiveModel
4
6
  module SecurePassword
5
7
  extend ActiveSupport::Concern
@@ -9,6 +11,8 @@ module ActiveModel
9
11
  # Hence need to put a restriction on password length.
10
12
  MAX_PASSWORD_LENGTH_ALLOWED = 72
11
13
 
14
+ DEFAULT_RESET_TOKEN_EXPIRES_IN = 15.minutes
15
+
12
16
  class << self
13
17
  attr_accessor :min_cost # :nodoc:
14
18
  end
@@ -39,9 +43,18 @@ module ActiveModel
39
43
  # <tt>validations: false</tt> as an argument. This allows complete
40
44
  # customizability of validation behavior.
41
45
  #
46
+ # A password reset token (valid for 15 minutes by default) is automatically
47
+ # configured when +reset_token+ is set to true (which it is by default)
48
+ # and the object responds to +generates_token_for+ (which Active Records do).
49
+ #
50
+ # Finally, the reset token expiry can be customized by passing a hash to
51
+ # +has_secure_password+:
52
+ #
53
+ # has_secure_password reset_token: { expires_in: 1.hour }
54
+ #
42
55
  # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile:
43
56
  #
44
- # gem 'bcrypt', '~> 3.1.7'
57
+ # gem "bcrypt", "~> 3.1.7"
45
58
  #
46
59
  # ==== Examples
47
60
  #
@@ -98,7 +111,18 @@ module ActiveModel
98
111
  # account.is_guest = true
99
112
  # account.valid? # => true
100
113
  #
101
- def has_secure_password(attribute = :password, validations: true)
114
+ # ===== Using the password reset token
115
+ #
116
+ # user = User.create!(name: "david", password: "123", password_confirmation: "123")
117
+ # token = user.password_reset_token
118
+ # User.find_by_password_reset_token(token) # returns user
119
+ #
120
+ # # 16 minutes later...
121
+ # User.find_by_password_reset_token(token) # returns nil
122
+ #
123
+ # # raises ActiveSupport::MessageVerifier::InvalidSignature since the token is expired
124
+ # User.find_by_password_reset_token!(token)
125
+ def has_secure_password(attribute = :password, validations: true, reset_token: true)
102
126
  # Load bcrypt gem only when has_secure_password is used.
103
127
  # This is to avoid ActiveModel (and by extension the entire framework)
104
128
  # being dependent on a binary library.
@@ -109,7 +133,7 @@ module ActiveModel
109
133
  raise
110
134
  end
111
135
 
112
- include InstanceMethodsOnActivation.new(attribute)
136
+ include InstanceMethodsOnActivation.new(attribute, reset_token: reset_token)
113
137
 
114
138
  if validations
115
139
  include ActiveModel::Validations
@@ -140,13 +164,37 @@ module ActiveModel
140
164
  end
141
165
  end
142
166
 
143
- validates_confirmation_of attribute, allow_blank: true
167
+ validates_confirmation_of attribute, allow_nil: true
168
+ end
169
+
170
+ # Only generate tokens for records that are capable of doing so (Active Records, not vanilla Active Models)
171
+ if reset_token && respond_to?(:generates_token_for)
172
+ reset_token_expires_in = reset_token.is_a?(Hash) ? reset_token[:expires_in] : DEFAULT_RESET_TOKEN_EXPIRES_IN
173
+
174
+ silence_redefinition_of_method(:"#{attribute}_reset_token_expires_in")
175
+ define_method(:"#{attribute}_reset_token_expires_in") { reset_token_expires_in }
176
+
177
+ generates_token_for :"#{attribute}_reset", expires_in: reset_token_expires_in do
178
+ public_send(:"#{attribute}_salt")&.last(10)
179
+ end
180
+
181
+ class_eval <<-RUBY, __FILE__, __LINE__ + 1
182
+ silence_redefinition_of_method :find_by_#{attribute}_reset_token
183
+ def self.find_by_#{attribute}_reset_token(token)
184
+ find_by_token_for(:#{attribute}_reset, token)
185
+ end
186
+
187
+ silence_redefinition_of_method :find_by_#{attribute}_reset_token!
188
+ def self.find_by_#{attribute}_reset_token!(token)
189
+ find_by_token_for!(:#{attribute}_reset, token)
190
+ end
191
+ RUBY
144
192
  end
145
193
  end
146
194
  end
147
195
 
148
196
  class InstanceMethodsOnActivation < Module
149
- def initialize(attribute)
197
+ def initialize(attribute, reset_token:)
150
198
  attr_reader attribute
151
199
 
152
200
  define_method("#{attribute}=") do |unencrypted_password|
@@ -184,7 +232,16 @@ module ActiveModel
184
232
  end
185
233
 
186
234
  alias_method :authenticate, :authenticate_password if attribute == :password
235
+
236
+ if reset_token
237
+ # Returns the class-level configured reset token for the password.
238
+ define_method("#{attribute}_reset_token") do
239
+ generate_token_for(:"#{attribute}_reset")
240
+ end
241
+ end
187
242
  end
188
243
  end
189
244
  end
245
+
246
+ ActiveSupport.run_load_hooks(:active_model_secure_password, SecurePassword)
190
247
  end
@@ -29,8 +29,8 @@ module ActiveModel
29
29
  # An +attributes+ hash must be defined and should contain any attributes you
30
30
  # need to be serialized. Attributes must be strings, not symbols.
31
31
  # When called, serializable hash will use instance methods that match the name
32
- # of the attributes hash's keys. In order to override this behavior, take a look
33
- # at the private method +read_attribute_for_serialization+.
32
+ # of the attributes hash's keys. In order to override this behavior, override
33
+ # the +read_attribute_for_serialization+ method.
34
34
  #
35
35
  # ActiveModel::Serializers::JSON module automatically includes
36
36
  # the +ActiveModel::Serialization+ module, so there is no need to
@@ -128,7 +128,7 @@ module ActiveModel
128
128
  return serializable_attributes(attribute_names) if options.blank?
129
129
 
130
130
  if only = options[:only]
131
- attribute_names &= Array(only).map(&:to_s)
131
+ attribute_names = Array(only).map(&:to_s) & attribute_names
132
132
  elsif except = options[:except]
133
133
  attribute_names -= Array(except).map(&:to_s)
134
134
  end
@@ -148,29 +148,29 @@ module ActiveModel
148
148
  hash
149
149
  end
150
150
 
151
+ # Hook method defining how an attribute value should be retrieved for
152
+ # serialization. By default this is assumed to be an instance named after
153
+ # the attribute. Override this method in subclasses should you need to
154
+ # retrieve the value for a given attribute differently:
155
+ #
156
+ # class MyClass
157
+ # include ActiveModel::Serialization
158
+ #
159
+ # def initialize(data = {})
160
+ # @data = data
161
+ # end
162
+ #
163
+ # def read_attribute_for_serialization(key)
164
+ # @data[key]
165
+ # end
166
+ # end
167
+ alias :read_attribute_for_serialization :send
168
+
151
169
  private
152
170
  def attribute_names_for_serialization
153
171
  attributes.keys
154
172
  end
155
173
 
156
- # Hook method defining how an attribute value should be retrieved for
157
- # serialization. By default this is assumed to be an instance named after
158
- # the attribute. Override this method in subclasses should you need to
159
- # retrieve the value for a given attribute differently:
160
- #
161
- # class MyClass
162
- # include ActiveModel::Serialization
163
- #
164
- # def initialize(data = {})
165
- # @data = data
166
- # end
167
- #
168
- # def read_attribute_for_serialization(key)
169
- # @data[key]
170
- # end
171
- # end
172
- alias :read_attribute_for_serialization :send
173
-
174
174
  def serializable_attributes(attribute_names)
175
175
  attribute_names.index_with { |n| read_attribute_for_serialization(n) }
176
176
  end
@@ -22,6 +22,8 @@ module ActiveModel
22
22
  module Translation
23
23
  include ActiveModel::Naming
24
24
 
25
+ singleton_class.attr_accessor :raise_on_missing_translations
26
+
25
27
  # Returns the +i18n_scope+ for the class. Override if you want custom lookup.
26
28
  def i18n_scope
27
29
  :activemodel
@@ -50,23 +52,38 @@ module ActiveModel
50
52
  namespace, _, attribute = attribute.rpartition(".")
51
53
  namespace.tr!(".", "/")
52
54
 
55
+ if attribute.present?
56
+ key = "#{namespace}.#{attribute}"
57
+ separator = "/"
58
+ else
59
+ key = namespace
60
+ separator = "."
61
+ end
62
+
53
63
  defaults = lookup_ancestors.map do |klass|
54
- :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}/#{namespace}.#{attribute}"
64
+ :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}#{separator}#{key}"
55
65
  end
56
- defaults << :"#{i18n_scope}.attributes.#{namespace}.#{attribute}"
66
+ defaults << :"#{i18n_scope}.attributes.#{key}"
67
+ defaults << :"attributes.#{key}"
57
68
  else
58
69
  defaults = lookup_ancestors.map do |klass|
59
70
  :"#{i18n_scope}.attributes.#{klass.model_name.i18n_key}.#{attribute}"
60
71
  end
61
72
  end
62
73
 
74
+ raise_on_missing = options.fetch(:raise, Translation.raise_on_missing_translations)
75
+
63
76
  defaults << :"attributes.#{attribute}"
64
77
  defaults << options[:default] if options[:default]
65
- defaults << MISSING_TRANSLATION
78
+ defaults << MISSING_TRANSLATION unless raise_on_missing
66
79
 
67
- translation = I18n.translate(defaults.shift, count: 1, **options, default: defaults)
68
- translation = attribute.humanize if translation == MISSING_TRANSLATION
80
+ translation = I18n.translate(defaults.shift, count: 1, raise: raise_on_missing, **options, default: defaults)
81
+ if translation == MISSING_TRANSLATION
82
+ translation = attribute.present? ? attribute.humanize : namespace.humanize
83
+ end
69
84
  translation
70
85
  end
71
86
  end
87
+
88
+ ActiveSupport.run_load_hooks(:active_model_translation, Translation)
72
89
  end
@@ -23,10 +23,31 @@ module ActiveModel
23
23
  # All casting and serialization are performed in the same way as the
24
24
  # standard ActiveModel::Type::Integer type.
25
25
  class BigInteger < Integer
26
+ def serialize(value) # :nodoc:
27
+ case value
28
+ when ::Integer
29
+ # noop
30
+ when ::String
31
+ int = value.to_i
32
+ if int.zero? && value != "0"
33
+ return if non_numeric_string?(value)
34
+ end
35
+ value = int
36
+ else
37
+ value = super
38
+ end
39
+
40
+ value
41
+ end
42
+
26
43
  def serialize_cast_value(value) # :nodoc:
27
44
  value
28
45
  end
29
46
 
47
+ def serializable?(value, &)
48
+ true
49
+ end
50
+
30
51
  private
31
52
  def max_value
32
53
  ::Float::INFINITY
@@ -12,6 +12,7 @@ module ActiveModel
12
12
  # - Empty strings are coerced to +nil+.
13
13
  # - All other values will be coerced to +true+.
14
14
  class Boolean < Value
15
+ include Helpers::Immutable
15
16
  FALSE_VALUES = [
16
17
  false, 0,
17
18
  "0", :"0",
@@ -24,6 +24,7 @@ module ActiveModel
24
24
  # String values are parsed using the ISO 8601 date format. Any other values
25
25
  # are cast using their +to_date+ method, if it exists.
26
26
  class Date < Value
27
+ include Helpers::Immutable
27
28
  include Helpers::Timezone
28
29
  include Helpers::AcceptsMultiparameterTime.new
29
30
 
@@ -50,6 +50,14 @@ module ActiveModel
50
50
  :datetime
51
51
  end
52
52
 
53
+ def mutable? # :nodoc:
54
+ # Time#zone can be mutated by #utc or #localtime
55
+ # However when serializing the time zone will always
56
+ # be coerced and even if the zone was mutated Time instances
57
+ # remain equal, so we don't need to implement `#changed_in_place?`
58
+ true
59
+ end
60
+
53
61
  private
54
62
  def cast_value(value)
55
63
  return apply_seconds_precision(value) unless value.is_a?(::String)
@@ -43,6 +43,7 @@ module ActiveModel
43
43
  # attribute :weight, :decimal, precision: 24
44
44
  # end
45
45
  class Decimal < Value
46
+ include Helpers::Immutable
46
47
  include Helpers::Numeric
47
48
  BIGDECIMAL_PRECISION = 18
48
49
 
@@ -15,14 +15,6 @@ module ActiveModel
15
15
  # attribute :weight, :float
16
16
  # end
17
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
18
  # bag = BagOfCoffee.new
27
19
  #
28
20
  # bag.weight = "0.25"
@@ -33,7 +25,16 @@ module ActiveModel
33
25
  #
34
26
  # bag.weight = "NaN"
35
27
  # bag.weight # => Float::NAN
28
+ #
29
+ # Values are cast using their +to_f+ method, except for the following
30
+ # strings:
31
+ #
32
+ # - Blank strings are cast to +nil+.
33
+ # - <tt>"Infinity"</tt> is cast to +Float::INFINITY+.
34
+ # - <tt>"-Infinity"</tt> is cast to <tt>-Float::INFINITY</tt>.
35
+ # - <tt>"NaN"</tt> is cast to +Float::NAN+.
36
36
  class Float < Value
37
+ include Helpers::Immutable
37
38
  include Helpers::Numeric
38
39
 
39
40
  def type
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveModel
4
+ module Type
5
+ module Helpers # :nodoc: all
6
+ module Immutable
7
+ def mutable? # :nodoc:
8
+ false
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -69,13 +69,14 @@ module ActiveModel
69
69
  \z
70
70
  /x
71
71
 
72
- if RUBY_VERSION >= "3.2"
72
+ if Time.new(2000, 1, 1, 0, 0, 0, "-00:00").yday != 1 # Early 3.2.x had a bug
73
+ # BUG: Wrapping the Time object with Time.at because Time.new with `in:` in Ruby 3.2.0
74
+ # used to return an invalid Time object
75
+ # see: https://bugs.ruby-lang.org/issues/19292
73
76
  def fast_string_to_time(string)
74
- return unless ISO_DATETIME.match?(string)
77
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
75
78
 
76
79
  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
80
  ::Time.at(::Time.new(string, in: "UTC"))
80
81
  else
81
82
  ::Time.new(string)
@@ -85,19 +86,15 @@ module ActiveModel
85
86
  end
86
87
  else
87
88
  def fast_string_to_time(string)
88
- return unless ISO_DATETIME =~ string
89
+ return unless string.include?("-") # Time.new("1234") # => 1234-01-01 00:00:00
89
90
 
90
- usec = $7.to_i
91
- usec_len = $7&.length
92
- if usec_len&.< 6
93
- usec *= 10**(6 - usec_len)
94
- end
95
-
96
- if $8
97
- offset = $8 == "Z" ? 0 : $8.to_i * 3600 + $9.to_i * 60
91
+ if is_utc?
92
+ ::Time.new(string, in: "UTC")
93
+ else
94
+ ::Time.new(string)
98
95
  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)
96
+ rescue ArgumentError
97
+ nil
101
98
  end
102
99
  end
103
100
  end
@@ -7,7 +7,11 @@ module ActiveModel
7
7
  module Helpers # :nodoc: all
8
8
  module Timezone
9
9
  def is_utc?
10
- ::Time.zone_default.nil? || ::Time.zone_default.match?("UTC")
10
+ if default = ::Time.zone_default
11
+ default.name == "UTC"
12
+ else
13
+ true
14
+ end
11
15
  end
12
16
 
13
17
  def default_timezone
@@ -3,5 +3,6 @@
3
3
  require "active_model/type/helpers/accepts_multiparameter_time"
4
4
  require "active_model/type/helpers/numeric"
5
5
  require "active_model/type/helpers/mutable"
6
+ require "active_model/type/helpers/immutable"
6
7
  require "active_model/type/helpers/time_value"
7
8
  require "active_model/type/helpers/timezone"
@@ -35,6 +35,8 @@ module ActiveModel
35
35
  #
36
36
  # person.active # => "aye"
37
37
  class ImmutableString < Value
38
+ include Helpers::Immutable
39
+
38
40
  def initialize(**args)
39
41
  @true = -(args.delete(:true)&.to_s || "t")
40
42
  @false = -(args.delete(:false)&.to_s || "f")
@@ -42,6 +42,7 @@ module ActiveModel
42
42
  # attribute :age, :integer, limit: 6
43
43
  # end
44
44
  class Integer < Value
45
+ include Helpers::Immutable
45
46
  include Helpers::Numeric
46
47
 
47
48
  # Column storage size in bytes.
@@ -50,7 +51,8 @@ module ActiveModel
50
51
 
51
52
  def initialize(**)
52
53
  super
53
- @range = min_value...max_value
54
+ @max = max_value
55
+ @min = min_value
54
56
  end
55
57
 
56
58
  def type
@@ -63,40 +65,50 @@ module ActiveModel
63
65
  end
64
66
 
65
67
  def serialize(value)
66
- return if value.is_a?(::String) && non_numeric_string?(value)
67
- ensure_in_range(super)
68
+ case value
69
+ when ::Integer
70
+ # noop
71
+ when ::String
72
+ int = value.to_i
73
+ if int.zero? && value != "0"
74
+ return if non_numeric_string?(value)
75
+ end
76
+ value = int
77
+ else
78
+ value = super
79
+ end
80
+
81
+ if out_of_range?(value)
82
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
83
+ end
84
+
85
+ value
68
86
  end
69
87
 
70
88
  def serialize_cast_value(value) # :nodoc:
71
- ensure_in_range(value)
89
+ if out_of_range?(value)
90
+ raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
91
+ end
92
+
93
+ value
72
94
  end
73
95
 
74
96
  def serializable?(value)
75
97
  cast_value = cast(value)
76
- in_range?(cast_value) || begin
77
- yield cast_value if block_given?
78
- false
79
- end
98
+ return true unless out_of_range?(cast_value)
99
+ yield cast_value if block_given?
100
+ false
80
101
  end
81
102
 
82
103
  private
83
- attr_reader :range
84
-
85
- def in_range?(value)
86
- !value || range.member?(value)
104
+ def out_of_range?(value)
105
+ value && (@max <= value || @min > value)
87
106
  end
88
107
 
89
108
  def cast_value(value)
90
109
  value.to_i rescue nil
91
110
  end
92
111
 
93
- def ensure_in_range(value)
94
- unless in_range?(value)
95
- raise ActiveModel::RangeError, "#{value} is out of range for #{self.class} with limit #{_limit} bytes"
96
- end
97
- value
98
- end
99
-
100
112
  def max_value
101
113
  1 << (_limit * 8 - 1) # 8 bits per byte with one bit for sign
102
114
  end