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