activemodel 3.0.0.beta

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 (34) hide show
  1. data/CHANGELOG +13 -0
  2. data/MIT-LICENSE +21 -0
  3. data/README +216 -0
  4. data/lib/active_model.rb +61 -0
  5. data/lib/active_model/attribute_methods.rb +391 -0
  6. data/lib/active_model/callbacks.rb +133 -0
  7. data/lib/active_model/conversion.rb +19 -0
  8. data/lib/active_model/deprecated_error_methods.rb +33 -0
  9. data/lib/active_model/dirty.rb +164 -0
  10. data/lib/active_model/errors.rb +277 -0
  11. data/lib/active_model/lint.rb +89 -0
  12. data/lib/active_model/locale/en.yml +26 -0
  13. data/lib/active_model/naming.rb +60 -0
  14. data/lib/active_model/observing.rb +196 -0
  15. data/lib/active_model/railtie.rb +2 -0
  16. data/lib/active_model/serialization.rb +87 -0
  17. data/lib/active_model/serializers/json.rb +96 -0
  18. data/lib/active_model/serializers/xml.rb +204 -0
  19. data/lib/active_model/test_case.rb +16 -0
  20. data/lib/active_model/translation.rb +60 -0
  21. data/lib/active_model/validations.rb +168 -0
  22. data/lib/active_model/validations/acceptance.rb +51 -0
  23. data/lib/active_model/validations/confirmation.rb +49 -0
  24. data/lib/active_model/validations/exclusion.rb +40 -0
  25. data/lib/active_model/validations/format.rb +64 -0
  26. data/lib/active_model/validations/inclusion.rb +40 -0
  27. data/lib/active_model/validations/length.rb +98 -0
  28. data/lib/active_model/validations/numericality.rb +111 -0
  29. data/lib/active_model/validations/presence.rb +41 -0
  30. data/lib/active_model/validations/validates.rb +108 -0
  31. data/lib/active_model/validations/with.rb +70 -0
  32. data/lib/active_model/validator.rb +160 -0
  33. data/lib/active_model/version.rb +9 -0
  34. metadata +96 -0
