activemodel 5.2.3
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 +7 -0
- data/CHANGELOG.md +114 -0
- data/MIT-LICENSE +21 -0
- data/README.rdoc +264 -0
- data/lib/active_model.rb +77 -0
- data/lib/active_model/attribute.rb +248 -0
- data/lib/active_model/attribute/user_provided_default.rb +52 -0
- data/lib/active_model/attribute_assignment.rb +57 -0
- data/lib/active_model/attribute_methods.rb +478 -0
- data/lib/active_model/attribute_mutation_tracker.rb +124 -0
- data/lib/active_model/attribute_set.rb +114 -0
- data/lib/active_model/attribute_set/builder.rb +126 -0
- data/lib/active_model/attribute_set/yaml_encoder.rb +41 -0
- data/lib/active_model/attributes.rb +111 -0
- data/lib/active_model/callbacks.rb +153 -0
- data/lib/active_model/conversion.rb +111 -0
- data/lib/active_model/dirty.rb +343 -0
- data/lib/active_model/errors.rb +517 -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 +318 -0
- data/lib/active_model/railtie.rb +14 -0
- data/lib/active_model/secure_password.rb +129 -0
- data/lib/active_model/serialization.rb +192 -0
- data/lib/active_model/serializers/json.rb +146 -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 +38 -0
- data/lib/active_model/type/date.rb +57 -0
- data/lib/active_model/type/date_time.rb +51 -0
- data/lib/active_model/type/decimal.rb +70 -0
- data/lib/active_model/type/float.rb +36 -0
- data/lib/active_model/type/helpers.rb +7 -0
- data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +41 -0
- data/lib/active_model/type/helpers/mutable.rb +20 -0
- data/lib/active_model/type/helpers/numeric.rb +37 -0
- data/lib/active_model/type/helpers/time_value.rb +68 -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 +70 -0
- data/lib/active_model/type/registry.rb +70 -0
- data/lib/active_model/type/string.rb +26 -0
- data/lib/active_model/type/time.rb +51 -0
- data/lib/active_model/type/value.rb +126 -0
- data/lib/active_model/validations.rb +439 -0
- data/lib/active_model/validations/absence.rb +33 -0
- data/lib/active_model/validations/acceptance.rb +106 -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
@@ -0,0 +1,248 @@
|
|
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
|
+
|
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
|
146
|
+
end
|
147
|
+
|
148
|
+
private
|
149
|
+
def initialize_dup(other)
|
150
|
+
if defined?(@value) && @value.duplicable?
|
151
|
+
@value = @value.dup
|
152
|
+
end
|
153
|
+
end
|
154
|
+
|
155
|
+
def changed_from_assignment?
|
156
|
+
assigned? && type.changed?(original_value, value, value_before_type_cast)
|
157
|
+
end
|
158
|
+
|
159
|
+
def _original_value_for_database
|
160
|
+
type.serialize(original_value)
|
161
|
+
end
|
162
|
+
|
163
|
+
class FromDatabase < Attribute # :nodoc:
|
164
|
+
def type_cast(value)
|
165
|
+
type.deserialize(value)
|
166
|
+
end
|
167
|
+
|
168
|
+
def _original_value_for_database
|
169
|
+
value_before_type_cast
|
170
|
+
end
|
171
|
+
end
|
172
|
+
|
173
|
+
class FromUser < Attribute # :nodoc:
|
174
|
+
def type_cast(value)
|
175
|
+
type.cast(value)
|
176
|
+
end
|
177
|
+
|
178
|
+
def came_from_user?
|
179
|
+
!type.value_constructed_by_mass_assignment?(value_before_type_cast)
|
180
|
+
end
|
181
|
+
end
|
182
|
+
|
183
|
+
class WithCastValue < Attribute # :nodoc:
|
184
|
+
def type_cast(value)
|
185
|
+
value
|
186
|
+
end
|
187
|
+
|
188
|
+
def changed_in_place?
|
189
|
+
false
|
190
|
+
end
|
191
|
+
end
|
192
|
+
|
193
|
+
class Null < Attribute # :nodoc:
|
194
|
+
def initialize(name)
|
195
|
+
super(name, nil, Type.default_value)
|
196
|
+
end
|
197
|
+
|
198
|
+
def type_cast(*)
|
199
|
+
nil
|
200
|
+
end
|
201
|
+
|
202
|
+
def with_type(type)
|
203
|
+
self.class.with_cast_value(name, nil, type)
|
204
|
+
end
|
205
|
+
|
206
|
+
def with_value_from_database(value)
|
207
|
+
raise ActiveModel::MissingAttributeError, "can't write unknown attribute `#{name}`"
|
208
|
+
end
|
209
|
+
alias_method :with_value_from_user, :with_value_from_database
|
210
|
+
alias_method :with_cast_value, :with_value_from_database
|
211
|
+
end
|
212
|
+
|
213
|
+
class Uninitialized < Attribute # :nodoc:
|
214
|
+
UNINITIALIZED_ORIGINAL_VALUE = Object.new
|
215
|
+
|
216
|
+
def initialize(name, type)
|
217
|
+
super(name, nil, type)
|
218
|
+
end
|
219
|
+
|
220
|
+
def value
|
221
|
+
if block_given?
|
222
|
+
yield name
|
223
|
+
end
|
224
|
+
end
|
225
|
+
|
226
|
+
def original_value
|
227
|
+
UNINITIALIZED_ORIGINAL_VALUE
|
228
|
+
end
|
229
|
+
|
230
|
+
def value_for_database
|
231
|
+
end
|
232
|
+
|
233
|
+
def initialized?
|
234
|
+
false
|
235
|
+
end
|
236
|
+
|
237
|
+
def forgetting_assignment
|
238
|
+
dup
|
239
|
+
end
|
240
|
+
|
241
|
+
def with_type(type)
|
242
|
+
self.class.new(name, type)
|
243
|
+
end
|
244
|
+
end
|
245
|
+
|
246
|
+
private_constant :FromDatabase, :FromUser, :Null, :Uninitialized, :WithCastValue
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,52 @@
|
|
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
|
+
protected
|
48
|
+
|
49
|
+
attr_reader :user_provided_value
|
50
|
+
end
|
51
|
+
end
|
52
|
+
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."
|
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,478 @@
|
|
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.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
|
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
|
+
# 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
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
# Define a method `name` in `mod` that dispatches to `send`
|
363
|
+
# using the given `extra` args. This falls back on `define_method`
|
364
|
+
# and `send` if the given names cannot be compiled.
|
365
|
+
def define_proxy_call(include_private, mod, name, send, *extra)
|
366
|
+
defn = if NAME_COMPILABLE_REGEXP.match?(name)
|
367
|
+
"def #{name}(*args)"
|
368
|
+
else
|
369
|
+
"define_method(:'#{name}') do |*args|"
|
370
|
+
end
|
371
|
+
|
372
|
+
extra = (extra.map!(&:inspect) << "*args").join(", ".freeze)
|
373
|
+
|
374
|
+
target = if CALL_COMPILABLE_REGEXP.match?(send)
|
375
|
+
"#{"self." unless include_private}#{send}(#{extra})"
|
376
|
+
else
|
377
|
+
"send(:'#{send}', #{extra})"
|
378
|
+
end
|
379
|
+
|
380
|
+
mod.module_eval <<-RUBY, __FILE__, __LINE__ + 1
|
381
|
+
#{defn}
|
382
|
+
#{target}
|
383
|
+
end
|
384
|
+
RUBY
|
385
|
+
end
|
386
|
+
|
387
|
+
class AttributeMethodMatcher #:nodoc:
|
388
|
+
attr_reader :prefix, :suffix, :method_missing_target
|
389
|
+
|
390
|
+
AttributeMethodMatch = Struct.new(:target, :attr_name, :method_name)
|
391
|
+
|
392
|
+
def initialize(options = {})
|
393
|
+
@prefix, @suffix = options.fetch(:prefix, ""), options.fetch(:suffix, "")
|
394
|
+
@regex = /^(?:#{Regexp.escape(@prefix)})(.*)(?:#{Regexp.escape(@suffix)})$/
|
395
|
+
@method_missing_target = "#{@prefix}attribute#{@suffix}"
|
396
|
+
@method_name = "#{prefix}%s#{suffix}"
|
397
|
+
end
|
398
|
+
|
399
|
+
def match(method_name)
|
400
|
+
if @regex =~ method_name
|
401
|
+
AttributeMethodMatch.new(method_missing_target, $1, method_name)
|
402
|
+
end
|
403
|
+
end
|
404
|
+
|
405
|
+
def method_name(attr_name)
|
406
|
+
@method_name % attr_name
|
407
|
+
end
|
408
|
+
|
409
|
+
def plain?
|
410
|
+
prefix.empty? && suffix.empty?
|
411
|
+
end
|
412
|
+
end
|
413
|
+
end
|
414
|
+
|
415
|
+
# Allows access to the object attributes, which are held in the hash
|
416
|
+
# returned by <tt>attributes</tt>, as though they were first-class
|
417
|
+
# methods. So a +Person+ class with a +name+ attribute can for example use
|
418
|
+
# <tt>Person#name</tt> and <tt>Person#name=</tt> and never directly use
|
419
|
+
# the attributes hash -- except for multiple assignments with
|
420
|
+
# <tt>ActiveRecord::Base#attributes=</tt>.
|
421
|
+
#
|
422
|
+
# It's also possible to instantiate related objects, so a <tt>Client</tt>
|
423
|
+
# class belonging to the +clients+ table with a +master_id+ foreign key
|
424
|
+
# can instantiate master through <tt>Client#master</tt>.
|
425
|
+
def method_missing(method, *args, &block)
|
426
|
+
if respond_to_without_attributes?(method, true)
|
427
|
+
super
|
428
|
+
else
|
429
|
+
match = matched_attribute_method(method.to_s)
|
430
|
+
match ? attribute_missing(match, *args, &block) : super
|
431
|
+
end
|
432
|
+
end
|
433
|
+
|
434
|
+
# +attribute_missing+ is like +method_missing+, but for attributes. When
|
435
|
+
# +method_missing+ is called we check to see if there is a matching
|
436
|
+
# attribute method. If so, we tell +attribute_missing+ to dispatch the
|
437
|
+
# attribute. This method can be overloaded to customize the behavior.
|
438
|
+
def attribute_missing(match, *args, &block)
|
439
|
+
__send__(match.target, match.attr_name, *args, &block)
|
440
|
+
end
|
441
|
+
|
442
|
+
# A +Person+ instance with a +name+ attribute can ask
|
443
|
+
# <tt>person.respond_to?(:name)</tt>, <tt>person.respond_to?(:name=)</tt>,
|
444
|
+
# and <tt>person.respond_to?(:name?)</tt> which will all return +true+.
|
445
|
+
alias :respond_to_without_attributes? :respond_to?
|
446
|
+
def respond_to?(method, include_private_methods = false)
|
447
|
+
if super
|
448
|
+
true
|
449
|
+
elsif !include_private_methods && super(method, true)
|
450
|
+
# If we're here then we haven't found among non-private methods
|
451
|
+
# but found among all methods. Which means that the given method is private.
|
452
|
+
false
|
453
|
+
else
|
454
|
+
!matched_attribute_method(method.to_s).nil?
|
455
|
+
end
|
456
|
+
end
|
457
|
+
|
458
|
+
private
|
459
|
+
def attribute_method?(attr_name)
|
460
|
+
respond_to_without_attributes?(:attributes) && attributes.include?(attr_name)
|
461
|
+
end
|
462
|
+
|
463
|
+
# Returns a struct representing the matching attribute method.
|
464
|
+
# The struct's attributes are prefix, base and suffix.
|
465
|
+
def matched_attribute_method(method_name)
|
466
|
+
matches = self.class.send(:attribute_method_matchers_matching, method_name)
|
467
|
+
matches.detect { |match| attribute_method?(match.attr_name) }
|
468
|
+
end
|
469
|
+
|
470
|
+
def missing_attribute(attr_name, stack)
|
471
|
+
raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
|
472
|
+
end
|
473
|
+
|
474
|
+
def _read_attribute(attr)
|
475
|
+
__send__(attr)
|
476
|
+
end
|
477
|
+
end
|
478
|
+
end
|