activemodel 5.2.6 → 6.1.4

Sign up to get free protection for your applications and to get access to all the features.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +58 -109
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +6 -4
  5. data/lib/active_model.rb +2 -1
  6. data/lib/active_model/attribute.rb +21 -21
  7. data/lib/active_model/attribute/user_provided_default.rb +1 -2
  8. data/lib/active_model/attribute_assignment.rb +4 -6
  9. data/lib/active_model/attribute_methods.rb +117 -40
  10. data/lib/active_model/attribute_mutation_tracker.rb +90 -33
  11. data/lib/active_model/attribute_set.rb +20 -28
  12. data/lib/active_model/attribute_set/builder.rb +81 -16
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
  14. data/lib/active_model/attributes.rb +65 -44
  15. data/lib/active_model/callbacks.rb +11 -9
  16. data/lib/active_model/conversion.rb +1 -1
  17. data/lib/active_model/dirty.rb +51 -101
  18. data/lib/active_model/error.rb +207 -0
  19. data/lib/active_model/errors.rb +347 -155
  20. data/lib/active_model/gem_version.rb +3 -3
  21. data/lib/active_model/lint.rb +1 -1
  22. data/lib/active_model/naming.rb +22 -7
  23. data/lib/active_model/nested_error.rb +22 -0
  24. data/lib/active_model/railtie.rb +6 -0
  25. data/lib/active_model/secure_password.rb +54 -55
  26. data/lib/active_model/serialization.rb +9 -7
  27. data/lib/active_model/serializers/json.rb +17 -9
  28. data/lib/active_model/translation.rb +1 -1
  29. data/lib/active_model/type/big_integer.rb +0 -1
  30. data/lib/active_model/type/binary.rb +1 -1
  31. data/lib/active_model/type/boolean.rb +0 -1
  32. data/lib/active_model/type/date.rb +0 -5
  33. data/lib/active_model/type/date_time.rb +3 -8
  34. data/lib/active_model/type/decimal.rb +0 -1
  35. data/lib/active_model/type/float.rb +2 -3
  36. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
  37. data/lib/active_model/type/helpers/numeric.rb +17 -6
  38. data/lib/active_model/type/helpers/time_value.rb +37 -15
  39. data/lib/active_model/type/helpers/timezone.rb +1 -1
  40. data/lib/active_model/type/immutable_string.rb +14 -11
  41. data/lib/active_model/type/integer.rb +15 -18
  42. data/lib/active_model/type/registry.rb +16 -16
  43. data/lib/active_model/type/string.rb +12 -3
  44. data/lib/active_model/type/time.rb +1 -6
  45. data/lib/active_model/type/value.rb +9 -2
  46. data/lib/active_model/validations.rb +6 -9
  47. data/lib/active_model/validations/absence.rb +2 -2
  48. data/lib/active_model/validations/acceptance.rb +34 -27
  49. data/lib/active_model/validations/callbacks.rb +15 -16
  50. data/lib/active_model/validations/clusivity.rb +6 -3
  51. data/lib/active_model/validations/confirmation.rb +4 -4
  52. data/lib/active_model/validations/exclusion.rb +1 -1
  53. data/lib/active_model/validations/format.rb +2 -3
  54. data/lib/active_model/validations/inclusion.rb +2 -2
  55. data/lib/active_model/validations/length.rb +3 -3
  56. data/lib/active_model/validations/numericality.rb +58 -44
  57. data/lib/active_model/validations/presence.rb +1 -1
  58. data/lib/active_model/validations/validates.rb +7 -6
  59. data/lib/active_model/validator.rb +8 -3
  60. metadata +14 -9
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1a59d44bf155127baa257f82cb61f50de9d0dd491fe45b4b78456dcadbd23558
4
- data.tar.gz: 8f060adbdac9756f51622f4e87dcb3bedf95e1eef85a33fef8be1a9ba2662200
3
+ metadata.gz: e4c60859ba62fdb88e69492cc1ee5788a0d8a57e51c483348c079f6fec17629f
4
+ data.tar.gz: a9714409e2bb586260f213472dad9c7f91d2e6d03799931c27543116397fe5f6
5
5
  SHA512:
