activemodel 6.0.4 → 6.1.4

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 (50) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -183
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +18 -17
  7. data/lib/active_model/attribute_assignment.rb +3 -4
  8. data/lib/active_model/attribute_methods.rb +74 -38
  9. data/lib/active_model/attribute_mutation_tracker.rb +8 -5
  10. data/lib/active_model/attribute_set.rb +18 -16
  11. data/lib/active_model/attribute_set/builder.rb +80 -13
  12. data/lib/active_model/attributes.rb +20 -24
  13. data/lib/active_model/callbacks.rb +1 -1
  14. data/lib/active_model/dirty.rb +17 -4
  15. data/lib/active_model/error.rb +207 -0
  16. data/lib/active_model/errors.rb +316 -208
  17. data/lib/active_model/gem_version.rb +1 -1
  18. data/lib/active_model/lint.rb +1 -1
  19. data/lib/active_model/naming.rb +2 -2
  20. data/lib/active_model/nested_error.rb +22 -0
  21. data/lib/active_model/railtie.rb +1 -1
  22. data/lib/active_model/secure_password.rb +14 -14
  23. data/lib/active_model/serialization.rb +9 -6
  24. data/lib/active_model/serializers/json.rb +7 -0
  25. data/lib/active_model/type/date_time.rb +2 -2
  26. data/lib/active_model/type/float.rb +2 -0
  27. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +11 -7
  28. data/lib/active_model/type/helpers/numeric.rb +8 -3
  29. data/lib/active_model/type/helpers/time_value.rb +27 -17
  30. data/lib/active_model/type/helpers/timezone.rb +1 -1
  31. data/lib/active_model/type/immutable_string.rb +14 -10
  32. data/lib/active_model/type/integer.rb +11 -2
  33. data/lib/active_model/type/registry.rb +5 -0
  34. data/lib/active_model/type/string.rb +12 -2
  35. data/lib/active_model/type/value.rb +9 -1
  36. data/lib/active_model/validations.rb +6 -6
  37. data/lib/active_model/validations/absence.rb +1 -1
  38. data/lib/active_model/validations/acceptance.rb +1 -1
  39. data/lib/active_model/validations/callbacks.rb +15 -15
  40. data/lib/active_model/validations/clusivity.rb +5 -1
  41. data/lib/active_model/validations/confirmation.rb +2 -2
  42. data/lib/active_model/validations/exclusion.rb +1 -1
  43. data/lib/active_model/validations/format.rb +2 -2
  44. data/lib/active_model/validations/inclusion.rb +1 -1
  45. data/lib/active_model/validations/length.rb +2 -2
  46. data/lib/active_model/validations/numericality.rb +54 -41
  47. data/lib/active_model/validations/presence.rb +1 -1
  48. data/lib/active_model/validations/validates.rb +6 -4
  49. data/lib/active_model/validator.rb +7 -1
  50. metadata +9 -7
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9610708333879d0a214dc8003af87b5250133d44d1ed9385b5f82f93ea6ad64c
4
- data.tar.gz: 70e1c5bc4de6a40ab7583a28e861cf78a79c11b31ca61c9d161f2b00c7ab7435
3
+ metadata.gz: e4c60859ba62fdb88e69492cc1ee5788a0d8a57e51c483348c079f6fec17629f
4
+ data.tar.gz: a9714409e2bb586260f213472dad9c7f91d2e6d03799931c27543116397fe5f6
5
5
  SHA512:
6
- metadata.gz: e4878f0711fef18ea963ca678032cf5d663c9eb7deb41bdd40a0058f9870c5fa74a7402d43d41c68412a5c9e1db35a0f5c53e5d0d77d4b9557c059c4df4a9bcd
7
- data.tar.gz: 7812c7fcf8eef96e13df5ceea79756cf8149e2bd28a8a02374523f2fb7a26e9fafc249a5698782b61dbe48cd0e5f96d9cc04fd61a7db9693599f1fdac24b4cbf
6
+ metadata.gz: 1c55bc3c301f001842595688b4b9f3e6448306ea0df795b4981d2e070a7052848c53e1b6d50eba905a83ebafe154e357ea0fba1b4543585d2e179685d1a46f64
7
+ data.tar.gz: 953a5dee9d6017155202ac7f5bb22da216afab18910bd8259029f7d3e31b714d2cd5b7248cf0fcaf737dc1214496044403b0f1315756117595b99ec3e57cf446
data/CHANGELOG.md CHANGED
@@ -1,237 +1,112 @@
1
- ## Rails 6.0.4 (June 15, 2021) ##
1
+ ## Rails 6.1.4 (June 24, 2021) ##
2
2
 
