activemodel 5.2.7.1 → 6.1.4.6

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 (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +65 -111
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -4
  5. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  6. data/lib/active_model/attribute.rb +21 -21
  7. data/lib/active_model/attribute_assignment.rb +4 -6
  8. data/lib/active_model/attribute_methods.rb +117 -40
  9. data/lib/active_model/attribute_mutation_tracker.rb +90 -33
  10. data/lib/active_model/attribute_set/builder.rb +81 -16
  11. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  12. data/lib/active_model/attribute_set.rb +20 -28
  13. data/lib/active_model/attributes.rb +65 -44
  14. data/lib/active_model/callbacks.rb +11 -9
  15. data/lib/active_model/conversion.rb +1 -1
  16. data/lib/active_model/dirty.rb +51 -101
  17. data/lib/active_model/error.rb +207 -0
  18. data/lib/active_model/errors.rb +347 -155
  19. data/lib/active_model/gem_version.rb +4 -4
  20. data/lib/active_model/lint.rb +1 -1
  21. data/lib/active_model/naming.rb +22 -7
  22. data/lib/active_model/nested_error.rb +22 -0
  23. data/lib/active_model/railtie.rb +6 -0
  24. data/lib/active_model/secure_password.rb +54 -55
  25. data/lib/active_model/serialization.rb +9 -7
  26. data/lib/active_model/serializers/json.rb +17 -9
  27. data/lib/active_model/translation.rb +1 -1
  28. data/lib/active_model/type/big_integer.rb +0 -1
  29. data/lib/active_model/type/binary.rb +1 -1
  30. data/lib/active_model/type/boolean.rb +0 -1
  31. data/lib/active_model/type/date.rb +0 -5
  32. data/lib/active_model/type/date_time.rb +3 -8
  33. data/lib/active_model/type/decimal.rb +0 -1
  34. data/lib/active_model/type/float.rb +2 -3
  35. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
  36. data/lib/active_model/type/helpers/numeric.rb +17 -6
  37. data/lib/active_model/type/helpers/time_value.rb +37 -15
  38. data/lib/active_model/type/helpers/timezone.rb +1 -1
  39. data/lib/active_model/type/immutable_string.rb +14 -11
  40. data/lib/active_model/type/integer.rb +15 -18
  41. data/lib/active_model/type/registry.rb +16 -16
  42. data/lib/active_model/type/string.rb +12 -3
  43. data/lib/active_model/type/time.rb +1 -6
  44. data/lib/active_model/type/value.rb +9 -2
  45. data/lib/active_model/validations/absence.rb +2 -2
  46. data/lib/active_model/validations/acceptance.rb +34 -27
  47. data/lib/active_model/validations/callbacks.rb +15 -16
  48. data/lib/active_model/validations/clusivity.rb +6 -3
  49. data/lib/active_model/validations/confirmation.rb +4 -4
  50. data/lib/active_model/validations/exclusion.rb +1 -1
  51. data/lib/active_model/validations/format.rb +2 -3
  52. data/lib/active_model/validations/inclusion.rb +2 -2
  53. data/lib/active_model/validations/length.rb +3 -3
  54. data/lib/active_model/validations/numericality.rb +58 -44
  55. data/lib/active_model/validations/presence.rb +1 -1
  56. data/lib/active_model/validations/validates.rb +7 -6
  57. data/lib/active_model/validations.rb +6 -9
  58. data/lib/active_model/validator.rb +8 -3
  59. data/lib/active_model.rb +2 -1
  60. metadata +14 -9
@@ -7,10 +7,10 @@ module ActiveModel
7
7
  end
8
8
 
9
9
  module VERSION
10
- MAJOR = 5
11
- MINOR = 2
12
- TINY = 7
13
- PRE = "1"
10
+ MAJOR = 6
11
+ MINOR = 1
12
+ TINY = 4
13
+ PRE = "6"
14
14
 
15
15
  STRING = [MAJOR, MINOR, TINY, PRE].compact.join(".")
16
16
  end
@@ -101,7 +101,7 @@ module ActiveModel
101
101
  # locale. If no error is present, the method should return an empty array.
102
102
  def test_errors_aref
103
103
  assert_respond_to model, :errors
104
- assert model.errors[:hello].is_a?(Array), "errors#[] should return an Array"
104
+ assert_equal [], model.errors[:hello], "errors#[] should return an empty Array"
105
105
  end
106
106
 
107
107
  private
@@ -8,7 +8,7 @@ module ActiveModel
8
8
  class Name
9
9
  include Comparable
10
10
 
11
- attr_reader :singular, :plural, :element, :collection,
11
+ attr_accessor :singular, :plural, :element, :collection,
12
12
  :singular_route_key, :route_key, :param_key, :i18n_key,
13
13
  :name
14
14
 
@@ -110,6 +110,22 @@ module ActiveModel
110
110
  # BlogPost.model_name.eql?('BlogPost') # => true
111
111
  # BlogPost.model_name.eql?('Blog Post') # => false
112
112
 
113
+ ##
114
+ # :method: match?
115
+ #
116
+ # :call-seq:
117
+ # match?(regexp)
118
+ #
119
+ # Equivalent to <tt>String#match?</tt>. Match the class name against the
120
+ # given regexp. Returns +true+ if there is a match, otherwise +false+.
121
+ #
122
+ # class BlogPost
123
+ # extend ActiveModel::Naming
124
+ # end
125
+ #
126
+ # BlogPost.model_name.match?(/Post/) # => true
127
+ # BlogPost.model_name.match?(/\d/) # => false
128
+
113
129
  ##
114
130
  # :method: to_s
115
131
  #
@@ -131,7 +147,7 @@ module ActiveModel
131
147
  # to_str()
132
148
  #
133
149
  # Equivalent to +to_s+.
134
- delegate :==, :===, :<=>, :=~, :"!~", :eql?, :to_s,
150
+ delegate :==, :===, :<=>, :=~, :"!~", :eql?, :match?, :to_s,
135
151
  :to_str, :as_json, to: :name
136
152
 
137
153
  # Returns a new ActiveModel::Name instance. By default, the +namespace+
@@ -150,7 +166,7 @@ module ActiveModel
150
166
 
151
167
  raise ArgumentError, "Class name cannot be blank. You need to supply a name argument when anonymous class given" if @name.blank?
152
168
 
153
- @unnamespaced = @name.sub(/^#{namespace.name}::/, "") if namespace
169
+ @unnamespaced = @name.delete_prefix("#{namespace.name}::") if namespace
154
170
  @klass = klass
155
171
  @singular = _singularize(@name)
156
172
  @plural = ActiveSupport::Inflector.pluralize(@singular)
@@ -187,13 +203,12 @@ module ActiveModel
187
203
  defaults << @human
188
204
 
189
205
  options = { scope: [@klass.i18n_scope, :models], count: 1, default: defaults }.merge!(options.except(:default))
190
- I18n.translate(defaults.shift, options)
206
+ I18n.translate(defaults.shift, **options)
191
207
  end
192
208
 
193
209
  private
194
-
195
210
  def _singularize(string)
196
- ActiveSupport::Inflector.underscore(string).tr("/".freeze, "_".freeze)
211
+ ActiveSupport::Inflector.underscore(string).tr("/", "_")
197
212
  end
198
213
  end
199
214
 
@@ -236,7 +251,7 @@ module ActiveModel
236
251
  # Person.model_name.plural # => "people"
237
252
  def model_name
238
253
  @_model_name ||= begin
239
- namespace = parents.detect do |n|
254
+ namespace = module_parents.detect do |n|
240
255
  n.respond_to?(:use_relative_model_naming?) && n.use_relative_model_naming?
241
256
  end
242
257
  ActiveModel::Name.new(self, namespace)
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_model/error"
4
+ require "forwardable"
5
+
6
+ module ActiveModel
7
+ class NestedError < Error
8
+ def initialize(base, inner_error, override_options = {})
9
+ @base = base
10
+ @inner_error = inner_error
11
+ @attribute = override_options.fetch(:attribute) { inner_error.attribute }
12
+ @type = override_options.fetch(:type) { inner_error.type }
13
+ @raw_type = inner_error.raw_type
14
+ @options = inner_error.options
15
+ end
16
+
17
+ attr_reader :inner_error
18
+
19
+ extend Forwardable
20
+ def_delegators :@inner_error, :message
21
+ end
22
+ end
@@ -7,8 +7,14 @@ module ActiveModel
7
7
  class Railtie < Rails::Railtie # :nodoc:
8
8
  config.eager_load_namespaces << ActiveModel
9
9
 
10
+ config.active_model = ActiveSupport::OrderedOptions.new
11
+
10
12
  initializer "active_model.secure_password" do
11
13
  ActiveModel::SecurePassword.min_cost = Rails.env.test?
12
14
  end
15
+
16
+ initializer "active_model.i18n_customize_full_message" do
17
+ ActiveModel::Error.i18n_customize_full_message = config.active_model.delete(:i18n_customize_full_message) || false
18
+ end
13
19
  end
14
20
  end
@@ -16,15 +16,16 @@ 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 +password_digest+ attribute.
19
+ # This mechanism requires you to have a +XXX_digest+ attribute.
20
+ # Where +XXX+ is the attribute name of your desired password.
20
21
  #
21
22
  # The following validations are added automatically:
22
23
  # * Password must be present on creation
23
24
  # * Password length should be less than or equal to 72 bytes
24
- # * Confirmation of password (using a +password_confirmation+ attribute)
25
+ # * Confirmation of password (using a +XXX_confirmation+ attribute)
25
26
  #
26
- # If password confirmation validation is not needed, simply leave out the
27
- # value for +password_confirmation+ (i.e. don't provide a form field for
27
+ # If confirmation validation is not needed, simply leave out the
28
+ # value for +XXX_confirmation+ (i.e. don't provide a form field for
28
29
  # it). When this attribute has a +nil+ value, the validation will not be
29
30
  # triggered.
30
31
  #
@@ -37,22 +38,27 @@ module ActiveModel
37
38
  #
38
39
  # Example using Active Record (which automatically includes ActiveModel::SecurePassword):
39
40
  #
40
- # # Schema: User(name:string, password_digest:string)
41
+ # # Schema: User(name:string, password_digest:string, recovery_password_digest:string)
41
42
  # class User < ActiveRecord::Base
42
43
  # has_secure_password
44
+ # has_secure_password :recovery_password, validations: false
43
45
  # end
44
46
  #
45
47
  # user = User.new(name: 'david', password: '', password_confirmation: 'nomatch')
46
- # user.save # => false, password required
48
+ # user.save # => false, password required
47
49
  # user.password = 'mUc3m00RsqyRe'
48
- # user.save # => false, confirmation doesn't match
50
+ # user.save # => false, confirmation doesn't match
49
51
  # user.password_confirmation = 'mUc3m00RsqyRe'
50
- # user.save # => true
51
- # user.authenticate('notright') # => false
52
- # user.authenticate('mUc3m00RsqyRe') # => user
53
- # User.find_by(name: 'david').try(:authenticate, 'notright') # => false
54
- # User.find_by(name: 'david').try(:authenticate, 'mUc3m00RsqyRe') # => user
55
- def has_secure_password(options = {})
52
+ # user.save # => true
53
+ # user.recovery_password = "42password"
54
+ # user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uXkTG65HR.qpW.bNhEjFP3ftli3o5DQC"
55
+ # user.save # => true
56
+ # user.authenticate('notright') # => false
57
+ # user.authenticate('mUc3m00RsqyRe') # => user
58
+ # user.authenticate_recovery_password('42password') # => user
59
+ # User.find_by(name: 'david')&.authenticate('notright') # => false
60
+ # User.find_by(name: 'david')&.authenticate('mUc3m00RsqyRe') # => user
61
+ def has_secure_password(attribute = :password, validations: true)
56
62
  # Load bcrypt gem only when has_secure_password is used.
57
63
  # This is to avoid ActiveModel (and by extension the entire framework)
58
64
  # being dependent on a binary library.
@@ -63,9 +69,9 @@ module ActiveModel
63
69
  raise
64
70
  end
65
71
 
66
- include InstanceMethodsOnActivation
72
+ include InstanceMethodsOnActivation.new(attribute)
67
73
 
68
- if options.fetch(:validations, true)
74
+ if validations
69
75
  include ActiveModel::Validations
70
76
 
71
77
  # This ensures the model has a password by checking whether the password_digest
@@ -73,56 +79,49 @@ module ActiveModel
73
79
  # when there is an error, the message is added to the password attribute instead
74
80
  # so that the error message will make sense to the end-user.
75
81
  validate do |record|
76
- record.errors.add(:password, :blank) unless record.password_digest.present?
82
+ record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present?
77
83
  end
78
84
 
79
- validates_length_of :password, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
80
- validates_confirmation_of :password, allow_blank: true
85
+ validates_length_of attribute, maximum: ActiveModel::SecurePassword::MAX_PASSWORD_LENGTH_ALLOWED
86
+ validates_confirmation_of attribute, allow_blank: true
81
87
  end
82
88
  end
83
89
  end
84
90
 
85
- module InstanceMethodsOnActivation
86
- # Returns +self+ if the password is correct, otherwise +false+.
87
- #
88
- # class User < ActiveRecord::Base
89
- # has_secure_password validations: false
90
- # end
91
- #
92
- # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
93
- # user.save
94
- # user.authenticate('notright') # => false
95
- # user.authenticate('mUc3m00RsqyRe') # => user
96
- def authenticate(unencrypted_password)
97
- BCrypt::Password.new(password_digest).is_password?(unencrypted_password) && self
98
- end
91
+ class InstanceMethodsOnActivation < Module
92
+ def initialize(attribute)
93
+ attr_reader attribute
99
94
 
100
- attr_reader :password
95
+ define_method("#{attribute}=") do |unencrypted_password|
96
+ if unencrypted_password.nil?
97
+ self.public_send("#{attribute}_digest=", nil)
98
+ elsif !unencrypted_password.empty?
99
+ instance_variable_set("@#{attribute}", unencrypted_password)
100
+ cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
101
+ self.public_send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
102
+ end
103
+ end
101
104
 
102
- # Encrypts the password into the +password_digest+ attribute, only if the
103
- # new password is not empty.
104
- #
105
- # class User < ActiveRecord::Base
106
- # has_secure_password validations: false
107
- # end
108
- #
109
- # user = User.new
110
- # user.password = nil
111
- # user.password_digest # => nil
112
- # user.password = 'mUc3m00RsqyRe'
113
- # user.password_digest # => "$2a$10$4LEA7r4YmNHtvlAvHhsYAeZmk/xeUVtMTYqwIvYY76EW5GUqDiP4."
114
- def password=(unencrypted_password)
115
- if unencrypted_password.nil?
116
- self.password_digest = nil
117
- elsif !unencrypted_password.empty?
118
- @password = unencrypted_password
119
- cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
120
- self.password_digest = BCrypt::Password.create(unencrypted_password, cost: cost)
105
+ define_method("#{attribute}_confirmation=") do |unencrypted_password|
106
+ instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
107
+ end
108
+
109
+ # Returns +self+ if the password is correct, otherwise +false+.
110
+ #
111
+ # class User < ActiveRecord::Base
112
+ # has_secure_password validations: false
113
+ # end
114
+ #
115
+ # user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
116
+ # user.save
117
+ # user.authenticate_password('notright') # => false
118
+ # user.authenticate_password('mUc3m00RsqyRe') # => user
119
+ define_method("authenticate_#{attribute}") do |unencrypted_password|
120
+ attribute_digest = public_send("#{attribute}_digest")
121
+ BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
121
122
  end
122
- end
123
123
 
124
- def password_confirmation=(unencrypted_password)
125
- @password_confirmation = unencrypted_password
124
+ alias_method :authenticate, :authenticate_password if attribute == :password
126
125
  end
127
126
  end
128
127
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "active_support/core_ext/hash/except"
4
- require "active_support/core_ext/hash/slice"
3
+ require "active_support/core_ext/enumerable"
5
4
 
6
5
  module ActiveModel
7
6
  # == Active \Model \Serialization
@@ -124,17 +123,17 @@ module ActiveModel
124
123
  # user.serializable_hash(include: { notes: { only: 'title' }})
125
124
  # # => {"name" => "Napoleon", "notes" => [{"title"=>"Battle of Austerlitz"}]}
126
125
  def serializable_hash(options = nil)
127
- options ||= {}
128
-
129
126
  attribute_names = attributes.keys
127
+
128
+ return serializable_attributes(attribute_names) if options.blank?
129
+
130
130
  if only = options[:only]
131
131
  attribute_names &= Array(only).map(&:to_s)
132
132
  elsif except = options[:except]
133
133
  attribute_names -= Array(except).map(&:to_s)
134
134
  end
135
135
 
136
- hash = {}
137
- attribute_names.each { |n| hash[n] = read_attribute_for_serialization(n) }
136
+ hash = serializable_attributes(attribute_names)
138
137
 
139
138
  Array(options[:methods]).each { |m| hash[m.to_s] = send(m) }
140
139
 
@@ -150,7 +149,6 @@ module ActiveModel
150
149
  end
151
150
 
152
151
  private
153
-
154
152
  # Hook method defining how an attribute value should be retrieved for
155
153
  # serialization. By default this is assumed to be an instance named after
156
154
  # the attribute. Override this method in subclasses should you need to
@@ -169,6 +167,10 @@ module ActiveModel
169
167
  # end
170
168
  alias :read_attribute_for_serialization :send
171
169
 
170
+ def serializable_attributes(attribute_names)
171
+ attribute_names.index_with { |n| read_attribute_for_serialization(n) }
172
+ end
173
+
172
174
  # Add associations specified via the <tt>:include</tt> option.
173
175
  #
174
176
  # Expects a block that takes as arguments:
@@ -26,13 +26,13 @@ module ActiveModel
26
26
  # user = User.find(1)
27
27
  # user.as_json
28
28
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
29
- # # "created_at" => "2006/08/01", "awesome" => true}
29
+ # # "created_at" => "2006-08-01T17:27:133.000Z", "awesome" => true}
30
30
  #
31
31
  # ActiveRecord::Base.include_root_in_json = true
32
32
  #
33
33
  # user.as_json
34
34
  # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
35
- # # "created_at" => "2006/08/01", "awesome" => true } }
35
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
36
36
  #
37
37
  # This behavior can also be achieved by setting the <tt>:root</tt> option
38
38
  # to +true+ as in:
@@ -40,7 +40,14 @@ module ActiveModel
40
40
  # user = User.find(1)
41
41
  # user.as_json(root: true)
42
42
  # # => { "user" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
43
- # # "created_at" => "2006/08/01", "awesome" => true } }
43
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
44
+ #
45
+ # If you prefer, <tt>:root</tt> may also be set to a custom string key instead as in:
46
+ #
47
+ # user = User.find(1)
48
+ # user.as_json(root: "author")
49
+ # # => { "author" => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
50
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true } }
44
51
  #
