module Seabright module Collections def hkey_col(ident = nil) "#{hkey}:collections" end def delete_child(obj) if col = get_collection(obj.collection_name) col.delete obj end end def collection_name self.class.collection_name end def ref_key(ident = nil) "#{hkey}:backreferences" end def reference(obj) raise "Not an object." unless obj.is_a?(RedisObject) get_collection(obj.collection_name) << obj.hkey obj.referenced_by self end def <<(obj) reference obj end def push(obj) reference obj end def remove_collection!(name) store.srem hkey_col, name end def referenced_by(obj) store.sadd(ref_key,obj.hkey) end def backreferences(cls = nil) out = store.smembers(ref_key).map do |backreference_hkey| obj = RedisObject.find_by_key(backreference_hkey) if cls && !obj.is_a?(cls) nil else obj end end out.compact end def dereference_from(obj) obj.get_collection(collection_name).delete(hkey) end def dereference_from_backreferences backreferences.each do |backreference| dereference_from(backreference) end end def load(o_id) super(o_id) store.smembers(hkey_col).each do |name| collections[name] = Seabright::Collection.load(name,self) define_access(name) do get_collection(name) end define_access(name.to_s.singularize) do get_collection(name).latest end end true end def has_collection?(name) collection_names.include?(name.to_s) end def get_collection(name) if has_collection?(name) collections[name.to_s] ||= Collection.load(name,self) else store.sadd hkey_col, name collection_names << name.to_s collections[name.to_s] ||= Collection.load(name,self) define_access(name.to_s.pluralize) do get_collection(name) end define_access(name.to_s.singularize) do get_collection(name).first end end collections[name.to_s] end def collections @collections ||= {} end def collection_names @collection_names ||= store.smembers(hkey_col) end def collect_type_by_key(col,*keys) collect = get_collection(col) keys.each do |k| collect << k end end module ClassMethods def intercept_sets_for_collecting! return if @intercepted_sets_for_collecting self.class_eval do filter_gets do |obj, k, v| if obj.has_collection?(k) obj.get_collection(k) elsif obj.has_collection?(pk = k.to_s.pluralize) obj.get_collection(pk).first else v end end filter_sets do |obj, k, v| if obj.has_collection?(k) obj.get_collection(k.to_s).replace(v) return [nil,nil] else [k,v] end end filter_msets do |obj, dat| dat.select {|k,v| !obj.collections[k.to_s] } end end @intercepted_sets_for_collecting = true end def hkey_col(ident = nil) "#{hkey(ident)}:collections" end def delete_child(obj) if col = get_collection(obj.collection_name) col.delete obj end end def collection_name self.name.split('::').last.pluralize.underscore.to_sym end def reference(obj) name = obj.collection_name store.sadd hkey_col, name get_collection(name) << obj.hkey end def <<(obj) reference obj end def push(obj) reference obj end def remove_collection!(name) store.srem hkey_col, name end def get(k) if has_collection?(k) return get_collection(k) elsif has_collection?(pk = k.to_s.pluralize) return get_collection(pk).first end nil end def has_collection?(name) store.sismember(hkey_col,name.to_s) end def get_collection(name) collections[name.to_s] ||= Collection.load(name,self) collections[name.to_s] end def collections @collections ||= {} end end def self.included(base) base.extend(ClassMethods) base.intercept_sets_for_collecting! end end class Collection < Array include Seabright::CachedScripts def initialize(name,owner) @name = name.to_s @owner = owner end def remove! @owner.remove_collection! @name end def latest indexed(:created_at,5,true).first || first end def indexed(idx,num=-1,reverse=false) keys = keys_by_index(idx,num,reverse) out = ListEnumerator.new(keys) do |y| keys.each do |member| if a = class_const.find_by_key(member) y << a end end end if block_given? out.each do |itm| yield itm end else out end end def temp_key "#{key}::zintersect_temp::#{RedisObject.new_id(4)}" end RedisObject::ScriptSources::FwdScript = "redis.call('ZINTERSTORE', KEYS[1], 2, KEYS[2], KEYS[3], 'WEIGHTS', 1, 0) local keys = redis.call('ZRANGE', KEYS[1], 0, KEYS[4]) redis.call('DEL', KEYS[1]) return keys".freeze RedisObject::ScriptSources::RevScript = "redis.call('ZINTERSTORE', KEYS[1], 2, KEYS[2], KEYS[3], 'WEIGHTS', 1, 0) local keys = redis.call('ZREVRANGE', KEYS[1], 0, KEYS[4]) redis.call('DEL', KEYS[1]) return keys".freeze def keys_by_index(idx,num=-1,reverse=false) keys = run_script(reverse ? :RevScript : :FwdScript, [temp_key, sort_index_key(idx), key, num>0 ? num - 1 : -1]) ListEnumerator.new(keys.uniq) do |y| keys.each do |member| y << member end end end def sort_index_key(idx) class_const.sort_index_key(idx) end def item_key(k) "#{class_const}:#{k}_h" end def find(k) if k.is_a? String return real_at(item_key(k)) elsif k.is_a? Hash return match(k) elsif k.is_a? Integer return real_at(at(k)) end return nil end def [](k) find k end NilPattern = 'nilpattern:' RedisObject::ScriptSources::ColMatcher = " local out = {} local val local pattern for i, v in ipairs(KEYS) do val = redis.call('HGET',v,ARGV[1]) if val then if ARGV[2]:find('^pattern:') then pattern = ARGV[2]:gsub('^pattern:','') if val:match(pattern) then table.insert(out,KEYS[i]) end elseif ARGV[2]:find('^ipattern:') then pattern = ARGV[2]:gsub('^ipattern:',''):lower() if val:lower():match(pattern) then table.insert(out,KEYS[i]) end else if val == ARGV[2] then table.insert(out,KEYS[i]) end end else if ARGV[2] == '#{NilPattern}' then table.insert(out,KEYS[i]) end end end return out".gsub(/\t/,'').freeze RedisObject::ScriptSources::ColMultiMatcher = " local out = {} local matchers = {} local matcher = {} local mod for i=1,#ARGV do mod = i % 2 if mod == 1 then matcher[1] = ARGV[i] else matcher[2] = ARGV[i] table.insert(matchers,matcher) matcher = {} end end local val local good local pattern for i, v in ipairs(KEYS) do good = true for n=1,#matchers do val = redis.call('HGET',v,matchers[n][1]) if val then if matchers[n][2]:find('^pattern:') then pattern = matchers[n][2]:gsub('^pattern:','') if val:match(pattern) then good = good else good = false break end elseif matchers[n][2]:find('^ipattern:') then pattern = matchers[n][2]:gsub('^ipattern:',''):lower() if val:lower():match(pattern) then good = good else good = false break end else if val ~= matchers[n][2] then good = false break end end else if matchers[n][2] == '#{NilPattern}' then good = good else good = false break end end end if good == true then table.insert(out,KEYS[i]) end end return out".gsub(/\t/,'').freeze RedisObject::ScriptSources::ColOrMatcher = " local out = {} local matchers = {} local matcher = {} local mod for i=1,#ARGV do mod = i % 2 if mod == 1 then matcher[1] = ARGV[i] else matcher[2] = ARGV[i] table.insert(matchers,matcher) matcher = {} end end local val local good local pattern for i, v in ipairs(KEYS) do good = false for n=1,#matchers do val = redis.call('HGET',v,matchers[n][1]) if val then if matchers[n][2]:find('^pattern:') then pattern = matchers[n][2]:gsub('^pattern:','') if val:match(pattern) then good = true break else good = good end elseif matchers[n][2]:find('^ipattern:') then pattern = matchers[n][2]:gsub('^ipattern:',''):lower() if val:lower():match(pattern) then good = true break else good = good end else if val == matchers[n][2] then good = true break end end else if matchers[n][2] == '#{NilPattern}' then good = good break else good = false end end end if good == true then table.insert(out,KEYS[i]) end end return out".gsub(/\t/,'').freeze def match(pkt, use_or=false) if use_or mtchr = :ColOrMatcher else mtchr = pkt.keys.count > 1 ? :ColMultiMatcher : :ColMatcher end pkt = pkt.flatten.reduce([]) do |i,v| x = case v when Regexp convert_regex_to_lua(v) when Array raise ArgumentError.new("An array can only be used with the find_or method") unless use_or inject_key(i.last, v) when NilClass NilPattern else v.to_s end i << x i end kys = run_script(mtchr,self,pkt.flatten) ListEnumerator.new(kys) do |y| kys.each do |k| y << class_const.find_by_key(k) end end end def inject_key(key,list) out = [] list.each do |i| if i == list.first out << i else out << key out << i end end out end def convert_regex_to_lua(reg) "#{reg.casefold? ? "i" : ""}pattern:#{reg.source.gsub("\\","")}" end # def match(pkt) # Enumerator.new do |y| # each do |i| # if pkt.all? {|hk,va| i.get(hk)==va } # y << i # end # end # end # end def real_at(key) class_const.find_by_key(key) end def objects each.to_a end def first class_const.find_by_key(super) end def last class_const.find_by_key(super) end def each out = Enumerator.new do |y| each_index do |key| if a = class_const.find_by_key(at(key)) y << a end end end if block_given? out.each do |a| yield a end else out end end def cleanup! each_index do |key| unless a = class_const.find_by_key(at(key)) Log.debug "Deleting #{key} because not #{a.inspect}" delete at(key) end end if size < 1 Log.debug "Deleting collection #{@name} because empty" remove! end end def map(&block) each.map(&block) end def select(&block) return nil unless block_given? Enumerator.new do |y| each_index do |key| if (a = class_const.find_by_key(at(key))) && block.call(a) y << a end end end end def delete(obj) k = obj.class == String ? obj : obj.hkey store.zrem(key,k) super(k) end def clear! store.zrem(key,self.join(" ")) end def <<(obj) k = obj.class == String ? obj : obj.hkey store.zadd(key,store.zcount(key,"-inf", "+inf"),k) super(k) end def push(obj) self << obj end def class_const self.class.class_const_for(@name) end def store class_const.store end def key "#{@owner ? "#{@owner.key}:" : ""}COLLECTION:#{@name}" end class << self def load(name,owner) out = new(name,owner) out.replace class_const_for(name).store.zrange(out.key,0,-1) out end def class_const_for(name) if cls = RedisObject.constant_lookups[name.to_s.classify.to_sym] return cls end Object.const_get(name.to_s.classify.to_sym) rescue RedisObject end end end end