redpear 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- data/lib/redpear.rb +23 -0
- data/lib/redpear/column.rb +53 -0
- data/lib/redpear/concern.rb +10 -0
- data/lib/redpear/connection.rb +17 -0
- data/lib/redpear/core_ext/stringify_keys.rb +17 -0
- data/lib/redpear/expiration.rb +27 -0
- data/lib/redpear/finders.rb +51 -0
- data/lib/redpear/index.rb +24 -0
- data/lib/redpear/machinist.rb +50 -0
- data/lib/redpear/model.rb +109 -0
- data/lib/redpear/namespace.rb +79 -0
- data/lib/redpear/nest.rb +13 -0
- data/lib/redpear/persistence.rb +134 -0
- data/lib/redpear/schema.rb +34 -0
- data/lib/redpear/schema/collection.rb +48 -0
- metadata +147 -0
    
        data/lib/redpear.rb
    ADDED
    
    | @@ -0,0 +1,23 @@ | |
| 1 | 
            +
            require "nest"
         | 
| 2 | 
            +
            require "redis"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Redpear
         | 
| 5 | 
            +
             | 
| 6 | 
            +
              def self.autoload(const, path = nil)
         | 
| 7 | 
            +
                path ||= "redpear/#{const.to_s.downcase}"
         | 
| 8 | 
            +
                super const, path
         | 
| 9 | 
            +
              end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              autoload :Column
         | 
| 12 | 
            +
              autoload :Concern
         | 
| 13 | 
            +
              autoload :Connection
         | 
| 14 | 
            +
              autoload :Expiration
         | 
| 15 | 
            +
              autoload :Finders
         | 
| 16 | 
            +
              autoload :Index
         | 
| 17 | 
            +
              autoload :Model
         | 
| 18 | 
            +
              autoload :Namespace
         | 
| 19 | 
            +
              autoload :Nest
         | 
| 20 | 
            +
              autoload :Persistence
         | 
| 21 | 
            +
              autoload :Schema
         | 
| 22 | 
            +
             | 
| 23 | 
            +
            end
         | 
| @@ -0,0 +1,53 @@ | |
| 1 | 
            +
            class Redpear::Column < String
         | 
| 2 | 
            +
              attr_reader :type, :model
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              # Creates a new column.
         | 
| 5 | 
            +
              # @param [Redpear::Model] model the model the column is associated with
         | 
| 6 | 
            +
              # @param [String] name the column name
         | 
| 7 | 
            +
              # @param [Symbol] type the column type (:string (default), :counter, :integer, :timestamp)
         | 
| 8 | 
            +
              def initialize(model, name, type = nil)
         | 
| 9 | 
            +
                super name.to_s
         | 
| 10 | 
            +
                @model = model
         | 
| 11 | 
            +
                @type  = type.to_sym if type
         | 
| 12 | 
            +
              end
         | 
| 13 | 
            +
             | 
| 14 | 
            +
              # Casts a value to a type
         | 
| 15 | 
            +
              #
         | 
| 16 | 
            +
              # @param value the value to cast
         | 
| 17 | 
            +
              # @param [Symbol, #optional] type the type to cast to, defaults to the column type
         | 
| 18 | 
            +
              # @return the casted value
         | 
| 19 | 
            +
              def type_cast(value, type = self.type)
         | 
| 20 | 
            +
                case type
         | 
| 21 | 
            +
                when :counter
         | 
| 22 | 
            +
                  value.to_i
         | 
| 23 | 
            +
                when :integer
         | 
| 24 | 
            +
                  Kernel::Integer(value) rescue nil if value
         | 
| 25 | 
            +
                when :timestamp
         | 
| 26 | 
            +
                  value = type_cast(value, :integer)
         | 
| 27 | 
            +
                  Time.at(value) if value
         | 
| 28 | 
            +
                else
         | 
| 29 | 
            +
                  value
         | 
| 30 | 
            +
                end
         | 
| 31 | 
            +
              end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              # @return [String] the column name
         | 
| 34 | 
            +
              def name
         | 
| 35 | 
            +
                to_s
         | 
| 36 | 
            +
              end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
              # @return [Boolean] true if the column is readable
         | 
| 39 | 
            +
              def readable?
         | 
| 40 | 
            +
                true
         | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
             | 
| 43 | 
            +
              # @return [Boolean] true if the column is writable
         | 
| 44 | 
            +
              def writable?
         | 
| 45 | 
            +
                type != :counter
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
              # @return [Boolean] true if the column is an index
         | 
| 49 | 
            +
              def index?
         | 
| 50 | 
            +
                is_a? Redpear::Index
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            module Redpear::Connection
         | 
| 2 | 
            +
              extend Redpear::Concern
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              module ClassMethods
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                # @return [Redis] the current connection
         | 
| 7 | 
            +
                def connection
         | 
| 8 | 
            +
                  @connection ||= (superclass.respond_to?(:connection) ? superclass.connection : Redis.current)
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                # @param [Redis] the connection to assign
         | 
| 12 | 
            +
                def connection=(value)
         | 
| 13 | 
            +
                  @connection = value
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              end
         | 
| 17 | 
            +
            end
         | 
| @@ -0,0 +1,17 @@ | |
| 1 | 
            +
            # Hash stringify_keys extension. "Borrowed" from ActiveSupport.
         | 
| 2 | 
            +
            class Hash
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              # Return a new hash with all keys converted to strings.
         | 
| 5 | 
            +
              def stringify_keys
         | 
| 6 | 
            +
                dup.stringify_keys!
         | 