45
52
  # Without any +options+, the returned Hash will include all the model's
46
53
  # attributes.
@@ -48,7 +55,7 @@ module ActiveModel
48
55
  # user = User.find(1)
49
56
  # user.as_json
50
57
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
51
- # # "created_at" => "2006/08/01", "awesome" => true}
58
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true}
52
59
  #
53
60
  # The <tt>:only</tt> and <tt>:except</tt> options can be used to limit
54
61
  # the attributes included, and work similar to the +attributes+ method.
@@ -63,14 +70,14 @@ module ActiveModel
63
70
  #
64
71
  # user.as_json(methods: :permalink)
65
72
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
66
- # # "created_at" => "2006/08/01", "awesome" => true,
73
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
67
74
  # # "permalink" => "1-konata-izumi" }
68
75
  #
69
76
  # To include associations use <tt>:include</tt>:
70
77
  #
71
78
  # user.as_json(include: :posts)
72
79
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
73
- # # "created_at" => "2006/08/01", "awesome" => true,
80
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
74
81
  # # "posts" => [ { "id" => 1, "author_id" => 1, "title" => "Welcome to the weblog" },
75
82
  # # { "id" => 2, "author_id" => 1, "title" => "So I was thinking" } ] }
76
83
  #
@@ -81,7 +88,7 @@ module ActiveModel
81
88
  # only: :body } },
