activemodel 6.0.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/CHANGELOG.md +172 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +266 -0
- data/lib/active_model.rb +77 -0
- data/lib/active_model/attribute.rb +247 -0
- data/lib/active_model/attribute/user_provided_default.rb +51 -0
- data/lib/active_model/attribute_assignment.rb +57 -0
- data/lib/active_model/attribute_methods.rb +517 -0
- data/lib/active_model/attribute_mutation_tracker.rb +178 -0
- data/lib/active_model/attribute_set.rb +106 -0
- data/lib/active_model/attribute_set/builder.rb +124 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
- data/lib/active_model/attributes.rb +138 -0
- data/lib/active_model/callbacks.rb +156 -0
- data/lib/active_model/conversion.rb +111 -0
- data/lib/active_model/dirty.rb +280 -0
- data/lib/active_model/errors.rb +601 -0
- data/lib/active_model/forbidden_attributes_protection.rb +31 -0
- data/lib/active_model/gem_version.rb +17 -0
- data/lib/active_model/lint.rb +118 -0
- data/lib/active_model/locale/en.yml +36 -0
- data/lib/active_model/model.rb +99 -0
- data/lib/active_model/naming.rb +334 -0
- data/lib/active_model/railtie.rb +20 -0
- data/lib/active_model/secure_password.rb +128 -0
- data/lib/active_model/serialization.rb +192 -0
- data/lib/active_model/serializers/json.rb +147 -0
- data/lib/active_model/translation.rb +70 -0
- data/lib/active_model/type.rb +53 -0
- data/lib/active_model/type/big_integer.rb +15 -0
- data/lib/active_model/type/binary.rb +52 -0
- data/lib/active_model/type/boolean.rb +47 -0
- data/lib/active_model/type/date.rb +53 -0
- data/lib/active_model/type/date_time.rb +47 -0
- data/lib/active_model/type/decimal.rb +70 -0
- data/lib/active_model/type/float.rb +34 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +45 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +44 -0
- data/lib/active_model/type/helpers/time_value.rb +81 -0
- data/lib/active_model/type/helpers/timezone.rb +19 -0
- data/lib/active_model/type/immutable_string.rb +32 -0
- data/lib/active_model/type/integer.rb +58 -0
- data/lib/active_model/type/registry.rb +62 -0
- data/lib/active_model/type/string.rb +26 -0
- data/lib/active_model/type/time.rb +47 -0
- data/lib/active_model/type/value.rb +126 -0
- data/lib/active_model/validations.rb +437 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +102 -0
- data/lib/active_model/validations/callbacks.rb +122 -0
- data/lib/active_model/validations/clusivity.rb +54 -0
- data/lib/active_model/validations/confirmation.rb +80 -0
- data/lib/active_model/validations/exclusion.rb +49 -0
- data/lib/active_model/validations/format.rb +114 -0
- data/lib/active_model/validations/helper_methods.rb +15 -0
- data/lib/active_model/validations/inclusion.rb +47 -0
- data/lib/active_model/validations/length.rb +129 -0
- data/lib/active_model/validations/numericality.rb +189 -0
- data/lib/active_model/validations/presence.rb +39 -0
- data/lib/active_model/validations/validates.rb +174 -0
- data/lib/active_model/validations/with.rb +147 -0
- data/lib/active_model/validator.rb +183 -0
- data/lib/active_model/version.rb +10 -0
- metadata +125 -0
data/lib/active_model.rb
ADDED
@@ -0,0 +1,77 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
#--
|
4
|
+
# Copyright (c) 2004-2019 David Heinemeier Hansson
|
5
|
+
#
|
6
|
+
# Permission is hereby granted, free of charge, to any person obtaining
|
7
|
+
# a copy of this software and associated documentation files (the
|
8
|
+
# "Software"), to deal in the Software without restriction, including
|
9
|
+
# without limitation the rights to use, copy, modify, merge, publish,
|
10
|
+
# distribute, sublicense, and/or sell copies of the Software, and to
|
11
|
+
# permit persons to whom the Software is furnished to do so, subject to
|
12
|
+
# the following conditions:
|
13
|
+
#
|
14
|
+
# The above copyright notice and this permission notice shall be
|
15
|
+
# included in all copies or substantial portions of the Software.
|
16
|
+
#
|
17
|
+
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
18
|
+
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
19
|
+
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
20
|
+
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
21
|
+
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
22
|
+
# OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
23
|
+
# WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
24
|
+
#++
|
25
|
+
|
26
|
+
require "active_support"
|
27
|
+
require "active_support/rails"
|
28
|
+
require "active_model/version"
|
29
|
+
|
30
|
+
module ActiveModel
|
31
|
+
extend ActiveSupport::Autoload
|
32
|
+
|
33
|
+
autoload :Attribute
|
34
|
+
autoload :Attributes
|
35
|
+
autoload :AttributeAssignment
|
36
|
+
autoload :AttributeMethods
|
37
|
+
autoload :BlockValidator, "active_model/validator"
|
38
|
+
autoload :Callbacks
|
39
|
+
autoload :Conversion
|
40
|
+
autoload :Dirty
|
41
|
+
autoload :EachValidator, "active_model/validator"
|
42
|
+
autoload :ForbiddenAttributesProtection
|
43
|
+
autoload :Lint
|
44
|
+
autoload :Model
|
45
|
+
autoload :Name, "active_model/naming"
|
46
|
+
autoload :Naming
|
47
|
+
autoload :SecurePassword
|
48
|
+
autoload :Serialization
|
49
|
+
autoload :Translation
|
50
|
+
autoload :Type
|
51
|
+
autoload :Validations
|
52
|
+
autoload :Validator
|
53
|
+
|
54
|
+
eager_autoload do
|
55
|
+
autoload :Errors
|
56
|
+
autoload :RangeError, "active_model/errors"
|
57
|
+
autoload :StrictValidationFailed, "active_model/errors"
|
58
|
+
autoload :UnknownAttributeError, "active_model/errors"
|
59
|
+
end
|
60
|
+
|
61
|
+
module Serializers
|
62
|
+
extend ActiveSupport::Autoload
|
63
|
+
|
64
|
+
eager_autoload do
|
65
|
+
autoload :JSON
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
def self.eager_load!
|
70
|
+
super
|
71
|
+
ActiveModel::Serializers.eager_load!
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
ActiveSupport.on_load(:i18n) do
|
76
|
+
I18n.load_path << File.expand_path("active_model/locale/en.yml", __dir__)
|
77
|
+
end
|
@@ -0,0 +1,247 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/object/duplicable"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
class Attribute # :nodoc:
|
7
|
+
class << self
|
8
|
+
def from_database(name, value, type)
|
9
|
+
FromDatabase.new(name, value, type)
|
10
|
+
end
|
11
|
+
|
12
|
+
def from_user(name, value, type, original_attribute = nil)
|
13
|
+
FromUser.new(name, value, type, original_attribute)
|
14
|
+
end
|
15
|
+
|
16
|
+
def with_cast_value(name, value, type)
|
17
|
+
WithCastValue.new(name, value, type)
|
18
|
+
end
|
19
|
+
|
20
|
+
def null(name)
|
21
|
+
Null.new(name)
|
22
|
+
end
|
23
|
+
|
24
|
+
def uninitialized(name, type)
|
25
|
+
Uninitialized.new(name, type)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
attr_reader :name, :value_before_type_cast, :type
|
30
|
+
|
31
|
+
# This method should not be called directly.
|
32
|
+
# Use #from_database or #from_user
|
33
|
+
def initialize(name, value_before_type_cast, type, original_attribute = nil)
|
34
|
+
@name = name
|
35
|
+
@value_before_type_cast = value_before_type_cast
|
36
|
+
@type = type
|
37
|
+
@original_attribute = original_attribute
|
38
|
+
end
|
39
|
+
|
40
|
+
def value
|
41
|
+
# `defined?` is cheaper than `||=` when we get back falsy values
|
42
|
+
@value = type_cast(value_before_type_cast) unless defined?(@value)
|
43
|
+
@value
|
44
|
+
end
|
45
|
+
|
46
|
+
def original_value
|
47
|
+
if assigned?
|
48
|
+
original_attribute.original_value
|
49
|
+
else
|
50
|
+
type_cast(value_before_type_cast)
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def value_for_database
|
55
|
+
type.serialize(value)
|
56
|
+
end
|
57
|
+
|
58
|
+
def changed?
|
59
|
+
changed_from_assignment? || changed_in_place?
|
60
|
+
end
|
61
|
+
|
62
|
+
def changed_in_place?
|
63
|
+
has_been_read? && type.changed_in_place?(original_value_for_database, value)
|
64
|
+
end
|
65
|
+
|
66
|
+
def forgetting_assignment
|
67
|
+
with_value_from_database(value_for_database)
|
68
|
+
end
|
69
|
+
|
70
|
+
def with_value_from_user(value)
|
71
|
+
type.assert_valid_value(value)
|
72
|
+
self.class.from_user(name, value, type, original_attribute || self)
|
73
|
+
end
|
74
|
+
|
75
|
+
def with_value_from_database(value)
|
76
|
+
self.class.from_database(name, value, type)
|
77
|
+
end
|
78
|
+
|
79
|
+
def with_cast_value(value)
|
80
|
+
self.class.with_cast_value(name, value, type)
|
81
|
+
end
|
82
|
+
|
83
|
+
def with_type(type)
|
84
|
+
if changed_in_place?
|
85
|
+
with_value_from_user(value).with_type(type)
|
86
|
+
else
|
87
|
+
self.class.new(name, value_before_type_cast, type, original_attribute)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def type_cast(*)
|
92
|
+
raise NotImplementedError
|
93
|
+
end
|
94
|
+
|
95
|
+
def initialized?
|
96
|
+
true
|
97
|
+
end
|
98
|
+
|
99
|
+
def came_from_user?
|
100
|
+
false
|
101
|
+
end
|
102
|
+
|
103
|
+
def has_been_read?
|
104
|
+
defined?(@value)
|
105
|
+
end
|
106
|
+
|
107
|
+
def ==(other)
|
108
|
+
self.class == other.class &&
|
109
|
+
name == other.name &&
|
110
|
+
value_before_type_cast == other.value_before_type_cast &&
|
111
|
+
type == other.type
|
112
|
+
end
|
113
|
+
alias eql? ==
|
114
|
+
|
115
|
+
def hash
|
116
|
+
[self.class, name, value_before_type_cast, type].hash
|
117
|
+
end
|
118
|
+
|
119
|
+
def init_with(coder)
|
120
|
+
@name = coder["name"]
|
121
|
+
@value_before_type_cast = coder["value_before_type_cast"]
|
122
|
+
@type = coder["type"]
|
123
|
+
@original_attribute = coder["original_attribute"]
|
124
|
+
@value = coder["value"] if coder.map.key?("value")
|
125
|
+
end
|
126
|
+
|
127
|
+
def encode_with(coder)
|
128
|
+
coder["name"] = name
|
129
|
+
coder["value_before_type_cast"] = value_before_type_cast unless value_before_type_cast.nil?
|
130
|
+
coder["type"] = type if type
|
131
|
+
coder["original_attribute"] = original_attribute if original_attribute
|
132
|
+
coder["value"] = value if defined?(@value)
|
133
|
+
end
|
134
|
+
|
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
|
142
|
+
end
|
143
|
+
|
144
|
+
private
|
145
|
+
attr_reader :original_attribute
|
146
|
+
alias :assigned? :original_attribute
|
147
|
+
|
148
|
+
def initialize_dup(other)
|
149
|
+
if defined?(@value) && @value.duplicable?
|
150
|
+
@value = @value.dup
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def changed_from_assignment?
|
155
|
+
assigned? && type.changed?(original_value, value, value_before_type_cast)
|
156
|
+
end
|
157
|
+
|
158
|
+
def _original_value_for_database
|
159
|
+
type.serialize(original_value)
|
160
|
+
end
|
161
|
+
|
162
|
+
class FromDatabase < Attribute # :nodoc:
|
163
|
+
def type_cast(value)
|
164
|
+
type.deserialize(value)
|
165
|
+
end
|
166
|
+
|
167
|
+
def _original_value_for_database
|
168
|
+
value_before_type_cast
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
class FromUser < Attribute # :nodoc:
|
173
|
+
def type_cast(value)
|
174
|
+
type.cast(value)
|
175
|
+
end
|
176
|
+
|
177
|
+
def came_from_user?
|
178
|
+
!type.value_constructed_by_mass_assignment?(value_before_type_cast)
|
179
|
+
end
|
180
|
+
end
|
181
|
+
|
182
|
+
class WithCastValue < Attribute # :nodoc:
|
183
|
+
def type_cast(value)
|
184
|
+
value
|
185
|
+
end
|
186
|
+
|
187
|
+
def changed_in_place?
|
188
|
+
false
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
class Null < Attribute # :nodoc:
|
193
|
+
def initialize(name)
|
194
|
+
super(name, nil, Type.default_value)
|
195
|
+
end
|
196
|
+
|
197
|
+
def type_cast(*)
|
198
|
+
nil
|
199
|
+
end
|
200
|
+
|
201
|
+
def with_type(type)
|
202
|
+
self.class.with_cast_value(name, nil, type)
|
203
|
+
end
|
204
|
+
|
205
|
+
def with_value_from_database(value)
|
206
|
+
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
|
207
|
+
end
|
208
|
+
alias_method :with_value_from_user, :with_value_from_database
|
209
|
+
alias_method :with_cast_value, :with_value_from_database
|
210
|
+
end
|
211
|
+
|
212
|
+
class Uninitialized < Attribute # :nodoc:
|
213
|
+
UNINITIALIZED_ORIGINAL_VALUE = Object.new
|
214
|
+
|
215
|
+
def initialize(name, type)
|
216
|
+
super(name, nil, type)
|
217
|
+
end
|
218
|
+
|
219
|
+
def value
|
220
|
+
if block_given?
|
221
|
+
yield name
|
222
|
+
end
|
223
|
+
end
|
224
|
+
|
225
|
+
def original_value
|
226
|
+
UNINITIALIZED_ORIGINAL_VALUE
|
227
|
+
end
|
228
|
+
|
229
|
+
def value_for_database
|
230
|
+
end
|
231
|
+
|
232
|
+
def initialized?
|
233
|
+
false
|
234
|
+
end
|
235
|
+
|
236
|
+
def forgetting_assignment
|
237
|
+
dup
|
238
|
+
end
|
239
|
+
|
240
|
+
def with_type(type)
|
241
|
+
self.class.new(name, type)
|
242
|
+
end
|
243
|
+
end
|
244
|
+
|
245
|
+
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
|
246
|
+
end
|
247
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_model/attribute"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
class Attribute # :nodoc:
|
7
|
+
class UserProvidedDefault < FromUser # :nodoc:
|
8
|
+
def initialize(name, value, type, database_default)
|
9
|
+
@user_provided_value = value
|
10
|
+
super(name, value, type, database_default)
|
11
|
+
end
|
12
|
+
|
13
|
+
def value_before_type_cast
|
14
|
+
if user_provided_value.is_a?(Proc)
|
15
|
+
@memoized_value_before_type_cast ||= user_provided_value.call
|
16
|
+
else
|
17
|
+
@user_provided_value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def with_type(type)
|
22
|
+
self.class.new(name, user_provided_value, type, original_attribute)
|
23
|
+
end
|
24
|
+
|
25
|
+
def marshal_dump
|
26
|
+
result = [
|
27
|
+
name,
|
28
|
+
value_before_type_cast,
|
29
|
+
type,
|
30
|
+
original_attribute,
|
31
|
+
]
|
32
|
+
result << value if defined?(@value)
|
33
|
+
result
|
34
|
+
end
|
35
|
+
|
36
|
+
def marshal_load(values)
|
37
|
+
name, user_provided_value, type, original_attribute, value = values
|
38
|
+
@name = name
|
39
|
+
@user_provided_value = user_provided_value
|
40
|
+
@type = type
|
41
|
+
@original_attribute = original_attribute
|
42
|
+
if values.length == 5
|
43
|
+
@value = value
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
attr_reader :user_provided_value
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "active_support/core_ext/hash/keys"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
module AttributeAssignment
|
7
|
+
include ActiveModel::ForbiddenAttributesProtection
|
8
|
+
|
9
|
+
# Allows you to set all the attributes by passing in a hash of attributes with
|
10
|
+
# keys matching the attribute names.
|
11
|
+
#
|
12
|
+
# If the passed hash responds to <tt>permitted?</tt> method and the return value
|
13
|
+
# of this method is +false+ an <tt>ActiveModel::ForbiddenAttributesError</tt>
|
14
|
+
# exception is raised.
|
15
|
+
#
|
16
|
+
# class Cat
|
17
|
+
# include ActiveModel::AttributeAssignment
|
18
|
+
# attr_accessor :name, :status
|
19
|
+
# end
|
20
|
+
#
|
21
|
+
# cat = Cat.new
|
22
|
+
# cat.assign_attributes(name: "Gorby", status: "yawning")
|
23
|
+
# cat.name # => 'Gorby'
|
24
|
+
# cat.status # => 'yawning'
|
25
|
+
# cat.assign_attributes(status: "sleeping")
|
26
|
+
# cat.name # => 'Gorby'
|
27
|
+
# cat.status # => 'sleeping'
|
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, #{new_attributes.class} passed."
|
31
|
+
end
|
32
|
+
return if new_attributes.empty?
|
33
|
+
|
34
|
+
attributes = new_attributes.stringify_keys
|
35
|
+
_assign_attributes(sanitize_for_mass_assignment(attributes))
|
36
|
+
end
|
37
|
+
|
38
|
+
alias attributes= assign_attributes
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def _assign_attributes(attributes)
|
43
|
+
attributes.each do |k, v|
|
44
|
+
_assign_attribute(k, v)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def _assign_attribute(k, v)
|
49
|
+
setter = :"#{k}="
|
50
|
+
if respond_to?(setter)
|
51
|
+
public_send(setter, v)
|
52
|
+
else
|
53
|
+
raise UnknownAttributeError.new(self, k)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
@@ -0,0 +1,517 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "concurrent/map"
|
4
|
+
|
5
|
+
module ActiveModel
|
6
|
+
# Raised when an attribute is not defined.
|
7
|
+
#
|
8
|
+
# class User < ActiveRecord::Base
|
9
|
+
# has_many :pets
|
10
|
+
# end
|
11
|
+
#
|
12
|
+
# user = User.first
|
13
|
+
# user.pets.select(:id).first.user_id
|
14
|
+
# # => ActiveModel::MissingAttributeError: missing attribute: user_id
|
15
|
+
class MissingAttributeError < NoMethodError
|
16
|
+
end
|
17
|
+
|
18
|
+
# == Active \Model \Attribute \Methods
|
19
|
+
#
|
20
|
+
# Provides a way to add prefixes and suffixes to your methods as
|
21
|
+
# well as handling the creation of <tt>ActiveRecord::Base</tt>-like
|
22
|
+
# class methods such as +table_name+.
|
23
|
+
#
|
24
|
+
# The requirements to implement <tt>ActiveModel::AttributeMethods</tt> are to:
|
25
|
+
#
|
26
|
+
# * <tt>include ActiveModel::AttributeMethods</tt> in your class.
|
27
|
+
# * Call each of its methods you want to add, such as +attribute_method_suffix+
|
28
|
+
# or +attribute_method_prefix+.
|
29
|
+
# * Call +define_attribute_methods+ after the other methods are called.
|
30
|
+
# * Define the various generic +_attribute+ methods that you have declared.
|
31
|
+
# * Define an +attributes+ method which returns a hash with each
|
32
|
+
# attribute name in your model as hash key and the attribute value as hash value.
|
33
|
+
# Hash keys must be strings.
|
34
|
+
#
|
35
|
+
# A minimal implementation could be:
|
36
|
+
#
|
37
|
+
# class Person
|
38
|
+
# include ActiveModel::AttributeMethods
|
39
|
+
#
|
40
|
+
# attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
|
41
|
+
# attribute_method_suffix '_contrived?'
|
42
|
+
# attribute_method_prefix 'clear_'
|
43
|
+
# define_attribute_methods :name
|
44
|
+
#
|
45
|
+
# attr_accessor :name
|
46
|
+
#
|
47
|
+
# def attributes
|
48
|
+
# { 'name' => @name }
|
49
|
+
# end
|
50
|
+
#
|
51
|
+
# private
|
52
|
+
#
|
53
|
+
# def attribute_contrived?(attr)
|
54
|
+
# true
|
55
|
+
# end
|
56
|
+
#
|
57
|
+
# def clear_attribute(attr)
|
58
|
+
# send("#{attr}=", nil)
|
59
|
+
# end
|
60
|
+
#
|
61
|
+
# def reset_attribute_to_default!(attr)
|
62
|
+
# send("#{attr}=", 'Default Name')
|
63
|
+
# end
|
64
|
+
# end
|
65
|
+
module AttributeMethods
|
66
|
+
extend ActiveSupport::Concern
|
67
|
+
|
68
|
+
NAME_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?=]?\z/
|
69
|
+
CALL_COMPILABLE_REGEXP = /\A[a-zA-Z_]\w*[!?]?\z/
|
70
|
+
|
71
|
+
included do
|
72
|
+
class_attribute :attribute_aliases, instance_writer: false, default: {}
|
73
|
+
class_attribute :attribute_method_matchers, instance_writer: false, default: [ ClassMethods::AttributeMethodMatcher.new ]
|
74
|
+
end
|
75
|
+
|
76
|
+
module ClassMethods
|
77
|
+
# Declares a method available for all attributes with the given prefix.
|
78
|
+
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
|
79
|
+
#
|
80
|
+
# #{prefix}#{attr}(*args, &block)
|
81
|
+
#
|
82
|
+
# to
|
83
|
+
#
|
84
|
+
# #{prefix}attribute(#{attr}, *args, &block)
|
85
|
+
#
|
86
|
+
# An instance method <tt>#{prefix}attribute</tt> must exist and accept
|
87
|
+
# at least the +attr+ argument.
|
88
|
+
#
|
89
|
+
# class Person
|
90
|
+
# include ActiveModel::AttributeMethods
|
91
|
+
#
|
92
|
+
# attr_accessor :name
|
93
|
+
# attribute_method_prefix 'clear_'
|
94
|
+
# define_attribute_methods :name
|
95
|
+
#
|
96
|
+
# private
|
97
|
+
#
|
98
|
+
# def clear_attribute(attr)
|
99
|
+
# send("#{attr}=", nil)
|
100
|
+
# end
|
101
|
+
# end
|
102
|
+
#
|
103
|
+
# person = Person.new
|
104
|
+
# person.name = 'Bob'
|
105
|
+
# person.name # => "Bob"
|
106
|
+
# person.clear_name
|
107
|
+
# person.name # => nil
|
108
|
+
def attribute_method_prefix(*prefixes)
|
109
|
+
self.attribute_method_matchers += prefixes.map! { |prefix| AttributeMethodMatcher.new prefix: prefix }
|
110
|
+
undefine_attribute_methods
|
111
|
+
end
|
112
|
+
|
113
|
+
# Declares a method available for all attributes with the given suffix.
|
114
|
+
# Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
|
115
|
+
#
|
116
|
+
# #{attr}#{suffix}(*args, &block)
|
117
|
+
#
|
118
|
+
# to
|
119
|
+
#
|
120
|
+
# attribute#{suffix}(#{attr}, *args, &block)
|
121
|
+
#
|
122
|
+
# An <tt>attribute#{suffix}</tt> instance method must exist and accept at
|
123
|
+
# least the +attr+ argument.
|
124
|
+
#
|
125
|
+
# class Person
|
126
|
+
# include ActiveModel::AttributeMethods
|
127
|
+
#
|
128
|
+
# attr_accessor :name
|
129
|
+
# attribute_method_suffix '_short?'
|
130
|
+
# define_attribute_methods :name
|
131
|
+
#
|
132
|
+
# private
|
133
|
+
#
|
134
|
+
# def attribute_short?(attr)
|
135
|
+
# send(attr).length < 5
|
136
|
+
# end
|
137
|
+
# end
|
138
|
+
#
|
139
|
+
# person = Person.new
|
140
|
+
# person.name = 'Bob'
|
141
|
+
# person.name # => "Bob"
|
142
|
+
# person.name_short? # => true
|
143
|
+
def attribute_method_suffix(*suffixes)
|
144
|
+
self.attribute_method_matchers += suffixes.map! { |suffix| AttributeMethodMatcher.new suffix: suffix }
|
145
|
+
undefine_attribute_methods
|
146
|
+
end
|
147
|
+
|
148
|
+
# Declares a method available for all attributes with the given prefix
|
149
|
+
# and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
|
150
|
+
# the method.
|
151
|
+
#
|
152
|
+
# #{prefix}#{attr}#{suffix}(*args, &block)
|
153
|
+
#
|
154
|
+
# to
|
155
|
+
#
|
156
|
+
# #{prefix}attribute#{suffix}(#{attr}, *args, &block)
|
157
|
+
#
|
158
|
+
# An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
|
159
|
+
# accept at least the +attr+ argument.
|
160
|
+
#
|
161
|
+
# class Person
|
162
|
+
# include ActiveModel::AttributeMethods
|
163
|
+
#
|
164
|
+
# attr_accessor :name
|
165
|
+
# attribute_method_affix prefix: 'reset_', suffix: '_to_default!'
|
166
|
+
# define_attribute_methods :name
|
167
|
+
#
|
168
|
+
# private
|
169
|
+
#
|
170
|
+
# def reset_attribute_to_default!(attr)
|
171
|
+
# send("#{attr}=", 'Default Name')
|
172
|
+
# end
|
173
|
+
# end
|
174
|
+
#
|
175
|
+
# person = Person.new
|
176
|
+
# person.name # => 'Gem'
|
177
|
+
# person.reset_name_to_default!
|
178
|
+
# person.name # => 'Default Name'
|
179
|
+
def attribute_method_affix(*affixes)
|
180
|
+
self.attribute_method_matchers += affixes.map! { |affix| AttributeMethodMatcher.new prefix: affix[:prefix], suffix: affix[:suffix] }
|
181
|
+
undefine_attribute_methods
|
182
|
+
end
|
183
|
+
|
184
|
+
# Allows you to make aliases for attributes.
|
185
|
+
#
|
186
|
+
# class Person
|
187
|
+
# include ActiveModel::AttributeMethods
|
188
|
+
#
|
189
|
+
# attr_accessor :name
|
190
|
+
# attribute_method_suffix '_short?'
|
191
|
+
# define_attribute_methods :name
|
192
|
+
#
|
193
|
+
# alias_attribute :nickname, :name
|
194
|
+
#
|
195
|
+
# private
|
196
|
+
#
|
197
|
+
# def attribute_short?(attr)
|
198
|
+
# send(attr).length < 5
|
199
|
+
# end
|
200
|
+
# end
|
201
|
+
#
|
202
|
+
# person = Person.new
|
203
|
+
# person.name = 'Bob'
|
204
|
+
# person.name # => "Bob"
|
205
|
+
# person.nickname # => "Bob"
|
206
|
+
# person.name_short? # => true
|
207
|
+
# person.nickname_short? # => true
|
208
|
+
def alias_attribute(new_name, old_name)
|
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
|
214
|
+
end
|
215
|
+
end
|
216
|
+
|
217
|
+
# Is +new_name+ an alias?
|
218
|
+
def attribute_alias?(new_name)
|
219
|
+
attribute_aliases.key? new_name.to_s
|
220
|
+
end
|
221
|
+
|
222
|
+
# Returns the original name for the alias +name+
|
223
|
+
def attribute_alias(name)
|
224
|
+
attribute_aliases[name.to_s]
|
225
|
+
end
|
226
|
+
|
227
|
+
# Declares the attributes that should be prefixed and suffixed by
|
228
|
+
# <tt>ActiveModel::AttributeMethods</tt>.
|
229
|
+
#
|
230
|
+
# To use, pass attribute names (as strings or symbols). Be sure to declare
|
231
|
+
# +define_attribute_methods+ after you define any prefix, suffix or affix
|
232
|
+
# methods, or they will not hook in.
|
233
|
+
#
|
234
|
+
# class Person
|
235
|
+
# include ActiveModel::AttributeMethods
|
236
|
+
#
|
237
|
+
# attr_accessor :name, :age, :address
|
238
|
+
# attribute_method_prefix 'clear_'
|
239
|
+
#
|
240
|
+
# # Call to define_attribute_methods must appear after the
|
241
|
+
# # attribute_method_prefix, attribute_method_suffix or
|
242
|
+
# # attribute_method_affix declarations.
|
243
|
+
# define_attribute_methods :name, :age, :address
|
244
|
+
#
|
245
|
+
# private
|
246
|
+
#
|
247
|
+
# def clear_attribute(attr)
|
248
|
+
# send("#{attr}=", nil)
|
249
|
+
# end
|
250
|
+
# end
|
251
|
+
def define_attribute_methods(*attr_names)
|
252
|
+
attr_names.flatten.each { |attr_name| define_attribute_method(attr_name) }
|
253
|
+
end
|
254
|
+
|
255
|
+
# Declares an attribute that should be prefixed and suffixed by
|
256
|
+
# <tt>ActiveModel::AttributeMethods</tt>.
|
257
|
+
#
|
258
|
+
# To use, pass an attribute name (as string or symbol). Be sure to declare
|
259
|
+
# +define_attribute_method+ after you define any prefix, suffix or affix
|
260
|
+
# method, or they will not hook in.
|
261
|
+
#
|
262
|
+
# class Person
|
263
|
+
# include ActiveModel::AttributeMethods
|
264
|
+
#
|
265
|
+
# attr_accessor :name
|
266
|
+
# attribute_method_suffix '_short?'
|
267
|
+
#
|
268
|
+
# # Call to define_attribute_method must appear after the
|
269
|
+
# # attribute_method_prefix, attribute_method_suffix or
|
270
|
+
# # attribute_method_affix declarations.
|
271
|
+
# define_attribute_method :name
|
272
|
+
#
|
273
|
+
# private
|
274
|
+
#
|
275
|
+
# def attribute_short?(attr)
|
276
|
+
# send(attr).length < 5
|
277
|
+
# end
|
278
|
+
# end
|
279
|
+
#
|
280
|
+
# person = Person.new
|
281
|
+
# person.name = 'Bob'
|
282
|
+
# person.name # => "Bob"
|
283
|
+
# 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
|
295
|
+
end
|
296
|
+
end
|
297
|
+
end
|
298
|
+
attribute_method_matchers_cache.clear
|
299
|
+
end
|
300
|
+
|
301
|
+
# Removes all the previously dynamically defined methods from the class.
|
302
|
+
#
|
303
|
+
# class Person
|
304
|
+
# include ActiveModel::AttributeMethods
|
305
|
+
#
|
306
|
+
# attr_accessor :name
|
307
|
+
# attribute_method_suffix '_short?'
|
308
|
+
# define_attribute_method :name
|
309
|
+
#
|
310
|
+
# private
|
311
|
+
#
|
312
|
+
# def attribute_short?(attr)
|
313
|
+
# send(attr).length < 5
|
314
|
+
# end
|
315
|
+
# end
|
316
|
+
#
|
317
|
+
# person = Person.new
|
318
|
+
# person.name = 'Bob'
|
319
|
+
# person.name_short? # => true
|
320
|
+
#
|
321
|
+
# Person.undefine_attribute_methods
|
322
|
+
#
|
323
|
+
# person.name_short? # => NoMethodError
|
324
|
+
def undefine_attribute_methods
|
325
|
+
generated_attribute_methods.module_eval do
|
326
|
+
instance_methods.each { |m| undef_method(m) }
|
327
|
+
end
|
328
|
+
attribute_method_matchers_cache.clear
|
329
|
+
end
|
330
|
+
|
331
|
+
private
|
332
|
+
def generated_attribute_methods
|
333
|
+
@generated_attribute_methods ||= Module.new.tap { |mod| include mod }
|
334
|
+
end
|
335
|
+
|
336
|
+
def instance_method_already_implemented?(method_name)
|
337
|
+
generated_attribute_methods.method_defined?(method_name)
|
338
|
+
end
|
339
|
+
|
340
|
+
# The methods +method_missing+ and +respond_to?+ of this module are
|
341
|
+
# invoked often in a typical rails, both of which invoke the method
|
342
|
+
# +matched_attribute_method+. The latter method iterates through an
|
343
|
+
# array doing regular expression matches, which results in a lot of
|
344
|
+
# object creations. Most of the time it returns a +nil+ match. As the
|
345
|
+
# match result is always the same given a +method_name+, this cache is
|
346
|
+
# used to alleviate the GC, which ultimately also speeds up the app
|
347
|
+
# significantly (in our case our test suite finishes 10% faster with
|
348
|
+
# this cache).
|
349
|
+
def attribute_method_matchers_cache
|
350
|
+
@attribute_method_matchers_cache ||= Concurrent::Map.new(initial_capacity: 4)
|
351
|
+
end
|
352
|
+
|
353
|
+
def attribute_method_matchers_matching(method_name)
|
354
|
+
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
|
360
|
+
end
|
361
|
+
end
|
362
|
+
|
363
|
+
# Define a method `name` in `mod` that dispatches to `send`
|
364
|
+
# using the given `extra` args. This falls back on `define_method`
|
365
|
+
# and `send` if the given names cannot be compiled.
|
366
|
+
def define_proxy_call(include_private, mod, name, target, *extra)
|
367
|
+
defn = if NAME_COMPILABLE_REGEXP.match?(name)
|
368
|
+
"def #{name}(*args)"
|
369
|
+
else
|
370
|
+
"define_method(:'#{name}') do |*args|"
|
371
|
+
end
|
372
|
+
|
373
|
+
extra = (extra.map!(&:inspect) << "*args").join(", ")
|
374
|
+
|
375
|
+
body = if CALL_COMPILABLE_REGEXP.match?(target)
|
376
|
+
"#{"self." unless include_private}#{target}(#{extra})"
|
377
|
+
else
|
378
|
+
"send(:'#{target}', #{extra})"
|
379
|
+
end
|
380
|
+
|
381
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
382
|
+
#{defn}
|
383
|
+
#{body}
|
384
|
+
end
|
385
|
+
RUBY
|
386
|
+
end
|
387
|
+
|
388
|
+
class AttributeMethodMatcher #:nodoc:
|
389
|
+
attr_reader :prefix, :suffix, :target
|
390
|
+
|
391
|
+
AttributeMethodMatch = Struct.new(:target, :attr_name)
|
392
|
+
|
393
|
+
def initialize(options = {})
|
394
|
+
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
|
395
|
+
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
|
396
|
+
@target = "#{@prefix}attribute#{@suffix}"
|
397
|
+
@method_name = "#{prefix}%s#{suffix}"
|
398
|
+
end
|
399
|
+
|
400
|
+
def match(method_name)
|
401
|
+
if @regex =~ method_name
|
402
|
+
AttributeMethodMatch.new(target, $1)
|
403
|
+
end
|
404
|
+
end
|
405
|
+
|
406
|
+
def method_name(attr_name)
|
407
|
+
@method_name % attr_name
|
408
|
+
end
|
409
|
+
|
410
|
+
def plain?
|
411
|
+
prefix.empty? && suffix.empty?
|
412
|
+
end
|
413
|
+
end
|
414
|
+
end
|
415
|
+
|
416
|
+
# Allows access to the object attributes, which are held in the hash
|
417
|
+
# returned by <tt>attributes</tt>, as though they were first-class
|
418
|
+
# methods. So a +Person+ class with a +name+ attribute can for example use
|
419
|
+
# <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
|
420
|
+
# the attributes hash -- except for multiple assignments with
|
421
|
+
# <tt>ActiveRecord::Base#attributes=</tt>.
|
422
|
+
#
|
423
|
+
# It's also possible to instantiate related objects, so a <tt>Client</tt>
|
424
|
+
# class belonging to the +clients+ table with a +master_id+ foreign key
|
425
|
+
# can instantiate master through <tt>Client#master</tt>.
|
426
|
+
def method_missing(method, *args, &block)
|
427
|
+
if respond_to_without_attributes?(method, true)
|
428
|
+
super
|
429
|
+
else
|
430
|
+
match = matched_attribute_method(method.to_s)
|
431
|
+
match ? attribute_missing(match, *args, &block) : super
|
432
|
+
end
|
433
|
+
end
|
434
|
+
|
435
|
+
# +attribute_missing+ is like +method_missing+, but for attributes. When
|
436
|
+
# +method_missing+ is called we check to see if there is a matching
|
437
|
+
# attribute method. If so, we tell +attribute_missing+ to dispatch the
|
438
|
+
# attribute. This method can be overloaded to customize the behavior.
|
439
|
+
def attribute_missing(match, *args, &block)
|
440
|
+
__send__(match.target, match.attr_name, *args, &block)
|
441
|
+
end
|
442
|
+
|
443
|
+
# A +Person+ instance with a +name+ attribute can ask
|
444
|
+
# <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
|
445
|
+
# and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
|
446
|
+
alias :respond_to_without_attributes? :respond_to?
|
447
|
+
def respond_to?(method, include_private_methods = false)
|
448
|
+
if super
|
449
|
+
true
|
450
|
+
elsif !include_private_methods && super(method, true)
|
451
|
+
# If we're here then we haven't found among non-private methods
|
452
|
+
# but found among all methods. Which means that the given method is private.
|
453
|
+
false
|
454
|
+
else
|
455
|
+
!matched_attribute_method(method.to_s).nil?
|
456
|
+
end
|
457
|
+
end
|
458
|
+
|
459
|
+
private
|
460
|
+
def attribute_method?(attr_name)
|
461
|
+
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
|
462
|
+
end
|
463
|
+
|
464
|
+
# Returns a struct representing the matching attribute method.
|
465
|
+
# The struct's attributes are prefix, base and suffix.
|
466
|
+
def matched_attribute_method(method_name)
|
467
|
+
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
468
|
+
matches.detect { |match| attribute_method?(match.attr_name) }
|
469
|
+
end
|
470
|
+
|
471
|
+
def missing_attribute(attr_name, stack)
|
472
|
+
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
473
|
+
end
|
474
|
+
|
475
|
+
def _read_attribute(attr)
|
476
|
+
__send__(attr)
|
477
|
+
end
|
478
|
+
|
479
|
+
module AttrNames # :nodoc:
|
480
|
+
DEF_SAFE_NAME = /\A[a-zA-Z_]\w*\z/
|
481
|
+
|
482
|
+
# We want to generate the methods via module_eval rather than
|
483
|
+
# define_method, because define_method is slower on dispatch.
|
484
|
+
# Evaluating many similar methods may use more memory as the instruction
|
485
|
+
# sequences are duplicated and cached (in MRI). define_method may
|
486
|
+
# be slower on dispatch, but if you're careful about the closure
|
487
|
+
# created, then define_method will consume much less memory.
|
488
|
+
#
|
489
|
+
# But sometimes the database might return columns with
|
490
|
+
# characters that are not allowed in normal method names (like
|
491
|
+
# 'my_column(omg)'. So to work around this we first define with
|
492
|
+
# the __temp__ identifier, and then use alias method to rename
|
493
|
+
# it to what we want.
|
494
|
+
#
|
495
|
+
# We are also defining a constant to hold the frozen string of
|
496
|
+
# the attribute name. Using a constant means that we do not have
|
497
|
+
# to allocate an object on each call to the attribute method.
|
498
|
+
# Making it frozen means that it doesn't get duped when used to
|
499
|
+
# key the @attributes in read_attribute.
|
500
|
+
def self.define_attribute_accessor_method(mod, attr_name, writer: false)
|
501
|
+
method_name = "#{attr_name}#{'=' if writer}"
|
502
|
+
if attr_name.ascii_only? && DEF_SAFE_NAME.match?(attr_name)
|
503
|
+
yield method_name, "'#{attr_name}'.freeze"
|
504
|
+
else
|
505
|
+
safe_name = attr_name.unpack1("h*")
|
506
|
+
const_name = "ATTR_#{safe_name}"
|
507
|
+
const_set(const_name, attr_name) unless const_defined?(const_name)
|
508
|
+
temp_method_name = "__temp__#{safe_name}#{'=' if writer}"
|
509
|
+
attr_name_expr = "::ActiveModel::AttributeMethods::AttrNames::#{const_name}"
|
510
|
+
yield temp_method_name, attr_name_expr
|
511
|
+
mod.alias_method method_name, temp_method_name
|
512
|
+
mod.undef_method temp_method_name
|
513
|
+
end
|
514
|
+
end
|
515
|
+
end
|
516
|
+
end
|
517
|
+
end
|