georgepalmer-couch_foo 0.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,316 @@
1
+ module CouchFoo
2
+ module AttributeMethods
3
+ DEFAULT_SUFFIXES = %w(= ? _before_type_cast)
4
+ ATTRIBUTE_TYPES_CACHED_BY_DEFAULT = [Time, DateTime, 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
+ end
12
+
13
+ module ClassMethods
14
+ # Declares a method available for all attributes with the given suffix.
15
+ # Uses +method_missing+ and <tt>respond_to?</tt> to rewrite the method
16
+ #
17
+ # #{attr}#{suffix}(*args, &block)
18
+ #
19
+ # to
20
+ #
21
+ # attribute#{suffix}(#{attr}, *args, &block)
22
+ #
23
+ # An <tt>attribute#{suffix}</tt> instance method must exist and accept at least
24
+ # the +attr+ argument.
25
+ #
26
+ # For example:
27
+ #
28
+ # class Person < ActiveRecord::Base
29
+ # attribute_method_suffix '_changed?'
30
+ #
31
+ # private
32
+ # def attribute_changed?(attr)
33
+ # ...
34
+ # end
35
+ # end
36
+ #
37
+ # person = Person.find(1)
38
+ # person.name_changed? # => false
39
+ # person.name = 'Hubert'
40
+ # person.name_changed? # => true
41
+ def attribute_method_suffix(*suffixes)
42
+ attribute_method_suffixes.concat suffixes
43
+ rebuild_attribute_method_regexp
44
+ end
45
+
46
+ # Returns MatchData if method_name is an attribute method.
47
+ def match_attribute_method?(method_name)
48
+ rebuild_attribute_method_regexp unless defined?(@@attribute_method_regexp) && @@attribute_method_regexp
49
+ @@attribute_method_regexp.match(method_name)
50
+ end
51
+
52
+ # Contains the names of the generated attribute methods.
53
+ def generated_methods #:nodoc:
54
+ @generated_methods ||= Set.new
55
+ end
56
+
57
+ def generated_methods?
58
+ !generated_methods.empty?
59
+ end
60
+
61
+ # Generates accessors, mutators and query methods for registered properties
62
+ def define_attribute_methods
63
+ return if generated_methods?
64
+ property_names.each do |name|
65
+ unless instance_method_already_implemented?(name)
66
+ define_read_method(name.to_sym)
67
+ end
68
+ unless instance_method_already_implemented?("#{name}=")
69
+ define_write_method(name.to_sym)
70
+ end
71
+ unless instance_method_already_implemented?("#{name}?")
72
+ define_question_method(name)
73
+ end
74
+ end
75
+ end
76
+ alias :define_read_methods :define_attribute_methods
77
+
78
+ # Checks whether the method is defined in the model or any of its subclasses
79
+ # that also derive from Couch Foo. Raises DangerousAttributeError if the
80
+ # method is defined by Couch Foo.
81
+ def instance_method_already_implemented?(method_name)
82
+ method_name = method_name.to_s
83
+ return true if method_name =~ /^id(=$|\?$|$)/
84
+ @_defined_class_methods ||= ancestors.first(ancestors.index(CouchFoo::Base)).sum([]) { |m| m.public_instance_methods(false) | m.private_instance_methods(false) | m.protected_instance_methods(false) }.map(&:to_s).to_set
85
+ @@_defined_couchfoo_methods ||= (CouchFoo::Base.public_instance_methods(false) | CouchFoo::Base.private_instance_methods(false) | CouchFoo::Base.protected_instance_methods(false)).map(&:to_s).to_set
86
+ raise DangerousAttributeError, "#{method_name} is defined by CouchFoo" if @@_defined_couchfoo_methods.include?(method_name)
87
+ @_defined_class_methods.include?(method_name)
88
+ end
89
+
90
+ # +cache_attributes+ allows you to declare which converted attribute values should
91
+ # be cached. Usually caching only pays off for attributes with expensive conversion
92
+ # methods, like time related columns (e.g. +created_at+, +updated_at+).
93
+ def cache_attributes(*attribute_names)
94
+ attribute_names.each {|attr| cached_attributes << attr.to_s}
95
+ end
96
+
97
+ # Returns the attributes which are cached. By default time related columns
98
+ # with datatype <tt>:datetime, :timestamp, :time, :date</tt> are cached.
99
+ def cached_attributes
100
+ @cached_attributes ||=
101
+ property_types.select{|k,v| attribute_types_cached_by_default.include?(v)}.map{|e| e.first.to_s}.to_set
102
+ end
103
+
104
+ # Returns +true+ if the provided attribute is being cached.
105
+ def cache_attribute?(attr_name)
106
+ cached_attributes.include?(attr_name)
107
+ end
108
+
109
+ private
110
+ # Suffixes a, ?, c become regexp /(a|\?|c)$/
111
+ def rebuild_attribute_method_regexp
112
+ suffixes = attribute_method_suffixes.map { |s| Regexp.escape(s) }
113
+ @@attribute_method_regexp = /(#{suffixes.join('|')})$/.freeze
114
+ end
115
+
116
+ # Default to =, ?, _before_type_cast
117
+ def attribute_method_suffixes
118
+ @@attribute_method_suffixes ||= []
119
+ end
120
+
121
+ # Define an attribute reader method
122
+ def define_read_method(symbol)
123
+ access_code = "@attributes['#{symbol}']"
124
+ evaluate_attribute_method symbol.to_s, "def #{symbol}; read_attribute('#{access_code}'); end"
125
+ end
126
+
127
+ # Defines a predicate method <tt>attr_name?</tt>
128
+ def define_question_method(attr_name)
129
+ evaluate_attribute_method attr_name, "def #{attr_name}?; query_attribute('#{attr_name}'); end", "#{attr_name}?"
130
+ end
131
+
132
+ # Defines an attribute writer method
133
+ def define_write_method(attr_name)
134
+ evaluate_attribute_method attr_name, "def #{attr_name}=(new_value);write_attribute('#{attr_name}', new_value);end", "#{attr_name}="
135
+ end
136
+
137
+ # Evaluate the definition for an attribute related method
138
+ def evaluate_attribute_method(attr_name, method_definition, method_name=attr_name)
139
+ unless unchangeable_property_names.include?(attr_name.to_sym)
140
+ generated_methods << method_name
141
+ end
142
+
143
+ begin
144
+ class_eval(method_definition, __FILE__, __LINE__)
145
+ rescue SyntaxError => err
146
+ generated_methods.delete(attr_name)
147
+ if logger
148
+ logger.warn "Exception occurred during reader method compilation."
149
+ logger.warn "Maybe #{attr_name} is not a valid Ruby identifier?"
150
+ logger.warn "#{err.message}"
151
+ end
152
+ end
153
+ end
154
+ end # ClassMethods
155
+
156
+ # Allows access to the object attributes, which are held in the <tt>@attributes</tt> hash, as
157
+ # though they were first-class methods. So a Person class with a name attribute can use
158
+ # Person#name and Person#name= and never directly use the attributes hash -- except for multiple
159
+ # assigns with CouchFoo#attributes=. A Milestone class can also ask Milestone#completed? to
160
+ # test that the completed attribute is not +nil+ or 0.
161
+ #
162
+ # It's also possible to instantiate related objects, so a Client class belonging to the clients
163
+ # table with a +master_id+ foreign key can instantiate master through Client#master.
164
+ def method_missing(method_id, *args, &block)
165
+ method_name = method_id.to_s
166
+
167
+ # Make sure methods are generated
168
+ if !self.class.generated_methods?
169
+ self.class.define_attribute_methods
170
+ if self.class.generated_methods.include?(method_name)
171
+ return self.send(method_id, *args, &block)
172
+ end
173
+ end
174
+
175
+ # Unchangeable properties are called directly, not through generated methods
176
+ if self.class.unchangeable_property_names.include?(method_id)
177
+ send(method_id, *args, &block)
178
+ elsif md = self.class.match_attribute_method?(method_name)
179
+ attribute_name, method_type = md.pre_match, md.to_s
180
+ if @attributes.include?(attribute_name)
181
+ __send__("attribute#{method_type}", attribute_name, *args, &block)
182
+ else
183
+ super
184
+ end
185
+ elsif attributes.include?(method_name)
186
+ read_attribute(method_name)
187
+ else
188
+ super
189
+ end
190
+ end
191
+
192
+ # Returns the value of the attribute identified by <tt>attr_name</tt> after it has been typecast (for example,
193
+ # "2004-12-12" in a data type is cast to a date object, like Date.new(2004, 12, 12)).
194
+ def read_attribute(attr_name)
195
+ if !(value = @attributes[attr_name.to_s]).nil?
196
+ if type = type_for_property(attr_name.to_sym)
197
+ convert_to_type(value, type)
198
+ else
199
+ value
200
+ end
201
+ else
202
+ nil
203
+ end
204
+ end
205
+
206
+ def read_attribute_before_type_cast(attr_name)
207
+ @attributes[attr_name]
208
+ end
209
+
210
+ # Updates the attribute identified by <tt>attr_name</tt> with the specified +value+. Empty strings for fixnum and float
211
+ # types are turned into +nil+.
212
+ def write_attribute(attr_name, value)
213
+ attr_name = attr_name.to_s
214
+ @attributes_cache.delete(attr_name)
215
+ @attributes[attr_name] = value
216
+ end
217
+
218
+ def query_attribute(attr_name)
219
+ unless value = read_attribute(attr_name)
220
+ false
221
+ else
222
+ column_type = type_for_property(attr_name)
223
+ if column_type.nil?
224
+ if Numeric === value || value !~ /[^0-9]/
225
+ !value.to_i.zero?
226
+ else
227
+ !value.blank?
228
+ end
229
+ elsif column_type == Integer || column_type == Float
230
+ !value.zero?
231
+ else
232
+ !value.blank?
233
+ end
234
+ end
235
+ end
236
+
237
+ # A Person object with a name attribute can ask <tt>person.respond_to?("name")</tt>,
238
+ # <tt>person.respond_to?("name=")</tt>, and <tt>person.respond_to?("name?")</tt>
239
+ # which will all return +true+.
240
+ alias :respond_to_without_attributes? :respond_to?
241
+ def respond_to?(method, include_priv = false)
242
+ method_name = method.to_s
243
+ if super
244
+ return true
245
+ elsif !self.class.generated_methods?
246
+ self.class.define_attribute_methods
247
+ if self.class.generated_methods.include?(method_name)
248
+ return true
249
+ end
250
+ end
251
+
252
+ if @attributes.nil?
253
+ return super
254
+ elsif @attributes.include?(method_name)
255
+ return true
256
+ elsif md = self.class.match_attribute_method?(method_name)
257
+ return true if @attributes.include?(md.pre_match)
258
+ end
259
+ super
260
+ end
261
+
262
+ protected
263
+ # Converts a value to its type, or if not specified tries calling to_json on the value before
264
+ # falling back on just using the value
265
+ def convert_to_type(value, type)
266
+ return nil if value.nil?
267
+
268
+ #Not keen on type hack for case statement
269
+ case type.to_s
270
+ when "String"
271
+ value
272
+ when "Integer"
273
+ value.to_i
274
+ when "Float"
275
+ value.to_f
276
+ when "DateTime"
277
+ DateTime.parse(value.to_s)
278
+ when "Time"
279
+ Time.at(value.to_f)
280
+ when "Date"
281
+ Date.new(value.year, value.month, value.day)
282
+ when "TrueClass"
283
+ convert_boolean(value)
284
+ when "Boolean"
285
+ convert_boolean(value)
286
+ else
287
+ value.to_json rescue value
288
+ end
289
+ end
290
+
291
+ private
292
+ def convert_boolean(value)
293
+ return false if value.nil? || value == "0" || value == 0 # Bit of a hack but keeps AR compatability
294
+ true & value
295
+ end
296
+
297
+ def missing_attribute(attr_name, stack)
298
+ raise ActiveRecord::MissingAttributeError, "missing attribute: #{attr_name}", stack
299
+ end
300
+
301
+ # Handle *? for method_missing.
302
+ def attribute?(attribute_name)
303
+ query_attribute(attribute_name)
304
+ end
305
+
306
+ # Handle *= for method_missing.
307
+ def attribute=(attribute_name, value)
308
+ write_attribute(attribute_name, value)
309
+ end
310
+
311
+ # Handle *_before_type_cast for method_missing.
312
+ def attribute_before_type_cast(attribute_name)
313
+ read_attribute_before_type_cast(attribute_name)
314
+ end
315
+ end
316
+ end