module Isomorfeus module Data class ObjectAccelerator def self.finalize(acc) proc { acc.close_store } end attr_reader :object_class, :object_class_name attr_accessor :store def initialize(ruby_class) @object_class = ruby_class @object_class_name = ruby_class.name @class_cache = Isomorfeus.production? @store_path = File.expand_path(File.join(Isomorfeus.data_documents_path, "#{@object_class_name.underscore}")) open_store ObjectSpace.define_finalizer(self, self.class.finalize(self)) end def object_from_ref(ref, already_loaded) _, iso, type_class_name, key = ref.split('---') raise "not a valid object reference '#{ref}'" unless iso == "iso-object-reference" raise "invalid data class #{type_class_name}" unless Isomorfeus.valid_data_class_name?(type_class_name) type_class = Isomorfeus.cached_data_class(type_class_name) type_class.load(key: key, _already_loaded: already_loaded) end def serialize(obj) Oj.dump(obj, mode: :object, circular: true, class_cache: @class_cache) end def unserialize(v) Oj.load(v, mode: :object, circular: true, class_cache: @class_cache) end def destroy_store close_store FileUtils.rm_rf(@store_path) end def close_store @store.close end def search_each(query, options, &block) @store.search_each(query, options, &block) end def each(&block) ft = @object_class.field_types @store.each do |doc| hash = doc.to_h do |k, v| [k, unserialize_or_load(v, ft[k], {})] end block.call hash end end def create_object(key, fields, already_saved) ft = @object_class.field_types hash = fields.to_h do |k, v| [k, serialize_or_save(v, ft[k], already_saved)] end @store.add_document(hash.merge!({key: key})) end def destroy_object(key) id = get_object_id(key) @store.delete(id) if id end def load_object(key: nil, id: nil, already_loaded: {}) hash = nil id = get_object_id(key) if key if id ft = @object_class.field_types hash = @store.doc(id)&.to_h do |k, v| [k, unserialize_or_load(v, ft[k], already_loaded)] end end hash end def save_object(key, fields, already_saved) id = get_object_id(key) raise "object not created yet" unless id ft = @object_class.field_types hash = fields.to_h do |k, v| [k, serialize_or_save(v, ft[k], already_saved)] end @store.update(id, hash.merge!({key: key})) true end private def unserialize_or_load(v, t, already_loaded) return unserialize(v) if t == :attribute if t == :object && v return nil if v.empty? return object_from_ref(v, already_loaded) end v end def serialize_or_save(v, t, already_saved) return serialize(v) if t == :attribute return create_or_save(v, already_saved) if t == :object && v v end def create_or_save(v, already_saved) if get_object_id(v.key) v.save(_already_saved: already_saved) else v.create end v.ref_s end def get_object_id(key) # special characters must be escaped, characters taken from the ferret query parser documentation escaped_key = key.gsub(/([\\\&\:\(\)\[\]\{\}\!\"\~\^\|\<\>\=\*\?\+\-\s])/, '\\\\\1') top_docs = @store.search("key:\"#{escaped_key}\"", limit: 1) id = top_docs.hits[0].doc if top_docs.total_hits == 1 end def open_store FileUtils.mkdir_p(Isomorfeus.data_documents_path) unless Dir.exist?(Isomorfeus.data_documents_path) field_infos = Isomorfeus::Ferret::Index::FieldInfos.new(store: :yes, index: :yes, term_vector: :no) @store = Isomorfeus::Ferret::Index::Index.new(path: @store_path, key: :key, auto_flush: true, lock_retry_time: 5, field_infos: field_infos) @store.field_infos.add_field(:key, store: :yes, index: :yes, term_vector: :yes) unless @store.field_infos[:key] @object_class.field_options.each do |field, options| @store.field_infos.add_field(field, options) unless @store.field_infos[field] end end end end end