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!