82
89
  # only: :title } })
83
90
  # # => { "id" => 1, "name" => "Konata Izumi", "age" => 16,
84
- # # "created_at" => "2006/08/01", "awesome" => true,
91
+ # # "created_at" => "2006-08-01T17:27:13.000Z", "awesome" => true,
85
92
  # # "posts" => [ { "comments" => [ { "body" => "1st post!" }, { "body" => "Second!" } ],
86
93
  # # "title" => "Welcome to the weblog" },
87
94
  # # { "comments" => [ { "body" => "Don't think too hard" } ],
@@ -93,11 +100,12 @@ module ActiveModel
93
100
  include_root_in_json
94
101
  end
95
102
 
103
+ hash = serializable_hash(options).as_json
96
104
  if root
97
105
  root = model_name.element if root == true
98
- { root => serializable_hash(options) }
106
+ { root => hash }
99
107
  else
100
- serializable_hash(options)
108
+ hash
101
109
  end
102
110
  end
103
111
 
@@ -64,7 +64,7 @@ module ActiveModel
64
64
  defaults << attribute.humanize
65
65
 
66
66
  options[:default] = defaults
67
- I18n.translate(defaults.shift, options)
67
+ I18n.translate(defaults.shift, **options)
68
68
  end
69
69
  end
70
70
  end
