canery 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- data/Gemfile +3 -0
- data/LICENSE +22 -0
- data/README.md +52 -0
- data/Rakefile +2 -0
- data/canery.gemspec +25 -0
- data/lib/canery.rb +2 -0
- data/lib/canery/backend.rb +150 -0
- data/lib/canery/client.rb +59 -0
- data/lib/canery/tub.rb +83 -0
- data/lib/canery/version.rb +5 -0
- data/spec/canery_client_spec.rb +71 -0
- data/spec/canery_tub_spec.rb +160 -0
- data/spec/spec_helper.rb +9 -0
- metadata +118 -0
data/Gemfile
ADDED
data/LICENSE
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
Copyright (c) 2012 Matthias Kalb, Railsmechanic
|
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.
|
data/README.md
ADDED
@@ -0,0 +1,52 @@
|
|
1
|
+
# Canery
|
2
|
+
Canery is a simple and easy to use key/value store with several commands to store and retrieve data. As the backend uses [Sequel](https://github.com/jeremyevans/sequel/) for persistence, Canery can be used in virtually any environment where a SQL database is available. (For more information please check out the section with the available database adapters.)
|
3
|
+
|
4
|
+
## Installation
|
5
|
+
gem install canery
|
6
|
+
|
7
|
+
## Usage
|
8
|
+
Canery provides a simple and understandable API which is a bit inspired by Redis. So everyone with a little knowledge of Redis should be able to use Canery, too.
|
9
|
+
|
10
|
+
require 'canery'
|
11
|
+
|
12
|
+
# Create a Client interface with an in-memory SQLite database
|
13
|
+
client = Canery::Client.new
|
14
|
+
|
15
|
+
# Create a Client interface connecting to a PostgreSQL database
|
16
|
+
client = Canery::Client.new("postgres://username:password@host:port/database_name")
|
17
|
+
|
18
|
+
# Create a new namespace
|
19
|
+
store = client.tub("store")
|
20
|
+
|
21
|
+
# Set a simple value in this defined namespace
|
22
|
+
store.set("github", "github is awesome") # => "github"
|
23
|
+
|
24
|
+
# Get this previously set value
|
25
|
+
store.get("github") # => "github is awesome"
|
26
|
+
|
27
|
+
# Canery can also handle complex data types
|
28
|
+
store.set("demo_hash", {:first_name => "John", :last_name => "Doe"}) # => "demo_hash"
|
29
|
+
|
30
|
+
# Get this complex value
|
31
|
+
store.get("demo_hash") # => {:first_name => "John", :last_name => "Doe"}
|
32
|
+
|
33
|
+
For more information consider the wiki. (Comming soon... I promise!)
|
34
|
+
|
35
|
+
|
36
|
+
## Information about data types
|
37
|
+
### Keys & Tub names
|
38
|
+
Theoretically every string can be used as a key/tub name, from a string like "foo" to the content of a JPEG file. Even an empty string is a valid key. But for better performance, short keys should be your first choice.
|
39
|
+
|
40
|
+
Please keep in mind that Canery uses strings for all keys/tub names, so any other data type will be converted to a string!
|
41
|
+
|
42
|
+
### Values
|
43
|
+
Canery makes use of Ruby's marshaling library for serializing the given values. So every data type which supports marshalling is compatible with Canery. If you want to add serialization support to your class (for example, if you want to serialize in some specific format), or if it contains objects that would otherwise not be serializable, you can implement your own serialization strategy. See [Ruby's Mashal Documentation](http://www.ruby-doc.org/core-1.9.3/Marshal.html) for more information.
|
44
|
+
|
45
|
+
## License
|
46
|
+
Copyright (c) 2012 Matthias Kalb, Railsmechanic
|
47
|
+
|
48
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
49
|
+
|
50
|
+
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
51
|
+
|
52
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/Rakefile
ADDED
data/canery.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require File.expand_path('../lib/canery/version', __FILE__)
|
4
|
+
|
5
|
+
Gem::Specification.new do |gem|
|
6
|
+
gem.authors = ["Railsmechanic"]
|
7
|
+
gem.email = ["info@railsmechanic.de"]
|
8
|
+
gem.description = %q{A simple but handy key/value store which is able to use a bunch of SQL databases as its backend.}
|
9
|
+
gem.summary = %q{Canery is a simple and easy to use key/value store with several commands to store and retrieve data. Because it uses a SQL database for storing the data, it can be used in many environments.}
|
10
|
+
gem.homepage = "https://github.com/railsmechanic/canery"
|
11
|
+
|
12
|
+
gem.files = `git ls-files`.split($\)
|
13
|
+
gem.executables = gem.files.grep(%r{^bin/}).map{ |f| File.basename(f) }
|
14
|
+
gem.test_files = gem.files.grep(%r{^(test|spec|features)/})
|
15
|
+
gem.name = "canery"
|
16
|
+
gem.require_paths = ["lib"]
|
17
|
+
gem.version = Canery::VERSION
|
18
|
+
|
19
|
+
# dependencies
|
20
|
+
gem.add_dependency "sequel", "~> 3.38.0"
|
21
|
+
|
22
|
+
# development dependencies
|
23
|
+
gem.add_development_dependency "rspec", ">= 2.11.0"
|
24
|
+
gem.add_dependency "sqlite3-ruby", "~> 1.3.3"
|
25
|
+
end
|
data/lib/canery.rb
ADDED
@@ -0,0 +1,150 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "sequel"
|
4
|
+
require "base64"
|
5
|
+
|
6
|
+
module Canery
|
7
|
+
|
8
|
+
class Backend
|
9
|
+
|
10
|
+
TABLE_PREFIX = "canery_"
|
11
|
+
|
12
|
+
def initialize(connection_uri)
|
13
|
+
raise ArgumentError, "connection_uri must be a String or nil" unless NilClass === connection_uri || String === connection_uri
|
14
|
+
@connection = connection_uri.nil? ? Sequel.sqlite : Sequel.connect(connection_uri)
|
15
|
+
@namespace_cache = {}
|
16
|
+
end
|
17
|
+
|
18
|
+
# Basic namespace methods #
|
19
|
+
###########################
|
20
|
+
|
21
|
+
def create_namespace(name)
|
22
|
+
connection.create_table basic_tub_name(name) do
|
23
|
+
String :key, :primary_key => true
|
24
|
+
String :value, :text => true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def delete_namespace(name)
|
29
|
+
@namespace_cache.delete(basic_tub_name(name)) if @namespace_cache[basic_tub_name(name)]
|
30
|
+
connection.drop_table?(basic_tub_name(name)) && "OK"
|
31
|
+
end
|
32
|
+
|
33
|
+
def namespace(name)
|
34
|
+
@namespace_cache[basic_tub_name(name)] ||= connection.dataset.select(:key, :value).from(basic_tub_name(name))
|
35
|
+
end
|
36
|
+
|
37
|
+
def namespaces
|
38
|
+
connection.tables.select{ |name| name =~ /^#{TABLE_PREFIX}/ }.map{ |tub_name| tub_name.to_s[TABLE_PREFIX.length..-1] }
|
39
|
+
end
|
40
|
+
|
41
|
+
def namespace?(name)
|
42
|
+
connection.table_exists?(basic_tub_name(name))
|
43
|
+
end
|
44
|
+
|
45
|
+
|
46
|
+
# Methods for get, set, etc. #
|
47
|
+
##############################
|
48
|
+
|
49
|
+
def get(name, key)
|
50
|
+
data = namespace(name).first(:key => build_key(key))
|
51
|
+
data.nil? ? nil : load(data[:value])
|
52
|
+
end
|
53
|
+
|
54
|
+
def mget(name, keys)
|
55
|
+
raise ArgumentError, "keys argument must be an Array of keys" unless Array === keys
|
56
|
+
keys.map!{ |key| build_key(key) }
|
57
|
+
|
58
|
+
# Sort data in the order they appear in the 'keys' Array
|
59
|
+
namespace(name).where(:key => keys).sort_by { |element| keys.index(element[:key]) }.map {|dataset| load(dataset[:value]) }
|
60
|
+
end
|
61
|
+
|
62
|
+
def set(name, key, value)
|
63
|
+
begin
|
64
|
+
namespace(name).insert(:key => build_key(key), :value => dump(value)) && build_key(key)
|
65
|
+
rescue Sequel::DatabaseError # raised if the key already exists, than update
|
66
|
+
update(name, key, value)
|
67
|
+
rescue
|
68
|
+
"ERROR"
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def mset(name, data)
|
73
|
+
raise ArgumentError, "data must be a Hash with keys and values" unless Hash === data
|
74
|
+
namespace(name).multi_insert(data.map{ |key, value| {:key => build_key(key), :value => dump(value)} }) && "OK"
|
75
|
+
end
|
76
|
+
|
77
|
+
def delete(name, key)
|
78
|
+
namespace(name).where(:key => build_key(key)).limit(1).delete && "OK"
|
79
|
+
end
|
80
|
+
|
81
|
+
def clear(name)
|
82
|
+
namespace(name).delete && "OK"
|
83
|
+
end
|
84
|
+
|
85
|
+
def update(name, key, value)
|
86
|
+
namespace(name).where(:key => build_key(key)).limit(1).update(:value => dump(value)) && build_key(key)
|
87
|
+
end
|
88
|
+
|
89
|
+
def has_key?(name, key)
|
90
|
+
!!get(name, key)
|
91
|
+
end
|
92
|
+
|
93
|
+
def keys(name)
|
94
|
+
namespace(name).map{ |dataset| dataset[:key] }
|
95
|
+
end
|
96
|
+
|
97
|
+
def values(name)
|
98
|
+
namespace(name).map{ |dataset| load(dataset[:value]) }
|
99
|
+
end
|
100
|
+
|
101
|
+
def sort(name, order, limit)
|
102
|
+
data = case order.downcase
|
103
|
+
when :asc, 'asc'
|
104
|
+
namespace(name).order(Sequel.asc(:key))
|
105
|
+
when :desc, 'desc'
|
106
|
+
namespace(name).order(Sequel.desc(:key))
|
107
|
+
else
|
108
|
+
namespace(name).order(Sequel.asc(:key))
|
109
|
+
end
|
110
|
+
|
111
|
+
unless limit.nil?
|
112
|
+
raise ArgumentError, "limit must be a positive Integer" unless Integer === limit
|
113
|
+
data = data.limit(limit)
|
114
|
+
end
|
115
|
+
data.map{ |dataset| dataset[:key] }
|
116
|
+
end
|
117
|
+
|
118
|
+
def rename(name, old_key, new_key)
|
119
|
+
namespace(name).where(:key => build_key(old_key)).limit(1).update(:key => build_key(new_key)) && "OK"
|
120
|
+
end
|
121
|
+
|
122
|
+
def length(name)
|
123
|
+
namespace(name).count
|
124
|
+
end
|
125
|
+
|
126
|
+
private
|
127
|
+
|
128
|
+
# Sequel seems to have problems with escaping, so we use Base64 encoding/decoding as a simple work around
|
129
|
+
def load(data)
|
130
|
+
Marshal.load(Base64.urlsafe_decode64(data))
|
131
|
+
end
|
132
|
+
|
133
|
+
def dump(data)
|
134
|
+
Base64.urlsafe_encode64(Marshal.dump(data))
|
135
|
+
end
|
136
|
+
|
137
|
+
def connection
|
138
|
+
@connection
|
139
|
+
end
|
140
|
+
|
141
|
+
def build_key(key)
|
142
|
+
key.strip.to_s
|
143
|
+
end
|
144
|
+
|
145
|
+
def basic_tub_name(name)
|
146
|
+
"#{TABLE_PREFIX}#{name.strip}"
|
147
|
+
end
|
148
|
+
|
149
|
+
end
|
150
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "canery/backend"
|
4
|
+
require "canery/tub"
|
5
|
+
|
6
|
+
module Canery
|
7
|
+
|
8
|
+
class CaneryError < StandardError; end
|
9
|
+
|
10
|
+
class Client
|
11
|
+
|
12
|
+
def initialize(connection_uri = nil)
|
13
|
+
@backend = Backend.new(connection_uri)
|
14
|
+
end
|
15
|
+
|
16
|
+
def tub(name)
|
17
|
+
create_tub(tub_name(name)) unless tub?(tub_name(name))
|
18
|
+
begin
|
19
|
+
@tub_cache ||= {}
|
20
|
+
@tub_cache[tub_name(name)] ||= Tub.new(backend, tub_name(name))
|
21
|
+
rescue
|
22
|
+
raise Canery::CaneryError, "This tub does not exist! You must create it before you can use it."
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete_tub(name)
|
27
|
+
@tub_cache.delete(tub_name(name)) if @tub_cache[tub_name(name)]
|
28
|
+
backend.delete_namespace(tub_name(name))
|
29
|
+
end
|
30
|
+
|
31
|
+
def has_tub?(name)
|
32
|
+
backend.namespace?(tub_name(name))
|
33
|
+
end
|
34
|
+
alias :tub? :has_tub?
|
35
|
+
|
36
|
+
def tubs
|
37
|
+
backend.namespaces
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def backend
|
43
|
+
@backend
|
44
|
+
end
|
45
|
+
|
46
|
+
def create_tub(name)
|
47
|
+
begin
|
48
|
+
backend.create_namespace(tub_name(name))
|
49
|
+
rescue => exception
|
50
|
+
raise Canery::CaneryError, exception
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def tub_name(name)
|
55
|
+
name.strip.to_s
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
data/lib/canery/tub.rb
ADDED
@@ -0,0 +1,83 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "securerandom"
|
4
|
+
|
5
|
+
module Canery
|
6
|
+
class Tub
|
7
|
+
|
8
|
+
attr_reader :name
|
9
|
+
|
10
|
+
def initialize(backend, name)
|
11
|
+
raise ArgumentError, "unknown backend type" unless Canery::Backend === backend
|
12
|
+
raise ArgumentError, "name of tub must be a String" unless String === name
|
13
|
+
@backend, @name = backend, name
|
14
|
+
end
|
15
|
+
|
16
|
+
def get(key)
|
17
|
+
backend.get(name, key)
|
18
|
+
end
|
19
|
+
alias :[] :get
|
20
|
+
|
21
|
+
def mget(keys)
|
22
|
+
backend.mget(name, keys)
|
23
|
+
end
|
24
|
+
|
25
|
+
def set(key, value)
|
26
|
+
backend.set(name, key || uuid, value)
|
27
|
+
end
|
28
|
+
alias :[]= :set
|
29
|
+
|
30
|
+
def mset(data)
|
31
|
+
backend.mset(name, data)
|
32
|
+
end
|
33
|
+
|
34
|
+
def delete(key)
|
35
|
+
backend.delete(name, key)
|
36
|
+
end
|
37
|
+
|
38
|
+
def clear
|
39
|
+
backend.clear(name)
|
40
|
+
end
|
41
|
+
|
42
|
+
def has_key?(key)
|
43
|
+
backend.has_key?(name, key)
|
44
|
+
end
|
45
|
+
alias :key? :has_key?
|
46
|
+
|
47
|
+
def keys
|
48
|
+
backend.keys(name)
|
49
|
+
end
|
50
|
+
|
51
|
+
def values
|
52
|
+
backend.values(name)
|
53
|
+
end
|
54
|
+
|
55
|
+
def sort(order, limit = nil)
|
56
|
+
begin
|
57
|
+
backend.sort(name, order, limit)
|
58
|
+
rescue ArgumentError
|
59
|
+
raise Canery::CaneryError, "limit parameter must be an Integer"
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def rename(old_key, new_key)
|
64
|
+
backend.rename(name, old_key, new_key)
|
65
|
+
end
|
66
|
+
|
67
|
+
def length
|
68
|
+
backend.length(name)
|
69
|
+
end
|
70
|
+
alias :size :length
|
71
|
+
|
72
|
+
private
|
73
|
+
|
74
|
+
def backend
|
75
|
+
@backend
|
76
|
+
end
|
77
|
+
|
78
|
+
def uuid
|
79
|
+
SecureRandom.uuid
|
80
|
+
end
|
81
|
+
|
82
|
+
end
|
83
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "canery"
|
4
|
+
|
5
|
+
describe Canery::Client do
|
6
|
+
context "initialization" do
|
7
|
+
it "should not require a connection string" do
|
8
|
+
expect { Canery::Client.new }.not_to raise_error
|
9
|
+
end
|
10
|
+
|
11
|
+
it "should hide the backend" do
|
12
|
+
@client = Canery::Client.new
|
13
|
+
expect { @client.backend }.to raise_error
|
14
|
+
end
|
15
|
+
|
16
|
+
it "should have no tubs initialized" do
|
17
|
+
@client = Canery::Client.new
|
18
|
+
@client.tubs.should be_empty
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
|
23
|
+
context "tub method" do
|
24
|
+
before(:each) do
|
25
|
+
@client = Canery::Client.new
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#tub" do
|
29
|
+
it "should create a new tub" do
|
30
|
+
@client.tub("store")
|
31
|
+
@client.tubs.include?("store").should be_true
|
32
|
+
end
|
33
|
+
|
34
|
+
it "tub should be a Tub object" do
|
35
|
+
@client.tub("store").should be_kind_of(Canery::Tub)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should use the an existing tub instead of creating a new one" do
|
39
|
+
@client.tub("store").should == @client.tub("store")
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
describe "#delete_tub" do
|
44
|
+
it "should delete a specified tub" do
|
45
|
+
@client.tub("store")
|
46
|
+
@client.tubs.length.should == 1
|
47
|
+
@client.delete_tub("store")
|
48
|
+
@client.tubs.length.should == 0
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
describe "#has_tub?" do
|
53
|
+
it "should check return true if a tub already exists" do
|
54
|
+
@client.tub("store")
|
55
|
+
@client.has_tub?("store").should be_true
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#tubs" do
|
60
|
+
it "should return a list of all tubs" do
|
61
|
+
@client.tub("first_store")
|
62
|
+
@client.tub("another_tub")
|
63
|
+
@client.tubs.should be_kind_of(Array)
|
64
|
+
@client.tubs.length.should == 2
|
65
|
+
@client.tubs.include?("first_store").should be_true
|
66
|
+
@client.tubs.include?("another_tub").should be_true
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
end
|
71
|
+
end
|
@@ -0,0 +1,160 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
require "canery"
|
5
|
+
|
6
|
+
describe Canery::Tub do
|
7
|
+
before(:each) do
|
8
|
+
@client = Canery::Client.new
|
9
|
+
end
|
10
|
+
|
11
|
+
context "on retrieving values" do
|
12
|
+
describe "#get" do
|
13
|
+
it "should retrieve value" do
|
14
|
+
store = @client.tub("store")
|
15
|
+
store.set("hello", "hello world")
|
16
|
+
store.get("hello").should == "hello world"
|
17
|
+
end
|
18
|
+
|
19
|
+
it "should retrieve value using the alias method" do
|
20
|
+
store = @client.tub("store")
|
21
|
+
store.set("hello", "hello world")
|
22
|
+
store["hello"].should == "hello world"
|
23
|
+
end
|
24
|
+
|
25
|
+
it "should return nil if value is not set" do
|
26
|
+
store = @client.tub("store")
|
27
|
+
store.get("unset_key").should be_nil
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
describe "#mget" do
|
32
|
+
it "should retrieve multiple values at once" do
|
33
|
+
store = @client.tub("store")
|
34
|
+
store.mset(demo_hash)
|
35
|
+
store.mget(demo_hash.keys).each do |value|
|
36
|
+
demo_hash.values.should include(value)
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "on setting values" do
|
43
|
+
describe "#set" do
|
44
|
+
it "should set value and return key" do
|
45
|
+
store = @client.tub("store")
|
46
|
+
store.set("hello", "hello world").should == "hello"
|
47
|
+
end
|
48
|
+
|
49
|
+
it "should set value using the alias method" do
|
50
|
+
store = @client.tub("store")
|
51
|
+
(store["hello"] = "hello world") == "hello"
|
52
|
+
end
|
53
|
+
|
54
|
+
it "should overwrite an existing value" do
|
55
|
+
store = @client.tub("store")
|
56
|
+
store.set("hello", "hello world").should == "hello"
|
57
|
+
store.get("hello") == "hello world"
|
58
|
+
store.set("hello", "new hello world").should == "hello"
|
59
|
+
store.get("hello") == "new hello world"
|
60
|
+
end
|
61
|
+
|
62
|
+
it "should automatically create an uuid key if key is nil" do
|
63
|
+
store = @client.tub("store")
|
64
|
+
uuid?(store.set(nil, "hello world")).should be_true
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#mset" do
|
69
|
+
it "should set multiple values at once" do
|
70
|
+
store = @client.tub("store")
|
71
|
+
store.mset(demo_hash).should == "OK"
|
72
|
+
demo_hash.each do |key, value|
|
73
|
+
store.get(key).should == value
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
describe "#delete" do
|
80
|
+
it "should delete a value" do
|
81
|
+
store = @client.tub("store")
|
82
|
+
store.set("hello", "hello world")
|
83
|
+
store.delete("hello").should == "OK"
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#clear" do
|
88
|
+
it "should delete all values of a tub" do
|
89
|
+
store = @client.tub("store")
|
90
|
+
store.mset(demo_hash)
|
91
|
+
store.size.should == demo_hash.length
|
92
|
+
store.clear
|
93
|
+
store.size.should == 0
|
94
|
+
end
|
95
|
+
end
|
96
|
+
|
97
|
+
describe "#has_key?" do
|
98
|
+
it "should return true if key is present in tub" do
|
99
|
+
store = @client.tub("store")
|
100
|
+
store.set("hello", "hello world")
|
101
|
+
store.has_key?("hello").should be_true
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
describe "#keys" do
|
106
|
+
it "should return all keys present in a tub" do
|
107
|
+
store = @client.tub("store")
|
108
|
+
store.mset(demo_hash)
|
109
|
+
(store.keys & demo_hash.keys).should == demo_hash.keys
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "#values" do
|
114
|
+
it "should return all values present in a tub" do
|
115
|
+
store = @client.tub("store")
|
116
|
+
store.mset(demo_hash)
|
117
|
+
(store.values & demo_hash.values).should == demo_hash.values
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
describe "#sort" do
|
122
|
+
it "should sort keys in ascending order" do
|
123
|
+
store = @client.tub("store")
|
124
|
+
store.mset(demo_hash)
|
125
|
+
(store.sort(:asc) & demo_hash.keys.sort).should == demo_hash.keys.sort
|
126
|
+
end
|
127
|
+
|
128
|
+
it "should sort keys in descending order" do
|
129
|
+
store = @client.tub("store")
|
130
|
+
store.mset(demo_hash)
|
131
|
+
data_hash = demo_hash.keys.sort{|a,b| b <=> a}
|
132
|
+
(store.sort(:desc) & data_hash).should == data_hash
|
133
|
+
end
|
134
|
+
|
135
|
+
it "should sort keys and limit" do
|
136
|
+
store = @client.tub("store")
|
137
|
+
store.mset(demo_hash)
|
138
|
+
(store.sort(:asc, 3) & demo_hash.keys.sort[0...3]).should == demo_hash.keys.sort[0...3]
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
describe "#rename" do
|
143
|
+
it "should rename a key without touching the value" do
|
144
|
+
store = @client.tub("store")
|
145
|
+
store.set("hello", "hello world")
|
146
|
+
store.get("hello").should == "hello world"
|
147
|
+
store.rename("hello", "new_hello")
|
148
|
+
store.get("new_hello").should == "hello world"
|
149
|
+
end
|
150
|
+
end
|
151
|
+
|
152
|
+
describe "#length" do
|
153
|
+
it "should return the number of elements in a tub" do
|
154
|
+
store = @client.tub("store")
|
155
|
+
store.mset(demo_hash)
|
156
|
+
store.length.should == demo_hash.length
|
157
|
+
end
|
158
|
+
end
|
159
|
+
|
160
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,9 @@
|
|
1
|
+
# encoding: utf-8
|
2
|
+
|
3
|
+
def uuid?(value)
|
4
|
+
(value =~ /^[0-9a-f]{8}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{4}\-[0-9a-f]{12}$/) != nil
|
5
|
+
end
|
6
|
+
|
7
|
+
def demo_hash
|
8
|
+
{"homer" => "Homer Simpson", "marge" => "Marge Simpson", "lisa" => "Lisa Simpson", "bart" => "Bart Simpson", "maggy" => "Maggy Simpson"}
|
9
|
+
end
|
metadata
ADDED
@@ -0,0 +1,118 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: canery
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Railsmechanic
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2012-08-25 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: sequel
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
none: false
|
18
|
+
requirements:
|
19
|
+
- - ~>
|
20
|
+
- !ruby/object:Gem::Version
|
21
|
+
version: 3.38.0
|
22
|
+
type: :runtime
|
23
|
+
prerelease: false
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
version: 3.38.0
|
30
|
+
- !ruby/object:Gem::Dependency
|
31
|
+
name: rspec
|
32
|
+
requirement: !ruby/object:Gem::Requirement
|
33
|
+
none: false
|
34
|
+
requirements:
|
35
|
+
- - ! '>='
|
36
|
+
- !ruby/object:Gem::Version
|
37
|
+
version: 2.11.0
|
38
|
+
type: :development
|
39
|
+
prerelease: false
|
40
|
+
version_requirements: !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ! '>='
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
version: 2.11.0
|
46
|
+
- !ruby/object:Gem::Dependency
|
47
|
+
name: sqlite3-ruby
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
49
|
+
none: false
|
50
|
+
requirements:
|
51
|
+
- - ~>
|
52
|
+
- !ruby/object:Gem::Version
|
53
|
+
version: 1.3.3
|
54
|
+
type: :runtime
|
55
|
+
prerelease: false
|
56
|
+
version_requirements: !ruby/object:Gem::Requirement
|
57
|
+
none: false
|
58
|
+
requirements:
|
59
|
+
- - ~>
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: 1.3.3
|
62
|
+
description: A simple but handy key/value store which is able to use a bunch of SQL
|
63
|
+
databases as its backend.
|
64
|
+
email:
|
65
|
+
- info@railsmechanic.de
|
66
|
+
executables: []
|
67
|
+
extensions: []
|
68
|
+
extra_rdoc_files: []
|
69
|
+
files:
|
70
|
+
- Gemfile
|
71
|
+
- LICENSE
|
72
|
+
- README.md
|
73
|
+
- Rakefile
|
74
|
+
- canery.gemspec
|
75
|
+
- lib/canery.rb
|
76
|
+
- lib/canery/backend.rb
|
77
|
+
- lib/canery/client.rb
|
78
|
+
- lib/canery/tub.rb
|
79
|
+
- lib/canery/version.rb
|
80
|
+
- spec/canery_client_spec.rb
|
81
|
+
- spec/canery_tub_spec.rb
|
82
|
+
- spec/spec_helper.rb
|
83
|
+
homepage: https://github.com/railsmechanic/canery
|
84
|
+
licenses: []
|
85
|
+
post_install_message:
|
86
|
+
rdoc_options: []
|
87
|
+
require_paths:
|
88
|
+
- lib
|
89
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
90
|
+
none: false
|
91
|
+
requirements:
|
92
|
+
- - ! '>='
|
93
|
+
- !ruby/object:Gem::Version
|
94
|
+
version: '0'
|
95
|
+
segments:
|
96
|
+
- 0
|
97
|
+
hash: 1096333460484850874
|
98
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
99
|
+
none: false
|
100
|
+
requirements:
|
101
|
+
- - ! '>='
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '0'
|
104
|
+
segments:
|
105
|
+
- 0
|
106
|
+
hash: 1096333460484850874
|
107
|
+
requirements: []
|
108
|
+
rubyforge_project:
|
109
|
+
rubygems_version: 1.8.24
|
110
|
+
signing_key:
|
111
|
+
specification_version: 3
|
112
|
+
summary: Canery is a simple and easy to use key/value store with several commands
|
113
|
+
to store and retrieve data. Because it uses a SQL database for storing the data,
|
114
|
+
it can be used in many environments.
|
115
|
+
test_files:
|
116
|
+
- spec/canery_client_spec.rb
|
117
|
+
- spec/canery_tub_spec.rb
|
118
|
+
- spec/spec_helper.rb
|