3
- * No changes.
4
-
5
-
6
- ## Rails 6.0.3.7 (May 05, 2021) ##
7
-
8
- * No changes.
9
-
10
-
11
- ## Rails 6.0.3.6 (March 26, 2021) ##
12
-
13
- * No changes.
14
-
15
-
16
- ## Rails 6.0.3.5 (February 10, 2021) ##
17
-
18
- * No changes.
19
-
20
-
21
- ## Rails 6.0.3.4 (October 07, 2020) ##
22
-
23
- * No changes.
3
+ * Fix `to_json` for `ActiveModel::Dirty` object.
24
4
 
5
+ Exclude +mutations_from_database+ attribute from json as it lead to recursion.
25
6
 
26
- ## Rails 6.0.3.3 (September 09, 2020) ##
27
-
28
- * No changes.
7
+ *Anil Maurya*
29
8
 
30
9
 
31
- ## Rails 6.0.3.2 (June 17, 2020) ##
10
+ ## Rails 6.1.3.2 (May 05, 2021) ##
32
11
 
33
12
  * No changes.
34
13
 
35
14
 
36
- ## Rails 6.0.3.1 (May 18, 2020) ##
15
+ ## Rails 6.1.3.1 (March 26, 2021) ##
37
16
 
38
17
  * No changes.
39
18
 
40
19
 
41
- ## Rails 6.0.3 (May 06, 2020) ##
20
+ ## Rails 6.1.3 (February 17, 2021) ##
42
21
 
43
22
  * No changes.
44
23
 
45
24
 
46
- ## Rails 6.0.2.2 (March 19, 2020) ##
25
+ ## Rails 6.1.2.1 (February 10, 2021) ##
47
26
 
48
27
  * No changes.
49
28
 
50
29
 
51
- ## Rails 6.0.2.1 (December 18, 2019) ##
30
+ ## Rails 6.1.2 (February 09, 2021) ##
52
31
 
53
32
  * No changes.
54
33
 
55
34
 
56
- ## Rails 6.0.2 (December 13, 2019) ##
35
+ ## Rails 6.1.1 (January 07, 2021) ##
57
36
 
58
37
  * No changes.
59
38
 
60
39
 
61
- ## Rails 6.0.1 (November 5, 2019) ##
40
+ ## Rails 6.1.0 (December 09, 2020) ##
62
41
 
63
- * No changes.
42
+ * Pass in `base` instead of `base_class` to Error.human_attribute_name
64
43
 
44
+ This is useful in cases where the `human_attribute_name` method depends
45
+ on other attributes' values of the class under validation to derive what the
46
+ attribute name should be.
65
47
 
66
- ## Rails 6.0.0 (August 16, 2019) ##
67
-
68
- * No changes.
69
-
70
-
71
- ## Rails 6.0.0.rc2 (July 22, 2019) ##
72
-
73
- * No changes.
48
+ *Filipe Sabella*
74
49
 
75
-
76
- ## Rails 6.0.0.rc1 (April 24, 2019) ##
77
-
78
- * Type cast falsy boolean symbols on boolean attribute as false.
79
-
80
- Fixes #35676.
50
+ * Deprecate marshalling load from legacy attributes format.
81
51
 
82
52
  *Ryuta Kamizono*
83
53
 
