activemodel 6.0.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +172 -0
  3. data/MIT-LICENSE +21 -0
  4. data/README.rdoc +266 -0
  5. data/lib/active_model.rb +77 -0
  6. data/lib/active_model/attribute.rb +247 -0
  7. data/lib/active_model/attribute/user_provided_default.rb +51 -0
  8. data/lib/active_model/attribute_assignment.rb +57 -0
  9. data/lib/active_model/attribute_methods.rb +517 -0
  10. data/lib/active_model/attribute_mutation_tracker.rb +178 -0
  11. data/lib/active_model/attribute_set.rb +106 -0
  12. data/lib/active_model/attribute_set/builder.rb +124 -0
  13. data/lib/active_model/attribute_set/yaml_encoder.rb +40 -0
  14. data/lib/active_model/attributes.rb +138 -0
  15. data/lib/active_model/callbacks.rb +156 -0
  16. data/lib/active_model/conversion.rb +111 -0
  17. data/lib/active_model/dirty.rb +280 -0
  18. data/lib/active_model/errors.rb +601 -0
  19. data/lib/active_model/forbidden_attributes_protection.rb +31 -0
  20. data/lib/active_model/gem_version.rb +17 -0
  21. data/lib/active_model/lint.rb +118 -0
  22. data/lib/active_model/locale/en.yml +36 -0
  23. data/lib/active_model/model.rb +99 -0
  24. data/lib/active_model/naming.rb +334 -0
  25. data/lib/active_model/railtie.rb +20 -0
  26. data/lib/active_model/secure_password.rb +128 -0
  27. data/lib/active_model/serialization.rb +192 -0
  28. data/lib/active_model/serializers/json.rb +147 -0
  29. data/lib/active_model/translation.rb +70 -0
  30. data/lib/active_model/type.rb +53 -0
  31. data/lib/active_model/type/big_integer.rb +15 -0
  32. data/lib/active_model/type/binary.rb +52 -0
  33. data/lib/active_model/type/boolean.rb +47 -0
  34. data/lib/active_model/type/date.rb +53 -0
  35. data/lib/active_model/type/date_time.rb +47 -0
  36. data/lib/active_model/type/decimal.rb +70 -0
  37. data/lib/active_model/type/float.rb +34 -0
  38. data/lib/active_model/type/helpers.rb +7 -0
  39. data/lib/active_model/type/helpers/accepts_multiparameter_time.rb +45 -0
  40. data/lib/active_model/type/helpers/mutable.rb +20 -0
  41. data/lib/active_model/type/helpers/numeric.rb +44 -0
  42. data/lib/active_model/type/helpers/time_value.rb +81 -0
  43. data/lib/active_model/type/helpers/timezone.rb +19 -0
  44. data/lib/active_model/type/immutable_string.rb +32 -0
  45. data/lib/active_model/type/integer.rb +58 -0
  46. data/lib/active_model/type/registry.rb +62 -0
  47. data/lib/active_model/type/string.rb +26 -0
  48. data/lib/active_model/type/time.rb +47 -0
  49. data/lib/active_model/type/value.rb +126 -0
  50. data/lib/active_model/validations.rb +437 -0
  51. data/lib/active_model/validations/absence.rb +33 -0
  52. data/lib/active_model/validations/acceptance.rb +102 -0
  53. data/lib/active_model/validations/callbacks.rb +122 -0
  54. data/lib/active_model/validations/clusivity.rb +54 -0
  55. data/lib/active_model/validations/confirmation.rb +80 -0
  56. data/lib/active_model/validations/exclusion.rb +49 -0
  57. data/lib/active_model/validations/format.rb +114 -0
  58. data/lib/active_model/validations/helper_methods.rb +15 -0
  59. data/lib/active_model/validations/inclusion.rb +47 -0
  60. data/lib/active_model/validations/length.rb +129 -0
  61. data/lib/active_model/validations/numericality.rb +189 -0
  62. data/lib/active_model/validations/presence.rb +39 -0
  63. data/lib/active_model/validations/validates.rb +174 -0
  64. data/lib/active_model/validations/with.rb +147 -0
  65. data/lib/active_model/validator.rb +183 -0
  66. data/lib/active_model/version.rb +10 -0
  67. metadata +125 -0
@@ -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