@@ -6,7 +6,6 @@ module ActiveModel
6
6
  module Type
7
7
  class BigInteger < Integer # :nodoc:
8
8
  private
9
-
10
9
  def max_value
11
10
  ::Float::INFINITY
12
11
  end
@@ -40,7 +40,7 @@ module ActiveModel
40
40
  alias_method :to_str, :to_s
41
41
 
42
42
  def hex
43
- @value.unpack("H*")[0]
43
+ @value.unpack1("H*")
44
44
  end
45
45
 
46
46
  def ==(other)
@@ -34,7 +34,6 @@ module ActiveModel
34
34
  end
35
35
 
36
36
  private
37
-
38
37
  def cast_value(value)
39
38
  if value == ""
40
39
  nil
@@ -10,16 +10,11 @@ module ActiveModel
10
10
  :date
11
11
  end
12
12
 
13
- def serialize(value)
14
- cast(value)
15
- end
16
-
17
13
  def type_cast_for_schema(value)
18
14
  value.to_s(:db).inspect
19
15
  end
20
16
 
21
17
  private
22
-
23
18
  def cast_value(value)
24
19
  if value.is_a?(::String)
25
20
  return if value.empty?
@@ -13,12 +13,7 @@ module ActiveModel
13
13
  :datetime
14
14
  end