84
- * Change how validation error translation strings are fetched: The new behavior
85
- will first try the more specific keys, including doing locale fallback, then try
86
- the less specific ones.
87
-
88
- For example, this is the order in which keys will now be tried for a `blank`
89
- error on a `product`'s `title` attribute with current locale set to `en-US`:
90
-
91
- en-US.activerecord.errors.models.product.attributes.title.blank
92
- en-US.activerecord.errors.models.product.blank
93
- en-US.activerecord.errors.messages.blank
94
-
95
- en.activerecord.errors.models.product.attributes.title.blank
96
- en.activerecord.errors.models.product.blank
97
- en.activerecord.errors.messages.blank
98
-
99
- en-US.errors.attributes.title.blank
100
- en-US.errors.messages.blank
101
-
102
- en.errors.attributes.title.blank
103
- en.errors.messages.blank
54
+ * `*_previously_changed?` accepts `:from` and `:to` keyword arguments like `*_changed?`.
104
55
 
105
- *Hugo Vacher*
56
+ topic.update!(status: :archived)
57
+ topic.status_previously_changed?(from: "active", to: "archived")
58
+ # => true
106
59
 
60
+ *George Claghorn*
107
61
 
108
- ## Rails 6.0.0.beta3 (March 11, 2019) ##
62
+ * Raise FrozenError when trying to write attributes that aren't backed by the database on an object that is frozen:
109
63
 
110
- * No changes.
111
-
112
-
113
- ## Rails 6.0.0.beta2 (February 25, 2019) ##
114
-
115
- * Fix date value when casting a multiparameter date hash to not convert
116
- from Gregorian date to Julian date.
117
-
118
- Before:
119
-
120
- Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
121
- # => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
122
-
123
- After:
124
-
125
- Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
126
- # => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
127
-
128
- Fixes #28521.
129
-
130
- *Sayan Chakraborty*
131
-
132
- * Fix year value when casting a multiparameter time hash.
133
-
134
- When assigning a hash to a time attribute that's missing a year component
135
- (e.g. a `time_select` with `:ignore_date` set to `true`) then the year
136
- defaults to 1970 instead of the expected 2000. This results in the attribute
137
- changing as a result of the save.
138
-
139
- Before:
140
- ```
141
- event = Event.new(start_time: { 4 => 20, 5 => 30 })
142
- event.start_time # => 1970-01-01 20:30:00 UTC
143
- event.save
144
- event.reload
145
- event.start_time # => 2000-01-01 20:30:00 UTC
146
- ```
147
-
148
- After:
149
- ```
150
- event = Event.new(start_time: { 4 => 20, 5 => 30 })
151
- event.start_time # => 2000-01-01 20:30:00 UTC
152
- event.save
153
- event.reload
154
- event.start_time # => 2000-01-01 20:30:00 UTC
155
- ```
156
-
157
- *Andrew White*
158
-
159
-
160
- ## Rails 6.0.0.beta1 (January 18, 2019) ##
161
-
162
- * Internal calls to `human_attribute_name` on an `Active Model` now pass attributes as strings instead of symbols
163
- in some cases.
164
-
165
- This is in line with examples in Rails docs and puts the code in line with the intention -
166
- the potential use of strings or symbols.
167
-
168
- It is recommended to cast the attribute input to your desired type as if you you are overriding that methid.
169
-
170
- *Martin Larochelle*
171
-
172
- * Add `ActiveModel::Errors#of_kind?`.
173
-
174
- *bogdanvlviv*, *Rafael Mendonça França*
175
-
176
- * Fix numericality equality validation of `BigDecimal` and `Float`
177
- by casting to `BigDecimal` on both ends of the validation.
178
-
179
- *Gannon McGibbon*
180
-
181
- * Add `#slice!` method to `ActiveModel::Errors`.
182
-
183
- *Daniel López Prat*
184
-
185
- * Fix numericality validator to still use value before type cast except Active Record.
186
-
187
- Fixes #33651, #33686.
188
-
189
- *Ryuta Kamizono*
190
-
191
- * Fix `ActiveModel::Serializers::JSON#as_json` method for timestamps.
64
+ class Animal
65
+ include ActiveModel::Attributes
66
+ attribute :age
67
+ end
192
68
 
193
- Before:
194
- ```
195
- contact = Contact.new(created_at: Time.utc(2006, 8, 1))
196
- contact.as_json["created_at"] # => 2006-08-01 00:00:00 UTC
197
- ```
69
+ animal = Animal.new
70
+ animal.freeze
71
+ animal.age = 25 # => FrozenError, "can't modify a frozen Animal"
198
72
 