| 7 | 
            +
              end
         | 
| 8 | 
            +
             | 
| 9 | 
            +
              # Destructively convert all keys to strings.
         | 
| 10 | 
            +
              def stringify_keys!
         | 
| 11 | 
            +
                keys.each do |key|
         | 
| 12 | 
            +
                  self[key.to_s] = delete(key) unless key.is_a?(String)
         | 
| 13 | 
            +
                end
         | 
| 14 | 
            +
                self
         | 
| 15 | 
            +
              end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
            end unless Hash.new.respond_to?(:symbolize_keys)
         | 
| @@ -0,0 +1,27 @@ | |
| 1 | 
            +
            module Redpear::Expiration
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # Expires the record.
         | 
| 4 | 
            +
              # @param [Time, Integer] either a Time or an Integer period (in seconds)
         | 
| 5 | 
            +
              def expire(value)
         | 
| 6 | 
            +
                return false unless persisted?
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                case value
         | 
| 9 | 
            +
                when Time
         | 
| 10 | 
            +
                  nest.expireat(value.to_i)
         | 
| 11 | 
            +
                when Integer
         | 
| 12 | 
            +
                  nest.expire(value)
         | 
| 13 | 
            +
                when String
         | 
| 14 | 
            +
                  value = Kernel::Integer(value) rescue nil
         | 
| 15 | 
            +
                  expire(value)
         | 
| 16 | 
            +
                else
         | 
| 17 | 
            +
                  false
         | 
| 18 | 
            +
                end
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
              # @return [Integer] the period this record has to live.
         | 
| 22 | 
            +
              # May return -1 for non-expiring records and nil for non-persisted records.
         | 
| 23 | 
            +
              def ttl
         | 
| 24 | 
            +
                nest.ttl if persisted?
         | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
            end
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            module Redpear::Finders
         | 
| 2 | 
            +
              extend Redpear::Concern
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              module ClassMethods
         | 
| 5 | 
            +
             | 
| 6 | 
            +
                # @return [Array] the IDs of all existing records
         | 
| 7 | 
            +
                def members
         | 
| 8 | 
            +
                  mb_nest.smembers
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                # @return [Integer] the number of total records
         | 
| 12 | 
            +
                def count
         | 
| 13 | 
            +
                  members.size
         | 
| 14 | 
            +
                end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                # @return [Array] all records
         | 
| 17 | 
            +
                def all
         | 
| 18 | 
            +
                  members.map {|id| find(id) }
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                # Finds a single record.
         | 
| 22 | 
            +
                #
         | 
| 23 | 
            +
                # @param id the ID of the record to retrieve
         | 
| 24 | 
            +
                # @param [Hash] options additional options
         | 
| 25 | 
            +
                # @option :lazy defaults to true, set to false to load the record instantly
         | 
| 26 | 
            +
                # @return [Redpear::Model] a record, or nil when not found
         | 
| 27 | 
            +
                def find(id, options = {})
         | 
| 28 | 
            +
                  record = instantiate('id' => id.to_s) # Initialize
         | 
| 29 | 
            +
                  if record.nest.exists         # Do we have a record key?
         | 
| 30 | 
            +
                    record.refresh_attributes if options[:lazy] == false
         | 
| 31 | 
            +
                    record
         | 
| 32 | 
            +
                  else                          # Must be an expired or orphaned one
         | 
| 33 | 
            +
                    record.destroy              # Destroy (removes from mb_nest set)
         | 
| 34 | 
            +
                    nil
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
                end
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                # @param id the ID to check
         | 
| 39 | 
            +
                # @return [Boolean] true or false
         | 
| 40 | 
            +
                def exists?(id)
         | 
| 41 | 
            +
                  mb_nest.sismember(id)
         | 
| 42 | 
            +
                end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                def instantiate(*a)
         | 
| 45 | 
            +
                  new(*a).tap do |instance|
         | 
| 46 | 
            +
                    instance.send :instance_variable_set, :@__loaded__, false
         | 
| 47 | 
            +
                  end
         | 
| 48 | 
            +
                end
         | 
| 49 | 
            +
             | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,24 @@ | |
| 1 | 
            +
            class Redpear::Index < Redpear::Column
         | 
| 2 | 
            +
             | 
| 3 | 
            +
              # @return [Redpear::Nest] the namespace of the index. Example:
         | 
| 4 | 
            +
              #
         | 
| 5 | 
            +
              #   index = Comment.columns.lookup["post_id"]
         | 
| 6 | 
            +
              #   index.namespace # => "comments:post_id"
         | 
| 7 | 
            +
              #
         | 
| 8 | 
            +
              def namespace
         | 
| 9 | 
            +
                model.namespace[self]
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              # @return [Redpear::Nest] the nest for a specific value. Example:
         | 
| 13 | 
            +
              #
         | 
| 14 | 
            +
              #   index = Comment.columns.lookup["post_id"]
         | 
| 15 | 
            +
              #   index.nest(123) # => "comments:post_id:123"
         | 
| 16 | 
            +
              #   index.nest(nil) # => nil
         | 
| 17 | 
            +
              #   index.nest("")  # => nil
         | 
| 18 | 
            +
              #
         | 
| 19 | 
            +
              def nest(value)
         | 
| 20 | 
            +
                return nil if value.nil? || (value.respond_to?(:empty?) && value.empty?)
         | 
| 21 | 
            +
                namespace[value]
         | 
| 22 | 
            +
              end
         | 
| 23 | 
            +
             | 
