require 'digest/sha1' require 'fileutils' class PoroRepository autoload :WeakHash, "poro_repository/weak_hash" autoload :RecordMetaData, "poro_repository/record_meta_data" autoload :BoundaryToken, "poro_repository/boundary_token" attr_accessor :remember def initialize root @root = root @instantiated_records = {} @boundaries = {} @remember = true end # When serialising, attributes identified as "boundaries" are not serialised with the # larger object, but are instead serialised separately. A placeholder is used in the # original object, with an ID for the extracted object. # @param type [Symbol] # @param instance_var [Symbol] def boundary type, instance_var @boundaries[type] ||= [] @boundaries[type] << instance_var end def nuke! really if really == 'yes, really' FileUtils.rm_rf @root else raise "wont do it!" end end def load_record type, id record = previous_instantiated type, id return record unless record.nil? data = read_if_exists(record_path(type, id)) data && deserialise(data).tap do |record| record.instance_variables.each do |inst_var| if (token = record.instance_variable_get(inst_var)).is_a? BoundaryToken object = load_record token.original_type, token.original_id record.instance_variable_set inst_var, object end end remember_record record if @remember end end # @return [String] record id def save_record record, remember=true id = id_from_record(record) path = record_path(type_from_record(record), id) open_for_write path do |file| with_boundary_objects_extracted record do |extracted| file.write serialise record extracted.each do |extracted_record| save_record extracted_record end end end remember_record record if @remember id end private def open_for_write path, &block FileUtils.mkdir_p File.dirname(path) File.open path, 'w', &block end # @return [String, nil] def read_if_exists path if File.exist? path File.read path else nil end end def record_metadata record record.instance_eval do @_repository_data ||= RecordMetaData.new end end def serialise record Marshal.dump(record) end def deserialise data Marshal.load(data) end # @return [String] def type_from_record record if record.respond_to? :type record.type else record.class.name.split('::').last end end def id_from_record record if record.respond_to?(:id) && record.id record.id else record_metadata(record).id end end def record_path type, id raise if id.nil? "#{@root}/#{type}/records/#{id}" end def index_path type, field "#{@root}/#{type}/index/#{field}" end def with_boundary_objects_extracted record, &block originals = {} boundaries(record).each do |inst_var| value = record.instance_variable_get(inst_var) originals[inst_var] = value record.instance_variable_set(inst_var, boundary_token(value)) end block.call originals.values ensure originals.each do |inst_var, original_value| record.instance_variable_set(inst_var, original_value) end end def boundaries record @boundaries[type_from_record(record).to_sym] || [] end def boundary_token record BoundaryToken.new type_from_record(record), id_from_record(record) end def remember_record record type = type_from_record(record) @instantiated_records[type] ||= WeakHash.new @instantiated_records[type][id_from_record(record)] = record end def previous_instantiated type, id records = @instantiated_records[type] || {} ref = records[id] ref && ref.actual end # this method is only used in test def remembered_records @instantiated_records.values.collect do |h| h.values.compact.collect(&:actual) end.flatten.compact end end