199
- After:
200
- ```
201
- contact = Contact.new(created_at: Time.utc(2006, 8, 1))
202
- contact.as_json["created_at"] # => "2006-08-01T00:00:00.000Z"
203
- ```
73
+ *Josh Brody*
204
74
 
205
- *Bogdan Gusiev*
75
+ * Add `*_previously_was` attribute methods when dirty tracking. Example:
206
76
 
207
- * Allows configurable attribute name for `#has_secure_password`. This
208
- still defaults to an attribute named 'password', causing no breaking
209
- change. There is a new method `#authenticate_XXX` where XXX is the
210
- configured attribute name, making the existing `#authenticate` now an
211
- alias for this when the attribute is the default 'password'.
77
+ pirate.update(catchphrase: "Ahoy!")
78
+ pirate.previous_changes["catchphrase"] # => ["Thar She Blows!", "Ahoy!"]
79
+ pirate.catchphrase_previously_was # => "Thar She Blows!"
212
80
 
213
- Example:
81
+ *DHH*
214
82
 
215
- class User < ActiveRecord::Base
216
- has_secure_password :recovery_password, validations: false
217
- end
83
+ * Encapsulate each validation error as an Error object.
218
84
 
219
- user = User.new()
220
- user.recovery_password = "42password"
221
- user.recovery_password_digest # => "$2a$04$iOfhwahFymCs5weB3BNH/uX..."
222
- user.authenticate_recovery_password('42password') # => user
85
+ The `ActiveModel`’s `errors` collection is now an array of these Error
86
+ objects, instead of messages/details hash.
223
87
 
224
- *Unathi Chonco*
88
+ For each of these `Error` object, its `message` and `full_message` methods
89
+ are for generating error messages. Its `details` method would return error’s
90
+ extra parameters, found in the original `details` hash.
225
91
 
226
- * Add `config.active_model.i18n_customize_full_message` in order to control whether
227
- the `full_message` error format can be overridden at the attribute or model
228
- level in the locale files. This is `false` by default.
92
+ The change tries its best at maintaining backward compatibility, however
93
+ some edge cases won’t be covered, like `errors#first` will return `ActiveModel::Error` and manipulating
94
+ `errors.messages` and `errors.details` hashes directly will have no effect. Moving forward,
95
+ please convert those direct manipulations to use provided API methods instead.
229
96
 
230
- *Martin Larochelle*
97
+ The list of deprecated methods and their planned future behavioral changes at the next major release are:
231
98
 
232
- * Rails 6 requires Ruby 2.5.0 or newer.
99
+ * `errors#slice!` will be removed.
100
+ * `errors#each` with the `key, value` two-arguments block will stop working, while the `error` single-argument block would return `Error` object.
101
+ * `errors#values` will be removed.
102
+ * `errors#keys` will be removed.
103
+ * `errors#to_xml` will be removed.
104
+ * `errors#to_h` will be removed, and can be replaced with `errors#to_hash`.
105
+ * Manipulating `errors` itself as a hash will have no effect (e.g. `errors[:foo] = 'bar'`).
106
+ * Manipulating the hash returned by `errors#messages` (e.g. `errors.messages[:foo] = 'bar'`) will have no effect.
107
+ * Manipulating the hash returned by `errors#details` (e.g. `errors.details[:foo].clear`) will have no effect.
233
108
 
234
- *Jeremy Daer*, *Kasper Timm Hansen*
109
+ *lulalala*
235
110
 
236
111
 
