Skip to content
This repository has been archived by the owner on Oct 18, 2022. It is now read-only.

Commit

Permalink
initial commit
Browse files Browse the repository at this point in the history
  • Loading branch information
andys committed Feb 10, 2012
0 parents commit b64f19b
Show file tree
Hide file tree
Showing 7 changed files with 347 additions and 0 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG
@@ -0,0 +1,2 @@

v0.1. First version
7 changes: 7 additions & 0 deletions Manifest
@@ -0,0 +1,7 @@
CHANGELOG
README
Rakefile
lib/blombo.rb
test/test_all.rb
test/test_helper.rb
Manifest
95 changes: 95 additions & 0 deletions README
@@ -0,0 +1,95 @@

Blombo
-------

Tread redis-server like a deep ruby hash.


Example usage
-------------

Blombo.redis = Redis.new
$blombo = Blombo.new('ServerApp')

$blombo.servers.status['web1'] = 'ok'
$blombo.servers.status['web2'] = 'down'

$blombo.servers.status[:web1]
#=> "ok"

$blombo.servers.status[:web3]
#=> nil

This creates a Redis Hash with the key 'blombo:ServerApp:servers:status',
and two fields ('web1', 'web2'), with two associated values.

You can store any Ruby objects as values, they will be automaticallly
Marshalled. (Strings are stored as-is). Blombo does not cache anything but
goes back to redis to get data whenever it is requested with a lookup[key].

Ruby's Enumerable is included along with each() so you can enjoy the usual
range of ruby hash methods:

$blombo.server.status
#=> #<Blombo:0x9ab0d84 @name="ServerApp:server:status" ...>

$blombo.servers.status.exists
=> true

$blombo.servers.status.keys
#=> ['web1', 'web2']

$blombo.servers.status.type
#=> 'hash'

$blombo.servers.status.select {|server, status| status == 'ok' }
#=> [["web1", "ok"]]


Blombo only saves the object back into redis if the []= method is used.
Assignment must always be used to save to db. This means you should
maintain ruby objects like arrays carefully:

$blombo.servers[:list] = ["web1"]
$blombo.servers[:list] << "web2"
$blombo.servers[:list]
#=> ["web1"] #oops

$blombo.servers[:list] += ["web2"]
$blombo.servers[:list]
#=> ["web1", "web2"] #ahh!


Other Redis types
-----------------

What if I don't want a Redis hash? Thats OK - Blombo passes through redis
commands curried with the key as the first parameter:

This: $blombo.joblist.rpush('job1')
Equals: $redis.rpush('blombo:ServerApp:joblist', 'job1')

Then I can pop it off the list:

$blombo.joblist.type
#=> "list"

$blombo.joblist.llen
#=> 1

$blombo.joblist.lpop
#=> "job1"






Contact the author
------------------

Andrew Snow <andrew@modulus.org>
Andys^ on irc.freenode.net


Blombo never forgets.
9 changes: 9 additions & 0 deletions Rakefile
@@ -0,0 +1,9 @@
require 'echoe'

Echoe.new("blombo") do |p|
p.author = "Andrew Snow"
p.email = 'andrew@modulus.org'
p.summary = "Blombo: Treat redis-server like a deep ruby hash"
p.url = "http://github.com/andys/blombo"
p.runtime_dependencies = ['redis']
end
129 changes: 129 additions & 0 deletions lib/blombo.rb
@@ -0,0 +1,129 @@

class Blombo

include Enumerable
include Comparable
attr_reader :blombo_parent

class << self
attr_accessor :redis
def is_marshalled?(str)
Marshal.dump(nil)[0,2] == str[0,2] # Marshall stores its version info in first 2 bytes
end
def to_redis_val(obj)
if Integer===obj || String===obj && obj !~ /^\d+$/ && !is_marshalled?(obj)
obj.to_s
else
Marshal.dump(obj)
end
end
def from_redis_val(str)
if(is_marshalled?(str))
Marshal.load(str)
elsif(str =~ /^\d+$/)
str.to_i
else
str
end
end
end

def redis
self.class.redis
end

def initialize(name, blombo_parent=nil)
@name = name
@blombo_parent = blombo_parent
end

def <=>(other)
@name <=> other.blombo_name
end

def blombo_key
"blombo:#{@name}"
end

def blombo_name
@name
end

def []=(key, val)
redis.hset(blombo_key, key.to_s, self.class.to_redis_val(val))
end

def defined?(key)
redis.exists(key.to_s)
end

def [](key)
if(val = redis.hget(blombo_key, key.to_s))
self.class.from_redis_val(val)
end
end

def nil?
empty?
end