| 24 | 
            +
            end
         | 
| @@ -0,0 +1,50 @@ | |
| 1 | 
            +
            require 'machinist'
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            # Machinist module for your tests/specs. Example:
         | 
| 4 | 
            +
            #
         | 
| 5 | 
            +
            #   # spec/support/blueprints.rb
         | 
| 6 | 
            +
            #   require "redpear/machinist"
         | 
| 7 | 
            +
            #
         | 
| 8 | 
            +
            #   Post.blueprint do
         | 
| 9 | 
            +
            #     title      { "A Title" }
         | 
| 10 | 
            +
            #     created_at { 2.days.ago }
         | 
| 11 | 
            +
            #   end
         | 
| 12 | 
            +
            #
         | 
| 13 | 
            +
            module Redpear::Machinist
         | 
| 14 | 
            +
             | 
| 15 | 
            +
              class Blueprint < Machinist::Blueprint
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                def make!(attributes = {})
         | 
| 18 | 
            +
                  make(attributes).tap &:save
         | 
| 19 | 
            +
                end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                def lathe_class #:nodoc:
         | 
| 22 | 
            +
                  Lathe
         | 
| 23 | 
            +
                end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              end
         | 
| 26 | 
            +
             | 
| 27 | 
            +
              class Lathe < Machinist::Lathe
         | 
| 28 | 
            +
                protected
         | 
| 29 | 
            +
             | 
| 30 | 
            +
                def make_one_value(attribute, args)
         | 
| 31 | 
            +
                  return unless block_given?
         | 
| 32 | 
            +
                  raise_argument_error(attribute) unless args.empty?
         | 
| 33 | 
            +
                  yield
         | 
| 34 | 
            +
                end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
                def assign_attribute(key, value) #:nodoc:
         | 
| 37 | 
            +
                  @assigned_attributes[key.to_sym] = value
         | 
| 38 | 
            +
                  @object.load key => value
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              end
         | 
| 42 | 
            +
            end
         | 
| 43 | 
            +
             | 
| 44 | 
            +
            class Redpear::Model #:nodoc:
         | 
| 45 | 
            +
              extend Machinist::Machinable
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              def self.blueprint_class
         | 
| 48 | 
            +
                Redpear::Machinist::Blueprint
         | 
| 49 | 
            +
              end
         | 
| 50 | 
            +
            end
         | 
| @@ -0,0 +1,109 @@ | |
| 1 | 
            +
            require "set"
         | 
| 2 | 
            +
            require "redpear/core_ext/stringify_keys"
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            =begin
         | 
| 5 | 
            +
            Redis is a simple key/value store, hence storing structured data can be a
         | 
| 6 | 
            +
            challenge. Redpear allows you to store/find/associate "records" in a Redis DB
         | 
| 7 | 
            +
            very efficiently, minimising IO operations and storage space where possible.
         | 
| 8 | 
            +
             | 
| 9 | 
            +
            For example:
         | 
| 10 | 
            +
             | 
| 11 | 
            +
              class Post < Redpear::Model
         | 
| 12 | 
            +
                column :title
         | 
| 13 | 
            +
                column :body
         | 
| 14 | 
            +
              end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
              class Comment < Redpear::Model
         | 
| 17 | 
            +
                column :body
         | 
| 18 | 
            +
                index :post_id
         | 
| 19 | 
            +
              end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
            Let's create a post and a comment:
         | 
| 22 | 
            +
             | 
| 23 | 
            +
              post = Post.save :title => "Hi!", :body => "I'm a new post"
         | 
| 24 | 
            +
              comment = Comment.save :post_id => post.id, :body => "I like this!"
         | 
| 25 | 
            +
             | 
| 26 | 
            +
            Redpear is VERY lightweight. Compared with other ORMs, it offers raw speed at
         | 
| 27 | 
            +
            the expense of convenience.
         | 
| 28 | 
            +
            =end
         | 
| 29 | 
            +
            class Redpear::Model < Hash
         | 
| 30 | 
            +
              include Redpear::Connection
         | 
| 31 | 
            +
              include Redpear::Namespace
         | 
| 32 | 
            +
              include Redpear::Persistence
         | 
| 33 | 
            +
              include Redpear::Expiration
         | 
| 34 | 
            +
              include Redpear::Schema
         | 
| 35 | 
            +
              include Redpear::Finders
         | 
| 36 | 
            +
             | 
| 37 | 
            +
              # Ensure we can read raw level values
         | 
| 38 | 
            +
              alias_method :__fetch__, :[]
         | 
| 39 | 
            +
             | 
| 40 | 
            +
              def initialize(attrs = {})
         | 
| 41 | 
            +
                super()
         | 
| 42 | 
            +
                @__attributes__ = {}
         | 
| 43 | 
            +
                @__loaded__     = true
         | 
| 44 | 
            +
                update(attrs)
         | 
| 45 | 
            +
              end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
              # Every record needs an ID
         | 
| 48 | 
            +
              def id
         | 
| 49 | 
            +
                value = __fetch__("id")
         | 
| 50 | 
            +
                value.to_s if value
         | 
| 51 | 
            +
              end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
              # Custom comparator
         | 
| 54 | 
            +
              def ==(other)
         | 
| 55 | 
            +
                case other
         | 
| 56 | 
            +
                when Redpear::Model
         | 
| 57 | 
            +
                  other.instance_of?(self.class) && to_hash(true) == other.to_hash(true)
         | 
| 58 | 
            +
                else
         | 
| 59 | 
            +
                  super
         | 