6
- metadata.gz: a7f2bd0e35b8cb4bc561bcc9a337054e9066861666e4445703ce333fe8c923f95deb0e781697a8b939f5990c7816f78319a8b8f5de77bdf2dc896a81fa4a2201
7
- data.tar.gz: aa5d44a55b7523a2486f4785dc59b427c97a36351ac5733d5352be359a8f9a003a4833891f0c3ba66ae791e6bd68fe0501ce3e05fa2b57a866cebca1c50cdb83
6
+ metadata.gz: 1c55bc3c301f001842595688b4b9f3e6448306ea0df795b4981d2e070a7052848c53e1b6d50eba905a83ebafe154e357ea0fba1b4543585d2e179685d1a46f64
7
+ data.tar.gz: 953a5dee9d6017155202ac7f5bb22da216afab18910bd8259029f7d3e31b714d2cd5b7248cf0fcaf737dc1214496044403b0f1315756117595b99ec3e57cf446
data/CHANGELOG.md CHANGED
@@ -1,163 +1,112 @@
1
- ## Rails 5.2.6 (May 05, 2021) ##
2
-
3
- * No changes.
1
+ ## Rails 6.1.4 (June 24, 2021) ##
4
2
 
3
+ * Fix `to_json` for `ActiveModel::Dirty` object.
5
4
 
6
- ## Rails 5.2.5 (March 26, 2021) ##
5
+ Exclude +mutations_from_database+ attribute from json as it lead to recursion.
7
6
 
8
- * No changes.
7
+ *Anil Maurya*
9
8
 
10
9
 
11
- ## Rails 5.2.4.6 (May 05, 2021) ##
10
+ ## Rails 6.1.3.2 (May 05, 2021) ##
12
11
 
13
12
  * No changes.
14
13
 
15
14
 
16
- ## Rails 5.2.4.5 (February 10, 2021) ##
15
+ ## Rails 6.1.3.1 (March 26, 2021) ##
17
16
 
18
17
  * No changes.
19
18
 
20
19
 
21
- ## Rails 5.2.4.4 (September 09, 2020) ##
20
+ ## Rails 6.1.3 (February 17, 2021) ##
22
21
 
23
22
  * No changes.
24
23
 
25
24
 
26
- ## Rails 5.2.4.3 (May 18, 2020) ##
25
+ ## Rails 6.1.2.1 (February 10, 2021) ##
27
26
 
28
27
  * No changes.
29
28
 
30
29
 
31
- ## Rails 5.2.4.2 (March 19, 2020) ##
30
+ ## Rails 6.1.2 (February 09, 2021) ##
32
31
 
33
32
  * No changes.
34
33
 
35
34
 
36
- ## Rails 5.2.4.1 (December 18, 2019) ##
35
+ ## Rails 6.1.1 (January 07, 2021) ##
37
36
 
38
37
  * No changes.
39
38
 
40
39
 
41
- ## Rails 5.2.4 (November 27, 2019) ##
42
-
43
- * Type cast falsy boolean symbols on boolean attribute as false.
44
-
45
- Fixes #35676.
46
-
47
- *Ryuta Kamizono*
48
-
49
-
50
- ## Rails 5.2.3 (March 27, 2019) ##
51
-
52
- * Fix date value when casting a multiparameter date hash to not convert
53
- from Gregorian date to Julian date.
40
+ ## Rails 6.1.0 (December 09, 2020) ##
54
41
 
55
- Before:
42
+ * Pass in `base` instead of `base_class` to Error.human_attribute_name
56
43
 
57
- Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
58
- => #<Day id: nil, day: "0001-01-03", created_at: nil, updated_at: nil>
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.
59
47
 
60
- After:
61
-
62
- Day.new({"day(1i)"=>"1", "day(2i)"=>"1", "day(3i)"=>"1"})
63
- => #<Day id: nil, day: "0001-01-01", created_at: nil, updated_at: nil>
64
-
65
- Fixes #28521.
66
-
67
- *Sayan Chakraborty*
68
-
69
- * Fix numericality equality validation of `BigDecimal` and `Float`
70
- by casting to `BigDecimal` on both ends of the validation.
71
-
72
- *Gannon McGibbon*
73
-
74
-
75
- ## Rails 5.2.2.1 (March 11, 2019) ##
76
-
77
- * No changes.
48
+ *Filipe Sabella*
78
49
 
79
-
80
- ## Rails 5.2.2 (December 04, 2018) ##
81
-
82
- * Fix numericality validator to still use value before type cast except Active Record.
83
-
84
- Fixes #33651, #33686.
50
+ * Deprecate marshalling load from legacy attributes format.
85
51
 
86
52
  *Ryuta Kamizono*
