Blob of lib/couch_object/persistable/meta_classes.rb (raw blob data)

1 module CouchObject
2 module Persistable
3 def self.included(klazz)
4 klazz.extend(ClassMethods)
5
6 #
7 # Using meta programming methods for handling, amongst others,
8 # * setting of the database uri at design time
9 # * including timestamps
10 # * managing relations
11 # are created
12 #
13 klazz.class_eval do
14
15 ##
16 # Timestamps
17 ##
18
19 # Timestamps are false by default
20 def self.couch_object_timestamp_on_update?; false; end
21 def self.couch_object_timestamp_on_create?; false; end
22
23 #
24 # Adds timestamps to the class.
25 #
26 # Example:
27 #
28 # class Vacation
29 # include CouchObject::Persistable
30 # add_timestamp_for :on_create, :on_update
31 # end
32 #
33 # my_vacation = Vacation.new
34 # my_vacation.save(db_address)
35 # my_vacation.created_at => Somedate
36 # my_vacation.updated_at => Somedate
37 #
38 def self.add_timestamp_for(*timestamp_actions)
39 timestamp_actions.each do |action|
40 case action
41 when :on_create
42 self.class_eval do
43 def self.couch_object_timestamp_on_create?; true; end
44 end
45 when :on_update
46 self.class_eval do
47 def self.couch_object_timestamp_on_update?; true; end
48 end
49 end
50 end
51 end
52
53
54
55 ##
56 # Change monitor
57 ##
58
59 # to be implemented later.
60 # could be implemented using MonitorFunctions
61 # (http://www.erikveen.dds.nl/monitorfunctions/)
62
63 # Each class monitors it's setters to see if it's content is changed,
64 # in which case a flag is set.
65 # For this purpose all setters are overridden
66
67 # self.instance_variable_set("@couch_has_unsaved_changes_flag", false)
68 # def unsaved_changes?
69 # @couch_has_unsaved_changes_flag
70 # end
71 # puts "Public setters:"
72 # self.public_methods.each do |method|
73 # if method.to_s[-1,1] == "="
74 # # We have to create an alias for the original method
75 # DO MAGIC HERE
76 # end
77 # end
78
79
80 ##
81 # Database storage location
82 ##
83
84 # Location methods are added both as instance methods and
85 # as class level methods. The class level methods are needed
86 # when loading new objects from the database and the instance
87 # methods are used throughout the class
88 def self.location; @couch_object_class_storage_location ||= nil; end
89 def location; @location; end
90 alias storage_location location
91
92 #
93 # Sets the location of the database to use by default
94 #
95 # Example:
96 #
97 # class AppleTree
98 # include CouchObject::Persistable
99 # database 'http://localhost:5984'
100 # end
101 #
102 # apple_tree = AppleTree.new
103 # apple_tree.save # saves automatically to the predefined
104 # # database location
105 #
106 def self.database(db_uri)
107 @couch_object_class_storage_location = db_uri
108 self.instance_eval do
109 define_method("location") do
110 @location ||= db_uri
111 end
112 end
113 end
114
115
116
117 ##
118 # Smart savign
119 ##
120
121 def use_smart_save; false; end
122 #
123 # Smart save (defaults to false), if activated, keeps a snapshot of
124 # the objects initial state and evaluates if the class needs to be
125 # saved to the database by comparing it to the snapshot when a save
126 # is requested.
127 #
128 # Please notice:
129 # Only activate this feature in cases where it is needed.
130 # It might slow down the performance of your app if you activate it
131 # for classes that you need many instances of and that you won't
132 # call the save method on after having loaded them from the database.
133 # Please also bare in mind that the class instance will store an
134 # extra copy of its contents which will lead to quite a big memory
135 # overhead for classes that store a lot of data!
136 #
137 def self.smart_save
138 self.instance_eval do
139 define_method("use_smart_save") do
140 true
141 end
142 end
143 end
144 #
145 # Smart save can also be used on a per-case basis if it is sometimes
146 # needed and sometimes not.
147 #
148 # Example:
149 #
150 # user_without_smart_save_1 = User.get("foo")
151 # User.smart_save
152 # user_with_smart_save = User.get("bar")
153 # User.deactivate_smart_save
154 # user_without_smart_save_2 = User.get("bong")
155 #
156 def self.deactivate_smart_save
157 self.instance_eval do
158 define_method("use_smart_save") do
159 false
160 end
161 end
162 end
163
164
165
166 ##
167 # Relations
168 ##
169
170 # Default values for has_many, belongs_to and belongs_to_as
171 def has_many; []; end
172 def has_one; []; end
173 def has; []; end
174 def belongs_to; []; end
175
176 #
177 # Defines a has_many relation which then again
178 # needs a corresponding belongs_to relation in the
179 # classes the relation is made with (see the documentation
180 # of belongs_to below)
181 #
182 # Takes:
183 # * a symbol indicating the name of the association.
184 # The association name can be freely chosen.
185 #
186 # Example:
187 #
188 # has_many :fruits
189 #
190 # Requires a belongs_to relation from the other part. F.ex:
191 #
192 # belongs_to: :fruit_basket, :as => :fruits
193 #
194 # Raises:
195 # * HasManyAssociationError if the association name
196 # is left blank.
197 #
198 def self.has_many(what_it_has = nil)
199 raise CouchObject::Errors::HasManyAssociationError if what_it_has == nil
200
201 @couch_object_has_many ||= []
202 @couch_object_has_many << what_it_has unless \
203 @couch_object_has_many.include?(what_it_has)
204
205 self.instance_eval do
206 # The objects are stored in this variable
207 has_many_object_variable = \
208 "@couch_object_#{what_it_has.to_s}"
209
210 #
211 # Getter which also works as a setter because:
212 # * it returns the array that contains the references
213 # * when a new relationship is added using << the action
214 # it performed by the array, and not self
215 #
216 define_method(what_it_has.to_s) do
217 eval("#{has_many_object_variable}.nil? ? " + \
218 "#{has_many_object_variable} = " + \
219 "couch_load_has_many_relations(\"#{what_it_has}\") : " + \
220 "#{has_many_object_variable}")
221 end
222
223 #
224 # Returns:
225 # * the name of the relation
226 #
227 # Example:
228 #
229 # apple_tree.has_many => :fruits
230 # apple_tree.fruits => [apple1, apple2]
231 #
232 all_the_things_it_has = @couch_object_has_many
233 define_method("has_many") do
234 # Filtering out the has_one relations so they don't show up
235 what_is_has_output = []
236 self.send(:has).each do |has|
237 what_is_has_output << has \
238 unless has.to_s[0..7] == "has_one_"
239 end
240 what_is_has_output
241 end
242 define_method("has") do
243 all_the_things_it_has
244 end
245 end
246
247 end
248
249 #
250 # Defines a belongs_to relation which then again
251 # needs a corresponding has_many relation in the
252 # class the relation is made with (see the documentation
253 # of has_many above)
254 #
255 # Takes:
256 # * a symbol indicating the name of the association.
257 # The association name can be freely chosen.
258 # * a symbol that indicates the name the corresponding has_many
259 # relationship in the owner class
260 #
261 # Example:
262 #
263 # belongs_to :fruit_basket, :as => :fruits
264 #
265 # Requires a has_many relation from the other class that looks
266 # something like this:
267 #
268 # has_many :fruits
269 #
270 # Raises:
271 # * BelongsToAssociationError if the association name,
272 # or the :as parameter is left blank.
273 #
274 def self.belongs_to(what_it_belongs_to = nil, as = nil)
275 raise CouchObject::Errors::BelongsToAssociationError \
276 if what_it_belongs_to.nil? || as.nil?
277
278 @couch_object_what_it_belongs_to ||= []
279 @couch_object_what_it_belongs_to << what_it_belongs_to unless \
280 @couch_object_what_it_belongs_to.include?(what_it_belongs_to)
281
282 self.instance_eval do
283 # The object are stored in this variable
284 belongs_to_object_variable = \
285 "@couch_object_#{what_it_belongs_to.to_s}"
286
287 # Getter
288 define_method(what_it_belongs_to.to_s) do
289 eval("#{belongs_to_object_variable} ||= " + \
290 " couch_load_belongs_to_relation(\"#{as[:as]}\")")
291 end
292
293 # Setter
294 define_method("#{what_it_belongs_to.to_s}=") do |object_to_add|
295 # The first thing we have to do is to check if it is in
296 # a has_one or has_many relationship!
297 if object_to_add.respond_to?("has_one_#{as[:as]}") || \
298 eval("#{belongs_to_object_variable}" + \
299 ".respond_to?(:has_one_#{as[:as]})")
300 is_a_has_many_relationship = false
301 else
302 is_a_has_many_relationship = true
303 end
304
305 # Now... there is no good reason loading a belongs_to relation
306 # from the database only to remove the relation with the child,
307 # because the relation is stored in the child anyway...
308 # We therefore temporarily deactivate the loading of belongs_to
309 # relations
310 original_state_of_load_belongs_to_relations = \
311 @do_not_load_belongs_to_relations
312 @do_not_load_belongs_to_relations = true
313
314 if is_a_has_many_relationship
315
316 # Remove the original relationship in the master object
317 eval("#{belongs_to_object_variable}." \
318 + "send(:end_relationsship_with, self, \"#{as[:as]}\" ) " \
319 + "unless #{belongs_to_object_variable} == nil")
320
321 # Sets the new relation
322 instance_variable_set("#{belongs_to_object_variable}", \
323 object_to_add)
324
325 # Set up the new relationship with the master object
326 self.add_relation_to_master(as[:as]) if object_to_add
327
328 else
329
330 # Remove old relationship and set the new one
331 self.set_has_one_relation_to_master(as[:as], nil)
332
333 # Sets the new relation
334 instance_variable_set("#{belongs_to_object_variable}", \
335 object_to_add)
336
337 # Setup the new relationship
338 self.set_has_one_relation_to_master(as[:as], self) \
339 if object_to_add
340
341 end
342
343 # And now we reset the @do_not_load_belongs_to_relations
344 # variable to its original value:
345 @do_not_load_belongs_to_relations = \
346 original_state_of_load_belongs_to_relations
347
348 end
349
350 # Setter without callback for new objects
351 # from the load relations method
352 define_method("#{what_it_belongs_to.to_s}" + \
353 "_without_call_back=") do |object_to_add|
354
355 # Sets the new relation
356 instance_variable_set("#{belongs_to_object_variable}", \
357 object_to_add)
358 end
359
360 #
361 # Returns:
362 # * the getter for a belongs_to relationship as a symbol
363 #
364 # Example:
365 #
366 # fruit.belongs_to => :tree
367 # fruit.tree => <AppleTree>
368 #
369 return_value_for_function = @couch_object_what_it_belongs_to
370 define_method("belongs_to") do
371 return_value_for_function
372 end
373
374 #
375 # Returns:
376 # * what the corresponding has_many relation is called
377 #
378 # Example:
379 #
380 # fruit.belongs_to => :tree
381 # fruit.tree = apple_tree
382 # fruit.belongs_to_as => :fruits
383 # apple_tree.fruits => [fruit]
384 #
385 define_method("belongs_to_#{what_it_belongs_to}_as") do
386 as[:as]
387 end
388
389 end
390
391 end
392
393 #
394 # has_one relations are added as a layer to the has_many
395 # There is created a has_many relation ship but getters and setters
396 # for the has_one relationship on top of that that interact with the
397 # has_many relationship.
398 def self.has_one(what_it_has = nil)
399 raise CouchObject::Errors::HasOneAssociationError if what_it_has == nil
400
401 related_has_many_relationship = "has_one_#{what_it_has.to_s}".to_sym
402
403 # Create the has_many relationship
404 self.send(:has_many, related_has_many_relationship)
405
406 # Create methods to get and set the relationship
407
408 # getter
409 define_method(what_it_has) do
410 self.send(related_has_many_relationship).first
411 end
412
413 define_method("#{what_it_has}=") do |new_relation|
414 # Remove the original relation
415 self.send(related_has_many_relationship).
416 remove(self.send(related_has_many_relationship).first) \
417 unless self.send(related_has_many_relationship) == []
418
419 # Disable callbacks
420 self.send(related_has_many_relationship).disable_call_back_on_add
421
422 if new_relation
423
424 # Create the new
425 self.send(related_has_many_relationship) << new_relation \
426 unless new_relation == nil
427
428 # Set the relationship in the child
429 what_it_belongs_to = define_relationship_name(new_relation)
430
431 new_relation.
432 send("#{what_it_belongs_to}_without_call_back=", self)
433
434 end
435
436 # Reenable callbacks
437 self.send(related_has_many_relationship).enable_call_back_on_add
438
439 end
440
441 define_method("has_one") do
442 what_is_has_output = []
443 self.send(:has).each do |has|
444 what_is_has_output << has.to_s[8..-1].to_sym \
445 if has.to_s[0..7] == "has_one_"
446 end
447 what_is_has_output
448 end
449
450 end
451
452 #
453 # Returns the name of the relation in itself matching one of the
454 # relations in the other object
455 #
456 # Example:
457 # other_object has defined the relationships:
458 # belongs_to :house, :as => :houses
459 # belongs_to :humanity
460 #
461 # self has the relation
462 # has_many :houses
463 #
464 # :houses is returned
465 #
466 def define_relationship_name(other_object)
467
468 belongs_to_relationship_name = nil
469
470 other_object.send(:belongs_to).each do |what_it_belongs_to|
471
472 name_of_relation_in_master = other_object.
473 send("belongs_to_#{what_it_belongs_to}_as".to_sym)
474
475 return what_it_belongs_to.to_s \
476 if self.respond_to?(name_of_relation_in_master)
477
478 end
479
480 # There couldn't be found a match... raising an error
481 raise "The master class #{self} doesn't have a relation" + \
482 " matching the relation defined in the child class " \
483 if belongs_to_relationship_name == nil
484
485 end
486
487 #
488 # This method is called from the method that assigns a
489 # belongs_to relation to inform the master object of the relation
490 # (has_many relations)
491 #
492 def add_relation_to_master(relation_name)
493
494 if master_class = get_master_for_relation(relation_name)
495
496 masters_objects_relations = \
497 master_class.send(relation_name)
498
499 if masters_objects_relations == []
500 masters_objects_relations << self
501 else
502 unless masters_objects_relations.include?(self)
503 masters_objects_relations << self
504 end
505 end
506
507 end
508
509 end
510
511 #
512 # This method is called from the method that assigns a
513 # belongs_to relation to inform the master object of the relation
514 # (has_one relations)
515 #
516 def set_has_one_relation_to_master(relation_name, to_what)
517
518 if master_class = get_master_for_relation(relation_name)
519
520 # set up the new relationship in the master
521 master_class.send("#{relation_name}=", to_what)
522
523 end
524
525 end
526
527 #
528 # This method is called from the method that assigns a
529 # belongs_to relation to inform the previous master object
530 # that the relation ship has ended
531 # (has_one relations)
532 #
533 def end_has_one_relation_to_master(relation_name)
534
535 set_has_one_relation_to_master(relation_name, nil)
536
537 end
538
539
540 def get_master_for_relation(relation_name)
541
542 accessor_for_what_it_belongs_to = nil
543 self.send(:belongs_to).each do |what_it_belongs_to|
544 # Only load the belongs to relation that is needed
545 # We therefore have to find out which of the relations to use
546 find_string = "belongs_to_#{what_it_belongs_to}_as"
547 accessor_for_what_it_belongs_to = what_it_belongs_to \
548 if self.send(find_string) == relation_name
549 end
550
551 return nil if accessor_for_what_it_belongs_to == nil
552
553 master_class = self.send(accessor_for_what_it_belongs_to)
554 return nil if master_class == nil
555
556 raise "The master class doesn't have a matching relation " + \
557 "defined" unless master_class.respond_to?(relation_name)
558
559 return master_class
560
561 end
562
563
564 end
565 end
566
567 end
568 end