| 60 | 
            +
                end
         | 
| 61 | 
            +
              end
         | 
| 62 | 
            +
             | 
| 63 | 
            +
              # Attribute reader with type-casting
         | 
| 64 | 
            +
              def [](name)
         | 
| 65 | 
            +
                __ensure_loaded__
         | 
| 66 | 
            +
                name = name.to_s
         | 
| 67 | 
            +
                @__attributes__[name] ||= begin
         | 
| 68 | 
            +
                  column = self.class.columns.lookup[name]
         | 
| 69 | 
            +
                  value  = super(name)
         | 
| 70 | 
            +
                  column ? column.type_cast(value) : value
         | 
| 71 | 
            +
                end
         | 
| 72 | 
            +
              end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
              # Attribute writer
         | 
| 75 | 
            +
              def []=(name, value)
         | 
| 76 | 
            +
                __ensure_loaded__
         | 
| 77 | 
            +
                name = name.to_s
         | 
| 78 | 
            +
                @__attributes__.delete(name)
         | 
| 79 | 
            +
                super
         | 
| 80 | 
            +
              end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
              # Returns a Hash with attributes
         | 
| 83 | 
            +
              def to_hash(clean = false)
         | 
| 84 | 
            +
                __ensure_loaded__
         | 
| 85 | 
            +
                attrs = clean ? reject {|_, v| v.nil? } : self
         | 
| 86 | 
            +
                {}.update(attrs)
         | 
| 87 | 
            +
              end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
              # Show information about this record
         | 
| 90 | 
            +
              def inspect
         | 
| 91 | 
            +
                __ensure_loaded__
         | 
| 92 | 
            +
                "#<#{self.class.name} #{super}>"
         | 
| 93 | 
            +
              end
         | 
| 94 | 
            +
             | 
| 95 | 
            +
              # Bulk-update attributes
         | 
| 96 | 
            +
              def update(attrs)
         | 
| 97 | 
            +
                attrs = (attrs ? attrs.stringify_keys : {})
         | 
| 98 | 
            +
                attrs["id"] = attrs["id"].to_s if attrs["id"]
         | 
| 99 | 
            +
                super
         | 
| 100 | 
            +
              end
         | 
| 101 | 
            +
              alias_method :load, :update
         | 
| 102 | 
            +
             | 
| 103 | 
            +
              private
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                def __ensure_loaded__
         | 
| 106 | 
            +
                  refresh_attributes unless @__loaded__
         | 
| 107 | 
            +
                end
         | 
| 108 | 
            +
             | 
| 109 | 
            +
            end
         | 
| @@ -0,0 +1,79 @@ | |
| 1 | 
            +
            # Namespace organization for models. Example:
         | 
| 2 | 
            +
            #
         | 
| 3 | 
            +
            #   class Comment < Model
         | 
| 4 | 
            +
            #     index :post_id
         | 
| 5 | 
            +
            #   end
         | 
| 6 | 
            +
            #   instance = Comment.save(:post_id => 2)
         | 
| 7 | 
            +
            #
         | 
| 8 | 
            +
            #   Comment.connection.keys
         | 
| 9 | 
            +
            #   # => ['comments:1', 'comments:+', 'comments:*', 'comments:post_id:2']
         | 
| 10 | 
            +
            #
         | 
| 11 | 
            +
            #   # Instance nesting
         | 
| 12 | 
            +
            #   instance.nest                  # => 'comments:1'
         | 
| 13 | 
            +
            #   instance.nest.mapped_hmget_all # => { "post_id" => "2" }
         | 
| 14 | 
            +
            #
         | 
| 15 | 
            +
            #   # Member nesting
         | 
| 16 | 
            +
            #   Comment.mb_nest               # "comments:*"
         | 
| 17 | 
            +
            #   Comment.mb_nest.smembers      # => #<Set: {1}>
         | 
| 18 | 
            +
            #
         | 
| 19 | 
            +
            #   # PK nesting
         | 
| 20 | 
            +
            #   Comment.pk_nest               # "comments:+"
         | 
| 21 | 
            +
            #   Comment.pk_nest.get           # 1 = last ID
         | 
| 22 | 
            +
            #
         | 
| 23 | 
            +
            #   # Index nesting
         | 
| 24 | 
            +
            #   Comment.columns["post_id"].nest(2) # "comments:post_id:2"
         | 
| 25 | 
            +
            #   Comment.columns["post_id"].nest(2).smembers # #<Set: {1}>
         | 
| 26 | 
            +
            #
         | 
| 27 | 
            +
            module Redpear::Namespace
         | 
| 28 | 
            +
              extend Redpear::Concern
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              module ClassMethods
         | 
| 31 | 
            +
             | 
| 32 | 
            +
                # @return [Redpear::Nest] the namespace of this model, Example:
         | 
| 33 | 
            +
                #
         | 
| 34 | 
            +
                #   Comment.namespace # => "comments":Nest
         | 
| 35 | 
            +
                #
         | 
| 36 | 
            +
                def namespace
         | 
| 37 | 
            +
                  @namespace ||= Redpear::Nest.new(scope, connection)
         | 
| 38 | 
            +
                end
         | 
| 39 | 
            +
             | 
| 40 | 
            +
                # @return [String] the scope of this model. Example:
         | 
| 41 | 
            +
                #
         | 
| 42 | 
            +
                #   Comment.scope # => "comments"
         | 
| 43 | 
            +
                #
         | 
| 44 | 
            +
                # Override if you want to use a differnet scope schema.
         | 
