Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 304 lines (258 sloc) 10.686 kB
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
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
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
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|
acbec3e @NZKoz Ensure that custom mutators aren't redefined by define_attribute_meth…
NZKoz authored
61 unless instance_method_already_defined?(name)
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
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
acbec3e @NZKoz Ensure that custom mutators aren't redefined by define_attribute_meth…
NZKoz authored
69 unless instance_method_already_defined?("#{name}=")
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
70 define_write_method(name.to_sym)
71 end
72
acbec3e @NZKoz Ensure that custom mutators aren't redefined by define_attribute_meth…
NZKoz authored
73 unless instance_method_already_defined?("#{name}?")
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
74 define_question_method(name)
75 end
76 end
77 end
acbec3e @NZKoz Ensure that custom mutators aren't redefined by define_attribute_meth…
NZKoz authored
78
79 def instance_method_already_defined?(method_name)
80 method_defined?(method_name) ||
81 private_method_defined?(method_name) ||
82 protected_method_defined?(method_name)
83 end
84
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
85 alias :define_read_methods :define_attribute_methods
86
87
88
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
89 private
90 # Suffixes a, ?, c become regexp /(a|\?|c)$/
91 def rebuild_attribute_method_regexp
92 suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
93 @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
94 end
95
96 # Default to =, ?, _before_type_cast
97 def attribute_method_suffixes
98 @@attribute_method_suffixes ||= []
99 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
100
101 # Define an attribute reader method. Cope with nil column.
102 def define_read_method(symbol, attr_name, column)
103 cast_code = column.type_cast_code('v') if column
104 access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
105
106 unless attr_name.to_s == self.primary_key.to_s
107 access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
108 end
109
110 evaluate_attribute_method attr_name, "def #{symbol}; @attributes_cache['#{attr_name}'] ||= begin; #{access_code}; end; end"
111 end
112
113 # Define read method for serialized attribute.
114 def define_read_method_for_serialized_attribute(attr_name)
115 evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
116 end
117
118 # Define an attribute ? method.
119 def define_question_method(attr_name)
120 evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
121 end
122
123 def define_write_method(attr_name)
124 evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
125 end
126
127 # Evaluate the definition for an attribute related method
128 def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
129
130 unless method_name.to_s == primary_key.to_s
131 generated_methods << method_name
132 end
133
134 begin
135 class_eval(method_definition)
136 rescue SyntaxError => err
137 generated_methods.delete(attr_name)
138 if logger
139 logger.warn "Exception occurred during reader method compilation."
140 logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
141 logger.warn "#{err.message}"
142 end
143 end
144 end
145 end # ClassMethods
146
147
148 # Allows access to the object attributes, which are held in the @attributes hash, as were
149 # they first-class methods. So a Person class with a name attribute can use Person#name and
150 # Person#name= and never directly use the attributes hash -- except for multiple assigns with
151 # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
152 # the completed attribute is not nil or 0.
153 #
154 # It's also possible to instantiate related objects, so a Client class belonging to the clients
155 # table with a master_id foreign key can instantiate master through Client#master.
156 def method_missing(method_id, *args, &block)
157 method_name = method_id.to_s
158
159 # If we haven't generated any methods yet, generate them, then
160 # see if we've created the method we're looking for.
161 if !self.class.generated_methods?
162 self.class.define_attribute_methods
163 if self.class.generated_methods.include?(method_name)
164 return self.send(method_id, *args, &block)
165 end
166 end
167
168 if self.class.primary_key.to_s == method_name
169 id
170 elsif md = self.class.match_attribute_method?(method_name)
171 attribute_name, method_type = md.pre_match, md.to_s
172 if @attributes.include?(attribute_name)
173 __send__("attribute#{method_type}", attribute_name, *args, &block)
174 else
175 super
176 end
177 elsif @attributes.include?(method_name)
178 read_attribute(method_name)
179 else
180 super
181 end
182 end
183
184 # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
185 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
186 def read_attribute(attr_name)
187 attr_name = attr_name.to_s
188 if !(value = @attributes[attr_name]).nil?
189 if column = column_for_attribute(attr_name)
190 if unserializable_attribute?(attr_name, column)
191 unserialize_attribute(attr_name)
192 else
193 column.type_cast(value)
194 end
195 else
196 value
197 end
198 else
199 nil
200 end
201 end
202
203 def read_attribute_before_type_cast(attr_name)
204 @attributes[attr_name]
205 end
206
207 # Returns true if the attribute is of a text column and marked for serialization.
208 def unserializable_attribute?(attr_name, column)
209 column.text? && self.class.serialized_attributes[attr_name]
210 end
211
212 # Returns the unserialized object of the attribute.
213 def unserialize_attribute(attr_name)
214 unserialized_object = object_from_yaml(@attributes[attr_name])
215
216 if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
51977bc @technoweenie Fix bug where unserializing an attribute attempts to modify a frozen …
technoweenie authored
217 @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
218 else
219 raise SerializationTypeMismatch,
220 "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
221 end
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
222 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
223
224
225 # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
226 # columns are turned into nil.
227 def write_attribute(attr_name, value)
228 attr_name = attr_name.to_s
229 @attributes_cache.delete(attr_name)
230 if (column = column_for_attribute(attr_name)) && column.number?
231 @attributes[attr_name] = convert_number_column_value(value)
232 else
233 @attributes[attr_name] = value
234 end
235 end
236
237
238 def query_attribute(attr_name)
239 unless value = read_attribute(attr_name)
240 false
241 else
242 column = self.class.columns_hash[attr_name]
243 if column.nil?
244 if Numeric === value || value !~ /[^0-9]/
245 !value.to_i.zero?
246 else
247 !value.blank?
248 end
249 elsif column.number?
250 !value.zero?
251 else
252 !value.blank?
253 end
254 end
255 end
256
257 # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
258 # person.respond_to?("name?") which will all return true.
259 alias :respond_to_without_attributes? :respond_to?
260 def respond_to?(method, include_priv = false)
261 method_name = method.to_s
262 if super
263 return true
264 elsif !self.class.generated_methods?
265 self.class.define_attribute_methods
266 if self.class.generated_methods.include?(method_name)
267 return true
268 end
269 end
270
271 if @attributes.nil?
272 return super
273 elsif @attributes.include?(method_name)
274 return true
275 elsif md = self.class.match_attribute_method?(method_name)
276 return true if @attributes.include?(md.pre_match)
277 end
278 super
279 end
280
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
281
282 private
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
283
284 def missing_attribute(attr_name, stack)
285 raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
286 end
287
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
288 # Handle *? for method_missing.
289 def attribute?(attribute_name)
290 query_attribute(attribute_name)
291 end
292
293 # Handle *= for method_missing.
294 def attribute=(attribute_name, value)
295 write_attribute(attribute_name, value)
296 end
297
298 # Handle *_before_type_cast for method_missing.
299 def attribute_before_type_cast(attribute_name)
300 read_attribute_before_type_cast(attribute_name)
301 end
302 end
303 end
Something went wrong with that request. Please try again.