Skip to content
This repository has been archived by the owner on Apr 24, 2023. It is now read-only.

Commit

Permalink
Refactored and enhanced finder methods, which now support finding a p…
Browse files Browse the repository at this point in the history
…articular index, ordering, etc.
  • Loading branch information
ysbaddaden committed Jan 26, 2012
1 parent 3e90f0d commit ec888c1
Show file tree
Hide file tree
Showing 3 changed files with 139 additions and 33 deletions.
12 changes: 11 additions & 1 deletion lib/redis_model/base.rb
@@ -1,7 +1,10 @@
# IMPROVE: UUID identifiers?
# IMPROVE: counters
module RedisModel
class RecordNotFound < StandardError
class RedisModelError < StandardError
end

class RecordNotFound < RedisModelError
end

class Base
Expand Down Expand Up @@ -31,5 +34,12 @@ def new_record?
def ==(other)
!new_record? && self.class == other.class && self.id == other.id
end

def self.instanciate(attributes)
record = new(attributes)
record.id = attributes[:id] || attributes['id']
record.persisted!
record
end
end
end
127 changes: 95 additions & 32 deletions lib/redis_model/finders.rb
Expand Up @@ -11,64 +11,127 @@ def hkey(attr_name)
key("*->#{attr_name}")
end

def all
_find_all(:id)
def exists?(id)
connection.exists(key(id))
end

def find(id)
# Finds records.
#
# Examples:
#
# posts = Post.find :all
# posts = Post.find :all, :offset => 20, :limit => 10
#
# The following calls are equivalent, and will return the same record:
#
# TODO: it actually doesn't work!
#
# post = Post.find :first, :by => :position, :order => :desc
# post = Post.find :last, :by => :position, :order => :asc
#
# Finds all comments for a given post:
#
# comments = Comment.find :all, :index => [ :post_id, 123 ], :by => :approved_at
#
# Options:
#
# - <tt>:index</tt> - either an indexed attribute name, or an array of [ attr_name, value ] (defaults to <tt>:id</tt>).
# - <tt>:by</tt> - an attribute name to sort by or <tt>:nosort</tt> to not sort result (defaults to <tt>:nosort</tt>).
# - <tt>:order</tt> - either <tt>:asc</tt>, <tt>:desc</tt> or <tt>:alpha</tt> or an array of <tt>:asc</tt> or <tt>:desc</tt> with <tt>:alpha</tt>.
# - <tt>:limit</tt> - an array of [ offset, limit ]
# - <tt>:select</tt> - an array of attribute names to get (defaults to all attributes)
#
def find(*args)
if args.first.is_a?(Symbol)
options = args.extract_options!

index = (options[:index] || :id)
index = [ index ] unless index.kind_of?(Array)
limit = options[:limit]
by = options[:by] unless options[:by].blank?

if options[:order].blank?
order = [ :asc ] unless by.blank?
else
order = options[:order]
order = [ order ] unless order.kind_of?(Array)
end
unless order.nil?
order << :alpha unless order.include?(:alpha) && by.nil? && [ :integer, :float ].include?(schema[by][:type])
order = order.join(" ").upcase
end

case args.first
when :all
when :first
return find_with_range(index, 0, 0) if by.nil? && order.blank?
limit = [ 0, 0 ]
when :last
return find_with_range(index, -1, -1) if by.nil? && order.blank?
limit = [ -1, -1 ]
else
raise RedisModelError.new("unknown find method #{args.first.inspect}")
end

fields = (options[:select] || attribute_names).sort
results = connection.sort(index_key(*index),
:get => fields.collect { |k| hkey(k) },
:by => by ? hkey(by) : :nosort,
:order => order,
:limit => limit
)
collection = []
results.each_slice(fields.size) do |values|
collection << instanciate(Hash[ *fields.zip(values).flatten ])
end

case args.first
when :all
collection
when :first, :last
collection.first
end
else
find_with_id(*args)
end
end

def find_with_range(index, offset, limit)
ids = connection.lrange(index_key(*index), offset, limit)
instanciate(connection.hgetall(key(ids.first))) if ids.any?
end

def find_with_id(id)
attributes = connection.hgetall(key(id))
raise RedisModel::RecordNotFound.new("No such #{model_name} with id: #{id}") if attributes.empty?
instanciate(attributes)
end

def exists?(id)
connection.exists(key(id))
def all
find(:all)
end

def first
ids = connection.lrange(index_key(:id), 0, 0)
instanciate(connection.hgetall(key(ids.first))) if ids.any?
find(:first)
end

def last
ids = connection.lrange(index_key(:id), -1, -1)
instanciate(connection.hgetall(key(ids.first))) if ids.any?
find(:last)
end

def method_missing(method_name, *args)
if method_name.to_s =~ /^find_(all_by|by)_(.*)$/
case $1
when 'all_by'
_find_all($2, args.first)
find :all, :index => [ $2, args.first ]
when 'by'
# find :first, :index = [ $2, args.first ]
super
end
else
super
end
end

protected
def instanciate(attributes)
record = new(attributes)
record.id = attributes[:id] || attributes['id']
record.persisted!
record
end

def _find_all(attr_name, value = nil)
keys = attribute_names.sort
results = connection.sort(
index_key(attr_name, value),
:by => :nosort,
:get => keys.collect { |k| hkey(k) }
)
collection = []
results.each_slice(keys.size) do |values|
collection << instanciate(Hash[ *keys.zip(values).flatten ])
end
collection
end
end

def reload
Expand Down
33 changes: 33 additions & 0 deletions test/finders_test.rb
Expand Up @@ -13,6 +13,39 @@ def test_find
assert_raises(RedisModel::RecordNotFound) { Post.find(12346890) }
end

def test_find_all
assert_equal [ posts(:welcome), posts(:post1) ], Post.find(:all)
assert_equal [], Row.find(:all)
end

def test_find_all_with_index
assert_equal [ posts(:welcome) ], Post.find(:all, :index => [ :approved, true ])
assert_equal [ posts(:post1) ], Post.find(:all, :index => [ :approved, false ])
end

def test_find_all_with_select
posts = Post.find(:all, :select => [ :id, :title ])
assert_equal [ posts(:welcome).id, posts(:post1).id ], posts.collect(&:id)
assert_equal [ posts(:welcome).title, posts(:post1).title ], posts.collect(&:title)
assert_equal [ nil, nil ], posts.collect(&:body)
end

def test_find_all_with_order
assert_equal [ posts(:post1), posts(:welcome) ], Post.find(:all, :by => :title)
end

def test_find_all_with_limit
end

def test_find_first
end

def test_find_first_with_order
end

def test_find_last_with_order
end

def test_all
assert_equal [ posts(:welcome).id, posts(:post1).id ], Post.all.collect(&:id)
assert_equal [], Row.all
Expand Down

0 comments on commit ec888c1

Please sign in to comment.