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.
- data/README.rdoc +113 -0
- data/VERSION.yml +4 -0
- data/lib/boolean.rb +3 -0
- data/lib/couch_foo/associations/association_collection.rb +346 -0
- data/lib/couch_foo/associations/association_proxy.rb +204 -0
- data/lib/couch_foo/associations/belongs_to_association.rb +57 -0
- data/lib/couch_foo/associations/belongs_to_polymorphic_association.rb +48 -0
- data/lib/couch_foo/associations/has_and_belongs_to_many_association.rb +111 -0
- data/lib/couch_foo/associations/has_many_association.rb +97 -0
- data/lib/couch_foo/associations/has_one_association.rb +95 -0
- data/lib/couch_foo/associations.rb +1118 -0
- data/lib/couch_foo/attribute_methods.rb +316 -0
- data/lib/couch_foo/base.rb +2117 -0
- data/lib/couch_foo/calculations.rb +117 -0
- data/lib/couch_foo/callbacks.rb +311 -0
- data/lib/couch_foo/database.rb +157 -0
- data/lib/couch_foo/dirty.rb +142 -0
- data/lib/couch_foo/named_scope.rb +168 -0
- data/lib/couch_foo/observer.rb +195 -0
- data/lib/couch_foo/reflection.rb +239 -0
- data/lib/couch_foo/timestamp.rb +41 -0
- data/lib/couch_foo/validations.rb +927 -0
- data/lib/couch_foo/view_methods.rb +234 -0
- data/lib/couch_foo.rb +43 -0
- data/test/couch_foo_test.rb +7 -0
- data/test/test_helper.rb +10 -0
- metadata +116 -0
@@ -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
|