├── .gitignore ├── .rspec ├── .travis.yml ├── CHANGELOG.md ├── Gemfile ├── LICENSE ├── README.md ├── Rakefile ├── UPGRADING.md ├── lib ├── redis-classy.rb ├── redis_classy.rb └── redis_classy │ └── version.rb ├── redis-classy.gemspec └── spec ├── redis_classy_spec.rb └── spec_helper.rb /.gitignore: -------------------------------------------------------------------------------- 1 | Gemfile.lock 2 | pkg 3 | -------------------------------------------------------------------------------- /.rspec: -------------------------------------------------------------------------------- 1 | --color 2 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: ruby 2 | services: 3 | - redis-server 4 | rvm: 5 | - 2.0.0 6 | - ruby-head 7 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | ## 2.2.0 2014-11-23 2 | 3 | * Class-level commands are forwarded again 4 | 5 | ## 2.1.0 2014-11-23 6 | 7 | * Lazy connection loading 8 | 9 | ## 2.0.0 2014-11-23 10 | 11 | * New feature: `singletons` to support predefined keys 12 | * New feature: `singleton` to deal with singleton data 13 | * New feature: `on` as a syntactic sugar for `new` 14 | * New feature: `multi`, `pipelined`, `exec` and `eval` are available as instance methods 15 | * No redis commands are delegated at the class level. Always use instance methods or explicitly declare singleton. 16 | * The base class `Redis::Classy` is now `RedisClassy` 17 | * `Redis::Classy.db = Redis.new` is now `RedisClassy.redis = Redis.new`. 18 | 19 | ## 1.1.1 20 | 21 | * Raise exception when Redis::Classy.db is not assigned 22 | 23 | ## 1.1.0 24 | 25 | * Explicitly require all files 26 | 27 | ## 1.0.1 28 | 29 | * Relaxed version dependency on redis-namespace 30 | 31 | ## 1.0.0 32 | 33 | * Play nice with Mongoid 34 | -------------------------------------------------------------------------------- /Gemfile: -------------------------------------------------------------------------------- 1 | source 'https://rubygems.org' 2 | 3 | gemspec 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2012 Kenn Ejima 2 | 3 | MIT License 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Redis Classy 2 | 3 | [![Build Status](https://secure.travis-ci.org/kenn/redis-classy.png)](http://travis-ci.org/kenn/redis-classy) 4 | 5 | A very simple, class-based namespace prefixing and encapsulation for Redis. Key features include: 6 | 7 | - Establishes a maintainable convention by prefixing keys with the class name (e.g. `YourClass:123`) 8 | - Delegates all method calls to the [redis-rb](https://github.com/redis/redis-rb) within the namespace 9 | - Adds a better abstraction layer around Redis objects and commands 10 | 11 | Here's an example: 12 | 13 | ```ruby 14 | class Timer < RedisClassy 15 | def start 16 | pipelined do 17 | set Time.now.to_i 18 | expire 120.seconds 19 | end 20 | end 21 | 22 | def stop 23 | del 24 | end 25 | 26 | def running? 27 | !!get 28 | end 29 | end 30 | 31 | timer = Timer.new(123) 32 | timer.start 33 | timer.running? 34 | => true 35 | 36 | Timer.keys 37 | => ["123"] 38 | RedisClassy.keys 39 | => ["Timer:123"] 40 | ``` 41 | 42 | The Timer class above is self-contained and more readable. 43 | 44 | This library is made intentionally small, yet powerful when you need better abstraction on Redis objects to keep things organized. 45 | 46 | ### UPGRADING FROM v1 47 | 48 | [**An important message about upgrading from 1.x**](UPGRADING.md) 49 | 50 | 51 | ## redis-rb vs redis-namespace vs redis-classy 52 | 53 | With the vanilla `redis` gem, you've been doing this: 54 | 55 | ```ruby 56 | redis = Redis.new 57 | redis.set 'foo', 'bar' 58 | redis.get 'foo' # => "bar" 59 | ``` 60 | 61 | With the `redis-namespace` gem, you can add a prefix in the following manner: 62 | 63 | ```ruby 64 | redis_ns = Redis::Namespace.new('ns', :redis => redis) 65 | redis_ns['foo'] = 'bar' # equivalent of => redis.set 'ns:foo', 'bar' 66 | redis_ns['foo'] # => "bar" 67 | ``` 68 | 69 | Now, with the `redis-classy` gem, you finally achieve a class-based naming convention: 70 | 71 | ```ruby 72 | class Something < RedisClassy 73 | end 74 | 75 | Something.on('foo').set('bar') # equivalent of => redis.set 'Something:foo', 'bar' 76 | Something.on('foo').get # => "bar" 77 | 78 | something = Something.new('foo') 79 | something.set 'bar' # equivalent of => redis.set 'Something:foo', 'bar' 80 | something.get # => "bar" 81 | ``` 82 | 83 | Usage 84 | ----- 85 | 86 | In Gemfile: 87 | 88 | ```ruby 89 | gem 'redis-classy' 90 | ``` 91 | 92 | Register the Redis server: (e.g. in `config/initializers/redis_classy.rb` for Rails) 93 | 94 | ```ruby 95 | RedisClassy.redis = Redis.current 96 | ``` 97 | 98 | Create a class that inherits RedisClassy. (e.g. in `app/redis/cache.rb` for Rails, for auto- and eager-loading) 99 | 100 | ```ruby 101 | class Cache < RedisClassy 102 | def put(content) 103 | pipelined do 104 | set content 105 | expire 5.seconds 106 | end 107 | end 108 | end 109 | 110 | cache = Cache.new(123) 111 | cache.put "This tape will self-destruct in five seconds. Good luck." 112 | ``` 113 | 114 | Since the `on` method is added as a syntactic sugar for `new`, you can also run a command in one shot as well: 115 | 116 | ```ruby 117 | Cache.on(123).put 118 | ``` 119 | 120 | For convenience, singleton and predefined static keys are also supported. 121 | 122 | ```ruby 123 | class Counter < RedisClassy 124 | singleton 125 | end 126 | 127 | Counter.incr # 'Counter:singleton' => '1' 128 | Counter.incr # 'Counter:singleton' => '2' 129 | Counter.get # => '2' 130 | ``` 131 | 132 | ``` ruby 133 | class Stats < RedisClassy 134 | singletons :median, :average 135 | end 136 | 137 | ages = [21,22,24,28,30] 138 | 139 | Stats.median.set ages[ages.size/2] # 'Stats:median' => '24' 140 | Stats.average.set ages.inject(:+)/ages.size # 'Stats:average' => '25' 141 | Stats.median.get # => '24' 142 | Stats.average.get # => '25' 143 | ``` 144 | 145 | Finally, you can also pass an arbitrary object that responds to `id` as a key. This is useful when used in combination with ActiveRecord, etc. 146 | 147 | ```ruby 148 | class Lock < RedisClassy 149 | end 150 | 151 | class Room < ActiveRecord::Base 152 | end 153 | 154 | room = Room.create 155 | 156 | lock = Lock.new(room) 157 | ``` 158 | 159 | When you need an access to the non-namespaced, raw Redis keys, it's available as `RedisClass.keys`. Keep in mind that this method is very slow at O(N) computational complexity and potentially hazardous when you have many keys. [Read the details](http://redis.io/commands/keys). 160 | 161 | ```ruby 162 | RedisClassy.keys 163 | => ["Stats:median", "Stats:average", "Counter"] 164 | 165 | RedisClassy.keys 'Stats:*' 166 | => ["Stats:median", "Stats:average"] 167 | ``` 168 | 169 | Since the `redis` attribute is a class instance variable, you can dynamically assign different databases for each class, without affecting other classes. 170 | 171 | ```ruby 172 | Cache.redis = Redis::Namespace.new('Cache', redis: Redis.new(host: 'another.host')) 173 | ``` 174 | 175 | Unicorn support 176 | --------------- 177 | 178 | If you run fork-based app servers such as **Unicorn** or **Passenger**, you need to reconnect to the Redis after forking. 179 | 180 | ```ruby 181 | after_fork do 182 | RedisClassy.redis.client.reconnect 183 | end 184 | ``` 185 | 186 | Note that since Redis Classy assigns a namespaced Redis instance upon the inheritance event of each subclass (`class Something < RedisClassy`), reconnecting the master (non-namespaced) connection that is referenced from all subclasses should probably be the safest and the most efficient way to survive a forking event. 187 | 188 | Reference 189 | --------- 190 | 191 | Dependency: 192 | 193 | * 194 | * 195 | 196 | Use case: 197 | 198 | * 199 | -------------------------------------------------------------------------------- /Rakefile: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env rake 2 | require "bundler/gem_tasks" 3 | 4 | # RSpec 5 | require 'rspec/core/rake_task' 6 | RSpec::Core::RakeTask.new('spec') 7 | task :default => :spec 8 | 9 | # Custom Tasks 10 | desc 'Flush the test database' 11 | task :flushdb do 12 | require 'redis' 13 | Redis.new(:db => 15).flushdb 14 | end 15 | -------------------------------------------------------------------------------- /UPGRADING.md: -------------------------------------------------------------------------------- 1 | # Upgrading from 1.x 2 | 3 | `redis-classy` 2.0 has brought quite a few changes. Please read these points carefully. 4 | 5 | Please post any implications we may have missed as a GitHub Issue or Pull Request. 6 | 7 | * The base class `Redis::Classy` is now `RedisClassy`. 8 | * `Redis::Classy.db = Redis.new` is now `RedisClassy.redis = Redis.new`. 9 | -------------------------------------------------------------------------------- /lib/redis-classy.rb: -------------------------------------------------------------------------------- 1 | require 'redis-namespace' 2 | require 'redis_classy' 3 | require 'redis_classy/version' 4 | -------------------------------------------------------------------------------- /lib/redis_classy.rb: -------------------------------------------------------------------------------- 1 | class RedisClassy 2 | class << self 3 | attr_writer :redis 4 | 5 | Redis::Namespace::COMMANDS.keys.each do |command| 6 | define_method(command) do |*args, &block| 7 | if @singleton 8 | new('singleton').send(command, *args, &block) 9 | else 10 | redis.send(command, *args, &block) 11 | end 12 | end 13 | end 14 | 15 | def redis 16 | @redis ||= begin 17 | if self == RedisClassy 18 | # only RedisClassy itself holds the raw non-namespaced Redis instance 19 | nil 20 | else 21 | # subclasses of RedisClassy 22 | raise Error.new('RedisClassy.redis must be assigned first') if RedisClassy.redis.nil? 23 | Redis::Namespace.new(self.name, redis: RedisClassy.redis) 24 | end 25 | end 26 | end 27 | 28 | def on(key) 29 | new(key) 30 | end 31 | 32 | def method_missing(command, *args, &block) 33 | if @singleton 34 | new('singleton').send(command, *args, &block) 35 | else 36 | super 37 | end 38 | end 39 | 40 | # Singletons 41 | 42 | attr_reader :singletons_keys 43 | 44 | def singletons(*args) 45 | args.each do |key| 46 | @singletons_keys ||= [] 47 | @singletons_keys << key 48 | define_singleton_method(key) do 49 | new key 50 | end 51 | end 52 | end 53 | 54 | def singleton 55 | @singleton = true 56 | end 57 | end 58 | 59 | # Instance methods 60 | 61 | attr_accessor :key, :redis, :object 62 | 63 | def initialize(object) 64 | @redis = self.class.redis 65 | @object = object 66 | 67 | case object 68 | when String, Symbol, Integer 69 | @key = object.to_s 70 | else 71 | if object.respond_to?(:id) 72 | @key = object.id.to_s 73 | else 74 | raise ArgumentError, 'object must be a string, symbol, integer or respond to :id method' 75 | end 76 | end 77 | end 78 | 79 | KEYLESS_COMMANDS = [:multi, :pipelined, :exec, :eval, :unwatch] 80 | 81 | def method_missing(command, *args, &block) 82 | if @redis.respond_to?(command) 83 | case command 84 | when *KEYLESS_COMMANDS 85 | @redis.send(command, *args, &block) 86 | else 87 | @redis.send(command, @key, *args, &block) 88 | end 89 | else 90 | super 91 | end 92 | end 93 | 94 | Error = Class.new(StandardError) 95 | end 96 | -------------------------------------------------------------------------------- /lib/redis_classy/version.rb: -------------------------------------------------------------------------------- 1 | class RedisClassy 2 | VERSION = '2.4.1' 3 | end 4 | -------------------------------------------------------------------------------- /redis-classy.gemspec: -------------------------------------------------------------------------------- 1 | # -*- encoding: utf-8 -*- 2 | require File.expand_path('../lib/redis_classy/version', __FILE__) 3 | 4 | Gem::Specification.new do |gem| 5 | gem.authors = ["Kenn Ejima"] 6 | gem.email = ["kenn.ejima@gmail.com"] 7 | gem.description = %q{Class-style namespace prefixing for Redis} 8 | gem.summary = %q{Class-style namespace prefixing for Redis} 9 | gem.homepage = "http://github.com/kenn/redis-classy" 10 | 11 | gem.files = `git ls-files`.split($\) 12 | gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) } 13 | gem.test_files = gem.files.grep(%r{^(test|spec|features)/}) 14 | gem.name = "redis-classy" 15 | gem.require_paths = ["lib"] 16 | gem.version = RedisClassy::VERSION 17 | 18 | gem.add_runtime_dependency "redis-namespace", "~> 1.0" 19 | gem.add_development_dependency "rspec" 20 | gem.add_development_dependency "bundler" 21 | 22 | # For Travis 23 | gem.add_development_dependency "rake" 24 | end 25 | -------------------------------------------------------------------------------- /spec/redis_classy_spec.rb: -------------------------------------------------------------------------------- 1 | require 'spec_helper' 2 | 3 | class Something < RedisClassy 4 | end 5 | 6 | describe RedisClassy do 7 | before do 8 | RedisClassy.redis.flushdb 9 | end 10 | 11 | after(:all) do 12 | RedisClassy.redis.flushdb 13 | RedisClassy.redis.quit 14 | end 15 | 16 | it 'raises RedisClassy::Error when connection is missing' do 17 | begin 18 | backup = RedisClassy.redis 19 | RedisClassy.redis = nil 20 | 21 | class NoConnection < RedisClassy 22 | end 23 | 24 | expect{ NoConnection.keys }.to raise_error(RedisClassy::Error) 25 | ensure 26 | RedisClassy.redis = backup 27 | end 28 | end 29 | 30 | it 'stores redis or redis-namespace' do 31 | expect(RedisClassy.redis.is_a?(Redis)).to be_truthy 32 | expect(RedisClassy.redis.is_a?(Redis::Namespace)).to be_falsy 33 | expect(Something.redis.is_a?(Redis)).to be_falsy 34 | expect(Something.redis.is_a?(Redis::Namespace)).to be_truthy 35 | end 36 | 37 | it 'gets keys' do 38 | Something.on(:foo).set('bar') 39 | expect(Something.keys).to eq(['foo']) 40 | expect(RedisClassy.keys).to eq(['Something:foo']) 41 | end 42 | 43 | it 'prepends class name to the key' do 44 | class Another < Something 45 | end 46 | 47 | module Deep 48 | class Klass < Another 49 | end 50 | end 51 | 52 | Something.on(:foo).set('bar') 53 | expect(Something.on(:foo).keys).to eq(['foo']) 54 | expect(RedisClassy.keys.size).to eq(1) 55 | expect(RedisClassy.keys).to include 'Something:foo' 56 | 57 | Another.on(:foo).set('bar') 58 | expect(Another.on(:foo).keys).to eq(['foo']) 59 | expect(RedisClassy.keys.size).to eq(2) 60 | expect(RedisClassy.keys).to include 'Another:foo' 61 | 62 | Deep::Klass.on(:foo).set('bar') 63 | expect(Deep::Klass.on(:foo).keys).to eq(['foo']) 64 | expect(RedisClassy.keys.size).to eq(3) 65 | expect(RedisClassy.keys).to include 'Deep::Klass:foo' 66 | end 67 | 68 | it 'delegates instance methods with the key binding' do 69 | something = Something.new('foo') 70 | 71 | expect(something.get).to eq(nil) 72 | something.set('bar') 73 | expect(something.get).to eq('bar') 74 | expect(Something.on(:foo).get).to eq('bar') 75 | end 76 | 77 | it 'handles multi block' do 78 | something = Something.new('foo') 79 | 80 | something.multi do 81 | something.sadd 1 82 | something.sadd 2 83 | something.sadd 3 84 | end 85 | 86 | expect(something.smembers).to eq(['1','2','3']) 87 | end 88 | 89 | it 'handles watch' do 90 | something = Something.new('foo') 91 | 92 | something.watch do 93 | something.unwatch 94 | end 95 | end 96 | 97 | it 'handles method_missing' do 98 | # class method 99 | expect { Something.bogus }.to raise_error(NoMethodError) 100 | 101 | # instance method 102 | something = Something.new('foo') 103 | expect { something.bogus }.to raise_error(NoMethodError) 104 | end 105 | 106 | it 'handles singleton key' do 107 | class Counter < RedisClassy 108 | singleton 109 | end 110 | 111 | Counter.incr 112 | expect(Counter.get).to eq('1') 113 | Counter.incr 114 | expect(Counter.get).to eq('2') 115 | end 116 | 117 | it 'handles predefined keys' do 118 | class Stats < RedisClassy 119 | singletons :median, :average 120 | end 121 | 122 | ages = [21,22,24,28,30] 123 | Stats.median.set ages[ages.size/2] 124 | Stats.average.set ages.inject(:+)/ages.size 125 | expect(Stats.median.get).to eq('24') 126 | expect(Stats.average.get).to eq('25') 127 | expect([:average, :median] - Stats.keys.map(&:to_sym)).to eq([]) 128 | expect(Stats.singletons_keys).to eq([:median, :average]) 129 | end 130 | 131 | it 'handles multiple key commands' do 132 | Something.mset :a, 1, :b, 2 133 | expect(Something.mget(:a, :b)).to eq(['1', '2']) 134 | expect(Something.mapped_mget(:a, :b)).to eq({'a' => '1', 'b' => '2'}) 135 | end 136 | 137 | it 'allows conditional assignment' do 138 | RedisClassy.redis = nil 139 | expect { 140 | RedisClassy.redis ||= Redis.new(db: 15) 141 | }.to_not raise_error 142 | end 143 | end 144 | -------------------------------------------------------------------------------- /spec/spec_helper.rb: -------------------------------------------------------------------------------- 1 | require 'rubygems' 2 | require 'bundler/setup' 3 | 4 | require 'rspec' 5 | require 'redis-classy' 6 | 7 | RSpec.configure do |config| 8 | # Use database 15 for testing so we don't accidentally step on you real data. 9 | RedisClassy.redis = Redis.new(db: 15) 10 | unless RedisClassy.keys.empty? 11 | abort '[ERROR]: Redis database 15 not empty! If you are sure, run "rake flushdb" beforehand.' 12 | end 13 | end 14 | --------------------------------------------------------------------------------