Skip to content
This repository
Newer
Older
100644 305 lines (262 sloc) 11.478 kb
2b3cc247 » jeremy
2006-07-31 r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
1 module ActiveRecord
2 module AttributeMethods #:nodoc:
3 DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
4
5 def self.included(base)
6 base.extend ClassMethods
7 base.attribute_method_suffix *DEFAULT_SUFFIXES
8 end
9
10 # Declare and check for suffixed attribute methods.
11 module ClassMethods
12 # Declare a method available for all attributes with the given suffix.
13 # Uses method_missing and respond_to? to rewrite the method
14 # #{attr}#{suffix}(*args, &block)
15 # to
16 # attribute#{suffix}(#{attr}, *args, &block)
17 #
18 # An attribute#{suffix} instance method must exist and accept at least
19 # the attr argument.
20 #
21 # For example:
22 # class Person < ActiveRecord::Base
23 # attribute_method_suffix '_changed?'
24 #
25 # private
26 # def attribute_changed?(attr)
27 # ...
28 # end
29 # end
30 #
31 # person = Person.find(1)
32 # person.name_changed? # => false
33 # person.name = 'Hubert'
34 # person.name_changed? # => true
35 def attribute_method_suffix(*suffixes)
36 attribute_method_suffixes.concat suffixes
37 rebuild_attribute_method_regexp
38 end
39
40 # Returns MatchData if method_name is an attribute method.
41 def match_attribute_method?(method_name)
42 rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
43 @@attribute_method_regexp.match(method_name)
44 end
45
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
46
47 # Contains the names of the generated attribute methods.
48 def generated_methods #:nodoc:
49 @generated_methods ||= Set.new
50 end
51
52 def generated_methods?
53 !generated_methods.empty?
54 end
55
56 # generates all the attribute related methods for columns in the database
57 # accessors, mutators and query methods
58 def define_attribute_methods
59 return if generated_methods?
60 columns_hash.each do |name, column|
b31aa639 » Tobias Lütke
2007-10-03 Allow column accessors to be created even if Kernel. or Object# metho…
61 unless instance_method_already_implemented?(name)
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
62 if self.serialized_attributes[name]
63 define_read_method_for_serialized_attribute(name)
64 else
65 define_read_method(name.to_sym, name, column)
66 end
67 end
68
b31aa639 » Tobias Lütke
2007-10-03 Allow column accessors to be created even if Kernel. or Object# metho…
69 unless instance_method_already_implemented?("#{name}=")
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
70 define_write_method(name.to_sym)
71 end
72
b31aa639 » Tobias Lütke
2007-10-03 Allow column accessors to be created even if Kernel. or Object# metho…
73 unless instance_method_already_implemented?("#{name}?")
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
74 define_question_method(name)
75 end
76 end
77 end
acbec3e5 » NZKoz
2007-09-17 Ensure that custom mutators aren't redefined by define_attribute_meth…
78
5b2e8b1e » technoweenie
2007-10-06 Fix that ActiveRecord would create attribute methods and override cus…
79 # Check to see if the method is defined in the model or any of it's subclasses that also derive from ActiveRecord.
80 # Raise DangerousAttributeError if the method is defined by ActiveRecord though.
b31aa639 » Tobias Lütke
2007-10-03 Allow column accessors to be created even if Kernel. or Object# metho…
81 def instance_method_already_implemented?(method_name)
5b2e8b1e » technoweenie
2007-10-06 Fix that ActiveRecord would create attribute methods and override cus…
82 return true if method_name =~ /^id(=$|\?$|$)/
83 @_defined_class_methods ||= Set.new(ancestors.first(ancestors.index(ActiveRecord::Base)).collect! { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.flatten)
84 @@_defined_activerecord_methods ||= Set.new(ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false))
85 raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
86 @_defined_class_methods.include?(method_name)
b31aa639 » Tobias Lütke
2007-10-03 Allow column accessors to be created even if Kernel. or Object# metho…
87 end
88
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
89 alias :define_read_methods :define_attribute_methods
90
2b3cc247 » jeremy
2006-07-31 r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
91 private
92 # Suffixes a, ?, c become regexp /(a|\?|c)$/
93 def rebuild_attribute_method_regexp
94 suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
95 @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
96 end
97
98 # Default to =, ?, _before_type_cast
99 def attribute_method_suffixes
100 @@attribute_method_suffixes ||= []
101 end
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
102
103 # Define an attribute reader method. Cope with nil column.
104 def define_read_method(symbol, attr_name, column)
105 cast_code = column.type_cast_code('v') if column
106 access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
107
108 unless attr_name.to_s == self.primary_key.to_s
109 access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
110 end
111
112 evaluate_attribute_method attr_name, "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{access_code}; end; end"
113 end
114
115 # Define read method for serialized attribute.
116 def define_read_method_for_serialized_attribute(attr_name)
117 evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
118 end
119
120 # Define an attribute ? method.
121 def define_question_method(attr_name)
122 evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
123 end
124
125 def define_write_method(attr_name)
126 evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
127 end
128
129 # Evaluate the definition for an attribute related method
130 def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
131
132 unless method_name.to_s == primary_key.to_s
133 generated_methods << method_name
134 end
135
136 begin
137 class_eval(method_definition)
138 rescue SyntaxError => err
139 generated_methods.delete(attr_name)
140 if logger
141 logger.warn "Exception occurred during reader method compilation."
142 logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
143 logger.warn "#{err.message}"
144 end
145 end
146 end
147 end # ClassMethods
148
149
0faa4ca0 » dhh
2007-09-22 Doc fix (closes #9323) [Henrik N]
150 # Allows access to the object attributes, which are held in the @attributes hash, as though they
151 # were first-class methods. So a Person class with a name attribute can use Person#name and
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
152 # Person#name= and never directly use the attributes hash -- except for multiple assigns with
153 # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
154 # the completed attribute is not nil or 0.
155 #
156 # It's also possible to instantiate related objects, so a Client class belonging to the clients
157 # table with a master_id foreign key can instantiate master through Client#master.
158 def method_missing(method_id, *args, &block)
159 method_name = method_id.to_s
160
161 # If we haven't generated any methods yet, generate them, then
162 # see if we've created the method we're looking for.
163 if !self.class.generated_methods?
164 self.class.define_attribute_methods
165 if self.class.generated_methods.include?(method_name)
166 return self.send(method_id, *args, &block)
167 end
168 end
169
170 if self.class.primary_key.to_s == method_name
171 id
172 elsif md = self.class.match_attribute_method?(method_name)
173 attribute_name, method_type = md.pre_match, md.to_s
174 if @attributes.include?(attribute_name)
175 __send__("attribute#{method_type}", attribute_name, *args, &block)
176 else
177 super
178 end
179 elsif @attributes.include?(method_name)
180 read_attribute(method_name)
181 else
182 super
183 end
184 end
185
186 # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
187 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
188 def read_attribute(attr_name)
189 attr_name = attr_name.to_s
190 if !(value = @attributes[attr_name]).nil?
191 if column = column_for_attribute(attr_name)
192 if unserializable_attribute?(attr_name, column)
193 unserialize_attribute(attr_name)
194 else
195 column.type_cast(value)
196 end
197 else
198 value
199 end
200 else
201 nil
202 end
203 end
204
205 def read_attribute_before_type_cast(attr_name)
206 @attributes[attr_name]
207 end
208
209 # Returns true if the attribute is of a text column and marked for serialization.
210 def unserializable_attribute?(attr_name, column)
211 column.text? && self.class.serialized_attributes[attr_name]
212 end
213
214 # Returns the unserialized object of the attribute.
215 def unserialize_attribute(attr_name)
216 unserialized_object = object_from_yaml(@attributes[attr_name])
217
218 if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
51977bc2 » technoweenie
2007-09-04 Fix bug where unserializing an attribute attempts to modify a frozen @…
219 @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
220 else
221 raise SerializationTypeMismatch,
222 "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
223 end
2b3cc247 » jeremy
2006-07-31 r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
224 end
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
225
226
227 # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
228 # columns are turned into nil.
229 def write_attribute(attr_name, value)
230 attr_name = attr_name.to_s
231 @attributes_cache.delete(attr_name)
232 if (column = column_for_attribute(attr_name)) && column.number?
233 @attributes[attr_name] = convert_number_column_value(value)
234 else
235 @attributes[attr_name] = value
236 end
237 end
238
239
240 def query_attribute(attr_name)
241 unless value = read_attribute(attr_name)
242 false
243 else
244 column = self.class.columns_hash[attr_name]
245 if column.nil?
246 if Numeric === value || value !~ /[^0-9]/
247 !value.to_i.zero?
248 else
249 !value.blank?
250 end
251 elsif column.number?
252 !value.zero?
253 else
254 !value.blank?
255 end
256 end
257 end
258
259 # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
260 # person.respond_to?("name?") which will all return true.
261 alias :respond_to_without_attributes? :respond_to?
262 def respond_to?(method, include_priv = false)
263 method_name = method.to_s
264 if super
265 return true
266 elsif !self.class.generated_methods?
267 self.class.define_attribute_methods
268 if self.class.generated_methods.include?(method_name)
269 return true
270 end
271 end
272
273 if @attributes.nil?
274 return super
275 elsif @attributes.include?(method_name)
276 return true
277 elsif md = self.class.match_attribute_method?(method_name)
278 return true if @attributes.include?(md.pre_match)
279 end
280 super
281 end
282
2b3cc247 » jeremy
2006-07-31 r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
283
284 private
5b801b59 » NZKoz
2007-08-14 Change the implementation of ActiveRecord's attribute reader and writ…
285
286 def missing_attribute(attr_name, stack)
287 raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
288 end
289
2b3cc247 » jeremy
2006-07-31 r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
290 # Handle *? for method_missing.
291 def attribute?(attribute_name)
292 query_attribute(attribute_name)
293 end
294
295 # Handle *= for method_missing.
296 def attribute=(attribute_name, value)
297 write_attribute(attribute_name, value)
298 end
299
300 # Handle *_before_type_cast for method_missing.
301 def attribute_before_type_cast(attribute_name)
302 read_attribute_before_type_cast(attribute_name)
303 end
304 end
305 end
Something went wrong with that request. Please try again.