activemodel 6.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (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