Froze rails gems
[depot.git] / vendor / rails / activerecord / lib / active_record / attribute_methods.rb
1 module ActiveRecord
2 module AttributeMethods #:nodoc:
3 DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
4 ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
5
6 def self.included(base)
7 base.extend ClassMethods
8 base.attribute_method_suffix(*DEFAULT_SUFFIXES)
9 base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
10 base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
11 base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
12 base.time_zone_aware_attributes = false
13 base.class_inheritable_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
14 base.skip_time_zone_conversion_for_attributes = []
15 end
16
17 # Declare and check for suffixed attribute methods.
18 module ClassMethods
19 # Declares a method available for all attributes with the given suffix.
20 # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
21 #
22 # #{attr}#{suffix}(*args, &block)
23 #
24 # to
25 #
26 # attribute#{suffix}(#{attr}, *args, &block)
27 #
28 # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
29 # the +attr+ argument.
30 #
31 # For example:
32 #
33 # class Person < ActiveRecord::Base
34 # attribute_method_suffix '_changed?'
35 #
36 # private
37 # def attribute_changed?(attr)
38 # ...
39 # end
40 # end
41 #
42 # person = Person.find(1)
43 # person.name_changed? # => false
44 # person.name = 'Hubert'
45 # person.name_changed? # => true
46 def attribute_method_suffix(*suffixes)
47 attribute_method_suffixes.concat suffixes
48 rebuild_attribute_method_regexp
49 end
50
51 # Returns MatchData if method_name is an attribute method.
52 def match_attribute_method?(method_name)
53 rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
54 @@attribute_method_regexp.match(method_name)
55 end
56
57
58 # Contains the names of the generated attribute methods.
59 def generated_methods #:nodoc:
60 @generated_methods ||= Set.new
61 end
62
63 def generated_methods?
64 !generated_methods.empty?
65 end
66
67 # Generates all the attribute related methods for columns in the database
68 # accessors, mutators and query methods.
69 def define_attribute_methods
70 return if generated_methods?
71 columns_hash.each do |name, column|
72 unless instance_method_already_implemented?(name)
73 if self.serialized_attributes[name]
74 define_read_method_for_serialized_attribute(name)
75 elsif create_time_zone_conversion_attribute?(name, column)
76 define_read_method_for_time_zone_conversion(name)
77 else
78 define_read_method(name.to_sym, name, column)
79 end
80 end
81
82 unless instance_method_already_implemented?("#{name}=")
83 if create_time_zone_conversion_attribute?(name, column)
84 define_write_method_for_time_zone_conversion(name)
85 else
86 define_write_method(name.to_sym)
87 end
88 end
89
90 unless instance_method_already_implemented?("#{name}?")
91 define_question_method(name)
92 end
93 end
94 end
95
96 # Checks whether the method is defined in the model or any of its subclasses
97 # that also derive from Active Record. Raises DangerousAttributeError if the
98 # method is defined by Active Record though.
99 def instance_method_already_implemented?(method_name)
100 method_name = method_name.to_s
101 return true if method_name =~ /^id(=$|\?$|$)/
102 @_defined_class_methods ||= ancestors.first(ancestors.index(ActiveRecord::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map(&:to_s).to_set
103 @@_defined_activerecord_methods ||= (ActiveRecord::Base.public_instance_methods(false) | ActiveRecord::Base.private_instance_methods(false) | ActiveRecord::Base.protected_instance_methods(false)).map(&:to_s).to_set
104 raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
105 @_defined_class_methods.include?(method_name)
106 end
107
108 alias :define_read_methods :define_attribute_methods
109
110 # +cache_attributes+ allows you to declare which converted attribute values should
111 # be cached. Usually caching only pays off for attributes with expensive conversion
112 # methods, like time related columns (e.g. +created_at+, +updated_at+).
113 def cache_attributes(*attribute_names)
114 attribute_names.each {|attr| cached_attributes << attr.to_s}
115 end
116
117 # Returns the attributes which are cached. By default time related columns
118 # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
119 def cached_attributes
120 @cached_attributes ||=
121 columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map(&:name).to_set
122 end
123
124 # Returns +true+ if the provided attribute is being cached.
125 def cache_attribute?(attr_name)
126 cached_attributes.include?(attr_name)
127 end
128
129 private
130 # Suffixes a, ?, c become regexp /(a|\?|c)$/
131 def rebuild_attribute_method_regexp
132 suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
133 @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
134 end
135
136 # Default to =, ?, _before_type_cast
137 def attribute_method_suffixes
138 @@attribute_method_suffixes ||= []
139 end
140
141 def create_time_zone_conversion_attribute?(name, column)
142 time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
143 end
144
145 # Define an attribute reader method. Cope with nil column.
146 def define_read_method(symbol, attr_name, column)
147 cast_code = column.type_cast_code('v') if column
148 access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
149
150 unless attr_name.to_s == self.primary_key.to_s
151 access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
152 end
153
154 if cache_attribute?(attr_name)
155 access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
156 end
157 evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
158 end
159
160 # Define read method for serialized attribute.
161 def define_read_method_for_serialized_attribute(attr_name)
162 evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
163 end
164
165 # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
166 # This enhanced read method automatically converts the UTC time stored in the database to the time zone stored in Time.zone.
167 def define_read_method_for_time_zone_conversion(attr_name)
168 method_body = <<-EOV
169 def #{attr_name}(reload = false)
170 cached = @attributes_cache['#{attr_name}']
171 return cached if cached && !reload
172 time = read_attribute('#{attr_name}')
173 @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
174 end
175 EOV
176 evaluate_attribute_method attr_name, method_body
177 end
178
179 # Defines a predicate method <tt>attr_name?</tt>.
180 def define_question_method(attr_name)
181 evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
182 end
183
184 def define_write_method(attr_name)
185 evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
186 end
187
188 # Defined for all +datetime+ and +timestamp+ attributes when +time_zone_aware_attributes+ are enabled.
189 # This enhanced write method will automatically convert the time passed to it to the zone stored in Time.zone.
190 def define_write_method_for_time_zone_conversion(attr_name)
191 method_body = <<-EOV
192 def #{attr_name}=(time)
193 unless time.acts_like?(:time)
194 time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
195 end
196 time = time.in_time_zone rescue nil if time
197 write_attribute(:#{attr_name}, time)
198 end
199 EOV
200 evaluate_attribute_method attr_name, method_body, "#{attr_name}="
201 end
202
203 # Evaluate the definition for an attribute related method
204 def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
205
206 unless method_name.to_s == primary_key.to_s
207 generated_methods << method_name
208 end
209
210 begin
211 class_eval(method_definition, __FILE__, __LINE__)
212 rescue SyntaxError => err
213 generated_methods.delete(attr_name)
214 if logger
215 logger.warn "Exception occurred during reader method compilation."
216 logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
217 logger.warn err.message
218 end
219 end
220 end
221 end # ClassMethods
222
223
224 # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as though they
225 # were first-class methods. So a Person class with a name attribute can use Person#name and
226 # Person#name= and never directly use the attributes hash -- except for multiple assigns with
227 # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
228 # the completed attribute is not +nil+ or 0.
229 #
230 # It's also possible to instantiate related objects, so a Client class belonging to the clients
231 # table with a +master_id+ foreign key can instantiate master through Client#master.
232 def method_missing(method_id, *args, &block)
233 method_name = method_id.to_s
234
235 if self.class.private_method_defined?(method_name)
236 raise NoMethodError.new("Attempt to call private method", method_name, args)
237 end
238
239 # If we haven't generated any methods yet, generate them, then
240 # see if we've created the method we're looking for.
241 if !self.class.generated_methods?
242 self.class.define_attribute_methods
243 if self.class.generated_methods.include?(method_name)
244 return self.send(method_id, *args, &block)
245 end
246 end
247
248 if self.class.primary_key.to_s == method_name
249 id
250 elsif md = self.class.match_attribute_method?(method_name)
251 attribute_name, method_type = md.pre_match, md.to_s
252 if @attributes.include?(attribute_name)
253 __send__("attribute#{method_type}", attribute_name, *args, &block)
254 else
255 super
256 end
257 elsif @attributes.include?(method_name)
258 read_attribute(method_name)
259 else
260 super
261 end
262 end
263
264 # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
265 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
266 def read_attribute(attr_name)
267 attr_name = attr_name.to_s
268 if !(value = @attributes[attr_name]).nil?
269 if column = column_for_attribute(attr_name)
270 if unserializable_attribute?(attr_name, column)
271 unserialize_attribute(attr_name)
272 else
273 column.type_cast(value)
274 end
275 else
276 value
277 end
278 else
279 nil
280 end
281 end
282
283 def read_attribute_before_type_cast(attr_name)
284 @attributes[attr_name]
285 end
286
287 # Returns true if the attribute is of a text column and marked for serialization.
288 def unserializable_attribute?(attr_name, column)
289 column.text? && self.class.serialized_attributes[attr_name]
290 end
291
292 # Returns the unserialized object of the attribute.
293 def unserialize_attribute(attr_name)
294 unserialized_object = object_from_yaml(@attributes[attr_name])
295
296 if unserialized_object.is_a?(self.class.serialized_attributes[attr_name]) || unserialized_object.nil?
297 @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
298 else
299 raise SerializationTypeMismatch,
300 "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
301 end
302 end
303
304
305 # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
306 # columns are turned into +nil+.
307 def write_attribute(attr_name, value)
308 attr_name = attr_name.to_s
309 @attributes_cache.delete(attr_name)
310 if (column = column_for_attribute(attr_name)) && column.number?
311 @attributes[attr_name] = convert_number_column_value(value)
312 else
313 @attributes[attr_name] = value
314 end
315 end
316
317
318 def query_attribute(attr_name)
319 unless value = read_attribute(attr_name)
320 false
321 else
322 column = self.class.columns_hash[attr_name]
323 if column.nil?
324 if Numeric === value || value !~ /[^0-9]/
325 !value.to_i.zero?
326 else
327 !value.blank?
328 end
329 elsif column.number?
330 !value.zero?
331 else
332 !value.blank?
333 end
334 end
335 end
336
337 # A Person object with a name attribute can ask <tt>person.respond_to?(:name)</tt>,
338 # <tt>person.respond_to?(:name=)</tt>, and <tt>person.respond_to?(:name?)</tt>
339 # which will all return +true+.
340 alias :respond_to_without_attributes? :respond_to?
341 def respond_to?(method, include_private_methods = false)
342 method_name = method.to_s
343 if super
344 return true
345 elsif !include_private_methods && super(method, true)
346 # If we're here than we haven't found among non-private methods
347 # but found among all methods. Which means that given method is private.
348 return false
349 elsif !self.class.generated_methods?
350 self.class.define_attribute_methods
351 if self.class.generated_methods.include?(method_name)
352 return true
353 end
354 end
355
356 if @attributes.nil?
357 return super
358 elsif @attributes.include?(method_name)
359 return true
360 elsif md = self.class.match_attribute_method?(method_name)
361 return true if @attributes.include?(md.pre_match)
362 end
363 super
364 end
365
366 private
367
368 def missing_attribute(attr_name, stack)
369 raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
370 end
371
372 # Handle *? for method_missing.
373 def attribute?(attribute_name)
374 query_attribute(attribute_name)
375 end
376
377 # Handle *= for method_missing.
378 def attribute=(attribute_name, value)
379 write_attribute(attribute_name, value)
380 end
381
382 # Handle *_before_type_cast for method_missing.
383 def attribute_before_type_cast(attribute_name)
384 read_attribute_before_type_cast(attribute_name)
385 end
386 end
387 end