lib/ohm.rb in ohm-0.0.26 vs lib/ohm.rb in ohm-0.0.27
- old
+ new
@@ -72,15 +72,18 @@
# @param options [Hash] options to sort the collection.
# @option options [#to_s] :by Model attribute to sort the instances by.
# @option options [#to_s] :order (ASC) Sorting order, which can be ASC or DESC.
# @option options [Integer] :limit (all) Number of items to return.
# @option options [Integer] :start (0) An offset from where the limit will be applied.
+ #
# @example Get the first ten users sorted alphabetically by name:
- # @event.attendees.sort(User, :by => :name, :order => "ALPHA", :limit => 10)
#
+ # @event.attendees.sort(:by => :name, :order => "ALPHA", :limit => 10)
+ #
# @example Get five posts sorted by number of votes and starting from the number 5 (zero based):
- # @blog.posts.sort(Post, :by => :votes, :start => 5, :limit => 10")
+ #
+ # @blog.posts.sort(:by => :votes, :start => 5, :limit => 10")
def sort(options = {})
return [] if empty?
options[:start] ||= 0
options[:limit] = [options[:start], options[:limit]] if options[:limit]
result = db.sort(key, options)
@@ -92,12 +95,13 @@
# @example Sorting elements by name:
#
# User.create :name => "B"
# User.create :name => "A"
#
- # user = User.all.sort_by :name, :order => "ALPHA"
- # user.name == "A" #=> true
+ # user = User.all.sort_by(:name, :order => "ALPHA").first
+ # user.name == "A"
+ # # => true
def sort_by(att, options = {})
sort(options.merge(:by => model.key("*", att)))
end
# Sort the model instances by id and return the first instance
@@ -126,10 +130,33 @@
# @return [true, false] Returns whether or not the collection is empty.
def empty?
size.zero?
end
+ # Clears the values in the collection.
+ def clear
+ db.del(key)
+ self
+ end
+
+ # Appends the given values to the collection.
+ def concat(values)
+ values.each { |value| self << value }
+ self
+ end
+
+ # Replaces the collection with the passed values.
+ def replace(values)
+ clear
+ concat(values)
+ end
+
+ # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
+ def add(model)
+ self << model.id
+ end
+
private
def instantiate(raw)
model ? raw.collect { |id| model[id] } : raw
end
@@ -178,10 +205,14 @@
# @return [Integer] Returns the number of elements in the list.
def size
db.llen(key)
end
+ def include?(value)
+ raw.include?(value)
+ end
+
def inspect
"#<List: #{raw.inspect}>"
end
end
@@ -204,17 +235,10 @@
# @param value [#to_s] Adds value to the list.
def << value
db.sadd(key, value)
end
- # @param value [Ohm::Model#id] Adds the id of the object if it's an Ohm::Model.
- def add model
- raise ArgumentError unless model.kind_of?(Ohm::Model)
- raise ArgumentError unless model.id
- self << model.id
- end
-
def delete(value)
db.srem(key, value)
end
def include?(value)
@@ -228,74 +252,67 @@
# @return [Integer] Returns the number of elements in the set.
def size
db.scard(key)
end
+ def inspect
+ "#<Set: #{raw.inspect}>"
+ end
+
# Returns an intersection with the sets generated from the passed hash.
#
- # @see Ohm::Model.filter
- # @yield [results] Results of the filtering. Beware that the set of results is deleted from Redis when the block ends.
+ # @see Ohm::Model.find
# @example
- # Event.filter(public: true) do |filter_results|
- # @events = filter_results.all
- # end
+ # @events = Event.find(public: true)
#
- # # You can also combine search and filter
- # Event.search(day: "2009-09-11") do |search_results|
- # search_results.filter(public: true) do |filter_results|
- # @events = filter_results.all
- # end
- # end
- def filter(hash, &block)
- raise ArgumentError, "filter expects a block" unless block_given?
- apply(:sinterstore, keys(hash).push(key), &block)
+ # # You can combine the result with sort and other set operations:
+ # @events.sort_by(:name)
+ def find(hash)
+ apply(:sinterstore, hash, "+")
end
- # Returns a union with the sets generated from the passed hash.
+ # Returns the difference between the receiver and the passed sets.
#
- # @see Ohm::Model.search
- # @yield [results] Results of the search. Beware that the set of results is deleted from Redis when the block ends.
# @example
- # Event.search(day: "2009-09-11") do |search_results|
- # events = search_results.all
- # end
- def search(hash, &block)
- raise ArgumentError, "search expects a block" unless block_given?
- apply(:sunionstore, keys(hash), &block)
+ # @events = Event.find(public: true).except(status: "sold_out")
+ def except(hash)
+ apply(:sdiffstore, hash, "-")
end
- def delete!
- db.del(key)
- end
+ private
- # Apply a redis operation on a collection of sets. Note that
- # the resulting set is removed inmediatly after use.
- def apply(operation, source, &block)
- target = source.uniq.join("+")
- db.send(operation, target, *source)
- set = self.class.new(db, target, model)
- block.call(set)
- set.delete! if source.size > 1
+ # Apply a redis operation on a collection of sets.
+ def apply(operation, hash, glue)
+ indices = keys(hash).unshift(key).uniq
+ target = indices.join(glue)
+ db.send(operation, target, *indices)
+ self.class.new(db, target, model)
end
- def inspect
- "#<Set: #{raw.inspect}>"
- end
-
- private
-
# Transform a hash of attribute/values into an array of keys.
def keys(hash)
hash.inject([]) do |acc, t|
acc + Array(t[1]).map do |v|
- model.key(t[0], model.encode(v))
+ model.index_key_for(t[0], v)
end
end
end
end
+
+ class Index < Set
+ def inspect
+ "#<Index: #{raw.inspect}>"
+ end
+
+ def clear
+ raise Ohm::Model::CannotDeleteIndex
+ end
+ end
end
+ Error = Class.new(StandardError)
+
class Model
module Validations
include Ohm::Validations
# Validates that the attribute or array of attributes are unique. For this,
@@ -305,25 +322,49 @@
# Validates that the name attribute is unique.
# @overload assert_unique [:street, :city]
# Validates that the :street and :city pair is unique.
def assert_unique(attrs)
result = db.sinter(*Array(attrs).map { |att| index_key_for(att, send(att)) })
- assert(result.empty? || result.include?(id.to_s), [attrs, :not_unique])
+ assert result.empty? || !new? && result.include?(id.to_s), [attrs, :not_unique]
end
end
include Validations
- ModelIsNew = Class.new(StandardError)
+ class MissingID < Error
+ def message
+ "You tried to perform an operation that needs the model ID, but it's not present."
+ end
+ end
+ class CannotDeleteIndex < Error
+ def message
+ "You tried to delete an internal index used by Ohm."
+ end
+ end
+
+ class IndexNotFound < Error
+ def initialize(att)
+ @att = att
+ end
+
+ def message
+ "Index #{@att.inspect} not found."
+ end
+ end
+
@@attributes = Hash.new { |hash, key| hash[key] = [] }
@@collections = Hash.new { |hash, key| hash[key] = [] }
@@counters = Hash.new { |hash, key| hash[key] = [] }
@@indices = Hash.new { |hash, key| hash[key] = [] }
- attr_accessor :id
+ attr_writer :id
+ def id
+ @id or raise MissingID
+ end
+
# Defines a string attribute for the model. This attribute will be persisted by Redis
# as a string. Any value stored here will be retrieved in its string representation.
#
# @param name [Symbol] Name of the attribute.
def self.attribute(name)
@@ -379,11 +420,11 @@
# attribute :email
# index :email
# end
#
# # Now this is possible:
- # User.find :email, "ohm@example.com"
+ # User.find email: "ohm@example.com"
#
# @param name [Symbol] Name of the attribute to be indexed.
def self.index(att)
indices << att
end
@@ -409,11 +450,11 @@
def self.to_proc
Proc.new { |id| self[id] }
end
def self.all
- @all ||= Attributes::Set.new(db, key(:all), self)
+ @all ||= Attributes::Index.new(db, key(:all), self)
end
def self.attributes
@@attributes[self]
end
@@ -434,42 +475,20 @@
model = new(*args)
model.create
model
end
- # Find all the records matching the specified attribute-value pair.
- #
- # @example
- # Event.find(:starts_on, Date.today)
- def self.find(attrs, value)
- Attributes::Set.new(db, key(attrs, encode(value)), self)
- end
-
# Search across multiple indices and return the intersection of the sets.
#
# @example Finds all the user events for the supplied days
# event1 = Event.create day: "2009-09-09", author: "Albert"
# event2 = Event.create day: "2009-09-09", author: "Benoit"
# event3 = Event.create day: "2009-09-10", author: "Albert"
- # Event.filter(author: "Albert", day: "2009-09-09") do |events|
- # assert_equal [event1], events
- # end
- def self.filter(hash, &block)
- self.all.filter(hash, &block)
- end
-
- # Search across multiple indices and return the union of the sets.
#
- # @example Finds all the events for the supplied days
- # event1 = Event.create day: "2009-09-09"
- # event2 = Event.create day: "2009-09-10"
- # event3 = Event.create day: "2009-09-11"
- # Event.search(day: ["2009-09-09", "2009-09-10", "2009-09-011"]) do |events|
- # assert_equal [event1, event2, event3], events
- # end
- def self.search(hash, &block)
- self.all.search(hash, &block)
+ # assert_equal [event1], Event.find(author: "Albert", day: "2009-09-09")
+ def self.find(hash)
+ all.find(hash)
end
def self.encode(value)
Base64.encode64(value.to_s).gsub("\n", "")
end
@@ -478,11 +497,11 @@
@_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) }
update_attributes(attrs)
end
def new?
- !id
+ !@id
end
def create
return unless valid?
initialize_id
@@ -522,23 +541,23 @@
delete_attributes(collections)
delete_model_membership
self
end
- # Increment the attribute denoted by :att.
+ # Increment the counter denoted by :att.
#
# @param att [Symbol] Attribute to increment.
def incr(att)
- raise ArgumentError unless counters.include?(att)
+ raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
write_local(att, db.incr(key(att)))
end
- # Decrement the attribute denoted by :att.
+ # Decrement the counter denoted by :att.
#
# @param att [Symbol] Attribute to decrement.
def decr(att)
- raise ArgumentError unless counters.include?(att)
+ raise ArgumentError, "#{att.inspect} is not a counter." unless counters.include?(att)
write_local(att, db.decr(key(att)))
end
def attributes
self.class.attributes
@@ -556,11 +575,11 @@
self.class.indices
end
def ==(other)
other.kind_of?(self.class) && other.key == key
- rescue ModelIsNew
+ rescue MissingID
false
end
# Lock the object before ejecuting the block, and release it once the block is done.
def mutex
@@ -572,24 +591,23 @@
def inspect
everything = (attributes + collections + counters).map do |att|
value = begin
send(att)
- rescue ModelIsNew
+ rescue MissingID
nil
end
[att, value.inspect]
end
- "#<#{self.class}:#{id || "?"} #{everything.map {|e| e.join("=") }.join(" ")}>"
+ "#<#{self.class}:#{new? ? "?" : id} #{everything.map {|e| e.join("=") }.join(" ")}>"
end
protected
def key(*args)
- raise ModelIsNew if new?
self.class.key(id, *args)
end
def write
attributes.each { |att| write_remote(att, send(att)) }
@@ -608,11 +626,11 @@
def self.exists?(id)
db.sismember(key(:all), id)
end
def initialize_id
- self.id = db.incr(self.class.key("id"))
+ self.id = db.incr(self.class.key("id")).to_s
end
def db
Ohm.redis
end
@@ -671,11 +689,11 @@
def write_local(att, value)
@_attributes[att] = value
end
def read_remote(att)
- id && db.get(key(att))
+ db.get(key(att)) unless new?
end
def write_remote(att, value)
value.nil? ?
db.del(key(att)) :
@@ -692,11 +710,16 @@
attrs.map do |att|
read_remote(att)
end
end
+ def self.index_key_for(att, value)
+ raise IndexNotFound, att unless indices.include?(att)
+ key(att, encode(value))
+ end
+
def index_key_for(att, value)
- self.class.key(att, self.class.encode(value))
+ self.class.index_key_for(att, value)
end
# Lock the object so no other instances can modify it.
# @see Model#mutex
def lock!