activemodel 5.2.6 → 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 (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