active_store 0.0.1
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +5 -0
- data/.rspec +2 -0
- data/.rvmrc +1 -0
- data/Gemfile +10 -0
- data/README +0 -0
- data/Rakefile +19 -0
- data/active_store.gemspec +26 -0
- data/lib/active_store/base.rb +188 -0
- data/lib/active_store/connection.rb +91 -0
- data/lib/active_store/version.rb +3 -0
- data/lib/active_store.rb +6 -0
- data/spec/lib/active_store/base_spec.rb +378 -0
- data/spec/lib/active_store/connection_spec.rb +86 -0
- data/spec/spec_helper.rb +9 -0
- metadata +108 -0
data/.rspec
ADDED
data/.rvmrc
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
rvm use jruby-1.6.5@active_store --create
|
data/Gemfile
ADDED
data/README
ADDED
File without changes
|
data/Rakefile
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
# Get your spec rake tasks working in RSpec 2.0
|
3
|
+
|
4
|
+
require 'rspec/core/rake_task'
|
5
|
+
|
6
|
+
desc "Run specs"
|
7
|
+
RSpec::Core::RakeTask.new do |t|
|
8
|
+
t.pattern = "./spec/**/*_spec.rb" # don't need this, it's default.
|
9
|
+
# Put spec opts in a file named .rspec in root
|
10
|
+
end
|
11
|
+
|
12
|
+
|
13
|
+
RUBIES = %w[jruby-1.6.5 1.9.2-p290]
|
14
|
+
desc "Run tests with ruby 1.8.7 and 1.9.2"
|
15
|
+
task :default do
|
16
|
+
RUBIES.each do |ruby|
|
17
|
+
sh "rvm #{ruby}@active_store rake spec"
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "active_store/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "active_store"
|
7
|
+
s.version = ActiveStore::VERSION
|
8
|
+
s.authors = ["Petter Remen", "Jean-Louis Giordano"]
|
9
|
+
s.email = ["petter@icehouse.se", "jean-louis@icehouse.se"]
|
10
|
+
s.homepage = ""
|
11
|
+
s.summary = %q{A active record-like wrapper for memcached protocol}
|
12
|
+
s.description = %q{}
|
13
|
+
|
14
|
+
s.rubyforge_project = "active_store"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
# specify any dependencies here; for example:
|
22
|
+
s.add_development_dependency "rspec"
|
23
|
+
s.add_development_dependency "rake"
|
24
|
+
s.add_runtime_dependency "dalli", '1.1.3'
|
25
|
+
s.add_runtime_dependency "activesupport"
|
26
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'active_support/core_ext/hash/indifferent_access'
|
2
|
+
|
3
|
+
module ActiveStore
|
4
|
+
class Base
|
5
|
+
|
6
|
+
def initialize(params = {})
|
7
|
+
set_attributes(params)
|
8
|
+
@created_at ||= Time.now
|
9
|
+
end
|
10
|
+
|
11
|
+
def ==(another_object)
|
12
|
+
(self.class.attributes - [:created_at]).all? do |attribute|
|
13
|
+
self.send(attribute) == another_object.send(attribute)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
def attributes
|
18
|
+
self.class.attributes.inject(HashWithIndifferentAccess.new) do |accu, attribute|
|
19
|
+
send(attribute).nil? ? accu : accu.merge({attribute => send(attribute)})
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def set_attributes(params = {})
|
24
|
+
params = params.with_indifferent_access
|
25
|
+
self.class.attributes.each do |attribute|
|
26
|
+
write_attribute(attribute, params[attribute])
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
def update_attribute(attribute, value)
|
31
|
+
self.send "#{attribute}=", value
|
32
|
+
save
|
33
|
+
end
|
34
|
+
|
35
|
+
def update_attributes(params)
|
36
|
+
params.each do |key, value|
|
37
|
+
send("#{key}=", value)
|
38
|
+
end
|
39
|
+
save
|
40
|
+
end
|
41
|
+
|
42
|
+
def reload
|
43
|
+
raise NoIdError.new("Could not reload without id.") if (id.nil? || id.empty?)
|
44
|
+
set_attributes(connection.get(id))
|
45
|
+
self
|
46
|
+
end
|
47
|
+
|
48
|
+
def save(ttl = self.class.default_ttl)
|
49
|
+
return false if (id.nil? || id.empty?)
|
50
|
+
connection.set(id, self.attributes, ttl)
|
51
|
+
true
|
52
|
+
end
|
53
|
+
|
54
|
+
def save!(ttl = self.class.default_ttl)
|
55
|
+
save(ttl) or raise NoIdError.new("Could not save without id.")
|
56
|
+
end
|
57
|
+
|
58
|
+
def connection
|
59
|
+
self.class.connection
|
60
|
+
end
|
61
|
+
|
62
|
+
protected
|
63
|
+
|
64
|
+
def write_attribute (attribute, value)
|
65
|
+
instance_variable_set("@#{attribute}", value)
|
66
|
+
end
|
67
|
+
|
68
|
+
def read_attribute (attribute)
|
69
|
+
instance_variable_get("@#{attribute}")
|
70
|
+
end
|
71
|
+
|
72
|
+
class << self
|
73
|
+
attr_reader :attributes
|
74
|
+
|
75
|
+
def define_attributes(*args)
|
76
|
+
@attributes ||= []
|
77
|
+
@attributes += args
|
78
|
+
@attributes |= [:id, :created_at]
|
79
|
+
attr_accessor *@attributes
|
80
|
+
end
|
81
|
+
|
82
|
+
def create(params = {})
|
83
|
+
obj = new(params)
|
84
|
+
obj.save!
|
85
|
+
obj
|
86
|
+
end
|
87
|
+
|
88
|
+
# Allows you to update an ActiveStore object within a
|
89
|
+
# CAS "transaction"
|
90
|
+
#
|
91
|
+
# Example:
|
92
|
+
# Model.cas_update(3) do |instance|
|
93
|
+
# instance.value += 5
|
94
|
+
# end
|
95
|
+
#
|
96
|
+
# Returns nil if the object could not be found.
|
97
|
+
# Returns false if the CAS operation failed due to
|
98
|
+
# the object being modified in the background.
|
99
|
+
def cas_update(id, ttl = default_ttl, &block)
|
100
|
+
block = attributes_block_wrapper(&block)
|
101
|
+
connection.cas(id, ttl, &block)
|
102
|
+
end
|
103
|
+
|
104
|
+
# Like cas_update, but also creates a new object if none could be
|
105
|
+
# found. The given block is guaranteed to be executed at most once.
|
106
|
+
#
|
107
|
+
# Example:
|
108
|
+
# Model.cas_update_or_create(3) do |instance|
|
109
|
+
# instance.value ||= 0
|
110
|
+
# instance.value += 5
|
111
|
+
# end
|
112
|
+
#
|
113
|
+
# Returns false if the CAS operation failed due to
|
114
|
+
# the object having been modified by another process, or if
|
115
|
+
# someone managed to create an object between the CAS and ADD
|
116
|
+
# operations
|
117
|
+
def cas_update_or_create(id, ttl = default_ttl, &block)
|
118
|
+
res = cas_update(id, ttl, &block)
|
119
|
+
res.nil? ? create_with_block(id, ttl, &block) : res
|
120
|
+
end
|
121
|
+
|
122
|
+
def stm_update_or_create (id, ttl = default_ttl, &block)
|
123
|
+
begin
|
124
|
+
success = cas_update_or_create(id, ttl = default_ttl, &block)
|
125
|
+
end until success
|
126
|
+
true
|
127
|
+
end
|
128
|
+
|
129
|
+
def create_with_block(id, ttl = default_ttl, &block)
|
130
|
+
block = attributes_block_wrapper(&block)
|
131
|
+
connection.add(id, block.call({:id => id}), ttl)
|
132
|
+
end
|
133
|
+
|
134
|
+
def find(id)
|
135
|
+
return nil if (id.nil? || id.empty?)
|
136
|
+
params = connection.get(id)
|
137
|
+
params ? new(params) : nil
|
138
|
+
end
|
139
|
+
|
140
|
+
def find_or_initialize_by_id(id)
|
141
|
+
find(id) || new(:id => id)
|
142
|
+
end
|
143
|
+
|
144
|
+
def connection
|
145
|
+
@connection ||= Connection.new(connection_string, namespace, default_ttl)
|
146
|
+
end
|
147
|
+
|
148
|
+
def connection_string= (connection_string)
|
149
|
+
@connection = nil
|
150
|
+
@connection_string = connection_string
|
151
|
+
end
|
152
|
+
|
153
|
+
def connection_string
|
154
|
+
@connection_string ||= (superclass.connection_string unless self == Base)
|
155
|
+
end
|
156
|
+
|
157
|
+
def namespace= (namespace)
|
158
|
+
@connection = nil
|
159
|
+
@namespace = namespace
|
160
|
+
end
|
161
|
+
|
162
|
+
def namespace
|
163
|
+
@namespace ||= self.name
|
164
|
+
end
|
165
|
+
|
166
|
+
def default_ttl= (default_ttl)
|
167
|
+
@connection = nil
|
168
|
+
@default_ttl = default_ttl
|
169
|
+
end
|
170
|
+
|
171
|
+
def default_ttl
|
172
|
+
@default_ttl ||= 30 * 24 * 3600
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
def attributes_block_wrapper(&block)
|
177
|
+
lambda do |attrs|
|
178
|
+
instance = new(attrs)
|
179
|
+
block.call(instance)
|
180
|
+
instance.attributes
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
class NoIdError < StandardError
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
require 'dalli'
|
2
|
+
require 'json'
|
3
|
+
|
4
|
+
module ActiveStore
|
5
|
+
class Connection
|
6
|
+
attr_accessor :connection_string, :namespace, :default_ttl
|
7
|
+
|
8
|
+
def initialize(connection_string, namespace, default_ttl)
|
9
|
+
@connection_string = connection_string
|
10
|
+
@namespace = namespace
|
11
|
+
@default_ttl = default_ttl
|
12
|
+
end
|
13
|
+
|
14
|
+
class ::Dalli::Client
|
15
|
+
# Monkey patch to avoid having a separate branch
|
16
|
+
def validate_key (key)
|
17
|
+
raise ArgumentError, "key cannot be blank" if key.nil? || key.strip.size == 0
|
18
|
+
raise ArgumentError, "key too long #{key.inspect}" if key.length > 250
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
attr_writer :store
|
23
|
+
|
24
|
+
def flush_all
|
25
|
+
store.flush_all
|
26
|
+
end
|
27
|
+
|
28
|
+
def incr(*args)
|
29
|
+
store.incr(*args)
|
30
|
+
end
|
31
|
+
|
32
|
+
def delete(*args)
|
33
|
+
store.delete(*args)
|
34
|
+
end
|
35
|
+
|
36
|
+
def get (key)
|
37
|
+
load(get_raw(key))
|
38
|
+
end
|
39
|
+
|
40
|
+
def set (key, value, ttl=default_ttl)
|
41
|
+
set_raw(key, dump(value), ttl)
|
42
|
+
end
|
43
|
+
|
44
|
+
def add (key, value, ttl=default_ttl)
|
45
|
+
add_raw(key, dump(value), ttl)
|
46
|
+
end
|
47
|
+
|
48
|
+
def cas (key, ttl=default_ttl)
|
49
|
+
store.cas(key, ttl, :raw => true) do |value|
|
50
|
+
raw_output = yield load(value)
|
51
|
+
dump(raw_output)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
def get_raw(key)
|
56
|
+
store.get(key)
|
57
|
+
end
|
58
|
+
|
59
|
+
def set_raw(key, value, ttl=default_ttl)
|
60
|
+
store.set(key, value, ttl, :raw => true)
|
61
|
+
end
|
62
|
+
|
63
|
+
def add_raw(key, value, ttl=default_ttl)
|
64
|
+
store.add(key, value, ttl, :raw => true)
|
65
|
+
end
|
66
|
+
|
67
|
+
def store
|
68
|
+
unless @store
|
69
|
+
@store = Dalli::Client.new(connection_string, :namespace => namespace, :expires_in => default_ttl, :socket_timeout => 1)
|
70
|
+
begin
|
71
|
+
@store.get('test')
|
72
|
+
rescue Dalli::RingError => e
|
73
|
+
@store = nil
|
74
|
+
raise e
|
75
|
+
end
|
76
|
+
end
|
77
|
+
@store
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
# Ugly tricks to fool parsers that don't allow literals
|
83
|
+
def dump (value)
|
84
|
+
JSON.dump([value])[1...-1]
|
85
|
+
end
|
86
|
+
|
87
|
+
def load (value)
|
88
|
+
JSON.load("[#{value}]").first
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
data/lib/active_store.rb
ADDED
@@ -0,0 +1,378 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe ActiveStore::Base do
|
4
|
+
before do
|
5
|
+
class ItemStore < ActiveStore::Base
|
6
|
+
define_attributes :a1, :a2
|
7
|
+
self.namespace = "item"
|
8
|
+
self.default_ttl = 0
|
9
|
+
self.connection_string = nil
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
describe ".connection_string" do
|
14
|
+
it "defaults to Base value if nil" do
|
15
|
+
ActiveStore::Base.connection_string = "some_string"
|
16
|
+
ItemStore.connection_string.should == "some_string"
|
17
|
+
ItemStore.connection_string = "some other string"
|
18
|
+
ActiveStore::Base.connection_string.should == "some_string"
|
19
|
+
ItemStore.connection_string.should == "some other string"
|
20
|
+
end
|
21
|
+
end
|
22
|
+
describe ".namespace" do
|
23
|
+
it "forces a new connection" do
|
24
|
+
connection = ItemStore.connection
|
25
|
+
ItemStore.namespace = "item"
|
26
|
+
ItemStore.connection.should_not be_eql connection
|
27
|
+
end
|
28
|
+
it "sends itself as a parameter to Connection.new" do
|
29
|
+
ItemStore.connection.namespace.should == "item"
|
30
|
+
ItemStore.namespace = "foo"
|
31
|
+
ItemStore.connection.namespace.should == "foo"
|
32
|
+
end
|
33
|
+
it "defaults to the name of the class" do
|
34
|
+
ItemStore.namespace = nil
|
35
|
+
ItemStore.namespace.should == 'ItemStore'
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
describe ".default_ttl" do
|
40
|
+
it "forces a new connection" do
|
41
|
+
connection = ItemStore.connection
|
42
|
+
ItemStore.default_ttl = 2
|
43
|
+
ItemStore.connection.should_not be_eql connection
|
44
|
+
end
|
45
|
+
it "sends itself as a parameter to Connection.new" do
|
46
|
+
ItemStore.connection.default_ttl.should == 0
|
47
|
+
ItemStore.default_ttl = 2
|
48
|
+
ItemStore.connection.default_ttl.should == 2
|
49
|
+
end
|
50
|
+
it "defaults to 30 days" do
|
51
|
+
ItemStore.default_ttl = nil
|
52
|
+
ItemStore.default_ttl.should == 30 * 24 * 3600
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
it "lists attibute names" do
|
57
|
+
ItemStore.attributes.should include :a1, :a2
|
58
|
+
end
|
59
|
+
|
60
|
+
it "provides accessors for its attributes" do
|
61
|
+
item = ItemStore.new
|
62
|
+
item.a1.should be_nil
|
63
|
+
(item.a1 = "new_value").should == "new_value"
|
64
|
+
item.a1.should == "new_value"
|
65
|
+
end
|
66
|
+
|
67
|
+
describe ".define_attributes" do
|
68
|
+
it "should add id and created_at in attributes" do
|
69
|
+
ItemStore.attributes.should include :id, :created_at
|
70
|
+
end
|
71
|
+
it "should add id and created_at in its instance accessors" do
|
72
|
+
ItemStore.new.methods.map(&:to_sym).should include :id, :id=, :created_at, :created_at=
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
describe "#initialize" do
|
77
|
+
it "initializes given attributes" do
|
78
|
+
params = { :a1 => "one", :a2 => "two" }
|
79
|
+
item = ItemStore.new params
|
80
|
+
item.attributes[:a1].should == params[:a1]
|
81
|
+
item.attributes[:a2].should == params[:a2]
|
82
|
+
end
|
83
|
+
it "initializes attributes even when the params keys are strings" do
|
84
|
+
params = { "a1" => "one", "a2" => "two" }
|
85
|
+
item = ItemStore.new params
|
86
|
+
item.attributes[:a1].should == params["a1"]
|
87
|
+
item.attributes[:a2].should == params["a2"]
|
88
|
+
end
|
89
|
+
it "doesn't require attributes" do
|
90
|
+
ItemStore.new.a1.should be_nil
|
91
|
+
end
|
92
|
+
it "sets created_at if not given in params" do
|
93
|
+
ItemStore.new.created_at.should be_within(1).of(Time.now)
|
94
|
+
end
|
95
|
+
it "sets created_at if given" do
|
96
|
+
ItemStore.new(:created_at => "some time").created_at.should == "some time"
|
97
|
+
end
|
98
|
+
it "can set attribute to false" do
|
99
|
+
item = ItemStore.new "a1" => false
|
100
|
+
item.attributes[:a1].should == false
|
101
|
+
end
|
102
|
+
end
|
103
|
+
|
104
|
+
describe "#==" do
|
105
|
+
it "returns true if all attributes are the same" do
|
106
|
+
params = { :a1 => "one", :a2 => "two" }
|
107
|
+
(ItemStore.new(params) == ItemStore.new(params)).should be_true
|
108
|
+
end
|
109
|
+
it "returns false any of the attributes are different" do
|
110
|
+
params1 = { :a1 => "one", :a2 => "two"}
|
111
|
+
params2 = { :a1 => "different", :a2 => "two" }
|
112
|
+
(ItemStore.new(params1) == ItemStore.new(params2)).should be_false
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
describe "#update_attribute" do
|
117
|
+
it "updates given attribute and saves" do
|
118
|
+
item = ItemStore.new :id => "myid"
|
119
|
+
item.update_attribute(:a1, "new_value").should be_true
|
120
|
+
item.reload.a1.should be_true
|
121
|
+
end
|
122
|
+
it "returns false if id is missing" do
|
123
|
+
item = ItemStore.new
|
124
|
+
item.update_attribute(:a1, "new_value").should be_false
|
125
|
+
end
|
126
|
+
it "raises if attribute is missing" do
|
127
|
+
item = ItemStore.new
|
128
|
+
expect { item.update_attribute :apa, true }.to raise_exception(NoMethodError)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#update_attributes" do
|
133
|
+
it "updates all given attributes" do
|
134
|
+
item = ItemStore.new :id => "myid"
|
135
|
+
item.update_attributes(:a1 => "one", :a2 => "two").should == true
|
136
|
+
item.reload
|
137
|
+
item.a1.should == "one"
|
138
|
+
item.a2.should == "two"
|
139
|
+
end
|
140
|
+
it "returns false if id is missing" do
|
141
|
+
item = ItemStore.new
|
142
|
+
item.update_attributes(:a1 => "new_value").should be_false
|
143
|
+
end
|
144
|
+
end
|
145
|
+
|
146
|
+
describe ".cas_update" do
|
147
|
+
context "when the given id does not exist" do
|
148
|
+
it "returns nil" do
|
149
|
+
ItemStore.cas_update("nonexistent").should be_nil
|
150
|
+
end
|
151
|
+
it "should not run the block" do
|
152
|
+
ItemStore.cas_update("nonexistent") do
|
153
|
+
true.should be_false
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
context "when the record has been updated before CAS set operation" do
|
158
|
+
it "returns false" do
|
159
|
+
ItemStore.create :id => "existent", :a1 => "foo"
|
160
|
+
ItemStore.cas_update("existent") do |item|
|
161
|
+
same_item = ItemStore.find("existent")
|
162
|
+
same_item.a1 = "bar"
|
163
|
+
same_item.save
|
164
|
+
item.a1 = "baz"
|
165
|
+
end.should be_false
|
166
|
+
end
|
167
|
+
it "doesn't modify the record" do
|
168
|
+
ItemStore.create :id => "existent", :a1 => "foo"
|
169
|
+
ItemStore.cas_update("existent") do |item|
|
170
|
+
same_item = ItemStore.find("existent")
|
171
|
+
same_item.a1 = "bar"
|
172
|
+
same_item.save
|
173
|
+
item.a1 = "baz"
|
174
|
+
end
|
175
|
+
ItemStore.find("existent").a1.should == "bar"
|
176
|
+
end
|
177
|
+
end
|
178
|
+
context "otherwise" do
|
179
|
+
it "returns true" do
|
180
|
+
ItemStore.create :id => "existent"
|
181
|
+
ItemStore.cas_update("existent") do |item|
|
182
|
+
item.a1 = "baz"
|
183
|
+
end.should be_true
|
184
|
+
end
|
185
|
+
it "updates the record" do
|
186
|
+
ItemStore.create :id => "existent"
|
187
|
+
ItemStore.cas_update("existent") do |item|
|
188
|
+
item.a1 = "baz"
|
189
|
+
end
|
190
|
+
ItemStore.find("existent").a1.should == "baz"
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
describe "cas_update_or_create" do
|
196
|
+
it "tries to update the record using cas_update" do
|
197
|
+
proc_mock = Proc.new {}
|
198
|
+
ItemStore.should_receive(:cas_update).with("old", ItemStore.default_ttl, &proc_mock).and_return(true)
|
199
|
+
ItemStore.cas_update_or_create("old", &proc_mock)
|
200
|
+
end
|
201
|
+
|
202
|
+
it "creates the record create_with_block if the CAS operation fails due to missing record" do
|
203
|
+
proc_mock = Proc.new {}
|
204
|
+
ItemStore.stub!(:cas_update).and_return(nil)
|
205
|
+
ItemStore.should_receive(:create_with_block).with("old", ItemStore.default_ttl, &proc_mock)
|
206
|
+
ItemStore.cas_update_or_create("old", &proc_mock)
|
207
|
+
end
|
208
|
+
|
209
|
+
it "returns false if update_cas returns false" do
|
210
|
+
proc_mock = Proc.new {}
|
211
|
+
ItemStore.stub!(:cas_update).and_return(false)
|
212
|
+
ItemStore.cas_update_or_create("foo", &proc_mock).should be_false
|
213
|
+
end
|
214
|
+
|
215
|
+
it "returns false if create_with_block returns false" do
|
216
|
+
proc_mock = Proc.new {}
|
217
|
+
ItemStore.stub!(:cas_update).and_return(nil)
|
218
|
+
ItemStore.stub!(:create_with_block).and_return(false)
|
219
|
+
ItemStore.cas_update_or_create("foo", &proc_mock).should be_false
|
220
|
+
end
|
221
|
+
|
222
|
+
it "returns true if the record was successfully created" do
|
223
|
+
ItemStore.cas_update_or_create("foo") {}.should be_true
|
224
|
+
end
|
225
|
+
|
226
|
+
it "returns true if the record was successfully modified" do
|
227
|
+
ItemStore.create(:id => "foo")
|
228
|
+
ItemStore.cas_update_or_create("foo") {}.should be_true
|
229
|
+
end
|
230
|
+
|
231
|
+
end
|
232
|
+
|
233
|
+
describe "stm_update_or_create" do
|
234
|
+
it "performs a cas_update_or_create" do
|
235
|
+
to_run = Proc.new(){}
|
236
|
+
ItemStore.should_receive(:cas_update_or_create) do |id, &block|
|
237
|
+
id.should == :some_id
|
238
|
+
block.should == to_run
|
239
|
+
true
|
240
|
+
end
|
241
|
+
ItemStore.stm_update_or_create(:some_id, &to_run).should be_true
|
242
|
+
end
|
243
|
+
it "performs a cas_update_or_create until success" do
|
244
|
+
ItemStore.should_receive(:cas_update_or_create).exactly(3).times.and_return(false, false, true)
|
245
|
+
ItemStore.stm_update_or_create(:some_id){}.should be_true
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
describe "#create_with_block" do
|
250
|
+
it "creates a new record if it doesn't exist" do
|
251
|
+
ItemStore.create_with_block("foo") do |item|
|
252
|
+
item.a1 = "bar"
|
253
|
+
end
|
254
|
+
ItemStore.find("foo").a1.should == "bar"
|
255
|
+
end
|
256
|
+
|
257
|
+
context "if the record already exists" do
|
258
|
+
before do
|
259
|
+
ItemStore.create(:id => "foo", :a1 => "bar")
|
260
|
+
end
|
261
|
+
|
262
|
+
it "returns false" do
|
263
|
+
ItemStore.create_with_block("foo") {}.should be_false
|
264
|
+
end
|
265
|
+
|
266
|
+
it "doesn't update the record" do
|
267
|
+
ItemStore.create_with_block("foo") do |item|
|
268
|
+
item.a1 = "baz"
|
269
|
+
end
|
270
|
+
ItemStore.find("foo").a1.should == "bar"
|
271
|
+
end
|
272
|
+
end
|
273
|
+
end
|
274
|
+
|
275
|
+
describe "#reload" do
|
276
|
+
it "reloads attributes from db" do
|
277
|
+
item = ItemStore.new :id => "myid"
|
278
|
+
ItemStore.connection.set("myid", :a1 => "new value")
|
279
|
+
item.reload
|
280
|
+
item.a1.should == "new value"
|
281
|
+
end
|
282
|
+
it "raises if no id is set" do
|
283
|
+
expect { ItemStore.new.reload }.to raise_exception(ActiveStore::Base::NoIdError)
|
284
|
+
end
|
285
|
+
it "raises if no id is empty string" do
|
286
|
+
expect { ItemStore.new(:id => "").reload }.to raise_exception(ActiveStore::Base::NoIdError)
|
287
|
+
end
|
288
|
+
end
|
289
|
+
|
290
|
+
describe "#save" do
|
291
|
+
it "saves attribute do db" do
|
292
|
+
ItemStore.find("myid").should be_nil
|
293
|
+
item = ItemStore.new :id =>"myid", :a1 => "one", :a2 => "two"
|
294
|
+
item.save.should be_true
|
295
|
+
item.created_at.should_not be_nil
|
296
|
+
ItemStore.find("myid").should == item
|
297
|
+
end
|
298
|
+
it "returns false if id is nil" do
|
299
|
+
item = ItemStore.new :id => nil
|
300
|
+
item.save.should be_false
|
301
|
+
end
|
302
|
+
it "saves with a given ttl" do
|
303
|
+
item = ItemStore.new :id => "myid", :a1 => "one", :a2 => "two"
|
304
|
+
ItemStore.connection.should_receive(:set).with(anything, anything, 2 * 24 * 3600)
|
305
|
+
item.save(2 * 24 * 3600)
|
306
|
+
end
|
307
|
+
it "saves with default ttl if not specified otherwise" do
|
308
|
+
item = ItemStore.new :id => "myid", :a1 => "one", :a2 => "two"
|
309
|
+
ItemStore.connection.should_receive(:set).with(anything, anything, ItemStore.default_ttl)
|
310
|
+
item.save
|
311
|
+
end
|
312
|
+
end
|
313
|
+
|
314
|
+
describe "#save!" do
|
315
|
+
it "calls save" do
|
316
|
+
item = ItemStore.new :id => "myid"
|
317
|
+
item.should_receive(:save).and_return(true)
|
318
|
+
item.save!
|
319
|
+
end
|
320
|
+
it "it returns true on successfull save" do
|
321
|
+
item = ItemStore.new :id => "myid"
|
322
|
+
item.save!.should be_true
|
323
|
+
end
|
324
|
+
it "raises if id is nil" do
|
325
|
+
item = ItemStore.new :id => nil
|
326
|
+
expect { item.save! }.to raise_exception(ActiveStore::Base::NoIdError)
|
327
|
+
end
|
328
|
+
it "saves with a given ttl" do
|
329
|
+
item = ItemStore.new :id => "myid"
|
330
|
+
ItemStore.connection.should_receive(:set).with(anything, anything, 2 * 24 * 3600)
|
331
|
+
item.save!(2 * 24 * 3600)
|
332
|
+
end
|
333
|
+
it "saves with default ttl if not specified otherwise" do
|
334
|
+
item = ItemStore.new :id => "myid"
|
335
|
+
ItemStore.connection.should_receive(:set).with(anything, anything, ItemStore.default_ttl)
|
336
|
+
item.save!
|
337
|
+
end
|
338
|
+
end
|
339
|
+
|
340
|
+
describe ".create" do
|
341
|
+
it "creates an item and saves it" do
|
342
|
+
item = ItemStore.create :id => "myid", :a1 => "one", :a2 => "two"
|
343
|
+
ItemStore.find("myid").should == item
|
344
|
+
end
|
345
|
+
it "returns the created campaign" do
|
346
|
+
ItemStore.create(:id => "myid").should be_a(ItemStore)
|
347
|
+
end
|
348
|
+
end
|
349
|
+
|
350
|
+
describe ".find" do
|
351
|
+
it "returns nil for nil" do
|
352
|
+
ItemStore.find(nil).should == nil
|
353
|
+
end
|
354
|
+
it "returns nil for empty string" do
|
355
|
+
ItemStore.find("").should == nil
|
356
|
+
end
|
357
|
+
it "returns nil if no match" do
|
358
|
+
ItemStore.find("no_match").should == nil
|
359
|
+
end
|
360
|
+
it "returns item if match" do
|
361
|
+
item = ItemStore.create :id => "match"
|
362
|
+
ItemStore.find("match").should == item
|
363
|
+
end
|
364
|
+
end
|
365
|
+
|
366
|
+
describe ".find_or_initialize_by_id" do
|
367
|
+
it "returns found dossier if present" do
|
368
|
+
ItemStore.should_receive(:find).with("some_id").and_return("found_dossier")
|
369
|
+
ItemStore.find_or_initialize_by_id("some_id").should == "found_dossier"
|
370
|
+
end
|
371
|
+
it "initializes a new dossier if none is present" do
|
372
|
+
ItemStore.should_receive(:find).with("some_id").and_return(nil)
|
373
|
+
ItemStore.find_or_initialize_by_id("some_id").id.should == "some_id"
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
378
|
+
|
@@ -0,0 +1,86 @@
|
|
1
|
+
# encoding: UTF-8
|
2
|
+
require 'spec_helper'
|
3
|
+
|
4
|
+
describe ActiveStore::Connection do
|
5
|
+
before do
|
6
|
+
@connection = ActiveStore::Connection.new(nil, 'test', 30 * 24 * 3600)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "it sets and gets values from db" do
|
10
|
+
@connection.set "test", "foo" => "boo", "sha" => "bada"
|
11
|
+
@connection.get("test").should == {"foo" => "boo", "sha" => "bada"}
|
12
|
+
end
|
13
|
+
|
14
|
+
it "sets data as JSON" do
|
15
|
+
@connection.set "test", :foo => "boo", :sha => "bada"
|
16
|
+
@connection.get_raw("test").should == '{"foo":"boo","sha":"bada"}'
|
17
|
+
end
|
18
|
+
|
19
|
+
it "adds data as JSON" do
|
20
|
+
@connection.add("test", :foo => "boo", :sha => "bada").should be_true
|
21
|
+
@connection.get_raw("test").should == '{"foo":"boo","sha":"bada"}'
|
22
|
+
end
|
23
|
+
|
24
|
+
it "get returns nil if the key doesn't exist" do
|
25
|
+
@connection.get("asdfgasda").should be_nil
|
26
|
+
end
|
27
|
+
|
28
|
+
it "get does not create Time-like objects from Time-like strings" do
|
29
|
+
time_str = Time.now.to_s
|
30
|
+
date_str = Date.today.to_s
|
31
|
+
@connection.set("test", "foo" => time_str, "bar" => date_str)
|
32
|
+
@connection.get("test").should == {
|
33
|
+
"foo" => time_str,
|
34
|
+
"bar" => date_str
|
35
|
+
}
|
36
|
+
end
|
37
|
+
|
38
|
+
it "cas data as JSON" do
|
39
|
+
@connection.set_raw("test", '{"foo":"boo","sha":"bada"}', 1)
|
40
|
+
@connection.cas "test" do |data|
|
41
|
+
data.should == {"foo" => "boo", "sha" => "bada"}
|
42
|
+
data["shi"] = "bidi"
|
43
|
+
data
|
44
|
+
end
|
45
|
+
@connection.get_raw("test").should == '{"foo":"boo","sha":"bada","shi":"bidi"}'
|
46
|
+
end
|
47
|
+
|
48
|
+
it "gets data as JSON" do
|
49
|
+
@connection.set_raw("test", '{"foo":"boo","sha":"bada"}', 1)
|
50
|
+
@connection.get("test").should == {"foo" => "boo", "sha" => "bada"}
|
51
|
+
end
|
52
|
+
|
53
|
+
it "Works with increment" do
|
54
|
+
@connection.set("test", 10)
|
55
|
+
@connection.incr("test").should == 11
|
56
|
+
end
|
57
|
+
|
58
|
+
it "Works with strings" do
|
59
|
+
@connection.set("test", "some value")
|
60
|
+
@connection.get("test").should == "some value"
|
61
|
+
end
|
62
|
+
|
63
|
+
it "supports non-ascii keys" do
|
64
|
+
@connection.set("testäöå", "3")
|
65
|
+
@connection.get("testäöå").should == "3"
|
66
|
+
end
|
67
|
+
|
68
|
+
it "supports spaces in keys" do
|
69
|
+
@connection.set(" foo\n", "2")
|
70
|
+
@connection.get(" foo\n").should == "2"
|
71
|
+
end
|
72
|
+
|
73
|
+
describe "#store" do
|
74
|
+
it "returns a Dalli::Client" do
|
75
|
+
@connection.store = nil
|
76
|
+
@connection.store.should be_instance_of(Dalli::Client)
|
77
|
+
end
|
78
|
+
|
79
|
+
it "raises an exception when it cannot connect" do
|
80
|
+
connection = ActiveStore::Connection.new("localhost:1234", "test", 0)
|
81
|
+
expect {connection.store}.to raise_error
|
82
|
+
# Tests that we clear the memoization to try again
|
83
|
+
expect {connection.store}.to raise_error
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
data/spec/spec_helper.rb
ADDED
metadata
ADDED
@@ -0,0 +1,108 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: active_store
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.1
|
5
|
+
prerelease:
|
6
|
+
platform: ruby
|
7
|
+
authors:
|
8
|
+
- Petter Remen
|
9
|
+
- Jean-Louis Giordano
|
10
|
+
autorequire:
|
11
|
+
bindir: bin
|
12
|
+
cert_chain: []
|
13
|
+
date: 2012-03-27 00:00:00.000000000Z
|
14
|
+
dependencies:
|
15
|
+
- !ruby/object:Gem::Dependency
|
16
|
+
name: rspec
|
17
|
+
requirement: &70327179856160 !ruby/object:Gem::Requirement
|
18
|
+
none: false
|
19
|
+
requirements:
|
20
|
+
- - ! '>='
|
21
|
+
- !ruby/object:Gem::Version
|
22
|
+
version: '0'
|
23
|
+
type: :development
|
24
|
+
prerelease: false
|
25
|
+
version_requirements: *70327179856160
|
26
|
+
- !ruby/object:Gem::Dependency
|
27
|
+
name: rake
|
28
|
+
requirement: &70327179855740 !ruby/object:Gem::Requirement
|
29
|
+
none: false
|
30
|
+
requirements:
|
31
|
+
- - ! '>='
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: *70327179855740
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: dalli
|
39
|
+
requirement: &70327179855240 !ruby/object:Gem::Requirement
|
40
|
+
none: false
|
41
|
+
requirements:
|
42
|
+
- - =
|
43
|
+
- !ruby/object:Gem::Version
|
44
|
+
version: 1.1.3
|
45
|
+
type: :runtime
|
46
|
+
prerelease: false
|
47
|
+
version_requirements: *70327179855240
|
48
|
+
- !ruby/object:Gem::Dependency
|
49
|
+
name: activesupport
|
50
|
+
requirement: &70327179854820 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ! '>='
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
version: '0'
|
56
|
+
type: :runtime
|
57
|
+
prerelease: false
|
58
|
+
version_requirements: *70327179854820
|
59
|
+
description: ''
|
60
|
+
email:
|
61
|
+
- petter@icehouse.se
|
62
|
+
- jean-louis@icehouse.se
|
63
|
+
executables: []
|
64
|
+
extensions: []
|
65
|
+
extra_rdoc_files: []
|
66
|
+
files:
|
67
|
+
- .gitignore
|
68
|
+
- .rspec
|
69
|
+
- .rvmrc
|
70
|
+
- Gemfile
|
71
|
+
- README
|
72
|
+
- Rakefile
|
73
|
+
- active_store.gemspec
|
74
|
+
- lib/active_store.rb
|
75
|
+
- lib/active_store/base.rb
|
76
|
+
- lib/active_store/connection.rb
|
77
|
+
- lib/active_store/version.rb
|
78
|
+
- spec/lib/active_store/base_spec.rb
|
79
|
+
- spec/lib/active_store/connection_spec.rb
|
80
|
+
- spec/spec_helper.rb
|
81
|
+
homepage: ''
|
82
|
+
licenses: []
|
83
|
+
post_install_message:
|
84
|
+
rdoc_options: []
|
85
|
+
require_paths:
|
86
|
+
- lib
|
87
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
88
|
+
none: false
|
89
|
+
requirements:
|
90
|
+
- - ! '>='
|
91
|
+
- !ruby/object:Gem::Version
|
92
|
+
version: '0'
|
93
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
94
|
+
none: false
|
95
|
+
requirements:
|
96
|
+
- - ! '>='
|
97
|
+
- !ruby/object:Gem::Version
|
98
|
+
version: '0'
|
99
|
+
requirements: []
|
100
|
+
rubyforge_project: active_store
|
101
|
+
rubygems_version: 1.8.6
|
102
|
+
signing_key:
|
103
|
+
specification_version: 3
|
104
|
+
summary: A active record-like wrapper for memcached protocol
|
105
|
+
test_files:
|
106
|
+
- spec/lib/active_store/base_spec.rb
|
107
|
+
- spec/lib/active_store/connection_spec.rb
|
108
|
+
- spec/spec_helper.rb
|