Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with
or
.
Download ZIP
Newer
Older
100644 368 lines (319 sloc) 14.412 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)
4db718e @NZKoz Only cache attributes which need it for performance reasons. Closes #…
NZKoz authored
4 ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [:datetime, :timestamp, :time, :date]
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
5
6 def self.included(base)
7 base.extend ClassMethods
8b5f4e4 @jeremy Ruby 1.9 compat: fix warnings, shadowed block vars, and unitialized i…
jeremy authored
8 base.attribute_method_suffix(*DEFAULT_SUFFIXES)
4db718e @NZKoz Only cache attributes which need it for performance reasons. Closes #…
NZKoz authored
9 base.cattr_accessor :attribute_types_cached_by_default, :instance_writer => false
10 base.attribute_types_cached_by_default = ATTRIBUTE_TYPES_CACHED_BY_DEFAULT
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
11 base.cattr_accessor :time_zone_aware_attributes, :instance_writer => false
12 base.time_zone_aware_attributes = false
13 base.cattr_accessor :skip_time_zone_conversion_for_attributes, :instance_writer => false
14 base.skip_time_zone_conversion_for_attributes = []
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
15 end
16
17 # Declare and check for suffixed attribute methods.
18 module ClassMethods
19 # Declare a method available for all attributes with the given suffix.
20 # Uses method_missing and respond_to? to rewrite the method
21 # #{attr}#{suffix}(*args, &block)
22 # to
23 # attribute#{suffix}(#{attr}, *args, &block)
24 #
25 # An attribute#{suffix} instance method must exist and accept at least
26 # the attr argument.
27 #
28 # For example:
29 # class Person < ActiveRecord::Base
30 # attribute_method_suffix '_changed?'
31 #
32 # private
33 # def attribute_changed?(attr)
34 # ...
35 # end
36 # end
37 #
38 # person = Person.find(1)
39 # person.name_changed? # => false
40 # person.name = 'Hubert'
41 # person.name_changed? # => true
42 def attribute_method_suffix(*suffixes)
43 attribute_method_suffixes.concat suffixes
44 rebuild_attribute_method_regexp
45 end
46
47 # Returns MatchData if method_name is an attribute method.
48 def match_attribute_method?(method_name)
49 rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
50 @@attribute_method_regexp.match(method_name)
51 end
52
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
53
54 # Contains the names of the generated attribute methods.
55 def generated_methods #:nodoc:
56 @generated_methods ||= Set.new
57 end
58
59 def generated_methods?
60 !generated_methods.empty?
61 end
62
63 # generates all the attribute related methods for columns in the database
64 # accessors, mutators and query methods
65 def define_attribute_methods
66 return if generated_methods?
67 columns_hash.each do |name, column|
b31aa63 Allow column accessors to be created even if Kernel. or Object# metho…
Tobias Lütke authored
68 unless instance_method_already_implemented?(name)
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
69 if self.serialized_attributes[name]
70 define_read_method_for_serialized_attribute(name)
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
71 elsif create_time_zone_conversion_attribute?(name, column)
72 define_read_method_for_time_zone_conversion(name)
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
73 else
74 define_read_method(name.to_sym, name, column)
75 end
76 end
77
b31aa63 Allow column accessors to be created even if Kernel. or Object# metho…
Tobias Lütke authored
78 unless instance_method_already_implemented?("#{name}=")
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
79 if create_time_zone_conversion_attribute?(name, column)
80 define_write_method_for_time_zone_conversion(name)
81 else
82 define_write_method(name.to_sym)
83 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
84 end
85
b31aa63 Allow column accessors to be created even if Kernel. or Object# metho…
Tobias Lütke authored
86 unless instance_method_already_implemented?("#{name}?")
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
87 define_question_method(name)
88 end
89 end
90 end
acbec3e @NZKoz Ensure that custom mutators aren't redefined by define_attribute_meth…
NZKoz authored
91
2af36bb @dhh Fix typos (closes #10378)
dhh authored
92 # Check to see if the method is defined in the model or any of its subclasses that also derive from ActiveRecord.
5b2e8b1 @technoweenie Fix that ActiveRecord would create attribute methods and override cus…
technoweenie authored
93 # Raise DangerousAttributeError if the method is defined by ActiveRecord though.
b31aa63 Allow column accessors to be created even if Kernel. or Object# metho…
Tobias Lütke authored
94 def instance_method_already_implemented?(method_name)
240b4c5 @jeremy Ruby 1.9 compat: attribute methods
jeremy authored
95 method_name = method_name.to_s
5b2e8b1 @technoweenie Fix that ActiveRecord would create attribute methods and override cus…
technoweenie authored
96 return true if method_name =~ /^id(=$|\?$|$)/
240b4c5 @jeremy Ruby 1.9 compat: attribute methods
jeremy authored
97 @_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
98 @@_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
5b2e8b1 @technoweenie Fix that ActiveRecord would create attribute methods and override cus…
technoweenie authored
99 raise DangerousAttributeError, "#{method_name} is defined by ActiveRecord" if @@_defined_activerecord_methods.include?(method_name)
100 @_defined_class_methods.include?(method_name)
b31aa63 Allow column accessors to be created even if Kernel. or Object# metho…
Tobias Lütke authored
101 end
102
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
103 alias :define_read_methods :define_attribute_methods
104
4db718e @NZKoz Only cache attributes which need it for performance reasons. Closes #…
NZKoz authored
105 # +cache_attributes+ allows you to declare which converted attribute values should
106 # be cached. Usually caching only pays off for attributes with expensive conversion
107 # methods, like date columns (e.g. created_at, updated_at).
108 def cache_attributes(*attribute_names)
109 attribute_names.each {|attr| cached_attributes << attr.to_s}
110 end
111
112 # returns the attributes where
113 def cached_attributes
114 @cached_attributes ||=
115 columns.select{|c| attribute_types_cached_by_default.include?(c.type)}.map(&:name).to_set
116 end
117
118 def cache_attribute?(attr_name)
119 cached_attributes.include?(attr_name)
120 end
121
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
122 private
123 # Suffixes a, ?, c become regexp /(a|\?|c)$/
124 def rebuild_attribute_method_regexp
125 suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
126 @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
127 end
128
129 # Default to =, ?, _before_type_cast
130 def attribute_method_suffixes
131 @@attribute_method_suffixes ||= []
132 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
133
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
134 def create_time_zone_conversion_attribute?(name, column)
135 time_zone_aware_attributes && !skip_time_zone_conversion_for_attributes.include?(name.to_sym) && [:datetime, :timestamp].include?(column.type)
136 end
137
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
138 # Define an attribute reader method. Cope with nil column.
139 def define_read_method(symbol, attr_name, column)
140 cast_code = column.type_cast_code('v') if column
141 access_code = cast_code ? "(v=@attributes['#{attr_name}']) && #{cast_code}" : "@attributes['#{attr_name}']"
142
143 unless attr_name.to_s == self.primary_key.to_s
144 access_code = access_code.insert(0, "missing_attribute('#{attr_name}', caller) unless @attributes.has_key?('#{attr_name}'); ")
145 end
146
4db718e @NZKoz Only cache attributes which need it for performance reasons. Closes #…
NZKoz authored
147 if cache_attribute?(attr_name)
148 access_code = "@attributes_cache['#{attr_name}'] ||= (#{access_code})"
149 end
150 evaluate_attribute_method attr_name, "def #{symbol}; #{access_code}; end"
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
151 end
152
153 # Define read method for serialized attribute.
154 def define_read_method_for_serialized_attribute(attr_name)
155 evaluate_attribute_method attr_name, "def #{attr_name}; unserialize_attribute('#{attr_name}'); end"
156 end
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
157
158 def define_read_method_for_time_zone_conversion(attr_name)
159 method_body = <<-EOV
160 def #{attr_name}(reload = false)
161 cached = @attributes_cache['#{attr_name}']
162 return cached if cached && !reload
163 time = read_attribute('#{attr_name}')
54ccdd3 @gbuesing Time, DateTime and TimeWithZone #in_time_zone defaults to Time.zone. …
gbuesing authored
164 @attributes_cache['#{attr_name}'] = time.acts_like?(:time) ? time.in_time_zone : time
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
165 end
166 EOV
167 evaluate_attribute_method attr_name, method_body
168 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
169
170 # Define an attribute ? method.
171 def define_question_method(attr_name)
172 evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
173 end
174
175 def define_write_method(attr_name)
176 evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
177 end
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
178
179 def define_write_method_for_time_zone_conversion(attr_name)
180 method_body = <<-EOV
181 def #{attr_name}=(time)
06a7c29 @gbuesing Time.zone.parse: return nil for strings with no date information
gbuesing authored
182 unless time.acts_like?(:time)
328fada @gbuesing ActiveRecord time zone aware attributes: blank string is treated as n…
gbuesing authored
183 time = time.is_a?(String) ? Time.zone.parse(time) : time.to_time rescue time
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
184 end
328fada @gbuesing ActiveRecord time zone aware attributes: blank string is treated as n…
gbuesing authored
185 time = time.in_time_zone rescue nil if time
72385a7 @technoweenie Add Time Zone support to ActiveRecord, and config.time_zone property …
technoweenie authored
186 write_attribute(:#{attr_name}, time)
187 end
188 EOV
189 evaluate_attribute_method attr_name, method_body, "#{attr_name}="
190 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
191
192 # Evaluate the definition for an attribute related method
193 def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
194
195 unless method_name.to_s == primary_key.to_s
196 generated_methods << method_name
197 end
198
199 begin
cff25aa @jeremy eval with __FILE__ and __LINE__
jeremy authored
200 class_eval(method_definition, __FILE__, __LINE__)
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
201 rescue SyntaxError => err
202 generated_methods.delete(attr_name)
203 if logger
204 logger.warn "Exception occurred during reader method compilation."
205 logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
206 logger.warn "#{err.message}"
207 end
208 end
209 end
210 end # ClassMethods
211
212
0faa4ca @dhh Doc fix (closes #9323) [Henrik N]
dhh authored
213 # Allows access to the object attributes, which are held in the @attributes hash, as though they
214 # were first-class methods. So a Person class with a name attribute can use Person#name and
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
215 # Person#name= and never directly use the attributes hash -- except for multiple assigns with
216 # ActiveRecord#attributes=. A Milestone class can also ask Milestone#completed? to test that
217 # the completed attribute is not nil or 0.
218 #
219 # It's also possible to instantiate related objects, so a Client class belonging to the clients
220 # table with a master_id foreign key can instantiate master through Client#master.
221 def method_missing(method_id, *args, &block)
222 method_name = method_id.to_s
223
224 # If we haven't generated any methods yet, generate them, then
225 # see if we've created the method we're looking for.
226 if !self.class.generated_methods?
227 self.class.define_attribute_methods
228 if self.class.generated_methods.include?(method_name)
229 return self.send(method_id, *args, &block)
230 end
231 end
232
233 if self.class.primary_key.to_s == method_name
234 id
235 elsif md = self.class.match_attribute_method?(method_name)
236 attribute_name, method_type = md.pre_match, md.to_s
237 if @attributes.include?(attribute_name)
238 __send__("attribute#{method_type}", attribute_name, *args, &block)
239 else
240 super
241 end
242 elsif @attributes.include?(method_name)
243 read_attribute(method_name)
244 else
245 super
246 end
247 end
248
249 # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
250 # "2004-12-12" in a data column is cast to a date object, like Date.new(2004, 12, 12)).
251 def read_attribute(attr_name)
252 attr_name = attr_name.to_s
253 if !(value = @attributes[attr_name]).nil?
254 if column = column_for_attribute(attr_name)
255 if unserializable_attribute?(attr_name, column)
256 unserialize_attribute(attr_name)
257 else
258 column.type_cast(value)
259 end
260 else
261 value
262 end
263 else
264 nil
265 end
266 end
267
268 def read_attribute_before_type_cast(attr_name)
269 @attributes[attr_name]
270 end
271
272 # Returns true if the attribute is of a text column and marked for serialization.
273 def unserializable_attribute?(attr_name, column)
274 column.text? && self.class.serialized_attributes[attr_name]
275 end
276
277 # Returns the unserialized object of the attribute.
278 def unserialize_attribute(attr_name)
279 unserialized_object = object_from_yaml(@attributes[attr_name])
280
281 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
282 @attributes.frozen? ? unserialized_object : @attributes[attr_name] = unserialized_object
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
283 else
284 raise SerializationTypeMismatch,
285 "#{attr_name} was supposed to be a #{self.class.serialized_attributes[attr_name]}, but was a #{unserialized_object.class.to_s}"
286 end
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
287 end
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
288
289
290 # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
291 # columns are turned into nil.
292 def write_attribute(attr_name, value)
293 attr_name = attr_name.to_s
294 @attributes_cache.delete(attr_name)
295 if (column = column_for_attribute(attr_name)) && column.number?
296 @attributes[attr_name] = convert_number_column_value(value)
297 else
298 @attributes[attr_name] = value
299 end
300 end
301
302
303 def query_attribute(attr_name)
304 unless value = read_attribute(attr_name)
305 false
306 else
307 column = self.class.columns_hash[attr_name]
308 if column.nil?
309 if Numeric === value || value !~ /[^0-9]/
310 !value.to_i.zero?
311 else
312 !value.blank?
313 end
314 elsif column.number?
315 !value.zero?
316 else
317 !value.blank?
318 end
319 end
320 end
321
322 # A Person object with a name attribute can ask person.respond_to?("name"), person.respond_to?("name="), and
323 # person.respond_to?("name?") which will all return true.
324 alias :respond_to_without_attributes? :respond_to?
325 def respond_to?(method, include_priv = false)
326 method_name = method.to_s
327 if super
328 return true
329 elsif !self.class.generated_methods?
330 self.class.define_attribute_methods
331 if self.class.generated_methods.include?(method_name)
332 return true
333 end
334 end
335
336 if @attributes.nil?
337 return super
338 elsif @attributes.include?(method_name)
339 return true
340 elsif md = self.class.match_attribute_method?(method_name)
341 return true if @attributes.include?(md.pre_match)
342 end
343 super
344 end
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
345
346 private
5b801b5 @NZKoz Change the implementation of ActiveRecord's attribute reader and writ…
NZKoz authored
347
348 def missing_attribute(attr_name, stack)
349 raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
350 end
351
2b3cc24 @jeremy r4854@ks: jeremy | 2006-07-30 00:59:18 -0700
jeremy authored
352 # Handle *? for method_missing.
353 def attribute?(attribute_name)
354 query_attribute(attribute_name)
355 end
356
357 # Handle *= for method_missing.
358 def attribute=(attribute_name, value)
359 write_attribute(attribute_name, value)
360 end
361
362 # Handle *_before_type_cast for method_missing.
363 def attribute_before_type_cast(attribute_name)
364 read_attribute_before_type_cast(attribute_name)
365 end
366 end
367 end
Something went wrong with that request. Please try again.