lib/ohm.rb in ohm-0.1.0.rc4 vs lib/ohm.rb in ohm-0.1.0.rc5

- old
+ new

@@ -1,14 +1,14 @@ # encoding: UTF-8 require "base64" require "redis" +require File.join(File.dirname(__FILE__), "ohm", "pattern") require File.join(File.dirname(__FILE__), "ohm", "validations") require File.join(File.dirname(__FILE__), "ohm", "compat-1.8.6") require File.join(File.dirname(__FILE__), "ohm", "key") -require File.join(File.dirname(__FILE__), "ohm", "collection") module Ohm # Provides access to the Redis database. This is shared accross all models and instances. def redis @@ -43,24 +43,21 @@ def connection(*options) Redis.new(*options) end def options - @options || [] + @options = [] unless defined? @options + @options end # Clear the database. def flush redis.flushdb end - def key(*args) - Key[*args] - end + module_function :connect, :connection, :flush, :redis, :redis=, :options, :threaded - module_function :key, :connect, :connection, :flush, :redis, :redis=, :options, :threaded - Error = Class.new(StandardError) class Model # Wraps a model name for lazy evaluation. @@ -95,48 +92,36 @@ end class Collection include Enumerable - attr :raw + attr :key attr :model - def initialize(key, model, db = nil) + def initialize(key, model) + @key = key @model = model.unwrap - @raw = self.class::Raw.new(key, db || @model.db) end - def <<(model) - raw << model.id + def add(model) + self << model end - alias add << - - def each(&block) - raw.each do |id| - block.call(model[id]) - end - end - - def key - raw.key - end - def first(options = {}) if options[:by] sort_by(options.delete(:by), options.merge(:limit => 1)).first else - model[raw.first(options)] + model[key.first(options)] end end def [](index) - model[raw[index]] + model[key[index]] end def sort(*args) - raw.sort(*args).map(&model) + key.sort(*args).map(&model) end # Sort the model instances by the given attribute. # # @example Sorting elements by name: @@ -149,88 +134,131 @@ # # => true def sort_by(att, options = {}) options.merge!(:by => model.key("*->#{att}")) if options[:get] - raw.sort(options.merge(:get => model.key("*->#{options[:get]}"))) + key.sort(options.merge(:get => model.key("*->#{options[:get]}"))) else sort(options) end end - def delete(model) - raw.delete(model.id) - model - end - def clear - raw.clear + key.del end def concat(models) - raw.concat(models.map { |model| model.id }) + models.each { |model| add(model) } self end def replace(models) - raw.replace(models.map { |model| model.id }) - self + clear + concat(models) end - def include?(model) - raw.include?(model.id) + def empty? + !key.exists end - def empty? - raw.empty? + def to_a + all end + end + class Set < Collection + def each(&block) + key.smembers.each { |id| block.call(model[id]) } + end + + def [](id) + model[id] if key.sismember(id) + end + + def << model + key.sadd(model.id) + end + + alias add << + def size - raw.size + key.scard end + def delete(member) + key.srem(member.id) + end + def all - raw.to_a.map(&model) + key.smembers.map(&model) end - alias to_a all - end + def find(options) + source = keys(options) + target = source.inject(key.volatile) { |chain, other| chain + other } + apply(:sinterstore, key, source, target) + end - class Set < Collection - Raw = Ohm::Set + def except(options) + source = keys(options) + target = source.inject(key.volatile) { |chain, other| chain - other } + apply(:sdiffstore, key, source, target) + end - def inspect - "#<Set (#{model}): #{all.inspect}>" + def sort(options = {}) + return [] unless key.exists + + options[:start] ||= 0 + options[:limit] = [options[:start], options[:limit]] if options[:limit] + + key.sort(options).map(&model) end - # Returns an intersection with the sets generated from the passed hash. + # Sort the model instances by the given attribute. # - # @see Ohm::Model.find - # @example - # @events = Event.find(public: true) + # @example Sorting elements by name: # - # # You can combine the result with sort and other set operations: - # @events.sort_by(:name) - def find(hash) - apply(:sinterstore, hash, :+) + # User.create :name => "B" + # User.create :name => "A" + # + # user = User.all.sort_by(:name, :order => "ALPHA").first + # user.name == "A" + # # => true + def sort_by(att, options = {}) + return [] unless key.exists + + options.merge!(:by => model.key["*->#{att}"]) + + if options[:get] + key.sort(options.merge(:get => model.key["*->#{options[:get]}"])) + else + sort(options) + end end - # Returns the difference between the receiver and the passed sets. - # - # @example - # @events = Event.find(public: true).except(status: "sold_out") - def except(hash) - apply(:sdiffstore, hash, :-) + def first(options = {}) + options.merge!(:limit => 1) + + if options[:by] + sort_by(options.delete(:by), options).first + else + sort(options).first + end end - private + def include?(model) + key.sismember(model.id) + end - # Apply a Redis operation on a collection of sets. - def apply(operation, hash, glue) - keys = keys(hash) - target = key.volatile.send(glue, Key[*keys]) - model.db.send(operation, target, key, *keys) + def inspect + "#<Set (#{model}): #{key.smembers.inspect}>" + end + + protected + + def apply(operation, key, source, target) + target.send(operation, key, *source) Set.new(target, Wrapper.wrap(model)) end # Transform a hash of attribute/values into an array of keys. def keys(hash) @@ -243,42 +271,79 @@ end end end end + class Index < Set + def find(options) + keys = keys(options) + return super(options) if keys.size > 1 + + Set.new(keys.first, Wrapper.wrap(model)) + end + end + class List < Collection - Raw = Ohm::List + def each(&block) + key.lrange(0, -1).each { |id| block.call(model[id]) } + end - def shift - if id = raw.shift - model[id] + def <<(model) + key.rpush(model.id) + end + + alias push << + + # Returns the element at index, or returns a subarray starting at + # start and continuing for length elements, or returns a subarray + # specified by range. Negative indices count backward from the end + # of the array (-1 is the last element). Returns nil if the index + # (or starting index) are out of range. + def [](index, limit = nil) + case [index, limit] + when Pattern[Fixnum, Fixnum] then + key.lrange(index, limit).collect { |id| model[id] } + when Pattern[Range, nil] then + key.lrange(index.first, index.last).collect { |id| model[id] } + when Pattern[Fixnum, nil] then + model[key.lindex(index)] end end + def first + self[0] + end + def pop - if id = raw.pop - model[id] - end + id = key.rpop + model[id] if id end + def shift + id = key.lpop + model[id] if id + end + def unshift(model) - raw.unshift(model.id) + key.lpush(model.id) end - def inspect - "#<List (#{model}): #{all.inspect}>" + def all + key.lrange(0, -1).map(&model) end - end - class Index < Set - def apply(operation, hash, glue) - if hash.keys.size == 1 - return Set.new(keys(hash).first, Wrapper.wrap(model)) - else - super - end + def size + key.llen end + + def include?(model) + key.lrange(0, -1).include?(model.id) + end + + def inspect + "#<List (#{model}): #{key.lrange(0, -1).inspect}>" + end end module Validations include Ohm::Validations @@ -354,22 +419,22 @@ # Defines a list attribute for the model. It can be accessed only after the model instance # is created. # # @param name [Symbol] Name of the list. - def self.list(name, model = nil) - attr_collection_reader(name, :List, model) + def self.list(name, model) + define_memoized_method(name) { List.new(key[name], Wrapper.wrap(model)) } collections << name unless collections.include?(name) end # Defines a set attribute for the model. It can be accessed only after the model instance # is created. Sets are recommended when insertion and retrival order is irrelevant, and # operations like union, join, and membership checks are important. # # @param name [Symbol] Name of the set. - def self.set(name, model = nil) - attr_collection_reader(name, :Set, model) + def self.set(name, model) + define_memoized_method(name) { Set.new(key[name], Wrapper.wrap(model)) } collections << name unless collections.include?(name) end # Creates an index (a set) that will be used for finding instances. # @@ -425,24 +490,29 @@ model = Wrapper.wrap(model) reader = :"#{name}_id" writer = :"#{name}_id=" - attribute reader + attributes << reader unless attributes.include?(reader) + index reader define_memoized_method(name) do model.unwrap[send(reader)] end define_method(:"#{name}=") do |value| - instance_variable_set("@#{name}", nil) + @_memo.delete(name) send(writer, value ? value.id : nil) end + define_method(reader) do + read_local(reader) + end + define_method(writer) do |value| - instance_variable_set("@#{name}", nil) + @_memo.delete(name) write_local(reader, value) end end # Define a collection of objects which have a {Ohm::Model::reference reference} @@ -491,23 +561,13 @@ def self.to_reference name.to_s.match(/^(?:.*::)*(.*)$/)[1].gsub(/([a-z\d])([A-Z])/, '\1_\2').downcase.to_sym end - def self.attr_collection_reader(name, type, model) - if model - model = Wrapper.wrap(model) - define_memoized_method(name) { Ohm::Model::const_get(type).new(key(name), model, db) } - else - define_memoized_method(name) { Ohm::const_get(type).new(key(name), db) } - end - end - def self.define_memoized_method(name, &block) define_method(name) do - instance_variable_get("@#{name}") || - instance_variable_set("@#{name}", instance_eval(&block)) + @_memo[name] ||= instance_eval(&block) end end def self.[](id) new(:id => id) if exists?(id) @@ -516,11 +576,11 @@ def self.to_proc Proc.new { |id| self[id] } end def self.all - Ohm::Model::Index.new(key(:all), Wrapper.wrap(self)) + Ohm::Model::Index.new(key[:all], Wrapper.wrap(self)) end def self.attributes @@attributes[self] end @@ -559,10 +619,12 @@ def self.encode(value) Base64.encode64(value.to_s).gsub("\n", "") end def initialize(attrs = {}) + @id = nil + @_memo = {} @_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) } update_attributes(attrs) end def new? @@ -734,12 +796,12 @@ self.db = Ohm.connection(*options) end protected - def key(*args) - self.class.key(id, *args) + def key + self.class.key[id] end def write unless (attributes + counters).empty? atts = (attributes + counters).inject([]) { |ret, att| @@ -787,28 +849,28 @@ def self.db=(connection) Ohm.threaded[self] = connection end - def self.key(*args) - Ohm.key(*args.unshift(self)) + def self.key + Key.new(self, db) end def self.exists?(id) - db.sismember(key(:all), id) + db.sismember(key[:all], id) end def initialize_id - self.id = db.incr(self.class.key("id")).to_s + self.id = db.incr(self.class.key[:id]).to_s end def db self.class.db end def delete_attributes(atts) - db.del(*atts.map { |att| key(att) }) + db.del(*atts.map { |att| key[att] }) end def create_model_membership self.class.all << self end @@ -840,19 +902,19 @@ end def add_to_index(att, value = send(att)) index = index_key_for(att, value) db.sadd(index, id) - db.sadd(key(:_indices), index) + db.sadd(key[:_indices], index) end def delete_from_indices - db.smembers(key(:_indices)).each do |index| + db.smembers(key[:_indices]).each do |index| db.srem(index, id) end - db.del(key(:_indices)) + db.del(key[:_indices]) end def read_local(att) @_attributes[att] end @@ -882,11 +944,11 @@ end end def self.index_key_for(att, value) raise IndexNotFound, att unless indices.include?(att) - key(att, encode(value)) + key[att][encode(value)] end def index_key_for(att, value) self.class.index_key_for(att, value) end @@ -895,22 +957,22 @@ # This method implements the design pattern for locks # described at: http://code.google.com/p/redis/wiki/SetnxCommand # # @see Model#mutex def lock! - until db.setnx(key(:_lock), lock_timeout) - next unless lock = db.get(key(:_lock)) + until db.setnx(key[:_lock], lock_timeout) + next unless lock = db.get(key[:_lock]) sleep(0.5) and next unless lock_expired?(lock) - break unless lock = db.getset(key(:_lock), lock_timeout) + break unless lock = db.getset(key[:_lock], lock_timeout) break if lock_expired?(lock) end end # Release the lock. # @see Model#mutex def unlock! - db.del(key(:_lock)) + db.del(key[:_lock]) end def lock_timeout Time.now.to_f + 1 end