| 45 | 
            +
                def scope
         | 
| 46 | 
            +
                  @scope ||= "#{name.split('::').last.downcase}s"
         | 
| 47 | 
            +
                end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                # @return [Redpear::Nest] the nest for the members store. Example:
         | 
| 50 | 
            +
                #
         | 
| 51 | 
            +
                #   Comment.mb_nest # => 'comments:*'
         | 
| 52 | 
            +
                #   Comment.mb_nest.smembers # => [1, 2, 3]
         | 
| 53 | 
            +
                #
         | 
| 54 | 
            +
                def mb_nest
         | 
| 55 | 
            +
                  @mb_nest ||= namespace["*"]
         | 
| 56 | 
            +
                end
         | 
| 57 | 
            +
             | 
| 58 | 
            +
                # @return [Redpear::Nest] the nest for the primary-key incrementor. Example:
         | 
| 59 | 
            +
                #
         | 
| 60 | 
            +
                #   Comment.pk_nest # => 'comments:+'
         | 
| 61 | 
            +
                #   Comment.pk_nest.get # => 0
         | 
| 62 | 
            +
                #   Comment.pk_nest.incr # => 1
         | 
| 63 | 
            +
                #
         | 
| 64 | 
            +
                def pk_nest
         | 
| 65 | 
            +
                  @pk_nest ||= namespace["+"]
         | 
| 66 | 
            +
                end
         | 
| 67 | 
            +
             | 
| 68 | 
            +
              end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
              # @return [Redpear::Nest] the nest for the current record. Example:
         | 
| 71 | 
            +
              #
         | 
| 72 | 
            +
              #   comment.nest # => 'comments:123'
         | 
| 73 | 
            +
              #   Comment.new.nest # => 'comments:_'
         | 
| 74 | 
            +
              #
         | 
| 75 | 
            +
              def nest
         | 
| 76 | 
            +
                self.class.namespace[id || '_']
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
            end
         | 
    
        data/lib/redpear/nest.rb
    ADDED
    
    
| @@ -0,0 +1,134 @@ | |
| 1 | 
            +
            # Redpear's persistence methods
         | 
| 2 | 
            +
            module Redpear::Persistence
         | 
| 3 | 
            +
              extend Redpear::Concern
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              module ClassMethods
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                # Runs a bulk-operation.
         | 
| 8 | 
            +
                # @yield [] operations that should be run in the transaction
         | 
| 9 | 
            +
                def transaction(&block)
         | 
| 10 | 
            +
                  connection.multi(&block)
         | 
| 11 | 
            +
                end
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                # Create or update a record. Example:
         | 
| 14 | 
            +
                #
         | 
| 15 | 
            +
                #   Post.save :body => "Hello World!" # => creates a new Post
         | 
| 16 | 
            +
                #   Post.save :id => 3, :body => "Hello World!" # => updates an existing Post
         | 
| 17 | 
            +
                #
         | 
| 18 | 
            +
                def save(*args)
         | 
| 19 | 
            +
                  new(*args).tap(&:save)
         | 
| 20 | 
            +
                end
         | 
| 21 | 
            +
             | 
| 22 | 
            +
                # Destroys a record. Example:
         | 
| 23 | 
            +
                # @param id the ID of the record to destroy
         | 
| 24 | 
            +
                # @return [Redpear::Model] the destroyed record
         | 
| 25 | 
            +
                def destroy(id)
         | 
| 26 | 
            +
                  new('id' => id).tap(&:destroy)
         | 
| 27 | 
            +
                end
         | 
| 28 | 
            +
             | 
| 29 | 
            +
                # Generates the next ID
         | 
| 30 | 
            +
                def next_id
         | 
| 31 | 
            +
                  pk_nest.incr.to_s
         | 
| 32 | 
            +
                end
         | 
| 33 | 
            +
             | 
| 34 | 
            +
              end
         | 
| 35 | 
            +
             | 
| 36 | 
            +
              # Returns true for new records
         | 
| 37 | 
            +
              def new_record?
         | 
| 38 | 
            +
                !id
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              # Returns true for existing records
         | 
| 42 | 
            +
              def persisted?
         | 
| 43 | 
            +
                !new_record?
         | 
| 44 | 
            +
              end
         | 
| 45 | 
            +
             | 
| 46 | 
            +
              # Reloads the record (destructive)
         | 
| 47 | 
            +
              def reload
         | 
| 48 | 
            +
                replace self.class.find(id, :lazy => false) if persisted?
         | 
| 49 | 
            +
                self
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
             | 
| 52 | 
            +
              # Load attributes from DB (destructive)
         | 
| 53 | 
            +
              def refresh_attributes
         | 
| 54 | 
            +
                update nest.mapped_hmget(*self.class.columns.names) if persisted?
         | 
| 55 | 
            +
                @__loaded__ = true
         | 
| 56 | 
            +
                self
         | 
| 57 | 
            +
              end
         | 
| 58 | 
            +
             | 
| 59 | 
            +
              # Saves the record.
         | 
| 60 | 
            +
              #
         | 
| 61 | 
            +
              # @param [Hash] options additional options
         | 
| 62 | 
            +
              # @option options [Integer|Date] :expire expiration period or timestamp
         | 
| 63 | 
            +
              # @yield [record] Additional block, applied as part of the save transaction
         | 
| 64 | 
            +
              # @return [Redpear::Model] the saved record
         | 
| 65 | 
            +
              def save(options = {}, &block)
         | 
| 66 | 
            +
                before_save
         | 