15
15
 
16
- def serialize(value)
17
- super(cast(value))
18
- end
19
-
20
16
  private
21
-
22
17
  def cast_value(value)
23
18
  return apply_seconds_precision(value) unless value.is_a?(::String)
24
19
  return if value.empty?
@@ -40,9 +35,9 @@ module ActiveModel
40
35
  end
41
36
 
42
37
  def value_from_multiparameter_assignment(values_hash)
43
- missing_parameter = (1..3).detect { |key| !values_hash.key?(key) }
44
- if missing_parameter
45
- raise ArgumentError, missing_parameter
38
+ missing_parameters = [1, 2, 3].delete_if { |key| values_hash.key?(key) }
39
+ unless missing_parameters.empty?
40
+ raise ArgumentError, "Provided hash #{values_hash} doesn't contain necessary keys: #{missing_parameters}"
46
41
  end
47
42
  super
48
43
  end
@@ -17,7 +17,6 @@ module ActiveModel
17
17
  end
18
18
 
19
19
  private
20
-
21
20
  def cast_value(value)
22
21
  casted_value = \
23
22
  case value
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/object/try"
4
+
3
5
  module ActiveModel
4
6
  module Type
5
7
  class Float < Value # :nodoc:
@@ -18,10 +20,7 @@ module ActiveModel
18
20
  end
