# encoding: UTF-8 require "base64" require "redis" require "nest" 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") module Ohm # Provides access to the _Redis_ database. It is highly recommended that you # use this sparingly, and only if you really know what you're doing. # # The better way to access the _Redis_ database and do raw _Redis_ # commands would be one of the following: # # 1. Use {Ohm::Model.key} or {Ohm::Model#key}. So if the name of your # model is *Post*, it would be *Post.key* or the protected method # *#key* which should be used within your *Post* model. # # 2. Use {Ohm::Model.db} or {Ohm::Model#db}. Although this is also # accessible, it is much cleaner and terse to use {Ohm::Model.key}. # # @example # # class Post < Ohm::Model # def comment_ids # key[:comments].zrange(0, -1) # end # # def add_comment_id(id) # key[:comments].zadd(Time.now.to_i, id) # end # # def remove_comment_id(id) # # Let's use the db style here just to demonstrate. # db.zrem key[:comments], id # end # end # # Post.key[:latest].sadd(1) # Post.key[:latest].smembers == ["1"] # # => true # # Post.key[:latest] == "Post:latest" # # => true # # p = Post.create # p.comment_ids == [] # # => true # # p.add_comment_id(101) # p.comment_ids == ["101"] # # => true # # p.remove_comment_id(101) # p.comment_ids == [] # # => true def self.redis threaded[:redis] ||= connection(*options) end # Assign a new _Redis_ connection. Internally used by {Ohm.connect} # to clear the cached _Redis_ instance. # # If you're looking to change the connection or reconnect with different # parameters, try {Ohm.connect} or {Ohm::Model.connect}. # @see connect # @see Model.connect # @param connection [Redis] an instance created using `Redis.new`. def self.redis=(connection) threaded[:redis] = connection end # @private Used internally by Ohm for thread safety. def self.threaded Thread.current[:ohm] ||= {} end # Connect to a _Redis_ database. # # It is also worth mentioning that you can pass in a *URI* e.g. # # Ohm.connect :url => "redis://127.0.0.1:6379/0" # # Note that the value *0* refers to the database number for the given # _Redis_ instance. # # Also you can use {Ohm.connect} without any arguments. The behavior will # be as follows: # # # Connect to redis://127.0.0.1:6379/0 # Ohm.connect # # # Connect to redis://10.0.0.100:22222/5 # ENV["REDIS_URL"] = "redis://10.0.0.100:22222/5" # Ohm.connect # # @param options [{Symbol => #to_s}] An options hash. # @see file:README.html#connecting Ohm.connect options documentation. # # @example Connect to a database in port 6380. # Ohm.connect(:port => 6380) def self.connect(*options) self.redis = nil @options = options end # @private Return a connection to Redis. # # This is a wrapper around Redis.connect(options) def self.connection(*options) Redis.connect(*options) end # @private Stores the connection options for Ohm.redis. def self.options @options = [] unless defined? @options @options end # Clear the database. You typically use this only during testing, # or when you seed your site. # # @see http://code.google.com/p/redis/wiki/FlushdbCommand FLUSHDB in the # Redis Command Reference. def self.flush redis.flushdb end # The base class of all *Ohm* errors. Can be used as a catch all for # Ohm related errors. class Error < StandardError; end # This is the class that you need to extend in order to define your # own models. # # Probably the most magic happening within {Ohm::Model} is the catching # of {Ohm::Model.const_missing} exceptions to allow the use of constants # even before they are defined. # # @example # # class Post < Ohm::Model # reference :author, User # no User definition yet! # end # # class User < Ohm::Model # end # # @see Model.const_missing class Model # Wraps a model name for lazy evaluation. class Wrapper < BasicObject # Allows you to use a constant even before it is defined. This solves # the issue of having to require inter-project dependencies in a very # simple and "magic-free" manner. # # Example of how it was done before Wrapper existed: # # require "./app/models/user" # require "./app/models/comment" # # class Post < Ohm::Model # reference :author, User # list :comments, Comment # end # # Now, you can simply do the following: # class Post < Ohm::Model # reference :author, User # list :comments, Comment # end # # @example # # module Commenting # def self.included(base) # base.list :comments, Ohm::Model::Wrapper.new(:Comment) { # Object.const_get(:Comment) # } # end # end # # # In your classes: # class Post < Ohm::Model # include Commenting # end # # class Comment < Ohm::Model # end # # p = Post.create # p.comments.empty? # # => true # # p.comments.push(Comment.create) # p.comments.size == 1 # # => true # # @param [Symbol, String] name Canonical name of wrapped class. # @param [#to_proc] block Closure for getting the name of the constant. def initialize(name, &block) @name = name @caller = ::Kernel.caller[2] @block = block class << self def method_missing(method_id, *args) ::Kernel.raise( ::NoMethodError, "You tried to call %s#%s, but %s is not defined on %s" % [ @name, method_id, @name, @caller ] ) end end end # Used as a convenience for wrapping an existing constant into a # {Ohm::Model::Wrapper wrapper object}. # # This is used extensively within the library for points where a user # defined class (e.g. _Post_, _User_, _Comment_) is expected. # # You can also use this if you need to do uncommon things, such as # creating your own {Ohm::Model::Set Set}, {Ohm::Model::List List}, etc. # # (*NOTE:* Keep in mind that the following code is given only as an # educational example, and is in no way prescribed as good design.) # # class User < Ohm::Model # end # # User.create(:id => "1001") # # Ohm.redis.sadd("myset", 1001) # # key = Ohm::Key.new("myset", Ohm.redis) # set = Ohm::Model::Set.new(key, Ohm::Model::Wrapper.wrap(User)) # # [User[1001]] == set.all.to_a # # => true # # @see http://ohm.keyvalue.org/tutorials/chaining Chaining Ohm Sets def self.wrap(object) object.class == self ? object : new(object.inspect) { object } end # Evaluates the passed block in {Ohm::Model::Wrapper#initialize}. # # @return [Class] The wrapped class. def unwrap @block.call end # Since {Ohm::Model::Wrapper} is a subclass of _BasicObject_ we have # to manually declare this. # # @return [Wrapper] def class Wrapper end # @return [String] A string describing this lazy object. def inspect "" end end # Defines the base implementation for all enumerable types in Ohm, # which includes {Ohm::Model::Set Sets}, {Ohm::Model::List Lists} and # {Ohm::Model::Index Indices}. class Collection include Enumerable # An instance of {Ohm::Key}. attr :key # A subclass of {Ohm::Model}. attr :model # @param [Key] key A key which includes a _Redis_ connection. # @param [Ohm::Model::Wrapper] model A wrapped subclass of {Ohm::Model}. def initialize(key, model) @key = key @model = model.unwrap end # Adds an instance of {Ohm::Model} to this collection. # # @param [#id] model A model with an ID. def add(model) self << model end # Sort this collection using the ID by default, or an attribute defined # in the elements of this collection. # # *NOTE:* It is worth mentioning that if you want to sort by a specific # attribute instead of an ID, you would probably want to use # {Ohm::Model::Collection#sort_by sort_by} instead. # # @example # class Post < Ohm::Model # attribute :title # end # # p1 = Post.create(:title => "Alpha") # p2 = Post.create(:title => "Beta") # p3 = Post.create(:title => "Gamma") # # [p1, p2, p3] == Post.all.sort.to_a # # => true # # [p3, p2, p1] == Post.all.sort(:order => "DESC").to_a # # => true # # [p1, p2, p3] == Post.all.sort(:by => "Post:*->title", # :order => "ASC ALPHA").to_a # # => true # # [p3, p2, p1] == Post.all.sort(:by => "Post:*->title", # :order => "DESC ALPHA").to_a # # => true # # @see file:README.html#sorting Sorting in the README. # @see http://code.google.com/p/redis/wiki/SortCommand SORT in the # Redis Command Reference. def sort(options = {}) return [] unless key.exists opts = options.dup opts[:start] ||= 0 opts[:limit] = [opts[:start], opts[:limit]] if opts[:limit] key.sort(opts).map(&model) end # Sort the model instances by the given attribute. # # @example Sorting elements by name: # # User.create :name => "B" # User.create :name => "A" # # user = User.all.sort_by(:name, :order => "ALPHA").first # user.name == "A" # # => true # # @see file:README.html#sorting Sorting in the README. def sort_by(att, options = {}) return [] unless key.exists opts = options.dup opts.merge!(:by => model.key["*->#{att}"]) if opts[:get] key.sort(opts.merge(:get => model.key["*->#{opts[:get]}"])) else sort(opts) end end # Delete this collection. # # @example # # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # post = Post.create # post.comments << Comment.create # # post.comments.size == 1 # # => true # # post.comments.clear # post.comments.size == 0 # # => true # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the Redis # Command Reference. def clear key.del end # Simultaneously clear and add all models. This wraps all operations # in a MULTI EXEC block to make the whole operation atomic. # # @example # # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # post = Post.create # post.comments << Comment.create(:id => 100) # # post.comments.map(&:id) == ["100"] # # => true # # comments = (101..103).to_a.map { |i| Comment.create(:id => i) } # # post.comments.replace(comments) # post.comments.map(&:id) == ["101", "102", "103"] # # => true # # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC # in the Redis Command Reference. def replace(models) model.db.multi do clear models.each { |model| add(model) } end end # @return [true, false] Whether or not this collection is empty. def empty? !key.exists end # @return [Array] Array representation of this collection. def to_a all end end # Provides a Ruby-esque interface to a _Redis_ *SET*. The *SET* is assumed # to be composed of ids which maps to {#model}. class Set < Collection # An implementation which relies on *SMEMBERS* and yields an instance # of {#model}. # # @example # # class Author < Ohm::Model # set :poems, Poem # end # # class Poem < Ohm::Model # end # # neruda = Author.create # neruda.poems.add(Poem.create) # # neruda.poems.each do |poem| # # do something with the poem # end # # # if you look at the source, you'll quickly see that this can # # easily be achieved by doing the following: # # neruda.poems.key.smembers.each do |id| # poem = Poem[id] # # do something with the poem # end # # @see http://code.google.com/p/redis/wiki/SmembersCommand SMEMBERS # in Redis Command Reference. def each(&block) key.smembers.each { |id| block.call(model.to_proc[id]) } end # Convenient way to scope access to a predefined set, useful for access # control. # # @example # # class User < Ohm::Model # set :photos, Photo # end # # class Photo < Ohm::Model # end # # @user = User.create # @user.photos.add(Photo.create(:id => "101")) # @user.photos.add(Photo.create(:id => "102")) # # Photo.create(:id => "500") # # @user.photos[101] == Photo[101] # # => true # # @user.photos[500] == nil # # => true # # @param [#to_s] id Any id existing within this set. # @return [Ohm::Model, nil] The model if it exists. def [](id) model[id] if key.sismember(id) end # Adds a model to this set. # # @param [#id] model Typically an instance of an {Ohm::Model} subclass. # # @see http://code.google.com/p/redis/wiki/SaddCommand SADD in Redis # Command Reference. def <<(model) key.sadd(model.id) end alias add << # Thin Ruby interface wrapper for *SCARD*. # # @return [Fixnum] The total number of members for this set. # @see http://code.google.com/p/redis/wiki/ScardCommand SCARD in Redis # Command Reference. def size key.scard end # Thin Ruby interface wrapper for *SREM*. # # @param [#id] member a member of this set. # @see http://code.google.com/p/redis/wiki/SremCommand SREM in Redis # Command Reference. def delete(member) key.srem(member.id) end # Array representation of this set. # # @example # # class Author < Ohm::Model # set :posts, Post # end # # class Post < Ohm::Model # end # # author = Author.create # author.posts.add(Author.create(:id => "101")) # author.posts.add(Author.create(:id => "102")) # # author.posts.all.is_a?(Array) # # => true # # author.posts.all.include?(Author[101]) # # => true # # author.posts.all.include?(Author[102]) # # => true # # @return [Array] All members of this set. def all key.smembers.map(&model) end # Allows you to find members of this set which fits the given criteria. # # @example # # class Post < Ohm::Model # attribute :title # attribute :tags # # index :title # index :tag # # def tag # tags.split(/\s+/) # end # end # # post = Post.create(:title => "Ohm", :tags => "ruby ohm redis") # Post.all.is_a?(Ohm::Model::Set) # # => true # # Post.all.find(:tag => "ruby").include?(post) # # => true # # # Post.find is actually just a wrapper around Post.all.find # Post.find(:tag => "ohm", :title => "Ohm").include?(post) # # => true # # Post.find(:tag => ["ruby", "python"]).empty? # # => true # # # Alternatively, you may choose to chain them later on. # ruby = Post.find(:tag => "ruby") # ruby.find(:title => "Ohm").include?(post) # # => true # # @param [Hash] options A hash of key value pairs. # @return [Ohm::Model::Set] A set satisfying the filter passed. def find(options) source = keys(options) target = source.inject(key.volatile) { |chain, other| chain + other } apply(:sinterstore, key, source, target) end # Similar to find except that it negates the criteria. # # @example # class Post < Ohm::Model # attribute :title # end # # ohm = Post.create(:title => "Ohm") # ruby = Post.create(:title => "Ruby") # # Post.except(:title => "Ohm").include?(ruby) # # => true # # Post.except(:title => "Ohm").size == 1 # # => true # # @param [Hash] options A hash of key value pairs. # @return [Ohm::Model::Set] A set satisfying the filter passed. def except(options) source = keys(options) target = source.inject(key.volatile) { |chain, other| chain - other } apply(:sdiffstore, key, source, target) end # Returns by default the lowest id value for this set. You may also # pass in options similar to {#sort}. # # @example # # class Post < Ohm::Model # attribute :title # end # # p1 = Post.create(:id => "101", :title => "Alpha") # p2 = Post.create(:id => "100", :title => "Beta") # p3 = Post.create(:id => "99", :title => "Gamma") # # Post.all.is_a?(Ohm::Model::Set) # # => true # # p3 == Post.all.first # # => true # # p1 == Post.all.first(:order => "DESC") # # => true # # p1 == Post.all.first(:by => :title, :order => "ASC ALPHA") # # => true # # # just ALPHA also means ASC ALPHA, for brevity. # p1 == Post.all.first(:by => :title, :order => "ALPHA") # # => true # # p3 == Post.all.first(:by => :title, :order => "DESC ALPHA") # # => true # # @param [Hash] options Sort options hash. # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if this # set is empty. # # @see file:README.html#sorting Sorting in the README. def first(options = {}) opts = options.dup opts.merge!(:limit => 1) if opts[:by] sort_by(opts.delete(:by), opts).first else sort(opts).first end end # Ruby-like interface wrapper around *SISMEMBER*. # # @param [#id] model Typically an {Ohm::Model} instance. # # @return [true, false] Whether or not the {Ohm::Model model} instance # is a member of this set. # # @see http://code.google.com/p/redis/wiki/SismemberCommand SISMEMBER # in Redis Command Reference. def include?(model) key.sismember(model.id) end def inspect "#" end protected # @private def apply(operation, key, source, target) target.send(operation, key, *source) Set.new(target, Wrapper.wrap(model)) end # @private # # Transform a hash of attribute/values into an array of keys. def keys(hash) [].tap do |keys| hash.each do |key, values| values = [values] unless values.kind_of?(Array) values.each do |v| keys << model.index_key_for(key, v) end end end end end class Index < Set # This method is here primarily as an optimization. Let's say you have # the following model: # # class Post < Ohm::Model # attribute :title # index :title # end # # ruby = Post.create(:title => "ruby") # redis = Post.create(:title => "redis") # # Post.key[:all].smembers == [ruby.id, redis.id] # # => true # # Post.index_key_for(:title, "ruby").smembers == [ruby.id] # # => true # # Post.index_key_for(:title, "redis").smembers == [redis.id] # # => true # # If we want to search for example all `Posts` entitled "ruby" or # "redis", then it doesn't make sense to do an INTERSECTION with # `Post.key[:all]` since it would be redundant. # # The implementation of {Ohm::Model::Index#find} avoids this redundancy. # # @see Ohm::Model::Set#find find in Ohm::Model::Set. # @see Ohm::Model.find find in Ohm::Model. def find(options) keys = keys(options) return super(options) if keys.size > 1 Set.new(keys.first, Wrapper.wrap(model)) end end # Provides a Ruby-esque interface to a _Redis_ *LIST*. The *LIST* is # assumed to be composed of ids which maps to {#model}. class List < Collection # An implementation which relies on *LRANGE* and yields an instance # of {#model}. # # @example # # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # post = Post.create # post.comments.add(Comment.create) # post.comments.add(Comment.create) # # post.comments.each do |comment| # # do something with the comment # end # # # reading the source reveals that this is achieved by doing: # post.comments.key.lrange(0, -1).each do |id| # comment = Comment[id] # # do something with the comment # end # # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE # in Redis Command Reference. def each(&block) key.lrange(0, -1).each { |id| block.call(model.to_proc[id]) } end # Thin wrapper around *RPUSH*. # # @example # # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # p = Post.create # p.comments << Comment.create # # @param [#id] model Typically an {Ohm::Model} instance. # # @see http://code.google.com/p/redis/wiki/RpushCommand RPUSH # in Redis Command Reference. 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. # # @example # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # post = Post.create # # 10.times { post.comments << Comment.create } # # post.comments[0] == Comment[1] # # => true # # post.comments[0, 4] == (1..5).map { |i| Comment[i] } # # => true # # post.comments[0, 4] == post.comments[0..4] # # => true # # post.comments.all == post.comments[0, -1] # # => true # # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE # in Redis Command Reference. def [](index, limit = nil) case [index, limit] when Pattern[Fixnum, Fixnum] then key.lrange(index, limit).collect { |id| model.to_proc[id] } when Pattern[Range, nil] then key.lrange(index.first, index.last).collect { |id| model.to_proc[id] } when Pattern[Fixnum, nil] then model[key.lindex(index)] end end # Convience method for doing list[0], similar to Ruby's Array#first # method. # # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list # is empty. def first self[0] end # Returns the model at the tail of this list, while simultaneously # removing it from the list. # # @return [Ohm::Model, nil] an {Ohm::Model} instance or nil if the list # is empty. # # @see http://code.google.com/p/redis/wiki/LpopCommand RPOP # in Redis Command Reference. def pop model[key.rpop] end # Returns the model at the head of this list, while simultaneously # removing it from the list. # # @return [Ohm::Model, nil] An {Ohm::Model} instance or nil if the list # is empty. # # @see http://code.google.com/p/redis/wiki/LpopCommand LPOP # in Redis Command Reference. def shift model[key.lpop] end # Prepends an {Ohm::Model} instance at the beginning of this list. # # @param [#id] model Typically an {Ohm::Model} instance. # # @see http://code.google.com/p/redis/wiki/RpushCommand LPUSH # in Redis Command Reference. def unshift(model) key.lpush(model.id) end # Returns an array representation of this list, with elements of the # array being an instance of {#model}. # # @return [Array] Instances of {Ohm::Model}. def all key.lrange(0, -1).map(&model) end # Thin Ruby interface wrapper for *LLEN*. # # @return [Fixnum] The total number of elements for this list. # # @see http://code.google.com/p/redis/wiki/LlenCommand LLEN in Redis # Command Reference. def size key.llen end # Ruby-like interface wrapper around *LRANGE*. # # @param [#id] model Typically an {Ohm::Model} instance. # # @return [true, false] Whether or not the {Ohm::Model} instance is # an element of this list. # # @see http://code.google.com/p/redis/wiki/LrangeCommand LRANGE # in Redis Command Reference. def include?(model) key.lrange(0, -1).include?(model.id) end def inspect "#" end end # All validations that need access to the _Redis_ database go here. # As of this writing, {Ohm::Model::Validations#assert_unique} is the only # assertion contained within this module. module Validations include Ohm::Validations # Validates that the attribute or array of attributes are unique. For # this, an index of the same kind must exist. # # @overload assert_unique :name # Validates that the name attribute is unique. # @overload assert_unique [:street, :city] # Validates that the :street and :city pair is unique. def assert_unique(atts, error = [atts, :not_unique]) indices = Array(atts).map { |att| index_key_for(att, send(att)) } result = db.sinter(*indices) assert result.empty? || !new? && result.include?(id.to_s), error end end include Validations # Raised when you try and get the *id* of an {Ohm::Model} without an id. # # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # ex = nil # begin # Post.new.id # rescue Exception => e # ex = e # end # # ex.kind_of?(Ohm::Model::MissingID) # # => true # # This is also one of the most common errors you'll be faced with when # you're new to {Ohm} coming from an ActiveRecord background, where you # are used to just assigning associations even before the base model is # persisted. # # # following from the example above: # post = Post.new # # ex = nil # begin # post.comments << Comment.new # rescue Exception => e # ex = e # end # # ex.kind_of?(Ohm::Model::MissingID) # # => true # # # Correct way: # post = Post.new # # if post.save # post.comments << Comment.create # end class MissingID < Error def message "You tried to perform an operation that needs the model ID, " + "but it's not present." end end # Raised when you try and do an {Ohm::Model::Set#find} operation and use # a key which you did not define as an index. # # class Post < Ohm::Model # attribute :title # end # # post = Post.create(:title => "Ohm") # # ex = nil # begin # Post.find(:title => "Ohm") # rescue Exception => e # ex = e # end # # ex.kind_of?(Ohm::Model::IndexNotFound) # # => true # # To correct this problem, simply define a _:title_ *index* in your class. # # class Post < Ohm::Model # attribute :title # index :title # 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] = [] } 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. # # If you're looking to have typecasting built in, you may want to look at # Ohm::Typecast in Ohm::Contrib. # # @param name [Symbol] Name of the attribute. # @see http://cyx.github.com/ohm-contrib/doc/Ohm/Typecast.html def self.attribute(name) define_method(name) do read_local(name) end define_method(:"#{name}=") do |value| write_local(name, value) end attributes << name unless attributes.include?(name) end # Defines a counter attribute for the model. This attribute can't be # assigned, only incremented or decremented. It will be zero by default. # # @param [Symbol] name Name of the counter. def self.counter(name) define_method(name) do read_local(name).to_i end counters << name unless counters.include?(name) end # Defines a list attribute for the model. It can be accessed only after # the model instance is created, or if you assign an :id during object # construction. # # @example # # class Post < Ohm::Model # list :comments, Comment # end # # class Comment < Ohm::Model # end # # # WRONG!!! # post = Post.new # post.comments << Comment.create # # # Right :-) # post = Post.create # post.comments << Comment.create # # # Alternative way if you want to have custom ids. # post = Post.new(:id => "my-id") # post.comments << Comment.create # post.create # # @param [Symbol] name Name of the list. 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 # retreival order is irrelevant, and operations like union, join, and # membership checks are important. # # @param [Symbol] name Name of the set. 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. # # If you want to find a model instance by some attribute value, then an # index for that attribute must exist. # # @example # # class User < Ohm::Model # attribute :email # index :email # end # # # Now this is possible: # User.find :email => "ohm@example.com" # # @param [Symbol] name Name of the attribute to be indexed. def self.index(att) indices << att unless indices.include?(att) end # Define a reference to another object. # # @example # # class Comment < Ohm::Model # attribute :content # reference :post, Post # end # # @post = Post.create :content => "Interesting stuff" # # @comment = Comment.create(:content => "Indeed!", :post => @post) # # @comment.post.content # # => "Interesting stuff" # # @comment.post = Post.create(:content => "Wonderful stuff") # # @comment.post.content # # => "Wonderful stuff" # # @comment.post.update(:content => "Magnific stuff") # # @comment.post.content # # => "Magnific stuff" # # @comment.post = nil # # @comment.post # # => nil # # @see file:README.html#references References Explained. # @see Ohm::Model.collection def self.reference(name, model) model = Wrapper.wrap(model) reader = :"#{name}_id" writer = :"#{name}_id=" attributes << reader unless attributes.include?(reader) index reader define_memoized_method(name) do model.unwrap[send(reader)] end define_method(:"#{name}=") do |value| @_memo.delete(name) send(writer, value ? value.id : nil) end define_method(reader) do read_local(reader) end define_method(writer) do |value| @_memo.delete(name) write_local(reader, value) end end # Define a collection of objects which have a # {Ohm::Model.reference reference} to this model. # # class Comment < Ohm::Model # attribute :content # reference :post, Post # end # # class Post < Ohm::Model # attribute :content # collection :comments, Comment # reference :author, Person # end # # class Person < Ohm::Model # attribute :name # # # When the name of the reference cannot be inferred, # # you need to specify it in the third param. # collection :posts, Post, :author # end # # @person = Person.create :name => "Albert" # @post = Post.create :content => "Interesting stuff", # :author => @person # @comment = Comment.create :content => "Indeed!", :post => @post # # @post.comments.first.content # # => "Indeed!" # # @post.author.name # # => "Albert" # # *Important*: Please note that even though a collection is a # {Ohm::Model::Set set}, you should not add or remove objects from this # collection directly. # # @see Ohm::Model.reference # @param name [Symbol] Name of the collection. # @param model [Constant] Model where the reference is defined. # @param reference [Symbol] Reference as defined in the associated # model. # # @see file:README.html#collections Collections Explained. def self.collection(name, model, reference = to_reference) model = Wrapper.wrap(model) define_method(name) { model.unwrap.find(:"#{reference}_id" => send(:id)) } end # Used by {Ohm::Model.collection} to infer the reference. # # @return [Symbol] Representation of this class in an all-lowercase # format, separated by underscores and demodulized. def self.to_reference name.to_s. match(/^(?:.*::)*(.*)$/)[1]. gsub(/([a-z\d])([A-Z])/, '\1_\2'). downcase.to_sym end # @private def self.define_memoized_method(name, &block) define_method(name) do @_memo[name] ||= instance_eval(&block) end end # Allows you to find an {Ohm::Model} instance by its *id*. # # @param [#to_s] id The id of the model you want to find. # @return [Ohm::Model, nil] The instance of Ohm::Model or nil of it does # not exist. def self.[](id) new(:id => id) if id && exists?(id) end # @private Used for conveniently doing [1, 2].map(&Post) for example. def self.to_proc lambda { |id| new(:id => id) } end # Returns a {Ohm::Model::Set set} containing all the members of a given # class. # # @example # # class Post < Ohm::Model # end # # post = Post.create # # Post.all.include?(post) # # => true # # post.delete # # Post.all.include?(post) # # => false def self.all Ohm::Model::Index.new(key[:all], Wrapper.wrap(self)) end # All the defined attributes within a class. # @see Ohm::Model.attribute def self.attributes @@attributes[self] end # All the defined counters within a class. # @see Ohm::Model.counter def self.counters @@counters[self] end # All the defined collections within a class. This will be comprised of # all {Ohm::Model::Set sets} and {Ohm::Model::List lists} defined within # your class. # # @example # class Post < Ohm::Model # set :authors, Author # list :comments, Comment # end # # Post.collections == [:authors, :comments] # # => true # # @see Ohm::Model.list # @see Ohm::Model.set def self.collections @@collections[self] end # All the defined indices within a class. # @see Ohm::Model.index def self.indices @@indices[self] end # Convenience method to create and return the newly created object. # # @example # # class Post < Ohm::Model # attribute :title # end # # post = Post.create(:title => "A new post") # # @param [Hash] args attribute-value pairs for the object. # @return [Ohm::Model] an instance of the class you're trying to create. def self.create(*args) model = new(*args) model.create model 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" # # [event1] == Event.find(author: "Albert", day: "2009-09-09").to_a # # => true def self.find(hash) unless hash.kind_of?(Hash) raise ArgumentError, "You need to supply a hash with filters. " + "If you want to find by ID, use #{self}[id] instead." end all.find(hash) end # Encode a value, making it safe to use as a key. Internally used by # {Ohm::Model.index_key_for} to canonicalize the indexed values. # # @param [#to_s] value Any object you want to be able to use as a key. # @return [String] A string which is safe to use as a key. # @see Ohm::Model.index_key_for def self.encode(value) Base64.encode64(value.to_s).gsub("\n", "") end # Constructor for all subclasses of {Ohm::Model}, which optionally # takes a Hash of attribute value pairs. # # Starting with Ohm 0.1.0, you can use custom ids instead of being forced # to use auto incrementing numeric ids, but keep in mind that you have # to pass in the preferred id during object initialization. # # @example # # class User < Ohm::Model # end # # class Post < Ohm::Model # attribute :title # reference :user, User # end # # user = User.create # p1 = Post.new(:title => "Redis", :user_id => user.id) # p1.save # # p1.user_id == user.id # # => true # # p1.user == user # # => true # # # You can also just pass the actual User object, which is the better # # way to do it: # Post.new(:title => "Different way", :user => user).user == user # # => true # # # Let's try and generate custom ids # p2 = Post.new(:id => "ohm-redis-library", :title => "Lib") # p2 == Post["ohm-redis-library"] # # => true # # @param [Hash] attrs Attribute value pairs. def initialize(attrs = {}) @id = nil @_memo = {} @_attributes = Hash.new { |hash, key| hash[key] = read_remote(key) } update_attributes(attrs) end # @return [true, false] Whether or not this object has an id. def new? !@id end # Create this model if it passes all validations. # # @return [Ohm::Model, nil] The newly created object or nil if it fails # validation. def create return unless valid? initialize_id mutex do create_model_membership write add_to_indices end end # Create or update this object based on the state of #new?. # # @return [Ohm::Model, nil] The saved object or nil if it fails # validation. def save return create if new? return unless valid? mutex do write update_indices end end # Update this object, optionally accepting new attributes. # # @param [Hash] attrs Attribute value pairs to use for the updated # version # @return [Ohm::Model, nil] The updated object or nil if it fails # validation. def update(attrs) update_attributes(attrs) save end # Locally update all attributes without persisting the changes. # Internally used by {Ohm::Model#initialize} and {Ohm::Model#update} # to set attribute value pairs. # # @param [Hash] attrs Attribute value pairs. def update_attributes(attrs) attrs.each do |key, value| send(:"#{key}=", value) end end # Delete this object from the _Redis_ datastore, ensuring that all # indices, attributes, collections, etc are also deleted with it. # # @return [Ohm::Model] Returns a reference of itself. def delete delete_from_indices delete_attributes(collections) unless collections.empty? delete_model_membership self end # Increment the counter denoted by :att. # # @param [Symbol] att Attribute to increment. # @param [Fixnum] count An optional increment step to use. def incr(att, count = 1) unless counters.include?(att) raise ArgumentError, "#{att.inspect} is not a counter." end write_local(att, key.hincrby(att, count)) end # Decrement the counter denoted by :att. # # @param [Symbol] att Attribute to decrement. # @param [Fixnum] count An optional decrement step to use. def decr(att, count = 1) incr(att, -count) end # Export the id and errors of the object. The `to_hash` takes the opposite # approach of providing all the attributes and instead favors a white # listed approach. # # @example # # person = Person.create(:name => "John Doe") # person.to_hash == { :id => '1' } # # => true # # # if the person asserts presence of name, the errors will be included # person = Person.create(:name => "John Doe") # person.name = nil # person.valid? # # => false # # person.to_hash == { :id => '1', :errors => [[:name, :not_present]] } # # => true # # # for cases where you want to provide white listed attributes just do: # # class Person < Ohm::Model # def to_hash # super.merge(:name => name) # end # end # # # now we have the name when doing a to_hash # person = Person.create(:name => "John Doe") # person.to_hash == { :id => '1', :name => "John Doe" } # # => true def to_hash attrs = {} attrs[:id] = id unless new? attrs[:errors] = errors unless errors.empty? attrs end # Returns the JSON representation of the {#to_hash} for this object. # Defining a custom {#to_hash} method will also affect this and return # a corresponding JSON representation of whatever you have in your # {#to_hash}. # # @example # require "json" # # class Post < Ohm::Model # attribute :title # # def to_hash # super.merge(:title => title) # end # end # # p1 = Post.create(:title => "Delta Force") # p1.to_hash == { :id => "1", :title => "Delta Force" } # # => true # # p1.to_json == "{\"id\":\"1\",\"title\":\"Delta Force\"}" # # => true # # @return [String] The JSON representation of this object defined in # terms of {#to_hash}. def to_json(*args) to_hash.to_json(*args) end # Convenience wrapper for {Ohm::Model.attributes}. def attributes self.class.attributes end # Convenience wrapper for {Ohm::Model.counters}. def counters self.class.counters end # Convenience wrapper for {Ohm::Model.collections}. def collections self.class.collections end # Convenience wrapper for {Ohm::Model.indices}. def indices self.class.indices end # Implementation of equality checking. Equality is defined by two simple # rules: # # 1. They have the same class. # 2. They have the same key (_Redis_ key e.g. Post:1 == Post:1). # # @return [true, false] Whether or not the passed object is equal. def ==(other) other.kind_of?(self.class) && other.key == key rescue MissingID false end alias :eql? :== # Allows you to safely use an instance of {Ohm::Model} as a key in a # Ruby hash without running into weird scenarios. # # @example # # class Post < Ohm::Model # end # # h = {} # p1 = Post.new # h[p1] = "Ruby" # h[p1] == "Ruby" # # => true # # p1.save # h[p1] == "Ruby" # # => false # # @return [Fixnum] An integer representing this object to be used # as the index for hashes in Ruby. def hash new? ? super : key.hash end # Lock the object before executing the block, and release it once the # block is done. # # This is used during {#create} and {#save} to ensure that no race # conditions occur. # # @see http://code.google.com/p/redis/wiki/SetnxCommand SETNX in the # Redis Command Reference. def mutex lock! yield self ensure unlock! end # Returns everything, including {Ohm::Model.attributes attributes}, # {Ohm::Model.collections collections}, {Ohm::Model.counters counters}, # and the id of this object. # # Useful for debugging and for doing irb work. def inspect everything = (attributes + collections + counters).map do |att| value = begin send(att) rescue MissingID nil end [att, value.inspect] end sprintf("#<%s:%s %s>", self.class, new? ? "?" : id, everything.map {|e| e.join("=") }.join(" ") ) end # Makes the model connect to a different Redis instance. This is useful # for scaling a large application, where one model can be stored in a # different Redis instance, and some other groups of models can be # in another Redis instance. # # This approach of splitting models is a lot simpler than doing a # distributed *Redis* solution and may well be the right solution for # certain cases. # # @example # # class Post < Ohm::Model # connect :port => 6380, :db => 2 # # attribute :body # end # # # Since these settings are usually environment-specific, # # you may want to call this method from outside of the class # # definition: # Post.connect(:port => 6380, :db => 2) # # @see file:README.html#connecting Ohm.connect options documentation. def self.connect(options = {}) @options = options end # @return [Ohm::Key] A key scoped to the model which uses this object's # id. # # @see http://github.com/soveran/nest The Nest library. def key self.class.key[id] end protected attr_writer :id # Write all the attributes and counters of this object. The operation # is actually a 2-step process: # # 1. Delete the current key, e.g. Post:2. # 2. Set all of the new attributes (using HMSET). # # The DEL and HMSET operations are wrapped in a MULTI EXEC block to ensure # the atomicity of the write operation. # # @see http://code.google.com/p/redis/wiki/DelCommand DEL in the # Redis Command Reference. # @see http://code.google.com/p/redis/wiki/HmsetCommand HMSET in the # Redis Command Reference. # @see http://code.google.com/p/redis/wiki/MultiExecCommand MULTI EXEC # in the Redis Command Reference. def write unless (attributes + counters).empty? atts = (attributes + counters).inject([]) { |ret, att| value = send(att).to_s ret.push(att, value) if not value.empty? ret } db.multi do key.del key.hmset(*atts.flatten) if atts.any? end end end # Write a single attribute both locally and remotely. It's very important # to know that this method skips validation checks, therefore you must # ensure data integrity and validity in your application code. # # @param [Symbol, String] att The name of the attribute to write. # @param [#to_s] value The value of the attribute to write. # # @see http://code.google.com/p/redis/wiki/HdelCommand HDEL in the # Redis Command Reference. # @see http://code.google.com/p/redis/wiki/HsetCommand HSET in the # Redis Command Reference. def write_remote(att, value) write_local(att, value) if value.to_s.empty? key.hdel(att) else key.hset(att, value) end end # Wraps any missing constants lazily in {Ohm::Model::Wrapper} delaying # the evaluation of constants until they are actually needed. # # @see Ohm::Model::Wrapper # @see http://en.wikipedia.org/wiki/Lazy_evaluation Lazy evaluation def self.const_missing(name) wrapper = Wrapper.new(name) { const_get(name) } # Allow others to hook to const_missing. begin super(name) rescue NameError end wrapper end private # Provides access to the Redis database. This is shared accross all models and instances. def self.db return Ohm.redis unless defined?(@options) Redis.connect(@options) end def self.db=(connection) Ohm.threaded[self] = connection end # Allows you to do key manipulations scoped solely to your class. def self.key Key.new(self, db) end def self.exists?(id) key[:all].sismember(id) end # The meat of the ID generation code for Ohm. For cases where you want to # customize ID generation (i.e. use GUIDs or Base62 ids) then you simply # override this method in your model. # # @example # # module UUID # def self.new # `uuidgen`.strip # end # end # # class Post < Ohm::Model # # private # def initialize_id # @id ||= UUID.new # end # end # def initialize_id @id ||= self.class.key[:id].incr.to_s end def db self.class.db end def delete_attributes(atts) db.del(*atts.map { |att| key[att] }) end def create_model_membership self.class.all << self end def delete_model_membership key.del self.class.all.delete(self) end def update_indices delete_from_indices add_to_indices end def add_to_indices indices.each do |att| next add_to_index(att) unless collection?(send(att)) send(att).each { |value| add_to_index(att, value) } end end def collection?(value) self.class.collection?(value) end def self.collection?(value) value.kind_of?(Enumerable) && value.kind_of?(String) == false end def add_to_index(att, value = send(att)) index = index_key_for(att, value) index.sadd(id) key[:_indices].sadd(index) end def delete_from_indices key[:_indices].smembers.each do |index| db.srem(index, id) end key[:_indices].del end # Get the value of a specific attribute. An important fact about # attributes in Ohm is that they are all loaded lazily. # # @param [Symbol] att The attribute you you want to get. # @return [String] The value of att. def read_local(att) @_attributes[att] end # Write the value of an attribute locally, without persisting it. # # @param [Symbol] att The attribute you want to set. # @param [#to_s] value The value of the attribute you want to set. def write_local(att, value) @_attributes[att] = value end # Used internally be the @_attributes hash to lazily load attributes # when you need them. You may also use this in your code if you know what # you are doing. # # @param [Symbol] att The attribute you you want to get. # @return [String] The value of att. def read_remote(att) unless new? value = key.hget(att) value.respond_to?(:force_encoding) ? value.force_encoding("UTF-8") : value end end # Read attributes en masse locally. def read_locals(attrs) attrs.map do |att| send(att) end end # Read attributes en masse remotely. def read_remotes(attrs) attrs.map do |att| read_remote(att) end end # Get the index name for a specific index and value pair. The return value # is an instance of {Ohm::Key}, which you can readily do Redis operations # on. # # @example # # class Post < Ohm::Model # attribute :title # index :title # end # # post = Post.create(:title => "Foo") # key = Post.index_key_for(:title, "Foo") # key == "Post:title:Rm9v" # key.scard == 1 # key.smembers == [post.id] # # => true # # @param [Symbol] name The name of the index. # @param [#to_s] value The value for the index. # @return [Ohm::Key] A {Ohm::Key key} which you can treat as a string, # but also do Redis operations on. def self.index_key_for(name, value) raise IndexNotFound, name unless indices.include?(name) key[name][encode(value)] end # Thin wrapper around {Ohm::Model.index_key_for}. def index_key_for(att, value) self.class.index_key_for(att, value) end # Lock the object so no other instances can modify it. # This method implements the design pattern for locks # described at: http://code.google.com/p/redis/wiki/SetnxCommand # # @see Model#mutex def lock! until key[:_lock].setnx(Time.now.to_f + 0.5) next unless timestamp = key[:_lock].get sleep(0.1) and next unless lock_expired?(timestamp) break unless timestamp = key[:_lock].getset(Time.now.to_f + 0.5) break if lock_expired?(timestamp) end end # Release the lock. # @see Model#mutex def unlock! key[:_lock].del end def lock_expired?(timestamp) timestamp.to_f < Time.now.to_f end end end