| 67 | 
            +
                update "id" => self.class.next_id unless persisted?
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                transaction do
         | 
| 70 | 
            +
                  nest.mapped_hmset __persistable_attributes__
         | 
| 71 | 
            +
                  __relevant_sets__.each {|s| s.sadd(id) }
         | 
| 72 | 
            +
                  expire options[:expire]
         | 
| 73 | 
            +
                  yield(self) if block
         | 
| 74 | 
            +
                end
         | 
| 75 | 
            +
              ensure
         | 
| 76 | 
            +
                after_save
         | 
| 77 | 
            +
              end
         | 
| 78 | 
            +
             | 
| 79 | 
            +
              # Destroy the record.
         | 
| 80 | 
            +
              # @return [Boolean] true or false
         | 
| 81 | 
            +
              def destroy
         | 
| 82 | 
            +
                return false unless persisted?
         | 
| 83 | 
            +
             | 
| 84 | 
            +
                transaction do
         | 
| 85 | 
            +
                  nest.del
         | 
| 86 | 
            +
                  __relevant_sets__.each {|s| s.srem(id) }
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
             | 
| 89 | 
            +
                true
         | 
| 90 | 
            +
              end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
              protected
         | 
| 93 | 
            +
             | 
| 94 | 
            +
                # Run in a DB transaction, returns self
         | 
| 95 | 
            +
                def transaction(&block)
         | 
| 96 | 
            +
                  self.class.transaction(&block)
         | 
| 97 | 
            +
                  self
         | 
| 98 | 
            +
                end
         | 
| 99 | 
            +
             | 
| 100 | 
            +
                # "Cheap" callback, override in subclasses
         | 
| 101 | 
            +
                def before_save
         | 
| 102 | 
            +
                end
         | 
| 103 | 
            +
             | 
| 104 | 
            +
                # "Cheap" callback, override in subclasses
         | 
| 105 | 
            +
                def after_save
         | 
| 106 | 
            +
                end
         | 
| 107 | 
            +
             | 
| 108 | 
            +
              private
         | 
| 109 | 
            +
             | 
| 110 | 
            +
                # Attributes that can be persisted
         | 
| 111 | 
            +
                def __persistable_attributes__
         | 
| 112 | 
            +
                  result = {}
         | 
| 113 | 
            +
                  each do |key, value|
         | 
| 114 | 
            +
                    next if key == "id"
         | 
| 115 | 
            +
                    result[key] = __persistable_value__(value)
         | 
| 116 | 
            +
                  end
         | 
| 117 | 
            +
                  result
         | 
| 118 | 
            +
                end
         | 
| 119 | 
            +
             | 
| 120 | 
            +
                def __persistable_value__(value)
         | 
| 121 | 
            +
                  case value
         | 
| 122 | 
            +
                  when Time
         | 
| 123 | 
            +
                    value.to_i
         | 
| 124 | 
            +
                  else
         | 
| 125 | 
            +
                    value
         | 
| 126 | 
            +
                  end
         | 
| 127 | 
            +
                end
         | 
| 128 | 
            +
             | 
| 129 | 
            +
                # Return relevant set nests
         | 
| 130 | 
            +
                def __relevant_sets__
         | 
| 131 | 
            +
                  @__relevant_sets__ ||= [self.class.mb_nest] + self.class.columns.indices.map {|i| i.nest self[i] }.compact
         | 
| 132 | 
            +
                end
         | 
| 133 | 
            +
             | 
| 134 | 
            +
            end
         | 
| @@ -0,0 +1,34 @@ | |
| 1 | 
            +
            module Redpear::Schema
         | 
| 2 | 
            +
              extend Redpear::Concern
         | 
| 3 | 
            +
              autoload :Collection, 'redpear/schema/collection'
         | 
| 4 | 
            +
             | 
| 5 | 
            +
              module ClassMethods
         | 
| 6 | 
            +
             | 
| 7 | 
            +
                # @return [Redpear::Schema::Collection] the columns of this model
         | 
| 8 | 
            +
                def columns
         | 
| 9 | 
            +
                  @columns ||= Redpear::Schema::Collection.new
         | 
| 10 | 
            +
                end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                # @param [multiple] the column definition. Please see Redpear::Column#initialize
         | 
| 13 | 
            +
                def column(*args)
         | 
| 14 | 
            +
                  columns.column(self, *args).tap do |col|
         | 
| 15 | 
            +
                    __define_attribute_accessors__(col)
         | 
| 16 | 
            +
                  end
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
             | 
| 19 | 
            +
                # @param [multiple] the index definition. Please see Redpear::Column#initialize
         | 
| 20 | 
            +
                def index(*args)
         | 
| 21 | 
            +
                  columns.index(self, *args).tap do |col|
         | 
| 22 | 
            +
                    __define_attribute_accessors__(col)
         | 
| 23 | 
            +
                  end
         | 
| 24 | 
            +
                end
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                private
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                  def __define_attribute_accessors__(col)
         | 
| 29 | 
            +
                    define_method(col.name) { self[col.name] } if col.readable?
         | 
| 30 | 
            +
                    define_method("#{col.name}=") {|v| self[col.name] = v } if col.writable?
         | 
| 31 | 
            +
                  end
         | 
| 32 | 
            +
             | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
            end
         | 
| @@ -0,0 +1,48 @@ | |
| 1 | 
            +
            # Stores the column information
         | 
| 2 | 
            +
            class Redpear::Schema::Collection < Array
         | 
| 3 | 
            +
             | 
