module CouchObject module Persistable def self.included(klazz) klazz.extend(ClassMethods) # # Using meta programming methods for handling, amongst others, # * setting of the database uri at design time # * including timestamps # * managing relations # are created # klazz.class_eval do ## # Timestamps ## # Timestamps are false by default def self.couch_object_timestamp_on_update?; false; end def self.couch_object_timestamp_on_create?; false; end # # Adds timestamps to the class. # # Example: # # class Vacation # include CouchObject::Persistable # add_timestamp_for :on_create, :on_update # end # # my_vacation = Vacation.new # my_vacation.save(db_address) # my_vacation.created_at => Somedate # my_vacation.updated_at => Somedate # def self.add_timestamp_for(*timestamp_actions) timestamp_actions.each do |action| case action when :on_create self.class_eval do def self.couch_object_timestamp_on_create?; true; end end when :on_update self.class_eval do def self.couch_object_timestamp_on_update?; true; end end end end end ## # Change monitor ## # to be implemented later. # could be implemented using MonitorFunctions # (http://www.erikveen.dds.nl/monitorfunctions/) # Each class monitors it's setters to see if it's content is changed, # in which case a flag is set. # For this purpose all setters are overridden # self.instance_variable_set("@couch_has_unsaved_changes_flag", false) # def unsaved_changes? # @couch_has_unsaved_changes_flag # end # puts "Public setters:" # self.public_methods.each do |method| # if method.to_s[-1,1] == "=" # # We have to create an alias for the original method # DO MAGIC HERE # end # end ## # Database storage location ## # Location methods are added both as instance methods and # as class level methods. The class level methods are needed # when loading new objects from the database and the instance # methods are used throughout the class def self.location; @couch_object_class_storage_location ||= nil; end def location; @location; end alias storage_location location # # Sets the location of the database to use by default # # Example: # # class AppleTree # include CouchObject::Persistable # database 'http://localhost:5984' # end # # apple_tree = AppleTree.new # apple_tree.save # saves automatically to the predefined # # database location # def self.database(db_uri) @couch_object_class_storage_location = db_uri self.instance_eval do define_method("location") do @location ||= db_uri end end end ## # Smart savign ## def use_smart_save; false; end # # Smart save (defaults to false), if activated, keeps a snapshot of # the objects initial state and evaluates if the class needs to be # saved to the database by comparing it to the snapshot when a save # is requested. # # Please notice: # Only activate this feature in cases where it is needed. # It might slow down the performance of your app if you activate it # for classes that you need many instances of and that you won't # call the save method on after having loaded them from the database. # Please also bare in mind that the class instance will store an # extra copy of its contents which will lead to quite a big memory # overhead for classes that store a lot of data! # def self.smart_save self.instance_eval do define_method("use_smart_save") do true end end end # # Smart save can also be used on a per-case basis if it is sometimes # needed and sometimes not. # # Example: # # user_without_smart_save_1 = User.get("foo") # User.smart_save # user_with_smart_save = User.get("bar") # User.deactivate_smart_save # user_without_smart_save_2 = User.get("bong") # def self.deactivate_smart_save self.instance_eval do define_method("use_smart_save") do false end end end ## # Relations ## # Default values for has_many, belongs_to and belongs_to_as def has_many; []; end def has_one; []; end def has; []; end def belongs_to; []; end # # Defines a has_many relation which then again # needs a corresponding belongs_to relation in the # classes the relation is made with (see the documentation # of belongs_to below) # # Takes: # * a symbol indicating the name of the association. # The association name can be freely chosen. # # Example: # # has_many :fruits # # Requires a belongs_to relation from the other part. F.ex: # # belongs_to: :fruit_basket, :as => :fruits # # Raises: # * HasManyAssociationError if the association name # is left blank. # def self.has_many(what_it_has = nil) raise CouchObject::Errors::HasManyAssociationError if what_it_has == nil @couch_object_has_many ||= [] @couch_object_has_many << what_it_has unless \ @couch_object_has_many.include?(what_it_has) self.instance_eval do # The objects are stored in this variable has_many_object_variable = \ "@couch_object_#{what_it_has.to_s}" # # Getter which also works as a setter because: # * it returns the array that contains the references # * when a new relationship is added using << the action # it performed by the array, and not self # define_method(what_it_has.to_s) do eval("#{has_many_object_variable}.nil? ? " + \ "#{has_many_object_variable} = " + \ "couch_load_has_many_relations(\"#{what_it_has}\") : " + \ "#{has_many_object_variable}") end # # Returns: # * the name of the relation # # Example: # # apple_tree.has_many => :fruits # apple_tree.fruits => [apple1, apple2] # all_the_things_it_has = @couch_object_has_many define_method("has_many") do # Filtering out the has_one relations so they don't show up what_is_has_output = [] self.send(:has).each do |has| what_is_has_output << has \ unless has.to_s[0..7] == "has_one_" end what_is_has_output end define_method("has") do all_the_things_it_has end end end # # Defines a belongs_to relation which then again # needs a corresponding has_many relation in the # class the relation is made with (see the documentation # of has_many above) # # Takes: # * a symbol indicating the name of the association. # The association name can be freely chosen. # * a symbol that indicates the name the corresponding has_many # relationship in the owner class # # Example: # # belongs_to :fruit_basket, :as => :fruits # # Requires a has_many relation from the other class that looks # something like this: # # has_many :fruits # # Raises: # * BelongsToAssociationError if the association name, # or the :as parameter is left blank. # def self.belongs_to(what_it_belongs_to = nil, as = nil) raise CouchObject::Errors::BelongsToAssociationError \ if what_it_belongs_to.nil? || as.nil? @couch_object_what_it_belongs_to ||= [] @couch_object_what_it_belongs_to << what_it_belongs_to unless \ @couch_object_what_it_belongs_to.include?(what_it_belongs_to) self.instance_eval do # The object are stored in this variable belongs_to_object_variable = \ "@couch_object_#{what_it_belongs_to.to_s}" # Getter define_method(what_it_belongs_to.to_s) do eval("#{belongs_to_object_variable} ||= " + \ " couch_load_belongs_to_relation(\"#{as[:as]}\")") end # Setter define_method("#{what_it_belongs_to.to_s}=") do |object_to_add| # The first thing we have to do is to check if it is in # a has_one or has_many relationship! if object_to_add.respond_to?("has_one_#{as[:as]}") || \ eval("#{belongs_to_object_variable}" + \ ".respond_to?(:has_one_#{as[:as]})") is_a_has_many_relationship = false else is_a_has_many_relationship = true end # Now... there is no good reason loading a belongs_to relation # from the database only to remove the relation with the child, # because the relation is stored in the child anyway... # We therefore temporarily deactivate the loading of belongs_to # relations original_state_of_load_belongs_to_relations = \ @do_not_load_belongs_to_relations @do_not_load_belongs_to_relations = true if is_a_has_many_relationship # Remove the original relationship in the master object eval("#{belongs_to_object_variable}." \ + "send(:end_relationsship_with, self, \"#{as[:as]}\" ) " \ + "unless #{belongs_to_object_variable} == nil") # Sets the new relation instance_variable_set("#{belongs_to_object_variable}", \ object_to_add) # Set up the new relationship with the master object self.add_relation_to_master(as[:as]) if object_to_add else # Remove old relationship and set the new one self.set_has_one_relation_to_master(as[:as], nil) # Sets the new relation instance_variable_set("#{belongs_to_object_variable}", \ object_to_add) # Setup the new relationship self.set_has_one_relation_to_master(as[:as], self) \ if object_to_add end # And now we reset the @do_not_load_belongs_to_relations # variable to its original value: @do_not_load_belongs_to_relations = \ original_state_of_load_belongs_to_relations end # Setter without callback for new objects # from the load relations method define_method("#{what_it_belongs_to.to_s}" + \ "_without_call_back=") do |object_to_add| # Sets the new relation instance_variable_set("#{belongs_to_object_variable}", \ object_to_add) end # # Returns: # * the getter for a belongs_to relationship as a symbol # # Example: # # fruit.belongs_to => :tree # fruit.tree => # return_value_for_function = @couch_object_what_it_belongs_to define_method("belongs_to") do return_value_for_function end # # Returns: # * what the corresponding has_many relation is called # # Example: # # fruit.belongs_to => :tree # fruit.tree = apple_tree # fruit.belongs_to_as => :fruits # apple_tree.fruits => [fruit] # define_method("belongs_to_#{what_it_belongs_to}_as") do as[:as] end end end # # has_one relations are added as a layer to the has_many # There is created a has_many relation ship but getters and setters # for the has_one relationship on top of that that interact with the # has_many relationship. def self.has_one(what_it_has = nil) raise CouchObject::Errors::HasOneAssociationError if what_it_has == nil related_has_many_relationship = "has_one_#{what_it_has.to_s}".to_sym # Create the has_many relationship self.send(:has_many, related_has_many_relationship) # Create methods to get and set the relationship # getter define_method(what_it_has) do self.send(related_has_many_relationship).first end define_method("#{what_it_has}=") do |new_relation| # Remove the original relation self.send(related_has_many_relationship). remove(self.send(related_has_many_relationship).first) \ unless self.send(related_has_many_relationship) == [] # Disable callbacks self.send(related_has_many_relationship).disable_call_back_on_add if new_relation # Create the new self.send(related_has_many_relationship) << new_relation \ unless new_relation == nil # Set the relationship in the child what_it_belongs_to = define_relationship_name(new_relation) new_relation. send("#{what_it_belongs_to}_without_call_back=", self) end # Reenable callbacks self.send(related_has_many_relationship).enable_call_back_on_add end define_method("has_one") do what_is_has_output = [] self.send(:has).each do |has| what_is_has_output << has.to_s[8..-1].to_sym \ if has.to_s[0..7] == "has_one_" end what_is_has_output end end # # Returns the name of the relation in itself matching one of the # relations in the other object # # Example: # other_object has defined the relationships: # belongs_to :house, :as => :houses # belongs_to :humanity # # self has the relation # has_many :houses # # :houses is returned # def define_relationship_name(other_object) belongs_to_relationship_name = nil other_object.send(:belongs_to).each do |what_it_belongs_to| name_of_relation_in_master = other_object. send("belongs_to_#{what_it_belongs_to}_as".to_sym) return what_it_belongs_to.to_s \ if self.respond_to?(name_of_relation_in_master) end # There couldn't be found a match... raising an error raise "The master class #{self} doesn't have a relation" + \ " matching the relation defined in the child class " \ if belongs_to_relationship_name == nil end # # This method is called from the method that assigns a # belongs_to relation to inform the master object of the relation # (has_many relations) # def add_relation_to_master(relation_name) if master_class = get_master_for_relation(relation_name) masters_objects_relations = \ master_class.send(relation_name) if masters_objects_relations == [] masters_objects_relations << self else unless masters_objects_relations.include?(self) masters_objects_relations << self end end end end # # This method is called from the method that assigns a # belongs_to relation to inform the master object of the relation # (has_one relations) # def set_has_one_relation_to_master(relation_name, to_what) if master_class = get_master_for_relation(relation_name) # set up the new relationship in the master master_class.send("#{relation_name}=", to_what) end end # # This method is called from the method that assigns a # belongs_to relation to inform the previous master object # that the relation ship has ended # (has_one relations) # def end_has_one_relation_to_master(relation_name) set_has_one_relation_to_master(relation_name, nil) end def get_master_for_relation(relation_name) accessor_for_what_it_belongs_to = nil self.send(:belongs_to).each do |what_it_belongs_to| # Only load the belongs to relation that is needed # We therefore have to find out which of the relations to use find_string = "belongs_to_#{what_it_belongs_to}_as" accessor_for_what_it_belongs_to = what_it_belongs_to \ if self.send(find_string) == relation_name end return nil if accessor_for_what_it_belongs_to == nil master_class = self.send(accessor_for_what_it_belongs_to) return nil if master_class == nil raise "The master class doesn't have a matching relation " + \ "defined" unless master_class.respond_to?(relation_name) return master_class end end end end end