activemodel 3.0.0.beta

Sign up to get free protection for your applications and to get access to all the features.
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