87
53
 
54
+ * `*_previously_changed?` accepts `:from` and `:to` keyword arguments like `*_changed?`.
88
55
 
89
- ## Rails 5.2.1.1 (November 27, 2018) ##
90
-
91
- * No changes.
92
-
93
-
94
- ## Rails 5.2.1 (August 07, 2018) ##
95
-
96
- * No changes.
97
-
98
-
99
- ## Rails 5.2.0 (April 09, 2018) ##
100
-
101
- * Do not lose all multiple `:includes` with options in serialization.
102
-
103
- *Mike Mangino*
104
-
105
- * Models using the attributes API with a proc default can now be marshalled.
106
-
107
- Fixes #31216.
108
-
109
- *Sean Griffin*
110
-
111
- * Fix to working before/after validation callbacks on multiple contexts.
112
-
113
- *Yoshiyuki Hirano*
56
+ topic.update!(status: :archived)
57
+ topic.status_previously_changed?(from: "active", to: "archived")
58
+ # => true
114
59
 
115
- * Execute `ConfirmationValidator` validation when `_confirmation`'s value is `false`.
60
+ *George Claghorn*
116
61
 
117
- *bogdanvlviv*
62
+ * Raise FrozenError when trying to write attributes that aren't backed by the database on an object that is frozen:
118
63
 
119
- * Allow passing a Proc or Symbol to length validator options.
64
+ class Animal
65
+ include ActiveModel::Attributes
66
+ attribute :age
67
+ end
120
68
 
121
- *Matt Rohrer*
69
+ animal = Animal.new
70
+ animal.freeze
71
+ animal.age = 25 # => FrozenError, "can't modify a frozen Animal"
122
72
 
123
- * Add method `#merge!` for `ActiveModel::Errors`.
73
+ *Josh Brody*
124
74
 
125
- *Jahfer Husain*
75
+ * Add `*_previously_was` attribute methods when dirty tracking. Example:
126
76
 
127
- * Fix regression in numericality validator when comparing Decimal and Float input
128
- values with more scale than the schema.
77
+ pirate.update(catchphrase: "Ahoy!")
78
+ pirate.previous_changes["catchphrase"] # => ["Thar She Blows!", "Ahoy!"]
79
+ pirate.catchphrase_previously_was # => "Thar She Blows!"
129
80
 
130
- *Bradley Priest*
81
+ *DHH*
131
82
 
132
- * Fix methods `#keys`, `#values` in `ActiveModel::Errors`.
83
+ * Encapsulate each validation error as an Error object.
133
84
 
134
- Change `#keys` to only return the keys that don't have empty messages.
85
+ The `ActiveModel`’s `errors` collection is now an array of these Error
86
+ objects, instead of messages/details hash.
135
87
 
136
- Change `#values` to only return the not empty values.
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.
137
91
 
138
- Example:
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.
139
96
 
140
- # Before
141
- person = Person.new
142
- person.errors.keys # => []
143
- person.errors.values # => []
144
- person.errors.messages # => {}
145
- person.errors[:name] # => []
146
- person.errors.messages # => {:name => []}
147
- person.errors.keys # => [:name]
148
- person.errors.values # => [[]]
97
+ The list of deprecated methods and their planned future behavioral changes at the next major release are:
149
98
 
150
- # After
151
- person = Person.new
152
- person.errors.keys # => []
153
- person.errors.values # => []
154
- person.errors.messages # => {}
155
- person.errors[:name] # => []
156
- person.errors.messages # => {:name => []}
157
- person.errors.keys # => []
158
- person.errors.values # => []
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.
159
108
 
160
- *bogdanvlviv*
109
+ *lulalala*
161
110
 
162
111
 