data/CHANGELOG ADDED
@@ -0,0 +1,13 @@
1
+ *Edge*
2
+
3
+ * Change the ActiveModel::Base.include_root_in_json default to true for Rails 3 [DHH]
4
+
5
+ * Add validates_format_of :without => /regexp/ option. #430 [Elliot Winkler, Peer Allan]
6
+
7
+ Example :
8
+
9
+ validates_format_of :subdomain, :without => /www|admin|mail/
10
+
11
+ * Introduce validates_with to encapsulate attribute validations in a class. #2630 [Jeff Dean]
12
+
13
+ * Extracted from Active Record and Active Resource.
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ Copyright (c) 2004-2010 David Heinemeier Hansson
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21
+
data/README ADDED
@@ -0,0 +1,216 @@
1
+ = Active Model - defined interfaces for Rails
2
+
3
+ Prior to Rails 3.0, if a plugin or gem developer wanted to be able to have
4
+ an object interact with Action Pack helpers, it was required to either
5
+ copy chunks of code from Rails, or monkey patch entire helpers to make them
6
+ handle objects that did not look like Active Record. This generated code
7
+ duplication and fragile applications that broke on upgrades.
8
+
9
+ Active Model is a solution for this problem.
10
+
11
+ Active Model provides a known set of interfaces that your objects can implement
12
+ to then present a common interface to the Action Pack helpers. You can include
13
+ functionality from the following modules:
14
+
15
+ * Adding attribute magic to your objects
16
+
17
+ Add prefixes and suffixes to defined attribute methods...
18
+
19
+ class Person
20
+ include ActiveModel::AttributeMethods
21
+
22
+ attribute_method_prefix 'clear_'
23
+ define_attribute_methods [:name, :age]
24
+
25
+ attr_accessor :name, :age
26
+
27
+ def clear_attribute(attr)
28
+ send("#{attr}=", nil)
29
+ end
30
+ end
31
+
32
+ ...gives you clear_name, clear_age.
33
+
34
+ {Learn more}[link:classes/ActiveModel/AttributeMethods.html]
35
+
36
+ * Adding callbacks to your objects
37
+
38
+ class Person
39
+ extend ActiveModel::Callbacks
40
+ define_model_callbacks :create
41
+
42
+ def create
43
+ _run_create_callbacks do
44
+ # Your create action methods here
45
+ end
46
+ end
47
+ end
48
+
49
+ ...gives you before_create, around_create and after_create class methods that
50
+ wrap your create method.
51
+
52
+ {Learn more}[link:classes/ActiveModel/CallBacks.html]
53
+
54
+ * For classes that already look like an Active Record object
55
+
56
+ class Person
57
+ include ActiveModel::Conversion
58
+ end
59
+
60
+ ...returns the class itself when sent :to_model
61
+
62
+ {Learn more}[link:classes/ActiveModel/Conversion.html]
63
+
64
+ * Tracking changes in your object
65
+
66
+ Provides all the value tracking features implemented by ActiveRecord...
67
+
68
+ person = Person.new
69
+ person.name # => nil
70
+ person.changed? # => false
71
+ person.name = 'bob'
72
+ person.changed? # => true
73
+ person.changed # => ['name']
74
+ person.changes # => { 'name' => [nil, 'bob'] }
75
+ person.name = 'robert'
76
+ person.save
77
+ person.previous_changes # => {'name' => ['bob, 'robert']}
78
+
79
+ {Learn more}[link:classes/ActiveModel/Dirty.html]
80
+
81
+ * Adding +errors+ support to your object
82
+
83
+ Provides the error messages to allow your object to interact with Action Pack
84
+ helpers seamlessly...
85
+
86
+ class Person
87
+
88
+ def initialize
89
+ @errors = ActiveModel::Errors.new(self)
90
+ end
91
+
92
+ attr_accessor :name
93
+ attr_reader :errors
94
+
95
+ def validate!
96
+ errors.add(:name, "can not be nil") if name == nil
97
+ end
98
+
99
+ def ErrorsPerson.human_attribute_name(attr, options = {})
100
+ "Name"
101
+ end
102
+
103
+ end
104
+
105
+ ... gives you...
106
+
107
+ person.errors.full_messages
108
+ # => ["Name Can not be nil"]
109
+ person.errors.full_messages
110
+ # => ["Name Can not be nil"]
111
+
112
+ {Learn more}[link:classes/ActiveModel/Errors.html]
113
+
114
+ * Testing the compliance of your object
115
+
116
+ Use ActiveModel::Lint to test the compliance of your object to the
117
+ basic ActiveModel API...
118
+
119
+ {Learn more}[link:classes/ActiveModel/Lint/Tests.html]
120
+
121
+ * Providing a human face to your object
122
+
123
+ ActiveModel::Naming provides your model with the model_name convention
124
+ and a human_name attribute...
125
+
126
+ class NamedPerson
127
+ extend ActiveModel::Naming
128
+ end
129
+
130
+ ...gives you...
131
+
132
+ NamedPerson.model_name #=> "NamedPerson"
133
+ NamedPerson.model_name.human #=> "Named person"
134
+
135
+ {Learn more}[link:classes/ActiveModel/Naming.html]
136
+
137
+ * Adding observer support to your objects
138
+
139
+ ActiveModel::Observers allows your object to implement the Observer
140
+ pattern in a Rails App and take advantage of all the standard observer
141
+ functions.
142
+
143
+ {Learn more}[link:classes/ActiveModel/Observer.html]
144
+
145
+ * Making your object serializable
146
+
147
+ ActiveModel::Serialization provides a standard interface for your object
148
+ to provide to_json or to_xml serialization...
149
+
150
+ s = SerialPerson.new
151
+ s.serializable_hash # => {"name"=>nil}
152
+ s.to_json # => "{\"name\":null}"
153
+ s.to_xml # => "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<serial-person...
154
+
155
+ {Learn more}[link:classes/ActiveModel/Serialization.html]
156
+
157
+
158
+ * Turning your object into a finite State Machine
159
+
160
+ ActiveModel::StateMachine provides a clean way to include all the methods
161
+ you need to transform your object into a finite State Machine...
162
+
163
+ light = TrafficLight.new
164
+ light.current_state #=> :red
165
+ light.change_color! #=> true
166
+ light.current_state #=> :green
167
+
168
+ {Learn more}[link:classes/ActiveModel/StateMachine.html]
169
+
170
+ * Integrating with Rail's internationalization (i18n) handling through
171
+ ActiveModel::Translations...
172
+
173
+ class Person
174
+ extend ActiveModel::Translation
175
+ end
176
+
177
+ {Learn more}[link:classes/ActiveModel/Translation.html]
178
+
179
+ * Providing a full Validation stack for your objects...
180
+
181
+ class Person
182
+ include ActiveModel::Validations
183
+
184
+ attr_accessor :first_name, :last_name
185
+
186
+ validates_each :first_name, :last_name do |record, attr, value|
187
+ record.errors.add attr, 'starts with z.' if value.to_s[0] == ?z
188
+ end
189
+ end
190
+
191
+ person = Person.new(:first_name => 'zoolander')
192
+ person.valid? #=> false
193
+
194
+ {Learn more}[link:classes/ActiveModel/Validations.html]
195
+
196
+ * Make custom validators
197
+
198
+ class Person
199
+ include ActiveModel::Validations
200
+ validates_with HasNameValidator
201
+ attr_accessor :name
202
+ end
203
+
204
+ class HasNameValidator < ActiveModel::Validator
205
+ def validate(record)
206
+ record.errors[:name] = "must exist" if record.name.blank?
207
+ end
208
+ end
209
+
210
+ p = ValidatorPerson.new
211
+ p.valid? #=> false
212
+ p.errors.full_messages #=> ["Name must exist"]
213
+ p.name = "Bob"
214
+ p.valid? #=> true
215
+
216
+ {Learn more}[link:classes/ActiveModel/Validator.html]
@@ -0,0 +1,61 @@
1
+ #--
2
+ # Copyright (c) 2004-2010 David Heinemeier Hansson
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining
5
+ # a copy of this software and associated documentation files (the
6
+ # "Software"), to deal in the Software without restriction, including
7
+ # without limitation the rights to use, copy, modify, merge, publish,
8
+ # distribute, sublicense, and/or sell copies of the Software, and to
9
+ # permit persons to whom the Software is furnished to do so, subject to
10
+ # the following conditions:
11
+ #
12
+ # The above copyright notice and this permission notice shall be
13
+ # included in all copies or substantial portions of the Software.
14
+ #
15
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
+ # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
+ # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
+ # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
+ # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
+ # OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
+ # WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
+ #++
23
+
24
+ activesupport_path = File.expand_path('../../../activesupport/lib', __FILE__)
25
+ $:.unshift(activesupport_path) if File.directory?(activesupport_path) && !$:.include?(activesupport_path)
26
+ require 'active_support'
27
+
28
+
29
+ module ActiveModel
30
+ extend ActiveSupport::Autoload
31
+
32
+ autoload :AttributeMethods
33
+ autoload :BlockValidator, 'active_model/validator'
34
+ autoload :Callbacks
35
+ autoload :Conversion
36
+ autoload :DeprecatedErrorMethods
37
+ autoload :Dirty
38
+ autoload :EachValidator, 'active_model/validator'
39
+ autoload :Errors
40
+ autoload :Lint
41
+ autoload :Name, 'active_model/naming'
42
+ autoload :Naming
43
+ autoload :Observer, 'active_model/observing'
44
+ autoload :Observing
45
+ autoload :Serialization
46
+ autoload :TestCase
47
+ autoload :Translation
48
+ autoload :VERSION
49
+ autoload :Validations
50
+ autoload :Validator
51
+
52
+ module Serializers
53
+ extend ActiveSupport::Autoload
54
+
55
+ autoload :JSON
56
+ autoload :Xml
57
+ end
58
+ end
59
+
60
+ require 'active_support/i18n'
61
+ I18n.load_path << File.dirname(__FILE__) + '/active_model/locale/en.yml'
@@ -0,0 +1,391 @@
1
+ require 'active_support/core_ext/hash/keys'
2
+ require 'active_support/core_ext/class/inheritable_attributes'
3
+
4
+ module ActiveModel
5
+ class MissingAttributeError < NoMethodError
6
+ end
7
+
8
+ # <tt>ActiveModel::AttributeMethods</tt> provides a way to add prefixes and suffixes
9
+ # to your methods as well as handling the creation of Active Record like class methods
10
+ # such as +table_name+.
11
+ #
12
+ # The requirements to implement ActiveModel::AttributeMethods are:
13
+ #
14
+ # * <tt>include ActiveModel::AttributeMethods</tt> in your object
15
+ # * Call each Attribute Method module method you want to add, such as
16
+ # attribute_method_suffix or attribute_method_prefix
17
+ # * Call <tt>define_attribute_methods</tt> after the other methods are
18
+ # called.
19
+ # * Define the various generic +_attribute+ methods that you have declared
20
+ #
21
+ # A minimal implementation could be:
22
+ #
23
+ # class Person
24
+ #
25
+ # include ActiveModel::AttributeMethods
26
+ #
27
+ # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!'
28
+ # attribute_method_suffix '_contrived?'
29
+ # attribute_method_prefix 'clear_'
30
+ # define_attribute_methods ['name']
31
+ #
32
+ # attr_accessor :name
33
+ #
34
+ # private
35
+ #
36
+ # def attribute_contrived?(attr)
37
+ # true
38
+ # end
39
+ #
40
+ # def clear_attribute(attr)
41
+ # send("#{attr}=", nil)
42
+ # end
43
+ #
44
+ # def reset_attribute_to_default!(attr)
45
+ # send("#{attr}=", "Default Name")
46
+ # end
47
+ #
48
+ # end
49
+ #
50
+ module AttributeMethods
51
+ extend ActiveSupport::Concern
52
+
53
+ # Declare and check for suffixed attribute methods.
54
+ module ClassMethods
55
+ # Defines an "attribute" method (like +inheritance_column+ or
56
+ # +table_name+). A new (class) method will be created with the
57
+ # given name. If a value is specified, the new method will
58
+ # return that value (as a string). Otherwise, the given block
59
+ # will be used to compute the value of the method.
60
+ #
61
+ # The original method will be aliased, with the new name being
62
+ # prefixed with "original_". This allows the new method to
63
+ # access the original value.
64
+ #
65
+ # Example:
66
+ #
67
+ # class Person
68
+ #
69
+ # include ActiveModel::AttributeMethods
70
+ #
71
+ # cattr_accessor :primary_key
72
+ # cattr_accessor :inheritance_column
73
+ #
74
+ # define_attr_method :primary_key, "sysid"
75
+ # define_attr_method( :inheritance_column ) do
76
+ # original_inheritance_column + "_id"
77
+ # end
78
+ #
79
+ # end
80
+ #
81
+ # Provivdes you with:
82
+ #
83
+ # AttributePerson.primary_key
84
+ # # => "sysid"
85
+ # AttributePerson.inheritance_column = 'address'
86
+ # AttributePerson.inheritance_column
87
+ # # => 'address_id'
88
+ def define_attr_method(name, value=nil, &block)
89
+ sing = metaclass
90
+ sing.send :alias_method, "original_#{name}", name
91
+ if block_given?
92
+ sing.send :define_method, name, &block
93
+ else
94
+ # use eval instead of a block to work around a memory leak in dev
95
+ # mode in fcgi
96
+ sing.class_eval "def #{name}; #{value.to_s.inspect}; end"
97
+ end
98
+ end
99
+
100
+ # Declares a method available for all attributes with the given prefix.
101
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
102
+ #
103
+ # #{prefix}#{attr}(*args, &block)
104
+ #
105
+ # to
106
+ #
107
+ # #{prefix}attribute(#{attr}, *args, &block)
108
+ #
109
+ # An <tt>#{prefix}attribute</tt> instance method must exist and accept at least
110
+ # the +attr+ argument.
111
+ #
112
+ # For example:
113
+ #
114
+ # class Person
115
+ #
116
+ # include ActiveModel::AttributeMethods
117
+ # attr_accessor :name
118
+ # attribute_method_prefix 'clear_'
119
+ # define_attribute_methods [:name]
120
+ #
121
+ # private
122
+ #
123
+ # def clear_attribute(attr)
124
+ # send("#{attr}=", nil)
125
+ # end
126
+ # end
127
+ #
128
+ # person = Person.new
129
+ # person.name = "Bob"
130
+ # person.name # => "Bob"
131
+ # person.clear_name
132
+ # person.name # => nil
133
+ def attribute_method_prefix(*prefixes)
134
+ attribute_method_matchers.concat(prefixes.map { |prefix| AttributeMethodMatcher.new :prefix => prefix })
135
+ undefine_attribute_methods
136
+ end
137
+
138
+ # Declares a method available for all attributes with the given suffix.
139
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method.
140
+ #
141
+ # #{attr}#{suffix}(*args, &block)
142
+ #
143
+ # to
144
+ #
145
+ # attribute#{suffix}(#{attr}, *args, &block)
146
+ #
147
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
148
+ # the +attr+ argument.
149
+ #
150
+ # For example:
151
+ #
152
+ # class Person
153
+ #
154
+ # include ActiveModel::AttributeMethods
155
+ # attr_accessor :name
156
+ # attribute_method_suffix '_short?'
157
+ # define_attribute_methods [:name]
158
+ #
159
+ # private
160
+ #
161
+ # def attribute_short?(attr)
162
+ # send(attr).length < 5
163
+ # end
164
+ # end
165
+ #
166
+ # person = Person.new
167
+ # person.name = "Bob"
168
+ # person.name # => "Bob"
169
+ # person.name_short? # => true
170
+ def attribute_method_suffix(*suffixes)
171
+ attribute_method_matchers.concat(suffixes.map { |suffix| AttributeMethodMatcher.new :suffix => suffix })
172
+ undefine_attribute_methods
173
+ end
174
+
175
+ # Declares a method available for all attributes with the given prefix
176
+ # and suffix. Uses +method_missing+ and <tt>respond_to?</tt> to rewrite
177
+ # the method.
178
+ #
179
+ # #{prefix}#{attr}#{suffix}(*args, &block)
180
+ #
181
+ # to
182
+ #
183
+ # #{prefix}attribute#{suffix}(#{attr}, *args, &block)
184
+ #
185
+ # An <tt>#{prefix}attribute#{suffix}</tt> instance method must exist and
186
+ # accept at least the +attr+ argument.
187
+ #
188
+ # For example:
189
+ #
190
+ # class Person
191
+ #
192
+ # include ActiveModel::AttributeMethods
193
+ # attr_accessor :name
194
+ # attribute_method_affix :prefix => 'reset_', :suffix => '_to_default!'
195
+ # define_attribute_methods [:name]
196
+ #
197
+ # private
198
+ #
199
+ # def reset_attribute_to_default!(attr)
200
+ # ...
201
+ # end
202
+ # end
203
+ #
204
+ # person = Person.new
205
+ # person.name # => 'Gem'
206
+ # person.reset_name_to_default!
207
+ # person.name # => 'Gemma'
208
+ def attribute_method_affix(*affixes)
209
+ attribute_method_matchers.concat(affixes.map { |affix| AttributeMethodMatcher.new :prefix => affix[:prefix], :suffix => affix[:suffix] })
210
+ undefine_attribute_methods
211
+ end
212
+
213
+ def alias_attribute(new_name, old_name)
214
+ attribute_method_matchers.each do |matcher|
215
+ module_eval <<-STR, __FILE__, __LINE__+1
216
+ def #{matcher.method_name(new_name)}(*args)
217
+ send(:#{matcher.method_name(old_name)}, *args)
218
+ end
219
+ STR
220
+ end
221
+ end
222
+
223
+ # Declares a the attributes that should be prefixed and suffixed by
224
+ # ActiveModel::AttributeMethods.
225
+ #
226
+ # To use, pass in an array of attribute names (as strings or symbols),
227
+ # be sure to declare +define_attribute_methods+ after you define any
228
+ # prefix, suffix or affix methods, or they will not hook in.
229
+ #
230
+ # class Person
231
+ #
232
+ # include ActiveModel::AttributeMethods
233
+ # attr_accessor :name, :age, :address
234
+ # attribute_method_prefix 'clear_'
235
+ #
236
+ # # Call to define_attribute_methods must appear after the
237
+ # # attribute_method_prefix, attribute_method_suffix or
238
+ # # attribute_method_affix declares.
239
+ # define_attribute_methods [:name, :age, :address]
240
+ #
241
+ # private
242
+ #
243
+ # def clear_attribute(attr)
244
+ # ...
245
+ # end
246
+ # end
247
+ def define_attribute_methods(attr_names)
248
+ return if attribute_methods_generated?
249
+ attr_names.each do |attr_name|
250
+ attribute_method_matchers.each do |matcher|
251
+ unless instance_method_already_implemented?(matcher.method_name(attr_name))
252
+ generate_method = "define_method_#{matcher.prefix}attribute#{matcher.suffix}"
253
+
254
+ if respond_to?(generate_method)
255
+ send(generate_method, attr_name)
256
+ else
257
+ generated_attribute_methods.module_eval <<-STR, __FILE__, __LINE__+1
258
+ def #{matcher.method_name(attr_name)}(*args)
259
+ send(:#{matcher.method_missing_target}, '#{attr_name}', *args)
260
+ end
261
+ STR
262
+ end
263
+ end
264
+ end
265
+ end
266
+ @attribute_methods_generated = true
267
+ end
268
+
269
+ # Removes all the preiously dynamically defined methods from the class
270
+ def undefine_attribute_methods
271
+ generated_attribute_methods.module_eval do
272
+ instance_methods.each { |m| undef_method(m) }
273
+ end
274
+ @attribute_methods_generated = nil
275
+ end
276
+
277
+ # Returns true if the attribute methods defined have been generated.
278
+ def generated_attribute_methods #:nodoc:
279
+ @generated_attribute_methods ||= begin
280
+ mod = Module.new
281
+ include mod
282
+ mod
283
+ end
284
+ end
285
+
286
+ def attribute_methods_generated?
287
+ @attribute_methods_generated ||= nil
288
+ end
289
+
290
+ protected
291
+ def instance_method_already_implemented?(method_name)
292
+ method_defined?(method_name)
293
+ end
294
+
295
+ private
296
+ class AttributeMethodMatcher
297
+ attr_reader :prefix, :suffix
298
+
299
+ AttributeMethodMatch = Struct.new(:target, :attr_name)
300
+
301
+ def initialize(options = {})
302
+ options.symbolize_keys!
303
+ @prefix, @suffix = options[:prefix] || '', options[:suffix] || ''
304
+ @regex = /^(#{Regexp.escape(@prefix)})(.+?)(#{Regexp.escape(@suffix)})$/
305
+ end
306
+
307
+ def match(method_name)
308
+ if matchdata = @regex.match(method_name)
309
+ AttributeMethodMatch.new(method_missing_target, matchdata[2])
310
+ else
311
+ nil
312
+ end
313
+ end
314
+
315
+ def method_name(attr_name)
316
+ "#{prefix}#{attr_name}#{suffix}"
317
+ end
318
+
319
+ def method_missing_target
320
+ :"#{prefix}attribute#{suffix}"
321
+ end
322
+ end
323
+
324
+ def attribute_method_matchers #:nodoc:
325
+ read_inheritable_attribute(:attribute_method_matchers) || write_inheritable_attribute(:attribute_method_matchers, [])
326
+ end
327
+ end
328
+
329
+ # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
330
+ # were first-class methods. So a Person class with a name attribute can use Person#name and
331
+ # Person#name= and never directly use the attributes hash -- except for multiple assigns with
332
+ # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
333
+ # the completed attribute is not +nil+ or 0.
334
+ #
335
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
336
+ # table with a +master_id+ foreign key can instantiate master through Client#master.
337
+ def method_missing(method_id, *args, &block)
338
+ method_name = method_id.to_s
339
+ if match = match_attribute_method?(method_name)
340
+ guard_private_attribute_method!(method_name, args)
341
+ return __send__(match.target, match.attr_name, *args, &block)
342
+ end
343
+ super
344
+ end
345
+
346
+ # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
347
+ # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
348
+ # which will all return +true+.
349
+ alias :respond_to_without_attributes? :respond_to?
350
+ def respond_to?(method, include_private_methods = false)
351
+ if super
352
+ return true
353
+ elsif !include_private_methods && super(method, true)
354
+ # If we're here then we haven't found among non-private methods
355
+ # but found among all methods. Which means that given method is private.
356
+ return false
357
+ elsif match_attribute_method?(method.to_s)
358
+ return true
359
+ end
360
+ super
361
+ end
362
+
363
+ protected
364
+ def attribute_method?(attr_name)
365
+ attributes.include?(attr_name)
366
+ end
367
+
368
+ private
369
+ # Returns a struct representing the matching attribute method.
370
+ # The struct's attributes are prefix, base and suffix.
371
+ def match_attribute_method?(method_name)
372
+ self.class.send(:attribute_method_matchers).each do |method|
373
+ if (match = method.match(method_name)) && attribute_method?(match.attr_name)
374
+ return match
375
+ end
376
+ end
377
+ nil
378
+ end
379
+
380
+ # prevent method_missing from calling private methods with #send
381
+ def guard_private_attribute_method!(method_name, args)
382
+ if self.class.private_method_defined?(method_name)
383
+ raise NoMethodError.new("Attempt to call private method", method_name, args)
384
+ end
385
+ end
386
+
387
+ def missing_attribute(attr_name, stack)
388
+ raise ActiveModel::MissingAttributeError, "missing attribute: #{attr_name}", stack
389
+ end
390
+ end
391
+ end