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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +58 -109
- data/MIT-LICENSE +1 -1
- data/README.rdoc +6 -4
- data/lib/active_model.rb +2 -1
- data/lib/active_model/attribute.rb +21 -21
- data/lib/active_model/attribute/user_provided_default.rb +1 -2
- data/lib/active_model/attribute_assignment.rb +4 -6
- data/lib/active_model/attribute_methods.rb +117 -40
- data/lib/active_model/attribute_mutation_tracker.rb +90 -33
- data/lib/active_model/attribute_set.rb +20 -28
- data/lib/active_model/attribute_set/builder.rb +81 -16
- data/lib/active_model/attribute_set/yaml_encoder.rb +1 -2
- data/lib/active_model/attributes.rb +65 -44
- data/lib/active_model/callbacks.rb +11 -9
- data/lib/active_model/conversion.rb +1 -1
- data/lib/active_model/dirty.rb +51 -101
- data/lib/active_model/error.rb +207 -0
- data/lib/active_model/errors.rb +347 -155
- data/lib/active_model/gem_version.rb +3 -3
- data/lib/active_model/lint.rb +1 -1
- data/lib/active_model/naming.rb +22 -7
- data/lib/active_model/nested_error.rb +22 -0
- data/lib/active_model/railtie.rb +6 -0
- data/lib/active_model/secure_password.rb +54 -55
- data/lib/active_model/serialization.rb +9 -7
- data/lib/active_model/serializers/json.rb +17 -9
- data/lib/active_model/translation.rb +1 -1
- data/lib/active_model/type/big_integer.rb +0 -1
- data/lib/active_model/type/binary.rb +1 -1
- data/lib/active_model/type/boolean.rb +0 -1
- data/lib/active_model/type/date.rb +0 -5
- data/lib/active_model/type/date_time.rb +3 -8
- data/lib/active_model/type/decimal.rb +0 -1
- data/lib/active_model/type/float.rb +2 -3
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +14 -6
- data/lib/active_model/type/helpers/numeric.rb +17 -6
- data/lib/active_model/type/helpers/time_value.rb +37 -15
- data/lib/active_model/type/helpers/timezone.rb +1 -1
- data/lib/active_model/type/immutable_string.rb +14 -11
- data/lib/active_model/type/integer.rb +15 -18
- data/lib/active_model/type/registry.rb +16 -16
- data/lib/active_model/type/string.rb +12 -3
- data/lib/active_model/type/time.rb +1 -6
- data/lib/active_model/type/value.rb +9 -2
- data/lib/active_model/validations.rb +6 -9
- data/lib/active_model/validations/absence.rb +2 -2
- data/lib/active_model/validations/acceptance.rb +34 -27
- data/lib/active_model/validations/callbacks.rb +15 -16
- data/lib/active_model/validations/clusivity.rb +6 -3
- data/lib/active_model/validations/confirmation.rb +4 -4
- data/lib/active_model/validations/exclusion.rb +1 -1
- data/lib/active_model/validations/format.rb +2 -3
- data/lib/active_model/validations/inclusion.rb +2 -2
- data/lib/active_model/validations/length.rb +3 -3
- data/lib/active_model/validations/numericality.rb +58 -44
- data/lib/active_model/validations/presence.rb +1 -1
- data/lib/active_model/validations/validates.rb +7 -6
- data/lib/active_model/validator.rb +8 -3
- metadata +14 -9
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e4c60859ba62fdb88e69492cc1ee5788a0d8a57e51c483348c079f6fec17629f
|
4
|
+
data.tar.gz: a9714409e2bb586260f213472dad9c7f91d2e6d03799931c27543116397fe5f6
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 1c55bc3c301f001842595688b4b9f3e6448306ea0df795b4981d2e070a7052848c53e1b6d50eba905a83ebafe154e357ea0fba1b4543585d2e179685d1a46f64
|
7
|
+
data.tar.gz: 953a5dee9d6017155202ac7f5bb22da216afab18910bd8259029f7d3e31b714d2cd5b7248cf0fcaf737dc1214496044403b0f1315756117595b99ec3e57cf446
|
data/CHANGELOG.md
CHANGED
@@ -1,163 +1,112 @@
|
|
1
|
-
## Rails
|
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
|
-
|
5
|
+
Exclude +mutations_from_database+ attribute from json as it lead to recursion.
|
7
6
|
|
8
|
-
*
|
7
|
+
*Anil Maurya*
|
9
8
|
|
10
9
|
|
11
|
-
## Rails
|
10
|
+
## Rails 6.1.3.2 (May 05, 2021) ##
|
12
11
|
|
13
12
|
* No changes.
|
14
13
|
|
15
14
|
|
16
|
-
## Rails
|
15
|
+
## Rails 6.1.3.1 (March 26, 2021) ##
|
17
16
|
|
18
17
|
* No changes.
|
19
18
|
|
20
19
|
|
21
|
-
## Rails
|
20
|
+
## Rails 6.1.3 (February 17, 2021) ##
|
22
21
|
|
23
22
|
* No changes.
|
24
23
|
|
25
24
|
|
26
|
-
## Rails
|
25
|
+
## Rails 6.1.2.1 (February 10, 2021) ##
|
27
26
|
|
28
27
|
* No changes.
|
29
28
|
|
30
29
|
|
31
|
-
## Rails
|
30
|
+
## Rails 6.1.2 (February 09, 2021) ##
|
32
31
|
|
33
32
|
* No changes.
|
34
33
|
|
35
34
|
|
36
|
-
## Rails
|
35
|
+
## Rails 6.1.1 (January 07, 2021) ##
|
37
36
|
|
38
37
|
* No changes.
|
39
38
|
|
40
39
|
|
41
|
-
## Rails
|
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
|
-
|
42
|
+
* Pass in `base` instead of `base_class` to Error.human_attribute_name
|
56
43
|
|
57
|
-
|
58
|
-
|
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
|
-
|
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
|
-
|
90
|
-
|
91
|
-
|
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
|
-
*
|
60
|
+
*George Claghorn*
|
116
61
|
|
117
|
-
|
62
|
+
* Raise FrozenError when trying to write attributes that aren't backed by the database on an object that is frozen:
|
118
63
|
|
119
|
-
|
64
|
+
class Animal
|
65
|
+
include ActiveModel::Attributes
|
66
|
+
attribute :age
|
67
|
+
end
|
120
68
|
|
121
|
-
|
69
|
+
animal = Animal.new
|
70
|
+
animal.freeze
|
71
|
+
animal.age = 25 # => FrozenError, "can't modify a frozen Animal"
|
122
72
|
|
123
|
-
*
|
73
|
+
*Josh Brody*
|
124
74
|
|
125
|
-
|
75
|
+
* Add `*_previously_was` attribute methods when dirty tracking. Example:
|
126
76
|
|
127
|
-
|
128
|
-
|
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
|
-
*
|
81
|
+
*DHH*
|
131
82
|
|
132
|
-
*
|
83
|
+
* Encapsulate each validation error as an Error object.
|
133
84
|
|
134
|
-
|
85
|
+
The `ActiveModel`’s `errors` collection is now an array of these Error
|
86
|
+
objects, instead of messages/details hash.
|
135
87
|
|
136
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
157
|
-
|
158
|
-
|
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
|
-
*
|
109
|
+
*lulalala*
|
161
110
|
|
162
111
|
|
163
|
-
Please check [
|
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
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,
|
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/
|
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
|
-
*
|
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://
|
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-
|
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,
|
9
|
-
FromDatabase.new(name,
|
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,
|
13
|
-
FromUser.new(name,
|
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,
|
17
|
-
WithCastValue.new(name,
|
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
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
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:
|
@@ -26,19 +26,17 @@ module ActiveModel
|
|
26
26
|
# cat.name # => 'Gorby'
|
27
27
|
# cat.status # => 'sleeping'
|
28
28
|
def assign_attributes(new_attributes)
|
29
|
-
|
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
|
-
|
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
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
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
|
-
|
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
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
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
|
-
|
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
|
-
|
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,
|
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(", "
|
415
|
+
extra = (extra.map!(&:inspect) << "*args").join(", ")
|
373
416
|
|
374
|
-
|
375
|
-
"#{"self." unless include_private}#{
|
417
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target)
|
418
|
+
"#{"self." unless include_private}#{target}(#{extra})"
|
376
419
|
else
|
377
|
-
"send(:'#{
|
420
|
+
"send(:'#{target}', #{extra})"
|
378
421
|
end
|
379
422
|
|
380
|
-
|
381
|
-
|
382
|
-
|
383
|
-
end
|
384
|
-
|
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, :
|
431
|
+
attr_reader :prefix, :suffix, :target
|
389
432
|
|
390
|
-
AttributeMethodMatch = Struct.new(:target, :attr_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
|
-
@
|
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(
|
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
|