Skip to content

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse files

First commit

Connects to Redis, constructs sets, and intersects them
  • Loading branch information...
commit 99ca2638dcb03c8689b240058e93102a2a243c2c 0 parents
@chasetopher chasetopher authored
2  .gitignore
@@ -0,0 +1,2 @@
+.bin
+Gemfile.lock
1  .rbenv-version
@@ -0,0 +1 @@
+1.9.3-p0
10 Gemfile
@@ -0,0 +1,10 @@
+source "http://rubygems.org"
+
+gemspec
+
+gem "ruby-debug", :platform => :ruby_18
+# Needed for using ruby-debug on 1.9
+gem "ruby-debug19", :platform => :ruby_19
+# Needed for ruby-debug when running on 1.9.3
+gem 'linecache19', :git => 'git://github.com/mark-moseley/linecache', :platform => :ruby_19
+gem 'ruby-debug-base19x', '~> 0.11.30.pre4', :platform => :ruby_19
22 lib/list_master.rb
@@ -0,0 +1,22 @@
+require 'redis'
+require 'redis-namespace'
+
+module ListMaster
+
+ extend self
+
+ # Accepts a Redis object
+ def redis=(redis)
+ @redis = Redis::Namespace.new :list_master, :redis => redis
+ end
+
+ # Returns the current Redis connection. If none has been created, create default
+ def redis
+ return @redis if @redis
+ self.redis = Redis.connect
+ self.redis
+ end
+
+end
+
+require 'list_master/base'
115 lib/list_master/base.rb
@@ -0,0 +1,115 @@
+# ListMaster::Base
+#
+# This is a base class for an object that maintains redis zsets for an ActiveRecord model.
+# A list of sets are defined in a mini DSL.
+#
+# For example, let's say you have a model Item with attributes 'power_level and 'category'.
+# You would define the ListMaster for this as follows:
+#
+# class ItemListMaster < ListMaster::Base
+# model 'Item'
+#
+# set 'power', :attribute => 'power_level'
+# set 'category'
+# end
+#
+# Now when ItemListMaster.new.process is called, the following sorted sets will be put in redis:
+#
+# power (items scored by there 'power_level' attribute)
+# category:category_one (all items have zero score)
+# category:category_two
+# category:category_three (...and so on for every value of 'category')
+#
+# These sorted sets simply hold ids to the objects that they represent collections of.
+#
+# You can then ask for arrays of ids for items that are in multiple sets:
+#
+# a = ItemListMaster.new
+# a.process
+# a.intersect 'power', 'category:category_one' #=> Array of ids of Items in 'category_one' ordered by 'power_level'
+
+module ListMaster
+ class Base
+
+ class << self
+ #
+ # Associating this list master with a model
+ #
+ def model model_name
+ @@model = model_name.constantize
+ end
+
+ #
+ # Defining sets to maintain
+ #
+ @@sets = []
+ def set *args
+ options = {
+ :attribute => nil,
+ :descending => nil
+ }.merge(args.extract_options!)
+ name = args.first
+ @@sets << {
+ name: name,
+ score: options[:attribute] || nil,
+ descending: options[:descending] || nil
+ }
+ end
+ end
+
+ #
+ # This instance's redis namespace
+ #
+ def redis
+ @redis ||= Redis::Namespace.new self.class.name.underscore, :redis => ListMaster.redis
+ end
+
+ #
+ # Goes through every record of the model and adds the id to every relevant set
+ #
+ def process
+ @@model.find_each do |obj|
+ @@sets.each do |set|
+ if set[:score]
+ score = obj.read_attribute(set[:score]).to_score
+ score *= -1 if set[:descending]
+ redis.zadd set[:name], score, obj.id
+ else
+ attribute = obj.read_attribute(set[:name]) # used for name of set
+ redis.zadd "#{set[:name]}:#{attribute}", 0, obj.id
+ end
+ end
+ end
+ end
+
+ #
+ # Takes a sequence of list names to intersect
+ # Also has options :limit (default 10) and :offset (default 0)
+ #
+ # Returns an Array of integer ids
+ #
+ def intersect *args
+ options = args.extract_options!
+ limit = options[:limit] || 10
+ offset = options[:offset] || 0
+
+ args = args.map { |a| 'list_master:item_list_master:' + a }
+
+ redis.zinterstore 'list_master:item_list_master:out', args if args.count > 1
+
+ redis.zrange('out', offset, offset + limit).map(&:to_i)
+ end
+ end
+end
+
+class Object
+ def to_score
+ if respond_to? :to_i
+ to_i
+ elsif is_a? Date
+ to_time.to_i
+ else
+ raise 'unable to convert #{self.inspect} to a zset score'
+ end
+ end
+end
3  lib/list_master/version.rb
@@ -0,0 +1,3 @@
+module ListMaster
+ VERSION = '0.0.1'
+end
27 list_master.gemspec
@@ -0,0 +1,27 @@
+lib = File.expand_path('../lib/', __FILE__)
+$:.unshift lib unless $:.include?(lib)
+
+require 'list_master/version'
+
+Gem::Specification.new do |s|
+ s.name = "list_master"
+ s.version = ListMaster::VERSION
+ s.platform = Gem::Platform::RUBY
+ s.authors = ["The I18n Team <3"]
+ s.email = ["tech_ops@change.org"]
+ s.homepage = "http://github.com/change/list_master"
+ s.summary = %q{A redis solution for presenting paginated, scoped lists of models}
+ s.description = %q{It is not finished}
+
+ s.files = Dir.glob("{bin,lib}/**/*") + %w(LICENSE README.md)
+ s.require_path = 'lib'
+
+ s.required_rubygems_version = ">= 1.3.6"
+
+ s.add_dependency "rails", "~> 3.0.0"
+ s.add_dependency "redis"
+ s.add_dependency "redis-namespace"
+
+ s.add_development_dependency "rspec-rails"
+ s.add_development_dependency "sqlite3"
+end
30 spec/fixtures.rb
@@ -0,0 +1,30 @@
+#
+# Create db and table
+#
+
+TEST_DB = 'tmp/testdb.sqlite3'
+FileUtils.rm_f TEST_DB
+
+SQLite3::Database.new(TEST_DB) do |db| db.execute_batch <<-SQL
+ CREATE TABLE items (
+ id INTEGER PRIMARY KEY,
+ name TEXT,
+ category TEXT,
+ created_at DATE,
+ updated_at DATE
+ );
+ SQL
+end
+
+#
+# An example model
+#
+
+ActiveRecord::Base.establish_connection adapter: 'sqlite3', database: TEST_DB
+
+class Item < ActiveRecord::Base
+end
+
+Item.create! name: 'foo', category: 'a', :created_at => 2.days.ago # id: 1
+Item.create! name: 'bar', category: 'b', :created_at => 1.days.ago # id: 2
+Item.create! name: 'baz', category: 'b', :created_at => 30.seconds.ago # id: 3
49 spec/item_list_master_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+class ItemListMaster < ListMaster::Base
+ model 'Item'
+
+ set 'recent', :attribute => 'created_at', :descending => true
+ set 'category'
+end
+
+describe ItemListMaster do
+
+ before do
+ @master = ItemListMaster.new
+ end
+
+ describe "#process" do
+
+ before do
+ @master.process
+ end
+
+ it 'should generate a zero priority zset for every attribute value for every declared set without priorty' do
+ @master.redis.type('category:a').should == 'zset'
+ @master.redis.type('category:b').should == 'zset'
+ @master.redis.zrange('category:b', 0, -1).map(&:to_i).to_set.should == Set.new([2, 3])
+ @master.redis.zscore('category:b', 2).to_i.should == 0
+ @master.redis.zscore('category:b', 3).to_i.should == 0
+ end
+
+ it 'should generate a zset for every declared set with priority' do
+ @master.redis.type('recent').should == 'zset'
+ @master.redis.zrange('recent', 0, -1).map(&:to_i).should == [3, 2, 1]
+ end
+
+ describe "#intersect" do
+
+ it 'should return an array of ids that are in both lists' do
+ @master.intersect('recent', 'category:b').should == [3, 2]
+ end
+
+ it 'should accept limit and offset' do
+ @master.intersect('recent', 'category:b', :limit => 1, :offset => 1).should == [2]
+ end
+
+ end
+
+ end
+
+end
17 spec/list_master_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe ListMaster do
+
+ describe "#redis" do
+
+ it 'should return a redis namespace' do
+ ListMaster.redis.class.should == Redis::Namespace
+ ListMaster.redis.ping.should be_present
+
+ ListMaster.redis.set 'foo', 'bar'
+ ListMaster.redis.get('foo').should be_eql 'bar'
+ end
+
+ end
+
+end
20 spec/spec_helper.rb
@@ -0,0 +1,20 @@
+require 'rails/all'
+require 'rspec-rails'
+require 'sqlite3'
+
+require 'list_master'
+
+require 'fixtures'
+
+ListMaster.redis = Redis.connect :db => 9
+ListMaster.redis.flushdb
+
+RSpec.configure do |config|
+ config.after(:each) do
+ ListMaster.redis.flushdb
+ end
+
+ config.after(:suite) do
+ FileUtils.rm TEST_DB
+ end
+end
2  tmp/.gitignore
@@ -0,0 +1,2 @@
+*
+!.gitignore
Please sign in to comment.
Something went wrong with that request. Please try again.