| 4 | 
            +
              # @param [multiple] the column definition. Please see Redpear::Column#initialize
         | 
| 5 | 
            +
              def column(*args)
         | 
| 6 | 
            +
                reset!
         | 
| 7 | 
            +
                Redpear::Column.new(*args).tap do |col|
         | 
| 8 | 
            +
                  self << col
         | 
| 9 | 
            +
                end
         | 
| 10 | 
            +
              end
         | 
| 11 | 
            +
             | 
| 12 | 
            +
              # @param [multiple] the index definition. Please see Redpear::Column#initialize
         | 
| 13 | 
            +
              def index(*args)
         | 
| 14 | 
            +
                reset!
         | 
| 15 | 
            +
                Redpear::Index.new(*args).tap do |col|
         | 
| 16 | 
            +
                  self << col
         | 
| 17 | 
            +
                end
         | 
| 18 | 
            +
              end
         | 
| 19 | 
            +
             | 
| 20 | 
            +
              # @return [Array] the names of the columns
         | 
| 21 | 
            +
              def names
         | 
| 22 | 
            +
                @names ||= lookup.keys
         | 
| 23 | 
            +
              end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
              # @return [Array] the names of the indices only
         | 
| 26 | 
            +
              def indices
         | 
| 27 | 
            +
                @indices ||= select(&:index?)
         | 
| 28 | 
            +
              end
         | 
| 29 | 
            +
             | 
| 30 | 
            +
              # @return [Hash] the column lookup, indexed by name
         | 
| 31 | 
            +
              def lookup
         | 
| 32 | 
            +
                @lookup ||= inject({}) {|r, c| r.update c.to_s => c }
         | 
| 33 | 
            +
              end
         | 
| 34 | 
            +
             | 
| 35 | 
            +
              # @param [String] the column name
         | 
| 36 | 
            +
              # @return [Redpear::Column] the column for the given name
         | 
| 37 | 
            +
              def [](name)
         | 
| 38 | 
            +
                lookup[name.to_s]
         | 
| 39 | 
            +
              end
         | 
| 40 | 
            +
             | 
| 41 | 
            +
              # Resets indexes and lookups
         | 
| 42 | 
            +
              def reset!
         | 
| 43 | 
            +
                instance_variables.each do |name|
         | 
| 44 | 
            +
                  instance_variable_set name, nil
         | 
| 45 | 
            +
                end
         | 
| 46 | 
            +
              end
         | 
| 47 | 
            +
             | 
| 48 | 
            +
            end
         | 
    
        metadata
    ADDED
    
    | @@ -0,0 +1,147 @@ | |
| 1 | 
            +
            --- !ruby/object:Gem::Specification
         | 
| 2 | 
            +
            name: redpear
         | 
| 3 | 
            +
            version: !ruby/object:Gem::Version
         | 
| 4 | 
            +
              version: 0.3.0
         | 
| 5 | 
            +
              prerelease: 
         | 
| 6 | 
            +
            platform: ruby
         | 
| 7 | 
            +
            authors:
         | 
| 8 | 
            +
            - Dimitrij Denissenko
         | 
| 9 | 
            +
            autorequire: 
         | 
| 10 | 
            +
            bindir: bin
         | 
| 11 | 
            +
            cert_chain: []
         | 
| 12 | 
            +
            date: 2011-10-24 00:00:00.000000000Z
         | 
| 13 | 
            +
            dependencies:
         | 
| 14 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 15 | 
            +
              name: redis
         | 
| 16 | 
            +
              requirement: &13539180 !ruby/object:Gem::Requirement
         | 
| 17 | 
            +
                none: false
         | 
| 18 | 
            +
                requirements:
         | 
| 19 | 
            +
                - - ~>
         | 
| 20 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 21 | 
            +
                    version: 2.2.0
         | 
| 22 | 
            +
              type: :runtime
         | 
| 23 | 
            +
              prerelease: false
         | 
| 24 | 
            +
              version_requirements: *13539180
         | 
| 25 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 26 | 
            +
              name: nest
         | 
| 27 | 
            +
              requirement: &13533640 !ruby/object:Gem::Requirement
         | 
| 28 | 
            +
                none: false
         | 
| 29 | 
            +
                requirements:
         | 
| 30 | 
            +
                - - ~>
         | 
| 31 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 32 | 
            +
                    version: 1.1.0
         | 
| 33 | 
            +
              type: :runtime
         | 
| 34 | 
            +
              prerelease: false
         | 
| 35 | 
            +
              version_requirements: *13533640
         | 
| 36 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 37 | 
            +
              name: rake
         | 
| 38 | 
            +
              requirement: &13533060 !ruby/object:Gem::Requirement
         | 
| 39 | 
            +
                none: false
         | 
| 40 | 
            +
                requirements:
         | 
| 41 | 
            +
                - - ! '>='
         | 
| 42 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 43 | 
            +
                    version: '0'
         | 
| 44 | 
            +
              type: :development
         | 
| 45 | 
            +
              prerelease: false
         | 
| 46 | 
            +
              version_requirements: *13533060
         | 
| 47 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 48 | 
            +
              name: bundler
         | 
| 49 | 
            +
              requirement: &13532580 !ruby/object:Gem::Requirement
         | 
| 50 | 
            +
                none: false
         | 
| 51 | 
            +
                requirements:
         | 
| 52 | 
            +
                - - ! '>='
         | 
| 53 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 54 | 
            +
                    version: '0'
         | 
| 55 | 
            +
              type: :development
         | 
| 56 | 
            +
              prerelease: false
         | 
