Skip to content

Instantly share code, notes, and snippets.

@actsasbuffoon
Created August 30, 2011 00:45
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save actsasbuffoon/8afb36d47237afdb5616 to your computer and use it in GitHub Desktop.
Save actsasbuffoon/8afb36d47237afdb5616 to your computer and use it in GitHub Desktop.
My entry for the CodeBrawl key/value store contest

Mnemosine

Mnemosine is a key/value store written in Ruby. It was created for the CodeBrawl competition located here: http://codebrawl.com/contests/key-value-stores

You can run the Mnemosine server by running "./mnemosine" from the root directory. There is also a Ruby adapter you can use. The API covers a small amount of what Redis can do, plus a tiny bit it can't.

Capabilities

Mnemosine's API is fairly similar to the Redis API, with a few differences. Like Redis, Mnemosine gives you several data types. You can use different functions depending on the data type.

Key

Key functions can be used on any value.

delete_all

Deletes every key and value pair in the entire database.

delete(k)

Deletes the key and its associated value.

keys

Lists all keys in the database.

exists(k)

Returns true if the key exists, false otherwise.

randomkey

Returns a random key from the database.

rename(old_key, new_key)

Moves the value from one key to another, overwriting any data that be have been on the new key.

renamenx(old_key, new_key)

Like rename, but returns an error if the new key already exists.

select_keys(src = nil)

Takes a block that is run on each key/value pair. Hopefully this can be expanded into a full map/reduce system at some point.

match_keys(regex, opts = "")

Takes a regular expression and returns any keys that match it.

Strings/Integers

These operations only work on strings or integers.

set(k, v)

Sets the key to the given value.

get(k)

Returns the value associated with the key.

append(k, v)

Only works with strings. Appends v to the end of the value associated with k.

incr(k)

Only works with integers. Adds one to the integer value associated with k.

decr(k)

Only works with integers. Subtracts one from the integer value associated with k.

incrby(k, v)

Only works with integers. Adds v to the integer value associated with k.

decrby(k, v)

Only works with integers. Subtracts v from the integer value associated with k.

Hashes

These functions only work with hashes.

hset(k, sk, v)

Assigns v to the sub-key sk.

hsetnx(k, sk, v)

Like hset, but it will not overwrite an existing key.

hget(k, sk)

Returns the value associated with the given key and sub-key.

hkeys(k)

Lists all keys in the hash.

hdel(k, sk)

Deletes the given sub-key from the hash, returning its value.

hexists(k, sk)

Checks to see if the given sub-key exists in the hash.

hlen(k)

Returns the number of keys in the hash.

hgetall(k)

Returns the entire hash.

hincr(k, sk)

Increments the integer stored in sk.

hdecr(k, sk)

Decrements the integer stored in sk.

hincrby(k, sk, v)

Adds v to the integer stored in sk.

hdecrby(k, sk, v)

Subtracts v from the integer stored in sk.

hmset(k, v)

Assigns v to the hash. Note that this overwrites everything. It would probably be preferable to merge it.

hmget(k)

Seems to be exactly the same as hgetall. I think I intended for this to take an array of sub-keys and just return those.

Arrays

This isn't very fleshed out yet. I intended to accomplish more with it, but my cat got very sick and the entire week has been spent dragging the poor guy to the emergency vet or various specialists. As such, there's not much here yet. I'd like to add some more functions, as well as length limited lists, which could be really cool for caching.

lset(k, sk, v)

Sets a value at the index specified by sk.

lget(k, sk)

Returns the value associated with sk.

lrange(k, start, stop)

Returns the values associated with the indexes between (and including) start and stop.

linsert(k, replace, v)

Inserts a value at replace, pushing any later keys further back. Redis is a little weird on this one, and I've emulated some of that weirdness. Redis wants you to specify whether you want to insert the value before or after the given key, which seems pointless. You're programmers, and you know how to add or subtract 1 from the index. However, Redis doesn't treat replace as an index, but as a value! That seems strange to me, and I'm not sure I like it. Still, that's how Redis does it, so I've made this function behave mostly the same.

Notes

Obviously Ruby is probably not a great choice for creating a database, as one typically wants very high performance. Instead, consider this to be a fun toy. It uses EventMachine on the server and passes JSON via persistent raw TCP connections. It also was also developed TDD style, so it could be good to look at if you're new to testing or Test::Unit.

I'll probably hack more features onto this as time permits. I'd like to get replication and sharding working, possibly a map/reduce system, and if I'm feeling really ambitious, perhaps some kind of indexed search.

