require 'yaml' require 'fileutils' module Og # A collection of utilities. Mainly to stay compatible with the # SQL based backends. # # WARNING: This store does not yet support all Og features. module MemoryUtils # FIXME: find a neutral name. def table(klass) klass.to_s end # FIXME: find a neutral name. def join_table(class1, class2, postfix = nil) "#{class1}#{class2}" end def quote(val) val.inspect end end # A Store that 'persists' objects into (RAM) memory. #-- # TODO: fully working query. # TODO: arbitrary pk. #++ class MemoryStore < Store extend MemoryUtils; include MemoryUtils class ObjectHash < Hash def [](key) super(key.to_s) end def []=(key, value) super(key.to_s, value) end end # This hash implements an in-memory database. @objects = ObjectHash.new def self.objects @objects end def self.objects=(val) @objects = val end # A pseudo-connection to the actual object store. attr_accessor :conn def self.destroy(options) FileUtils.rm_rf("#{options[:name]}.db") rescue Logger.info "Cannot destroy '#{name}'" end def initialize(options) super begin @conn = self.class.objects = YAML.load_file("#{@options[:name]}.db") rescue @conn = self.class.objects = ObjectHash.new end end def close File.open("#{@options[:name]}.db", 'w') { |f| f << @conn.to_yaml } end # Enchants a class. def enchant(klass, manager) klass.property :oid, Fixnum super @conn[klass] ||= {} eval_og_insert(klass) eval_og_update(klass) eval_og_read(klass) eval_og_delete(klass) end # :section: Lifecycle methods. # Loads an object from the store using the primary key. def load(pk, klass) @conn[klass][pk] end # Update selected properties of an object or class of # objects. def update_properties(target, set, options = nil) set = set.gsub(/,/, ';') if target.is_a?(Class) if options condition = options[:condition] || options[:where] else condition = 'true' end for obj in @conn[target].values obj.instance_eval %{ if #{condition.gsub(/=/, '==')} #{set} end } end else target.instance_eval(set) end end alias_method :pupdate, :update_properties alias_method :update_property, :update_properties # Find a collection of objects. # # === Examples # # User.find(:condition => 'age > 15', :order => 'score ASC', :offet => 10, :limit =>10) # Comment.find(:include => :entry) # store.find(:class => User, :where => 'age > 15') def find(options) query(options) end # Find one object. def find_one(options) query(options).first end # Reloads an object from the store. def reload(obj, pk) # Nop, the memory store is always synchronized. end # Count results. def count(options) objects = 0 if condition = options[:condition] || options[:where] condition = "obj." + condition.gsub(/=/, '==') else condition = true end eval %{ for obj in @conn[options[:class]].values objects += 1 if #{condition} end } return objects end # Relate two objects through an intermediate join table. # Typically used in joins_many and many_to_many relations. def join(obj1, obj2, table) # nop end # Query. def query(options) objects = [] if condition = options[:condition] || options[:where] condition = "obj." + condition.gsub(/=/, '==') else condition = true end eval %{ for obj in @conn[options[:class]].values objects << obj if #{condition} end } if order = options[:order] desc = (order =~ /DESC/) order = order.gsub(/DESC/, '').gsub(/ASC/, '') eval "objects.sort { |x, y| x.#{order} <=> y.#{order} }" objects.reverse! if desc end return objects end # :section: Transaction methods. # Start a new transaction. def start end # Commit a transaction. def commit end # Rollback a transaction. def rollback end private # :section: Lifecycle method compilers. # Compile the og_update method for the class. def eval_og_insert(klass) pk = klass.primary_key.first klass.class_eval %{ def og_insert(store) #{Aspects.gen_advice_code(:og_insert, klass.advices, :pre) if klass.respond_to?(:advices)} @#{pk} = store.conn[#{klass}].size + 1 store.conn[#{klass}][@#{pk}] = self #{Aspects.gen_advice_code(:og_insert, klass.advices, :post) if klass.respond_to?(:advices)} end } end # Compile the og_update method for the class. def eval_og_update(klass) pk = klass.primary_key.first klass.class_eval %{ def og_update(store, options) #{Aspects.gen_advice_code(:og_update, klass.advices, :pre) if klass.respond_to?(:advices)} store.conn[#{klass}][@#{pk}] = self #{Aspects.gen_advice_code(:og_update, klass.advices, :post) if klass.respond_to?(:advices)} end } end # Not really useful in this store, kept for compatibility, # just to call the aspects. def eval_og_read(klass) klass.class_eval %{ def og_read #{Aspects.gen_advice_code(:og_read, klass.advices, :pre) if klass.respond_to?(:advices)} #{Aspects.gen_advice_code(:og_read, klass.advices, :post) if klass.respond_to?(:advices)} end } end def eval_og_delete(klass) klass.module_eval %{ def og_delete(store, pk, cascade = true) #{Aspects.gen_advice_code(:og_delete, klass.advices, :pre) if klass.respond_to?(:advices)} pk ||= @#{klass.primary_key.first} transaction do |tx| tx.conn[#{klass}].delete(pk) if cascade and #{klass}.__meta[:descendants] #{klass}.__meta[:descendants].each do |dclass, foreign_key| eval "tx.conn[dclass].delete_if { |dobj| dobj.\#{foreign_key} == \#{pk} }" end end end #{Aspects.gen_advice_code(:og_delete, klass.advices, :post) if klass.respond_to?(:advices)} end } end end end