| 57 | 
            +
              version_requirements: *13532580
         | 
| 58 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 59 | 
            +
              name: rspec
         | 
| 60 | 
            +
              requirement: &13531860 !ruby/object:Gem::Requirement
         | 
| 61 | 
            +
                none: false
         | 
| 62 | 
            +
                requirements:
         | 
| 63 | 
            +
                - - ! '>='
         | 
| 64 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 65 | 
            +
                    version: '0'
         | 
| 66 | 
            +
              type: :development
         | 
| 67 | 
            +
              prerelease: false
         | 
| 68 | 
            +
              version_requirements: *13531860
         | 
| 69 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 70 | 
            +
              name: fakeredis
         | 
| 71 | 
            +
              requirement: &13529820 !ruby/object:Gem::Requirement
         | 
| 72 | 
            +
                none: false
         | 
| 73 | 
            +
                requirements:
         | 
| 74 | 
            +
                - - ! '>='
         | 
| 75 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 76 | 
            +
                    version: '0'
         | 
| 77 | 
            +
              type: :development
         | 
| 78 | 
            +
              prerelease: false
         | 
| 79 | 
            +
              version_requirements: *13529820
         | 
| 80 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 81 | 
            +
              name: shoulda-matchers
         | 
| 82 | 
            +
              requirement: &13528760 !ruby/object:Gem::Requirement
         | 
| 83 | 
            +
                none: false
         | 
| 84 | 
            +
                requirements:
         | 
| 85 | 
            +
                - - ! '>='
         | 
| 86 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 87 | 
            +
                    version: '0'
         | 
| 88 | 
            +
              type: :development
         | 
| 89 | 
            +
              prerelease: false
         | 
| 90 | 
            +
              version_requirements: *13528760
         | 
| 91 | 
            +
            - !ruby/object:Gem::Dependency
         | 
| 92 | 
            +
              name: machinist
         | 
| 93 | 
            +
              requirement: &13527200 !ruby/object:Gem::Requirement
         | 
| 94 | 
            +
                none: false
         | 
| 95 | 
            +
                requirements:
         | 
| 96 | 
            +
                - - ~>
         | 
| 97 | 
            +
                  - !ruby/object:Gem::Version
         | 
| 98 | 
            +
                    version: 2.0.0.beta
         | 
| 99 | 
            +
              type: :development
         | 
| 100 | 
            +
              prerelease: false
         | 
| 101 | 
            +
              version_requirements: *13527200
         | 
| 102 | 
            +
            description: Simple, elegant & efficient ORM for Redis
         | 
| 103 | 
            +
            email: dimitrij@blacksquaremedia.com
         | 
| 104 | 
            +
            executables: []
         | 
| 105 | 
            +
            extensions: []
         | 
| 106 | 
            +
            extra_rdoc_files: []
         | 
| 107 | 
            +
            files:
         | 
| 108 | 
            +
            - lib/redpear.rb
         | 
| 109 | 
            +
            - lib/redpear/connection.rb
         | 
| 110 | 
            +
            - lib/redpear/schema/collection.rb
         | 
| 111 | 
            +
            - lib/redpear/core_ext/stringify_keys.rb
         | 
| 112 | 
            +
            - lib/redpear/concern.rb
         | 
| 113 | 
            +
            - lib/redpear/namespace.rb
         | 
| 114 | 
            +
            - lib/redpear/nest.rb
         | 
| 115 | 
            +
            - lib/redpear/finders.rb
         | 
| 116 | 
            +
            - lib/redpear/persistence.rb
         | 
| 117 | 
            +
            - lib/redpear/expiration.rb
         | 
| 118 | 
            +
            - lib/redpear/column.rb
         | 
| 119 | 
            +
            - lib/redpear/model.rb
         | 
| 120 | 
            +
            - lib/redpear/index.rb
         | 
| 121 | 
            +
            - lib/redpear/schema.rb
         | 
| 122 | 
            +
            - lib/redpear/machinist.rb
         | 
| 123 | 
            +
            homepage: https://github.com/bsm/redpear
         | 
| 124 | 
            +
            licenses: []
         | 
| 125 | 
            +
            post_install_message: 
         | 
| 126 | 
            +
            rdoc_options: []
         | 
| 127 | 
            +
            require_paths:
         | 
| 128 | 
            +
            - lib
         | 
| 129 | 
            +
            required_ruby_version: !ruby/object:Gem::Requirement
         | 
| 130 | 
            +
              none: false
         | 
| 131 | 
            +
              requirements:
         | 
| 132 | 
            +
              - - ! '>='
         | 
| 133 | 
            +
                - !ruby/object:Gem::Version
         | 
| 134 | 
            +
                  version: 1.8.7
         | 
| 135 | 
            +
            required_rubygems_version: !ruby/object:Gem::Requirement
         | 
| 136 | 
            +
              none: false
         | 
| 137 | 
            +
              requirements:
         | 
| 138 | 
            +
              - - ! '>='
         | 
| 139 | 
            +
                - !ruby/object:Gem::Version
         | 
| 140 | 
            +
                  version: 1.6.0
         | 
| 141 | 
            +
            requirements: []
         | 
| 142 | 
            +
            rubyforge_project: 
         | 
| 143 | 
            +
            rubygems_version: 1.8.10
         | 
| 144 | 
            +
            signing_key: 
         | 
| 145 | 
            +
            specification_version: 3
         | 
| 146 | 
            +
            summary: Redpear, a Redis ORM
         | 
| 147 | 
            +
            test_files: []
         |