19
21
  end
20
22
 
21
- alias serialize cast
22
-
23
23
  private
24
-
25
24
  def cast_value(value)
26
25
  case value
27
26
  when ::Float then value
@@ -4,8 +4,12 @@ module ActiveModel
4
4
  module Type
5
5
  module Helpers # :nodoc: all
6
6
  class AcceptsMultiparameterTime < Module
7
- def initialize(defaults: {})
8
- define_method(:cast) do |value|
7
+ module InstanceMethods
8
+ def serialize(value)
9
+ super(cast(value))
10
+ end
11
+
12
+ def cast(value)
9
13
  if value.is_a?(Hash)
10
14
  value_from_multiparameter_assignment(value)
11
15
  else
@@ -13,7 +17,7 @@ module ActiveModel
13
17
  end
14
18
  end
15
19
 
16
- define_method(:assert_valid_value) do |value|
20
+ def assert_valid_value(value)
17
21
  if value.is_a?(Hash)
18
22
  value_from_multiparameter_assignment(value)
19
23
  else
@@ -21,17 +25,21 @@ module ActiveModel
21
25
  end
22
26
  end
23
27
 
24
- define_method(:value_constructed_by_mass_assignment?) do |value|
28
+ def value_constructed_by_mass_assignment?(value)
25
29
  value.is_a?(Hash)
