Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Newer
Older
100644 304 lines (258 sloc) 10.693 kb
2b3cc24 Jeremy Kemper 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 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
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 Michael Koziarski Ensure that custom mutators aren't redefined by define_attribute_methods...
NZKoz authored
61 unless instance_method_already_defined?(name)
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
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 Michael Koziarski Ensure that custom mutators aren't redefined by define_attribute_methods...
NZKoz authored
69 unless instance_method_already_defined?("#{name}=")
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
NZKoz authored
70 define_write_method(name.to_sym)
71 end
72
acbec3e Michael Koziarski Ensure that custom mutators aren't redefined by define_attribute_methods...
NZKoz authored
73 unless instance_method_already_defined?("#{name}?")
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
NZKoz authored
74 define_question_method(name)
75 end
76 end
77 end
acbec3e Michael Koziarski Ensure that custom mutators aren't redefined by define_attribute_methods...
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 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
NZKoz authored
85 alias :define_read_methods :define_attribute_methods
86
87
88
2b3cc24 Jeremy Kemper 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 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
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
0faa4ca David Heinemeier Hansson Doc fix (closes #9323) [Henrik N]
dhh authored
148 # Allows access to the object attributes, which are held in the @attributes hash, as though they
149 # were first-class methods. So a Person class with a name attribute can use Person#name and
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
NZKoz authored
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 risk danger olson Fix bug where unserializing an attribute attempts to modify a frozen @at...
technoweenie authored
217 @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
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 Kemper r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
222 end
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
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 Kemper r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
281
282 private
5b801b5 Michael Koziarski Change the implementation of ActiveRecord's attribute reader and writer ...
NZKoz authored
283
284 def missing_attribute(attr_name, stack)
285 raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
286 end
287
2b3cc24 Jeremy Kemper 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.