def empty?
redis.hlen(blombo_key) == 0
end

def to_hash
redis.hgetall(blombo_key)
end

def each(*args, &bl)
to_hash.each(*args, &bl)
end

def to_a
redis.hgetall(blombo_key).to_a
end

def keys
self.class.redis.hkeys(blombo_key)
end

def values
self.class.redis.hvals(blombo_key).map {|v| self.class.from_redis_val(v) }
end

def type
@type ||= (t = Blombo.redis.type(blombo_key)) && t != 'none' && t || nil
end

def method_missing(meth, *params, &bl)
if Blombo.redis.respond_to?(meth)
Blombo.redis.send(meth, blombo_key, *params, &bl)
elsif params.empty? && meth =~ /^[a-z_][a-z0-9_]*$/i
Blombo.new("#{@name}:#{meth}", self)
else
super(meth, *params, &bl)
end
end

def blombo_children
self.class.redis.keys("#{blombo_key}:*").map {|k| Blombo.new(k.gsub(/^blombo:/,''), self) }
end

end


=begin
blombo = Blombo.new(redis server details)
blombo.blah = {'hello' => 'world'}
blombo.blah = OpenStruct(..)
blombo.blah = activerecord model
May as well assign each object a uniq id (using redis counters?)
to allow referencing / assocations
=end
101 changes: 101 additions & 0 deletions test/test_all.rb
@@ -0,0 +1,101 @@
require "#{File.dirname(__FILE__)}/../lib/blombo"
require 'test/unit'
require 'redis'
require "#{File.dirname(__FILE__)}/test_helper"

class TestBlombo < Test::Unit::TestCase
def setup
$redis.flushdb
Blombo.redis = $redis
@blombo = Blombo.new('test')
@blombo[:flibble] = "test123"
@blombo['derp'] = 'test321'
@blombo.deep[:firstname] = 'Herp'
@blombo.deep[:lastname] = 'Derpington'
end

def test_defined
assert_equal false, @blombo.defined?('foo')
end

def test_undefined_redis_type
assert_nil @blombo.foo.type
end

def test_empty_hash_lookup
assert_nil @blombo['foo']
end

def test_hash_type
assert_equal 'hash', @blombo.type
end

def test_hash_setter
assert_equal({'flibble' => 'test123', 'derp' => 'test321'}, $redis.hgetall('blombo:test'))
end

def test_values
assert_equal(['test123', 'test321'], @blombo.values.sort)
end

def test_keys
assert_equal(['derp', 'flibble'], @blombo.keys.sort)
end

def test_each
# Blombo includes Enumerable so we can test #each with #inject
result = @blombo.inject({}) {|hsh, keyval| hsh.merge!(keyval.first => keyval.last) }
assert_equal($redis.hgetall('blombo:test'), result)
end

def test_deep_empty_type
assert Blombo===@blombo.deep
end

def test_deep_empty_array
assert_equal [], @blombo.empty.to_a
end

def test_deep_empty_method
assert_equal true, @blombo.empty.empty?
assert_equal false, @blombo.deep.empty?
end

def test_deep_empty_hash_lookup
assert_nil @blombo.deep['foo']
end

def test_redis_keys
assert_equal(['blombo:test', 'blombo:test:deep'], $redis.keys.sort)
end

def test_deep_hash_setter
assert_equal({'firstname' => 'Herp', 'lastname' => 'Derpington'}, $redis.hgetall('blombo:test:deep'))
end

def test_deeper_hash
@blombo.a.b.c['d'] = 'e'
assert_equal({'d' => 'e'}, $redis.hgetall('blombo:test:a:b:c'))
assert_equal [@blombo.a.b.c], @blombo.a.blombo_children
end

def test_marshal
@blombo.marshaltest[:number] = 12345
@blombo.marshaltest[:nil] = nil
assert_equal 12345, @blombo.marshaltest[:number]
assert_equal nil, @blombo.marshaltest[:nil]
end

def test_redis_ops
@blombo.listy.rpush('job1')
@blombo.listy.rpush('job2')
assert_equal 2, $redis.llen('blombo:test:listy')
assert_equal ['job1', 'job2'], @blombo.listy.lrange(0, -1)
end

def test_redis_list_type
@blombo.listy.rpush('job1')
assert_equal 'list', @blombo.listy.type
end

end
4 changes: 4 additions & 0 deletions test/test_helper.rb
@@ -0,0 +1,4 @@

# WARNING: The database specified here will be CLEARED of ALL DATA
$redis = Redis.new(:host => 'localhost', :port => 6379, :db => 15)

0 comments on commit b64f19b

Please sign in to comment.