active_store 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|