237
- Please check [5-2-stable](https://github.com/rails/rails/blob/5-2-stable/activemodel/CHANGELOG.md) for previous changes.
112
+ Please check [6-0-stable](https://github.com/rails/rails/blob/6-0-stable/activemodel/CHANGELOG.md) for previous changes.
data/MIT-LICENSE CHANGED
@@ -1,4 +1,4 @@
1
- Copyright (c) 2004-2019 David Heinemeier Hansson
1
+ Copyright (c) 2004-2020 David Heinemeier Hansson
2
2
 
3
3
  Permission is hereby granted, free of charge, to any person obtaining
4
4
  a copy of this software and associated documentation files (the
data/README.rdoc CHANGED
@@ -200,7 +200,7 @@ behavior out of the box:
200
200
  attr_accessor :first_name, :last_name
201
201
 
202
202
  validates_each :first_name, :last_name do |record, attr, value|
203
- record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
203
+ record.errors.add attr, "starts with z." if value.start_with?("z")
204
204
  end
205
205
  end
206
206
 
data/lib/active_model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2004-2019 David Heinemeier Hansson
4
+ # Copyright (c) 2004-2020 David Heinemeier Hansson
5
5
  #
6
6
  # Permission is hereby granted, free of charge, to any person obtaining
7
7
  # a copy of this software and associated documentation files (the
@@ -53,6 +53,7 @@ module ActiveModel
53
53
 
54
54
  eager_autoload do
55
55
  autoload :Errors
56
+ autoload :Error
56
57
  autoload :RangeError, "active_model/errors"
57
58
  autoload :StrictValidationFailed, "active_model/errors"
58
59
  autoload :UnknownAttributeError, "active_model/errors"
@@ -5,16 +5,16 @@ require "active_support/core_ext/object/duplicable"
5
5
  module ActiveModel
6
6
  class Attribute # :nodoc:
7
7
  class << self
8
- def from_database(name, value, type)
9
- FromDatabase.new(name, value, type)
8
+ def from_database(name, value_before_type_cast, type, value = nil)
9
+ FromDatabase.new(name, value_before_type_cast, type, nil, value)
10
10
  end
11
11
 
12
- def from_user(name, value, type, original_attribute = nil)
13
- FromUser.new(name, value, type, original_attribute)
12
+ def from_user(name, value_before_type_cast, type, original_attribute = nil)
13
+ FromUser.new(name, value_before_type_cast, type, original_attribute)
14
14
  end
15
15
 
16
- def with_cast_value(name, value, type)
17
- WithCastValue.new(name, value, type)
16
+ def with_cast_value(name, value_before_type_cast, type)
17
+ WithCastValue.new(name, value_before_type_cast, type)
18
18
  end
19
19
 
20
20
  def null(name)
@@ -30,11 +30,12 @@ module ActiveModel
30
30
 
31
31
  # This method should not be called directly.
32
32
  # Use #from_database or #from_user
33
- def initialize(name, value_before_type_cast, type, original_attribute = nil)
33
+ def initialize(name, value_before_type_cast, type, original_attribute = nil, value = nil)
34
34
  @name = name
35
35
  @value_before_type_cast = value_before_type_cast
36
36
  @type = type
37
37
  @original_attribute = original_attribute
38
+ @value = value unless value.nil?
38
39
  end
39
40
 
40
41
  def value
@@ -132,14 +133,13 @@ module ActiveModel
132
133
  coder["value"] = value if defined?(@value)
133
134
  end
134
135
 
135
- protected
136
- def original_value_for_database
137
- if assigned?
138
- original_attribute.original_value_for_database
139
- else
140
- _original_value_for_database
141
- end
136
+ def original_value_for_database
137
+ if assigned?
138
+ original_attribute.original_value_for_database
139
+ else
140
+ _original_value_for_database
142
141
  end
142
+ end
143
143
 
144
144
  private
145
145
  attr_reader :original_attribute
@@ -164,9 +164,10 @@ module ActiveModel
164
164
  type.deserialize(value)
165
165
  end
166
166
 
167
- def _original_value_for_database
168
- value_before_type_cast
169
- end
167
+ private
168
+ def _original_value_for_database
169
+ value_before_type_cast
170
+ end
170
171
  end
171
172
 
172
173
  class FromUser < Attribute # :nodoc:
@@ -26,13 +26,12 @@ module ActiveModel
26
26
  # cat.name # => 'Gorby'
27
27
  # cat.status # => 'sleeping'
28
28
  def assign_attributes(new_attributes)
29
- if !new_attributes.respond_to?(:stringify_keys)
29
+ unless new_attributes.respond_to?(:each_pair)
30
30
  raise ArgumentError, "When assigning attributes, you must pass a hash as an argument, #{new_attributes.class} passed."
31
31
  end
32
32
  return if new_attributes.empty?
33
33
 
34
- attributes = new_attributes.stringify_keys
35
- _assign_attributes(sanitize_for_mass_assignment(attributes))
34
+ _assign_attributes(sanitize_for_mass_assignment(new_attributes))
36
35
  end
37
36
 
38
37
  alias attributes= assign_attributes
@@ -49,7 +48,7 @@ module ActiveModel
49
48
  if respond_to?(setter)
50
49
  public_send(setter, v)
51
50
  else
52
- raise UnknownAttributeError.new(self, k)
51
+ raise UnknownAttributeError.new(self, k.to_s)
53
52
  end
54
53
  end
55
54
  end
@@ -207,10 +207,12 @@ module ActiveModel
207
207
  # person.nickname_short? # => true
208
208
  def alias_attribute(new_name, old_name)
209
209
  self.attribute_aliases = attribute_aliases.merge(new_name.to_s => old_name.to_s)
210
- attribute_method_matchers.each do |matcher|
211
- matcher_new = matcher.method_name(new_name).to_s
212
- matcher_old = matcher.method_name(old_name).to_s
213
- define_proxy_call false, self, matcher_new, matcher_old
210
+ CodeGenerator.batch(self, __FILE__, __LINE__) do |owner|
211
+ attribute_method_matchers.each do |matcher|
212
+ matcher_new = matcher.method_name(new_name).to_s
213
+ matcher_old = matcher.method_name(old_name).to_s
214
+ define_proxy_call false, owner, matcher_new, matcher_old
215
+ end
214
216
  end
215
217
  end
216
218
 
@@ -249,7 +251,9 @@ module ActiveModel
249
251
  # end
250
252
  # end
251
253
  def define_attribute_methods(*attr_names)
252
- attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
254
+ CodeGenerator.batch(generated_attribute_methods, __FILE__, __LINE__) do |owner|
255
+ attr_names.flatten.each { |attr_name| define_attribute_method(attr_name, _owner: owner) }
256
+ end
253
257
  end
254
258
 
255
259
  # Declares an attribute that should be prefixed and suffixed by
@@ -281,21 +285,23 @@ module ActiveModel
281
285
  # person.name = 'Bob'
282
286
  # person.name # => "Bob"
283
287
  # person.name_short? # => true
284
- def define_attribute_method(attr_name)
285
- attribute_method_matchers.each do |matcher|
286
- method_name = matcher.method_name(attr_name)
287
-
288
- unless instance_method_already_implemented?(method_name)
289
- generate_method = "define_method_#{matcher.target}"
290
-
291
- if respond_to?(generate_method, true)
292
- send(generate_method, attr_name.to_s)
293
- else
294
- define_proxy_call true, generated_attribute_methods, method_name, matcher.target, attr_name.to_s
288
+ def define_attribute_method(attr_name, _owner: generated_attribute_methods)
289
+ CodeGenerator.batch(_owner, __FILE__, __LINE__) do |owner|
290
+ attribute_method_matchers.each do |matcher|
291
+ method_name = matcher.method_name(attr_name)
292
+
293
+ unless instance_method_already_implemented?(method_name)
294
+ generate_method = "define_method_#{matcher.target}"
295
+
296
+ if respond_to?(generate_method, true)
297
+ send(generate_method, attr_name.to_s, owner: owner)
298
+ else
299
+ define_proxy_call true, owner, method_name, matcher.target, attr_name.to_s
300
+ end
295
301
  end
296
302
  end
303
+ attribute_method_matchers_cache.clear
297
304
  end
298
- attribute_method_matchers_cache.clear
299
305
  end
300
306
 
301
307
  # Removes all the previously dynamically defined methods from the class.
@@ -323,12 +329,52 @@ module ActiveModel
323
329
  # person.name_short? # => NoMethodError
324
330
  def undefine_attribute_methods
325
331
  generated_attribute_methods.module_eval do
326
- instance_methods.each { |m| undef_method(m) }
332
+ undef_method(*instance_methods)
327
333
  end
328
334
  attribute_method_matchers_cache.clear
329
335
  end
330
336
 
331
337
  private
338
+ class CodeGenerator
339
+ class << self
340
+ def batch(owner, path, line)
341
+ if owner.is_a?(CodeGenerator)
342
+ yield owner
343
+ else
344
+ instance = new(owner, path, line)
345
+ result = yield instance
346
+ instance.execute
347
+ result
348
+ end
349
+ end
350
+ end
351
+
352
+ def initialize(owner, path, line)
353
+ @owner = owner
354
+ @path = path
355
+ @line = line
356
+ @sources = ["# frozen_string_literal: true\n"]
357
+ @renames = {}
358
+ end
359
+
360
+ def <<(source_line)
361
+ @sources << source_line
362
+ end
363
+
364
+ def rename_method(old_name, new_name)
365
+ @renames[old_name] = new_name
366
+ end
367
+
368
+ def execute
369
+ @owner.module_eval(@sources.join(";"), @path, @line - 1)
370
+ @renames.each do |old_name, new_name|
371
+ @owner.alias_method new_name, old_name
372
+ @owner.undef_method old_name
373
+ end
374
+ end
375
+ end
376
+ private_constant :CodeGenerator
377
+
332
378
  def generated_attribute_methods
333
379
  @generated_attribute_methods ||= Module.new.tap { |mod| include mod }
334
380
  end
@@ -352,18 +398,14 @@ module ActiveModel
352
398
 
353
399
  def attribute_method_matchers_matching(method_name)
354
400
  attribute_method_matchers_cache.compute_if_absent(method_name) do
355
- # Bump plain matcher to last place so that only methods that do not
356
- # match any other pattern match the actual attribute name.
357
- # This is currently only needed to support legacy usage.
358
- matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
359
- matchers.map { |matcher| matcher.match(method_name) }.compact
401
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
360
402
  end
361
403
  end
362
404
 
363
405
  # Define a method `name` in `mod` that dispatches to `send`
364
406
  # using the given `extra` args. This falls back on `define_method`
365
407
  # and `send` if the given names cannot be compiled.
366
- def define_proxy_call(include_private, mod, name, target, *extra)
408
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
367
409
  defn = if NAME_COMPILABLE_REGEXP.match?(name)
368
410
  "def #{name}(*args)"
369
411
  else
@@ -378,12 +420,11 @@ module ActiveModel
378
420
  "send(:'#{target}', #{extra})"
379
421
  end
380
422
 
381
- mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
382
- #{defn}
383
- #{body}
384
- end
385
- ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)
386
- RUBY
423
+ code_generator <<
424
+ defn <<
425
+ body <<
426
+ "end" <<
427
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
387
428
  end
388
429
 
389
430
  class AttributeMethodMatcher #:nodoc:
@@ -407,10 +448,6 @@ module ActiveModel
407
448
  def method_name(attr_name)
408
449
  @method_name % attr_name
409
450
  end
410
-
411
- def plain?
412
- prefix.empty? && suffix.empty?
413
- end
414
451
  end
415
452
  end
416
453
 
@@ -499,10 +536,10 @@ module ActiveModel
499
536
  # to allocate an object on each call to the attribute method.
500
537
  # Making it frozen means that it doesn't get duped when used to
501
538
  # key the @attributes in read_attribute.
502
- def self.define_attribute_accessor_method(mod, attr_name, writer: false)
539
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
503
540
  method_name = "#{attr_name}#{'=' if writer}"
504
541
  if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
505
- yield method_name, "'#{attr_name}'.freeze"
542
+ yield method_name, "'#{attr_name}'"
506
543
  else
507
544
  safe_name = attr_name.unpack1("h*")
508
545
  const_name = "ATTR_#{safe_name}"
@@ -510,8 +547,7 @@ module ActiveModel
510
547
  temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
511
548
  attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
512
549
  yield temp_method_name, attr_name_expr
513
- mod.alias_method method_name, temp_method_name
514
- mod.undef_method temp_method_name
550
+ owner.rename_method(temp_method_name, method_name)
515
551
  end
516
552
  end
517
553
  end