There's currently no security on the database, and it only runs on localhost. As I said, this is just a fun little project, not a serious DB engine.

I'm putting this under the MIT license, so feel free to do whatever you want with it. My only request is that you don't use this as the basis for an entry into the upcoming CodeBrawl competition. While that would be hilarious, it would make me a sad panda.

Mnemosine offers a search system based on code execution. It's kind of like map reduce, but simpler and worse.

Requirements

Mnemosine is only compatible with Ruby 1.9. You'll need to install the following gems to use Mnemosine:

  • eventmachine
  • json
  • slop
  • sourcify

Run the following command to install sourcify:

gem install ruby_parser file-tail sourcify

guard 'shell' do
watch(%r{^lib/(.+)\.rb$}) {|m| `ruby test/test_helper.rb` }
watch(%r{^test/.+\.rb$}) {|m| `ruby test/test_helper.rb` }
end
class Mnemosine
class Server
def hset(k, sk, v)
ensure_hash(@storage[k], empty: true) || (@storage[k] ||= {}; @storage[k][sk] = v)
end
def hget(k, sk)
ensure_hash(@storage[k], empty: true) || (@storage[k] && @storage[k][sk])
end
def hkeys(k)
ensure_hash(@storage[k], empty: true) || (@storage[k].keys)
end
def hdel(k, sk)
ensure_hash(@storage[k], empty: true) || (@storage[k].delete sk)
end
def hexists(k, sk)
ensure_hash(@storage[k]) || !!@storage[k][sk]
end
def hlen(k)
ensure_hash(@storage[k]) || @storage[k].length
end
def hgetall(k)
ensure_hash(@storage[k], empty: true) || @storage[k]
end
def hincr(k, sk)
ensure_hash(@storage[k]) || ensure_numeric(@storage[k][sk]) || @storage[k][sk] += 1
end
def hdecr(k, sk)
ensure_hash(@storage[k]) || ensure_numeric(@storage[k][sk]) || @storage[k][sk] -= 1
end
def hincrby(k, sk, v)
ensure_hash(@storage[k]) || ensure_numeric(@storage[k][sk]) || @storage[k][sk] += v
end
def hdecrby(k, sk, v)
ensure_hash(@storage[k]) || ensure_numeric(@storage[k][sk]) || @storage[k][sk] -= v
end
def hmset(k, v)
ensure_hash(@storage[k], empty: true) || @storage[k] = v
end
def hmget(k)
ensure_hash(@storage[k]) || @storage[k]
end
def hsetnx(k, sk, v)
if !@storage[k]
@storage[k] ||= {sk => v}
else
g = ensure_hash(@storage[k])
if g
g
elsif !@storage[k][sk]
@storage[k][sk] = v
else
{"error" => "That key is already assigned"}
end
end
end
end
end
class Mnemosine
class Server
def delete_all
@storage = new_storage
end
def delete(k)
@storage.delete k
end
def keys
@storage.keys
end
def exists(k)
!!@storage[k]
end
def randomkey
@storage.keys[rand * @storage.keys.length]
end
def rename(old_key, new_key)
@storage[new_key] = delete(old_key)
end
def renamenx(old_key, new_key)
if exists(new_key)
{"error" => "That key is already assigned"}
else
rename old_key, new_key
end
end
def select_keys(src = nil)
if src
p = eval(src)
@storage.select(&p).map(&:first)
elsif block_given?
@storage.select {|k, v| yield(k, v)}.map(&:first)
end
end
def match_keys(regex, opts = "")
if regex.class == String
rgx = Regexp.new(regex, opts)
@storage.keys.select {|k| k =~ rgx}
else
@storage.keys.select {|k| k =~ regex}
end
end
end
end
class Mnemosine
class Server
def lset(k, sk, v)
ensure_integer_key(sk) || ensure_array(@storage[k], empty: true) || (@storage[k] ||= []; @storage[k][sk] = v)
end
def lget(k, sk)
ensure_integer_key(sk) || ensure_array(@storage[k]) || @storage[k][sk]
end
def lrange(k, start, stop)
ensure_array(@storage[k]) || @storage[k][start..stop]
end
def linsert(k, replace, v)
g = ensure_array(@storage[k])
return g if g
replace_idx = nil
@storage[k].each.with_index do |val, idx|
if val == replace
replace_idx = idx
break
end
end
replace_idx ? @storage[k].insert(replace_idx, v) : {"error" => "Value to replace not found"}
end
end
end
require 'socket'
require 'json'
class Mnemosine
class Client
private
def send_data(msg)
@socket.puts msg.to_json
JSON.parse(@socket.readline)["val"]
end
def self.api_methods(*mthds)
mthds.each do |mthd|
define_method mthd, ->(*args) do
send_data({mthd => args})
end
end
end
public
def initialize
@socket = TCPSocket.open('localhost', 4291)
end
api_methods :set, :get, :delete_all, :delete, :keys, :exists, :rename, :renamenx, :incr,
:decr, :incrby, :decrby, :append, :randomkey, :hset, :hget, :hmset, :hmget,
:hlen, :hkeys, :hincr, :hincrby, :hdecr, :hdecrby, :hgetall, :hexists, :hdel,
:hsetnx, :lset, :lget, :linsert, :lrange
def select_keys(&blk)
send_data({"select_keys" => blk.to_source})
end
def match_keys(regex)
send_data({"match_keys" => [regex.source, regex.options]})
end
end
end
#!/usr/bin/env ruby
require 'rubygems'
require 'slop'
require File.join(File.dirname(__FILE__), "lib", "server", "mnemosine")
opts = Slop.parse do
on :p, :port, "Port the server will run on", true, default: 4291, :as => :integer
on :s, :host, "Host the server will run on", true, default: 'localhost'
on :h, :help, 'Show this message', :tail => true, do
puts help
exit
end
end
Mnemosine::Server.new(opts)
require 'sourcify'
require File.expand_path(File.join(File.dirname(__FILE__), "lib", "messages.rb"))
require 'eventmachine'
require 'json'
require 'sourcify'
%w[type_check key string hash list persistence process].each do |file|
require File.expand_path(File.join(File.dirname(__FILE__), "lib", "#{file}.rb"))
end
class Mnemosine
class Server
def save(location = nil)
if location || @file_location
persist(location || @file_location)
else
{"error" => "No location given"}
end
end
def save!(location = nil)
if location || @file_location
persist!(location || @file_location)
else
{"error" => "No location given"}
end
end
def restore(location = nil)
if location || @file_location
load(location || @file_location)
else
{"error" => "No location given"}
end
end
def set_location(location)
@file_location = location
end
def unset_location
@file_location = nil
end
private
def persist(loc)
if File.exist?(loc)
{error: "File already exists"}
else
persist!(loc)
end
end
def persist!(loc)
File.open(loc, "w") {|f| f.write @storage.to_json}
true
end
def load(loc)
if File.exist? loc
@storage = JSON.parse(File.read(loc))
else
{"error" => "File does not exist"}
end
end
end
end
class Mnemosine
class Server
attr_accessor :file_location
API_METHODS = %w[set get delete_all delete keys exists randomkey rename renamenx append incr decr incrby decrby
select_keys match_keys hset hget hmset hmget hlen hkeys hincr hincrby hdecr hdecrby hgetall
hexists hdel hsetnx lset lget linsert lrange]
def initialize(args = {})
@storage = new_storage
@port = args[:p] || 4291
@host = args[:s] || 'localhost'
unless args[:lib]
run_loop
end
end
def new_storage
{}
end
def receive_data(msg)
return :send_nothing_back if msg == "\n"
data = JSON.parse(msg)
k = data.keys.first
v = data.values.first
if API_METHODS.include?(k)
self.send(k, *v)
else
{"error" => "#{k} is not an API method"}
end
end
def run_loop
r = Runner
r.setup(self)
EventMachine.run do
EventMachine.start_server @host, @port, r
puts "Mnemosine listening at #{@host}:#{@port}"
end
end
module Runner
def self.setup(server)
@@server = server
end
def receive_data(data)
puts "Receiving data: #{data.inspect}"
msg = @@server.receive_data(data)
unless msg == :send_nothing_back
puts "Sending msg: #{msg}"
send_data({val: msg}.to_json + "\n")
puts "Sent message"
puts "\n"
else
puts "No response"
end
end
end
end
end
class Mnemosine
class Server
def set(k, v)
ensure_string_or_numeric(@storage[k], empty: true) || @storage[k] = v
end
def get(k)
ensure_string_or_numeric(@storage[k], empty: true) || @storage[k]
end
def append(k, v)
g = ensure_string(@storage[k], empty: true)
return g if g
if exists(k)
@storage[k] += v
else
set k, v
end
end
def incr(k)
ensure_numeric(@storage[k]) || @storage[k] += 1
end
def decr(k)
ensure_numeric(@storage[k]) || @storage[k] -= 1
end
def incrby(k, v)
ensure_numeric(@storage[k]) || @storage[k] += v
end
def decrby(k, v)
ensure_numeric(@storage[k]) || @storage[k] -= v
end
end
end
module HashTest
def test_hset
@db.hset "foo", "bar", "baz"
assert_equal "baz", @db.hget("foo", "bar")
end
def test_hset_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hset("foo", "bar", "baz"))
end
def test_hget_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hget("foo", "bar"))
end
def test_hkeys
@db.hset "foo", "bar", "baz"
@db.hset "foo", "lol", "cat"
assert_equal ["bar", "lol"], @db.hkeys("foo").sort
end
def test_hkeys_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hkeys("foo"))
end
def test_hdel
@db.hset "foo", "bar", "baz"
@db.hset "foo", "lol", "cat"
@db.hdel "foo", "bar"
assert_equal ["lol"], @db.hkeys("foo")
end
def test_hdel_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hdel("foo", "bar"))
end
def test_hexists
@db.hset "foo", "bar", "baz"
assert_equal false, @db.hexists("foo", "lol")
assert_equal true, @db.hexists("foo", "bar")
end
def test_hexists_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hexists("foo", "bar"))
end
def test_hlen
@db.hset "foo", "bar", "baz"
@db.hset "foo", "lol", "cat"
assert_equal 2, @db.hlen("foo")
end
def test_hlen_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hlen("foo"))
end
def test_hgetall
@db.hset "foo", "bar", "baz"
@db.hset "foo", "lol", "cat"
assert_equal({"bar" => "baz", "lol" => "cat"}, @db.hgetall("foo"))
end
def test_hgetall_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hgetall("foo"))
end
def test_hincr
@db.hset "foo", "bar", 2
@db.hincr "foo", "bar"
assert_equal 3, @db.hget("foo", "bar")
end
def test_hincr_hash_string
@db.hset "foo", "bar", "baz"
assert_equal({"error" => @numeric_only_message}, @db.hincr("foo", "bar"))
end
def test_hincr_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hincr("foo", "bar"))
end
def test_hdecr
@db.hset "foo", "bar", 2
@db.hdecr "foo", "bar"
assert_equal 1, @db.hget("foo", "bar")
end
def test_hdecr_hash_string
@db.hset "foo", "bar", "baz"
assert_equal({"error" => @numeric_only_message}, @db.hdecr("foo", "bar"))
end
def test_hdecr_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hdecr("foo", "bar"))
end
def test_hincrby
@db.hset "foo", "bar", 2
@db.hincrby "foo", "bar", 2
assert_equal 4, @db.hget("foo", "bar")
end
def test_hincrby_hash_string
@db.hset "foo", "bar", "baz"
assert_equal({"error" => @numeric_only_message}, @db.hincrby("foo", "bar", 2))
end
def test_hincrby_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hincrby("foo", "baz", 2))
end
def test_hdecrby
@db.hset "foo", "bar", 2
@db.hdecrby "foo", "bar", 2
assert_equal 0, @db.hget("foo", "bar")
end
def test_hdecrby_hash_string
@db.hset "foo", "bar", "baz"
assert_equal({"error" => @numeric_only_message}, @db.hdecrby("foo", "bar", 2))
end
def test_hdecrby_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hdecrby("foo", "bar", 2))
end
def test_hmset_and_get
@db.hmset "foo", {"bar" => "baz", "lol" => "cat"}
assert_equal({"bar" => "baz", "lol" => "cat"}, @db.hmget("foo"))
end
def test_hmset_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hmset("foo", {"bar" => "baz", "lol" => "cat"}))
end
def test_hmget_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hmget("foo"))
end
def test_hsetnx
@db.hsetnx("foo", "bar", "baz")
assert_equal("baz", @db.hget("foo", "bar"))
end
def test_hsetnx_on_string
@db.set "foo", "bar"
assert_equal({"error" => @hash_only_message}, @db.hsetnx("foo", "bar", "baz"))
end
def test_hsetnx_taken
@db.hset "foo", "bar", "baz"
assert_equal({"error" => "That key is already assigned"}, @db.hsetnx("foo", "bar", "lol"))
end
end
require 'test/unit'
require 'FileUtils'
class MnemosineTest < Test::Unit::TestCase
# When run in lib mode, the database just runs as a Ruby lib in your process rather
# than starting EventMachine and taking TCP connections. Lib mode is only included
# to make testing easier. It doesn't make any sense to use the DB in lib mode for
# normal processing.
#
# In order to use Mnemosine properly, use the client. The client tests are in
# the test_client.rb file.
def setup
@numeric_only_message = "That operation is only valid on: Fixnum"
@string_only_message = "That operation is only valid on: String"
@string_numeric_only_message = "That operation is only valid on: Fixnum, String"
@hash_only_message = "That operation is only valid on: Hash"
@list_only_message = "That operation is only valid on: Array"
end
def teardown
@db.delete_all
end
end
require File.expand_path(File.join File.dirname(__FILE__), 'test_key.rb')
require File.expand_path(File.join File.dirname(__FILE__), 'test_string.rb')
require File.expand_path(File.join File.dirname(__FILE__), 'test_hash.rb')
require File.expand_path(File.join File.dirname(__FILE__), 'test_list.rb')
require File.expand_path(File.join File.dirname(__FILE__), 'test_persistence.rb')
require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "server", "mnemosine.rb"))
class ServerTest < MnemosineTest
def setup
@db = Mnemosine::Server.new(lib: true)
super
end
include KeyTest
include StringTest
include HashTest
include ListTest
include PersistenceTest
end
require File.expand_path(File.join(File.dirname(__FILE__), "..", "lib", "client", "mnemosine.rb"))
class ClientTest < MnemosineTest
def setup
@db = Mnemosine::Client.new
super
end
include KeyTest
include StringTest
include HashTest
include ListTest
end
module KeyTest
def test_delete_all
@db.set "foo", "bar"
@db.delete_all
assert_nil @db.get("foo")
end
def test_delete
@db.set "foo", "bar"
assert_equal "bar", @db.delete("foo")
assert_nil @db.get("foo")
end
def test_keys
@db.set "foo", "bar"
@db.set "lol", "cat"
assert_equal ["foo", "lol"], @db.keys.sort
end
def test_exists
@db.set "foo", "bar"
assert_equal true, @db.exists("foo")
assert_equal false, @db.exists("lol")
end
def test_randomkey
@db.set "foo", "bar"
@db.set "lol", "cat"
assert ["foo", "lol"].include?(@db.randomkey), '"foo" or "lol" expected but was'
end
def test_rename
@db.set "foo", "bar"
@db.rename "foo", "baz"
assert_nil @db.get "foo"
assert_equal "bar", @db.get("baz")
end
def test_renamenx
@db.set "foo", "bar"
@db.set "baz", "wut"
assert_equal({"error" => "That key is already assigned"}, @db.renamenx("foo", "baz"))
assert_equal "bar", @db.get("foo")
assert_equal "wut", @db.get("baz")
end
def test_select_keys
@db.set "foo", 3
@db.set "bar", 12
@db.set "baz", 9
@db.set "lol", 28
@db.set "cat", 17
assert_equal ["bar", "lol"], @db.select_keys {|k, v| v % 2 == 0}.sort
end
def test_match
@db.set "foO", "bar"
@db.set "bar", "wut"
assert_equal ["foO"], @db.match_keys(/(\w)\1/i)
end
end
module ListTest
def test_lset
@db.lset "foo", 0, "bar"
assert_equal "bar", @db.lget("foo", 0)
end
def test_lset_string_key
assert_equal({"error" => "Key must be an integer"}, @db.lset("foo", "bar", "baz"))
end
def test_lget_string_key
assert_equal({"error" => "Key must be an integer"}, @db.lget("foo", "bar"))
end
def test_lset_on_string
@db.set "foo", "bar"
assert_equal({"error" => @list_only_message}, @db.lset("foo", 0, "bar"))
end
def test_lget_on_string
@db.set "foo", "bar"
assert_equal({"error" => @list_only_message}, @db.lget("foo", 0))
end
def test_lrange
@db.lset "foo", 0, "bar"
@db.lset "foo", 1, "baz"
assert_equal ["bar", "baz"], @db.lrange("foo", 0, 1)
end
def test_lrange_on_string
@db.set "foo", "bar"
assert_equal({"error" => @list_only_message}, @db.lrange("foo", 0, 1))
end
def test_linsert
@db.lset "foo", 0, "bar"
@db.lset "foo", 1, "baz"
@db.linsert "foo", "baz", "lol"
assert_equal ["bar", "lol", "baz"], @db.lrange("foo", 0, 2)
end
def test_linsert_on_string
@db.set "foo", "bar"
assert_equal({"error" => @list_only_message}, @db.linsert("foo", "bar", "baz"))
end
def test_linsert_value_not_found
@db.lset "foo", 0, "bar"
assert_equal({"error" => "Value to replace not found"}, @db.linsert("foo", "lol", "cat"))
end
end
module PersistenceTest
def test_save
@db.set "foo", "bar"
loc = File.expand_path(File.join(File.dirname(__FILE__), "..", "test_db.mns"))
assert_equal true, @db.save(loc)
assert File.exist?(File.expand_path(File.join(File.dirname(__FILE__), "..", "test_db.mns"))), "expected file to exist, but it does not"
FileUtils.rm loc
end
def test_save_without_location
@db.set "foo", "bar"
assert_equal "No location given" , @db.save.values.first
end
def test_save_already_exists
@db.set "foo", "bar"
loc = File.expand_path(File.join(File.dirname(__FILE__), "..", "test_db.mns"))
@db.save(loc)
assert_equal "File already exists" , @db.save(loc).values.first
FileUtils.rm loc
end
def test_restore
@db.set "foo", "bar"
loc = File.expand_path(File.join(File.dirname(__FILE__), "..", "test_db.mns"))
@db.save(loc)
@db.delete "foo"
@db.restore loc
assert_equal "bar", @db.get("foo")
FileUtils.rm loc
end
def test_restore_no_file
loc = File.expand_path(File.join(File.dirname(__FILE__), "..", "test_db.mns"))
assert_equal "File does not exist", @db.restore(loc).values.first
end
def test_save_default_location
loc = File.expand_path(File.join(File.dirname(__FILE__), "..", "test_db.mns"))
@db.file_location = loc
assert_equal true, @db.save
FileUtils.rm loc
end
end
module StringTest
def test_set
@db.set "foo", "bar"
assert_equal "bar", @db.get("foo")
end
def test_set_on_hash
@db.hset "foo", "bar", "baz"
assert_equal({"error" => @string_numeric_only_message}, @db.set("foo", "bar"))
end
def test_get_on_hash
@db.hset "foo", "bar", "baz"
assert_equal({"error" => @string_numeric_only_message}, @db.get("foo"))
end
def test_append_empty
@db.append "foo", "bar"
assert_equal "bar", @db.get("foo")
end
def test_append_present
@db.set "foo", "lol"
@db.append "foo", "cat"
assert_equal "lolcat", @db.get("foo")
end
def test_append_number
@db.set "foo", 42
assert_equal({"error" => @string_only_message}, @db.append("foo", "bar"))
end
def test_incr
@db.set "foo", 2
@db.incr "foo"
assert_equal 3, @db.get("foo")
end
def test_incr_string
@db.set "foo", "bar"
assert_equal({"error" => @numeric_only_message}, @db.incr("foo"))
end
def test_decr
@db.set "foo", 2
@db.decr "foo"
assert_equal 1, @db.get("foo")
end
def test_decr_string
@db.set "foo", "bar"
assert_equal({"error" => @numeric_only_message}, @db.decr("foo"))
end
def test_incrby
@db.set "foo", 2
@db.incrby "foo", 2
assert_equal 4, @db.get("foo")
end
def test_incrby_string
@db.set "foo", "bar"
assert_equal({"error" => @numeric_only_message}, @db.incrby("foo", 2))
end
def test_decrby
@db.set "foo", 2
@db.decrby "foo", 2
assert_equal 0, @db.get("foo")
end
def test_decrby_string
@db.set "foo", "bar"
assert_equal({"error" => @numeric_only_message}, @db.decrby("foo", 2))
end
end
class Mnemosine
class Server
def self.ensure_type(name, classes, opts = {})
classes = [classes] unless classes.class == Array
define_method "ensure_#{name}".to_sym, ->(value, args = {}) do
if !args[:empty] && !value
return {"error" => "Cannot perform that operation on an empty value"}
elsif !value
return nil
elsif !classes.include?(value.class)
return {"error" => (opts[:message] || "That operation is only valid on: #{classes.map {|c| c.to_s}.sort.join(", ")}")}
end
end
end
ensure_type :hash, Hash
ensure_type :string, String
ensure_type :numeric, Fixnum
ensure_type :integer_key, Fixnum, :message => "Key must be an integer"
ensure_type :string_or_numeric, [String, Fixnum]
ensure_type :array, Array
end
end
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment