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.
- data/CHANGELOG +13 -0
- data/MIT-LICENSE +21 -0
- data/README +216 -0
- data/lib/active_model.rb +61 -0
- data/lib/active_model/attribute_methods.rb +391 -0
- data/lib/active_model/callbacks.rb +133 -0
- data/lib/active_model/conversion.rb +19 -0
- data/lib/active_model/deprecated_error_methods.rb +33 -0
- data/lib/active_model/dirty.rb +164 -0
- data/lib/active_model/errors.rb +277 -0
- data/lib/active_model/lint.rb +89 -0
- data/lib/active_model/locale/en.yml +26 -0
- data/lib/active_model/naming.rb +60 -0
- data/lib/active_model/observing.rb +196 -0
- data/lib/active_model/railtie.rb +2 -0
- data/lib/active_model/serialization.rb +87 -0
- data/lib/active_model/serializers/json.rb +96 -0
- data/lib/active_model/serializers/xml.rb +204 -0
- data/lib/active_model/test_case.rb +16 -0
- data/lib/active_model/translation.rb +60 -0
- data/lib/active_model/validations.rb +168 -0
- data/lib/active_model/validations/acceptance.rb +51 -0
- data/lib/active_model/validations/confirmation.rb +49 -0
- data/lib/active_model/validations/exclusion.rb +40 -0
- data/lib/active_model/validations/format.rb +64 -0
- data/lib/active_model/validations/inclusion.rb +40 -0
- data/lib/active_model/validations/length.rb +98 -0
- data/lib/active_model/validations/numericality.rb +111 -0
- data/lib/active_model/validations/presence.rb +41 -0
- data/lib/active_model/validations/validates.rb +108 -0
- data/lib/active_model/validations/with.rb +70 -0
- data/lib/active_model/validator.rb +160 -0
- data/lib/active_model/version.rb +9 -0
- 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]
|
data/lib/active_model.rb
ADDED
@@ -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
|