26
30
  end
31
+ end
32
+
33
+ def initialize(defaults: {})
34
+ include InstanceMethods
27
35
 
28
36
  define_method(:value_from_multiparameter_assignment) do |values_hash|
29
37
  defaults.each do |k, v|
30
38
  values_hash[k] ||= v
31
39
  end
32
40
  return unless values_hash[1] && values_hash[2] && values_hash[3]
33
- values = values_hash.sort.map(&:last)
34
- ::Time.send(default_timezone, *values)
41
+ values = values_hash.sort.map!(&:last)
42
+ ::Time.public_send(default_timezone, *values)
35
43
  end
36
44
  private :value_from_multiparameter_assignment
37
45
  end
@@ -4,14 +4,23 @@ module ActiveModel
4
4
  module Type
5
5
  module Helpers # :nodoc: all
6
6
  module Numeric
7
+ def serialize(value)
8
+ cast(value)
9
+ end
10
+
7
11
  def cast(value)
8
- value = \
12
+ # Checks whether the value is numeric. Spaceship operator
13
+ # will return nil if value is not numeric.
14
+ value = if value <=> 0
15
+ value
16
+ else
9
17
  case value
10
18
  when true then 1
11
19
  when false then 0
12
- when ::String then value.presence
13
- else value
20
+ else value.presence
14
21
  end
22
+ end
23
+
15
24
  super(value)
16
25
  end
17
26
 
@@ -20,17 +29,19 @@ module ActiveModel
20
29
  end
21
30
 
22
31
  private
23
-
24
32
  def number_to_non_number?(old_value, new_value_before_type_cast)
25
- old_value != nil && non_numeric_string?(new_value_before_type_cast)
33
+ old_value != nil && non_numeric_string?(new_value_before_type_cast.to_s)
26
34
  end
27
35
 
28
36
  def non_numeric_string?(value)
29
37
  # 'wibble'.to_i will give zero, we want to make sure
30
38
  # that we aren't marking int zero to string zero as
31
39
  # changed.
32
- !/\A[-+]?\d+/.match?(value.to_s)
40
+ !NUMERIC_REGEX.match?(value)
33
41
  end
42
+
43
+ NUMERIC_REGEX = /\A\s*[+-]?\d/
44
+ private_constant :NUMERIC_REGEX
34
45
  end
35
46
  end
36
47
  end