163
- Please check [5-1-stable](https://github.com/rails/rails/blob/5-1-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-2018 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
@@ -5,6 +5,8 @@ They allow for Action Pack helpers to interact with non-Active Record models,
5
5
  for example. Active Model also helps with building custom ORMs for use outside of
6
6
  the Rails framework.
7
7
 
8
+ You can read more about Active Model in the {Active Model Basics}[https://edgeguides.rubyonrails.org/active_model_basics.html] guide.
9
+
8
10
  Prior to Rails 3.0, if a plugin or gem developer wanted to have an object
9
11
  interact with Action Pack helpers, it was required to either copy chunks of
10
12
  code from Rails, or monkey patch entire helpers to make them handle objects
@@ -198,7 +200,7 @@ behavior out of the box:
198
200
  attr_accessor :first_name, :last_name
199
201
 
200
202
  validates_each :first_name, :last_name do |record, attr, value|
201
- 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")
202
204
  end
203
205
  end
204
206
 
@@ -239,7 +241,7 @@ The latest version of Active Model can be installed with RubyGems:
239
241
 
240
242
  Source code can be downloaded as part of the Rails project on GitHub
241
243
 
242
- * https://github.com/rails/rails/tree/5-2-stable/activemodel
244
+ * https://github.com/rails/rails/tree/main/activemodel
243
245
 
244
246
 
245
247
  == License
@@ -253,7 +255,7 @@ Active Model is released under the MIT license:
253
255
 
254
256
  API documentation is at:
255
257
 
256
- * http://api.rubyonrails.org
258
+ * https://api.rubyonrails.org
257
259
 
258
260
  Bug reports for the Ruby on Rails project can be filed here:
259
261
 
@@ -261,4 +263,4 @@ Bug reports for the Ruby on Rails project can be filed here:
261
263
 
262
264
  Feature requests should be discussed on the rails-core mailing list here:
263
265
 
264
- * https://groups.google.com/forum/?fromgroups#!forum/rubyonrails-core
266
+ * https://discuss.rubyonrails.org/c/rubyonrails-core
data/lib/active_model.rb CHANGED
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  #--
4
- # Copyright (c) 2004-2018 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,20 +133,18 @@ module ActiveModel
132
133
  coder["value"] = value if defined?(@value)
133
134
  end
134
135
 
135
- protected
136
-
137
- attr_reader :original_attribute
138
- alias_method :assigned?, :original_attribute
139
-
140
- def original_value_for_database
141
- if assigned?
142
- original_attribute.original_value_for_database
143
- else
144
- _original_value_for_database
145
- end
136
+ def original_value_for_database
137
+ if assigned?
138
+ original_attribute.original_value_for_database
139
+ else
140
+ _original_value_for_database
146
141
  end
142
+ end
147
143
 
148
144
  private
145
+ attr_reader :original_attribute
146
+ alias :assigned? :original_attribute
147
+
149
148
  def initialize_dup(other)
150
149
  if defined?(@value) && @value.duplicable?
151
150
  @value = @value.dup
@@ -165,9 +164,10 @@ module ActiveModel
165
164
  type.deserialize(value)
166
165
  end
167
166
 
168
- def _original_value_for_database
169
- value_before_type_cast
170
- end
167
+ private
168
+ def _original_value_for_database
169
+ value_before_type_cast
170
+ end
171
171
  end
172
172
 
173
173
  class FromUser < Attribute # :nodoc:
@@ -44,8 +44,7 @@ module ActiveModel
44
44
  end
45
45
  end
46
46
 
47
- protected
48
-
47
+ private
49
48
  attr_reader :user_provided_value
50
49
  end
51
50
  end
@@ -26,19 +26,17 @@ 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)
30
- raise ArgumentError, "When assigning attributes, you must pass a hash as an argument."
29
+ unless new_attributes.respond_to?(:each_pair)
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
39
38
 
40
39
  private
41
-
42
40
  def _assign_attributes(attributes)
43
41
  attributes.each do |k, v|
44
42
  _assign_attribute(k, v)
@@ -50,7 +48,7 @@ module ActiveModel
50
48
  if respond_to?(setter)
51
49
  public_send(setter, v)
52
50
  else
53
- raise UnknownAttributeError.new(self, k)
51
+ raise UnknownAttributeError.new(self, k.to_s)
54
52
  end
55
53
  end
56
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.method_missing_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.method_missing_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,63 +398,56 @@ 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
- # Must try to match prefixes/suffixes first, or else the matcher with no prefix/suffix
356
- # will match every time.
357
- matchers = attribute_method_matchers.partition(&:plain?).reverse.flatten(1)
358
- matchers.map { |method| method.match(method_name) }.compact
401
+ attribute_method_matchers.map { |matcher| matcher.match(method_name) }.compact
359
402
  end
360
403
  end
361
404
 
362
405
  # Define a method `name` in `mod` that dispatches to `send`
363
406
  # using the given `extra` args. This falls back on `define_method`
364
407
  # and `send` if the given names cannot be compiled.
365
- def define_proxy_call(include_private, mod, name, send, *extra)
408
+ def define_proxy_call(include_private, code_generator, name, target, *extra)
366
409
  defn = if NAME_COMPILABLE_REGEXP.match?(name)
367
410
  "def #{name}(*args)"
368
411
  else
369
412
  "define_method(:'#{name}') do |*args|"
370
413
  end
371
414
 
372
- extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
415
+ extra = (extra.map!(&:inspect) << "*args").join(", ")
373
416
 
374
- target = if CALL_COMPILABLE_REGEXP.match?(send)
375
- "#{"self." unless include_private}#{send}(#{extra})"
417
+ body = if CALL_COMPILABLE_REGEXP.match?(target)
418
+ "#{"self." unless include_private}#{target}(#{extra})"
376
419
  else
377
- "send(:'#{send}', #{extra})"
420
+ "send(:'#{target}', #{extra})"
378
421
  end
379
422
 
380
- mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
381
- #{defn}
382
- #{target}
383
- end
384
- RUBY
423
+ code_generator <<
424
+ defn <<
425
+ body <<
426
+ "end" <<
427
+ "ruby2_keywords(:'#{name}') if respond_to?(:ruby2_keywords, true)"
385
428
  end
386
429
 
387
430
  class AttributeMethodMatcher #:nodoc:
388
- attr_reader :prefix, :suffix, :method_missing_target
431
+ attr_reader :prefix, :suffix, :target
389
432
 
390
- AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
433
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
391
434
 
392
435
  def initialize(options = {})
393
436
  @prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
394
437
  @regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
395
- @method_missing_target = "#{@prefix}attribute#{@suffix}"
438
+ @target = "#{@prefix}attribute#{@suffix}"
396
439
  @method_name = "#{prefix}%s#{suffix}"
397
440
  end
398
441
 
399
442
  def match(method_name)
400
443
  if @regex =~ method_name
401
- AttributeMethodMatch.new(method_missing_target, $1, method_name)
444
+ AttributeMethodMatch.new(target, $1)
402
445
  end
403
446
  end
404
447
 
405
448
  def method_name(attr_name)
406
449
  @method_name % attr_name
407
450
  end
408
-
409
- def plain?
410
- prefix.empty? && suffix.empty?
411
- end
412
451
  end
413
452
  end
414
453
 
@@ -430,6 +469,7 @@ module ActiveModel
430
469
  match ? attribute_missing(match, *args, &block) : super
431
470
  end
432
471
  end
472
+ ruby2_keywords(:method_missing) if respond_to?(:ruby2_keywords, true)
433
473
 
434
474
  # +attribute_missing+ is like +method_missing+, but for attributes. When
435
475
  # +method_missing+ is called we check to see if there is a matching
@@ -474,5 +514,42 @@ module ActiveModel
474
514
  def _read_attribute(attr)
475
515
  __send__(attr)
476
516
  end
517
+
518
+ module AttrNames # :nodoc:
519
+ DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
520
+
521
+ # We want to generate the methods via module_eval rather than
522
+ # define_method, because define_method is slower on dispatch.
523
+ # Evaluating many similar methods may use more memory as the instruction
524
+ # sequences are duplicated and cached (in MRI). define_method may
525
+ # be slower on dispatch, but if you're careful about the closure
526
+ # created, then define_method will consume much less memory.
527
+ #
528
+ # But sometimes the database might return columns with
529
+ # characters that are not allowed in normal method names (like
530
+ # 'my_column(omg)'. So to work around this we first define with
531
+ # the __temp__ identifier, and then use alias method to rename
532
+ # it to what we want.
533
+ #
534
+ # We are also defining a constant to hold the frozen string of
535
+ # the attribute name. Using a constant means that we do not have
536
+ # to allocate an object on each call to the attribute method.
537
+ # Making it frozen means that it doesn't get duped when used to
538
+ # key the @attributes in read_attribute.
539
+ def self.define_attribute_accessor_method(owner, attr_name, writer: false)
540
+ method_name = "#{attr_name}#{'=' if writer}"
541
+ if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
542
+ yield method_name, "'#{attr_name}'"
543
+ else
544
+ safe_name = attr_name.unpack1("h*")
545
+ const_name = "ATTR_#{safe_name}"
546
+ const_set(const_name, attr_name) unless const_defined?(const_name)
547
+ temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
548
+ attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
549
+ yield temp_method_name, attr_name_expr
550
+ owner.rename_method(temp_method_name, method_name)
551
+ end
552
+ end
553
+ end
477
554
  end
478
555
  end