riak-client 0.7.0
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/Rakefile +74 -0
- data/lib/riak.rb +49 -0
- data/lib/riak/bucket.rb +176 -0
- data/lib/riak/cache_store.rb +82 -0
- data/lib/riak/client.rb +139 -0
- data/lib/riak/client/curb_backend.rb +82 -0
- data/lib/riak/client/http_backend.rb +209 -0
- data/lib/riak/client/net_http_backend.rb +49 -0
- data/lib/riak/failed_request.rb +37 -0
- data/lib/riak/i18n.rb +20 -0
- data/lib/riak/invalid_response.rb +25 -0
- data/lib/riak/link.rb +73 -0
- data/lib/riak/locale/en.yml +37 -0
- data/lib/riak/map_reduce.rb +248 -0
- data/lib/riak/map_reduce_error.rb +20 -0
- data/lib/riak/robject.rb +267 -0
- data/lib/riak/util/escape.rb +12 -0
- data/lib/riak/util/fiber1.8.rb +48 -0
- data/lib/riak/util/headers.rb +44 -0
- data/lib/riak/util/multipart.rb +52 -0
- data/lib/riak/util/translation.rb +29 -0
- data/lib/riak/walk_spec.rb +117 -0
- data/spec/fixtures/cat.jpg +0 -0
- data/spec/fixtures/multipart-blank.txt +7 -0
- data/spec/fixtures/multipart-with-body.txt +16 -0
- data/spec/integration/riak/cache_store_spec.rb +129 -0
- data/spec/riak/bucket_spec.rb +247 -0
- data/spec/riak/client_spec.rb +174 -0
- data/spec/riak/curb_backend_spec.rb +53 -0
- data/spec/riak/escape_spec.rb +21 -0
- data/spec/riak/headers_spec.rb +34 -0
- data/spec/riak/http_backend_spec.rb +131 -0
- data/spec/riak/link_spec.rb +82 -0
- data/spec/riak/map_reduce_spec.rb +352 -0
- data/spec/riak/multipart_spec.rb +36 -0
- data/spec/riak/net_http_backend_spec.rb +28 -0
- data/spec/riak/object_spec.rb +538 -0
- data/spec/riak/walk_spec_spec.rb +208 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/http_backend_implementation_examples.rb +215 -0
- data/spec/support/mock_server.rb +61 -0
- data/spec/support/mocks.rb +3 -0
- metadata +187 -0
@@ -0,0 +1,12 @@
|
|
1
|
+
module Riak
|
2
|
+
module Util
|
3
|
+
module Escape
|
4
|
+
# URI-escapes bucket or key names that may contain slashes for use in URLs.
|
5
|
+
# @param [String] bucket_or_key the bucket or key name
|
6
|
+
# @return [String] the escaped path segment
|
7
|
+
def escape(bucket_or_key)
|
8
|
+
URI.escape(bucket_or_key.to_s).gsub("/", "%2F")
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# Poor Man's Fiber (API compatible Thread based Fiber implementation for Ruby 1.8)
|
2
|
+
# (c) 2008 Aman Gupta (tmm1)
|
3
|
+
|
4
|
+
unless defined? Fiber
|
5
|
+
require 'thread'
|
6
|
+
|
7
|
+
class FiberError < StandardError; end
|
8
|
+
|
9
|
+
class Fiber
|
10
|
+
def initialize
|
11
|
+
raise ArgumentError, 'new Fiber requires a block' unless block_given?
|
12
|
+
|
13
|
+
@yield = Queue.new
|
14
|
+
@resume = Queue.new
|
15
|
+
|
16
|
+
@thread = Thread.new{ @yield.push [ *yield(*@resume.pop) ] }
|
17
|
+
@thread.abort_on_exception = true
|
18
|
+
@thread[:fiber] = self
|
19
|
+
end
|
20
|
+
attr_reader :thread
|
21
|
+
|
22
|
+
def resume *args
|
23
|
+
raise FiberError, 'dead fiber called' unless @thread.alive?
|
24
|
+
@resume.push(args)
|
25
|
+
result = @yield.pop
|
26
|
+
result.size > 1 ? result : result.first
|
27
|
+
end
|
28
|
+
|
29
|
+
def yield *args
|
30
|
+
@yield.push(args)
|
31
|
+
result = @resume.pop
|
32
|
+
result.size > 1 ? result : result.first
|
33
|
+
end
|
34
|
+
|
35
|
+
def self.yield *args
|
36
|
+
raise FiberError, "can't yield from root fiber" unless fiber = Thread.current[:fiber]
|
37
|
+
fiber.yield(*args)
|
38
|
+
end
|
39
|
+
|
40
|
+
def self.current
|
41
|
+
Thread.current[:fiber] or raise FiberError, 'not inside a fiber'
|
42
|
+
end
|
43
|
+
|
44
|
+
def inspect
|
45
|
+
"#<#{self.class}:0x#{self.object_id.to_s(16)}>"
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
module Util
|
18
|
+
# Represents headers from an HTTP response
|
19
|
+
class Headers
|
20
|
+
include Net::HTTPHeader
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
initialize_http_header({})
|
24
|
+
end
|
25
|
+
|
26
|
+
# Parse a single header line into its key and value
|
27
|
+
# @param [String] chunk a single header line
|
28
|
+
def self.parse(chunk)
|
29
|
+
line = chunk.strip
|
30
|
+
# thanks Net::HTTPResponse
|
31
|
+
return [nil,nil] if chunk =~ /\AHTTP(?:\/(\d+\.\d+))?\s+(\d\d\d)\s*(.*)\z/in
|
32
|
+
m = /\A([^:]+):\s*/.match(line)
|
33
|
+
[m[1], m.post_match] rescue [nil, nil]
|
34
|
+
end
|
35
|
+
|
36
|
+
# Parses a header line and adds it to the header collection
|
37
|
+
# @param [String] chunk a single header line
|
38
|
+
def parse(chunk)
|
39
|
+
key, value = self.class.parse(chunk)
|
40
|
+
add_field(key, value) if key && value
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
module Util
|
18
|
+
# Utility methods for handling multipart/mixed responses
|
19
|
+
module Multipart
|
20
|
+
extend self
|
21
|
+
# Parses a multipart/mixed body into its constituent parts, including nested multipart/mixed sections
|
22
|
+
# @param [String] data the multipart body data
|
23
|
+
# @param [String] boundary the boundary string given in the Content-Type header
|
24
|
+
def parse(data, boundary)
|
25
|
+
contents = data.match(/\r?\n--#{Regexp.escape(boundary)}--\r?\n/).pre_match rescue ""
|
26
|
+
contents.split(/\r?\n--#{Regexp.escape(boundary)}\r?\n/).reject(&:blank?).map do |part|
|
27
|
+
headers = Headers.new
|
28
|
+
if md = part.match(/\r?\n\r?\n/)
|
29
|
+
body = md.post_match
|
30
|
+
md.pre_match.split(/\r?\n/).each do |line|
|
31
|
+
headers.parse(line)
|
32
|
+
end
|
33
|
+
|
34
|
+
if headers["content-type"] =~ /multipart\/mixed/
|
35
|
+
boundary = extract_boundary(headers.to_hash["content-type"].first)
|
36
|
+
parse(body, boundary)
|
37
|
+
else
|
38
|
+
{:headers => headers.to_hash, :body => body}
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end.compact
|
42
|
+
end
|
43
|
+
|
44
|
+
# Extracts the boundary string from a Content-Type header that is a multipart type
|
45
|
+
# @param [String] header_string the Content-Type header
|
46
|
+
# @return [String] the boundary string separating each part
|
47
|
+
def extract_boundary(header_string)
|
48
|
+
$1 if header_string =~ /boundary=([A-Za-z0-9\'()+_,-.\/:=?]+)/
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
module Util
|
18
|
+
module Translation
|
19
|
+
def i18n_scope
|
20
|
+
:riak
|
21
|
+
end
|
22
|
+
|
23
|
+
def t(message, options={})
|
24
|
+
I18n.t("#{i18n_scope}.#{message}", options)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
@@ -0,0 +1,117 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require 'riak'
|
15
|
+
|
16
|
+
module Riak
|
17
|
+
|
18
|
+
# The specification of how to follow links from one object to another in Riak,
|
19
|
+
# when using the link-walker resource.
|
20
|
+
# Example link-walking operation:
|
21
|
+
# GET /riak/artists/REM/albums,_,_/tracks,_,1
|
22
|
+
# This operation would have two WalkSpecs:
|
23
|
+
# Riak::WalkSpec.new({:bucket => 'albums'})
|
24
|
+
# Riak::WalkSpec.new({:bucket => 'tracks', :result => true})
|
25
|
+
class WalkSpec
|
26
|
+
include Util::Translation
|
27
|
+
include Util::Escape
|
28
|
+
|
29
|
+
# @return [String] The bucket followed links should be restricted to. "_" represents all buckets.
|
30
|
+
attr_accessor :bucket
|
31
|
+
|
32
|
+
# @return [String] The "riaktag" or "rel" that followed links should be restricted to. "_" represents all tags.
|
33
|
+
attr_accessor :tag
|
34
|
+
|
35
|
+
# @return [Boolean] Whether objects should be returned from this phase of link walking. Default is false.
|
36
|
+
attr_accessor :keep
|
37
|
+
|
38
|
+
# Normalize a list of walk specs into WalkSpec objects.
|
39
|
+
def self.normalize(*params)
|
40
|
+
params.flatten!
|
41
|
+
specs = []
|
42
|
+
while params.length > 0
|
43
|
+
param = params.shift
|
44
|
+
case param
|
45
|
+
when Hash
|
46
|
+
specs << new(param)
|
47
|
+
when WalkSpec
|
48
|
+
specs << param
|
49
|
+
else
|
50
|
+
if params.length >= 2
|
51
|
+
specs << new(param, params.shift, params.shift)
|
52
|
+
else
|
53
|
+
raise ArgumentError, t("too_few_arguments", :params => params.inspect)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
specs
|
58
|
+
end
|
59
|
+
|
60
|
+
# Creates a walk-spec for use in finding other objects in Riak.
|
61
|
+
# @overload initialize(hash)
|
62
|
+
# Creates a walk-spec from a hash.
|
63
|
+
# @param [Hash] hash options for the walk-spec
|
64
|
+
# @option hash [String] :bucket ("_") the bucket the links should point to (default '_' is all)
|
65
|
+
# @option hash [String] :tag ("_") the tag to filter links by (default '_' is all)
|
66
|
+
# @option hash [Boolean] :keep (false) whether to return results from following this link specification
|
67
|
+
# @overload initialize(bucket, tag, keep)
|
68
|
+
# Creates a walk-spec from a bucket-tag-result triple.
|
69
|
+
# @param [String] bucket the bucket the links should point to (default '_' is all)
|
70
|
+
# @param [String] tag the tag to filter links by (default '_' is all)
|
71
|
+
# @param [Boolean] keep whether to return results from following this link specification
|
72
|
+
# @see {Riak::RObject#walk}
|
73
|
+
def initialize(*args)
|
74
|
+
args.flatten!
|
75
|
+
case args.size
|
76
|
+
when 1
|
77
|
+
hash = args.first
|
78
|
+
raise ArgumentError, t("hash_type", :hash => hash.inspect) unless Hash === hash
|
79
|
+
assign(hash[:bucket], hash[:tag], hash[:keep])
|
80
|
+
when 3
|
81
|
+
assign(*args)
|
82
|
+
else
|
83
|
+
raise ArgumentError, t("wrong_argument_count_walk_spec")
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
# Converts the walk-spec into the form required by the link-walker resource URL
|
88
|
+
def to_s
|
89
|
+
b = @bucket && escape(@bucket) || '_'
|
90
|
+
t = @tag && escape(@tag) || '_'
|
91
|
+
"#{b},#{t},#{@keep ? '1' : '_'}"
|
92
|
+
end
|
93
|
+
|
94
|
+
def ==(other)
|
95
|
+
other.is_a?(WalkSpec) && other.bucket == bucket && other.tag == tag && other.keep == keep
|
96
|
+
end
|
97
|
+
|
98
|
+
def ===(other)
|
99
|
+
self == other || case other
|
100
|
+
when WalkSpec
|
101
|
+
other.keep == keep &&
|
102
|
+
(bucket == "_" || bucket == other.bucket) &&
|
103
|
+
(tag == "_" || tag == other.tag)
|
104
|
+
when Link
|
105
|
+
(bucket == "_" || bucket == other.url.split("/")[2]) &&
|
106
|
+
(tag == "_" || tag == other.rel)
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
private
|
111
|
+
def assign(bucket, tag, result)
|
112
|
+
@bucket = bucket || "_"
|
113
|
+
@tag = tag || "_"
|
114
|
+
@keep = result || false
|
115
|
+
end
|
116
|
+
end
|
117
|
+
end
|
Binary file
|
@@ -0,0 +1,16 @@
|
|
1
|
+
|
2
|
+
--5EiMOjuGavQ2IbXAqsJPLLfJNlA
|
3
|
+
Content-Type: multipart/mixed; boundary=7extjTzvYIKVMVHowUiTn0LfvSs
|
4
|
+
|
5
|
+
--7extjTzvYIKVMVHowUiTn0LfvSs
|
6
|
+
X-Riak-Vclock: a85hYGBgyWDKBVHMr9s3ZzAlMuaxMtyZcPAIH1RYyObHDqiwxIZjcOG1M98chAq3bUQIz7SSFQEKM4FUbwMKZwEA
|
7
|
+
Location: /riak/foo/baz
|
8
|
+
Content-Type: text/plain
|
9
|
+
Link: </riak/foo>; rel="up"
|
10
|
+
Etag: 6JdI51eFrvv5lDwY6un7a2
|
11
|
+
Last-Modified: Sat, 16 Jan 2010 22:13:44 GMT
|
12
|
+
|
13
|
+
SCP sloooow....
|
14
|
+
--7extjTzvYIKVMVHowUiTn0LfvSs--
|
15
|
+
|
16
|
+
--5EiMOjuGavQ2IbXAqsJPLLfJNlA--
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# Copyright 2010 Sean Cribbs, Sonian Inc., and Basho Technologies, Inc.
|
2
|
+
#
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
4
|
+
# you may not use this file except in compliance with the License.
|
5
|
+
# You may obtain a copy of the License at
|
6
|
+
#
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
8
|
+
#
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
12
|
+
# See the License for the specific language governing permissions and
|
13
|
+
# limitations under the License.
|
14
|
+
require File.expand_path("../../spec_helper", File.dirname(__FILE__))
|
15
|
+
|
16
|
+
describe Riak::CacheStore do
|
17
|
+
before do
|
18
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store)
|
19
|
+
@cleanup = true
|
20
|
+
end
|
21
|
+
|
22
|
+
after do
|
23
|
+
@cache.bucket.keys(:force => true).each do |k|
|
24
|
+
@cache.bucket.delete(k, :rw => 1) unless k.blank?
|
25
|
+
end if @cleanup
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "Riak integration" do
|
29
|
+
before do
|
30
|
+
@cleanup = false
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should have a client" do
|
34
|
+
@cache.should respond_to(:client)
|
35
|
+
@cache.client.should be_kind_of(Riak::Client)
|
36
|
+
end
|
37
|
+
|
38
|
+
it "should have a bucket to store entries in" do
|
39
|
+
@cache.bucket.should be_kind_of(Riak::Bucket)
|
40
|
+
end
|
41
|
+
|
42
|
+
it "should configure the client according to the initialized options" do
|
43
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store, :port => 10000)
|
44
|
+
@cache.client.port.should == 10000
|
45
|
+
end
|
46
|
+
|
47
|
+
it "should choose the bucket according to the initializer option" do
|
48
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store, :bucket => "foobar")
|
49
|
+
@cache.bucket.name.should == "foobar"
|
50
|
+
end
|
51
|
+
|
52
|
+
it "should set the N value to 2 by default" do
|
53
|
+
@cache.bucket.n_value.should == 2
|
54
|
+
end
|
55
|
+
|
56
|
+
it "should set the N value to the specified value" do
|
57
|
+
@cache = ActiveSupport::Cache.lookup_store(:riak_store, :n_value => 1)
|
58
|
+
@cache.bucket.n_value.should == 1
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
|
63
|
+
it "should read and write strings" do
|
64
|
+
@cache.write('foo', 'bar')
|
65
|
+
@cache.read('foo').should == 'bar'
|
66
|
+
end
|
67
|
+
|
68
|
+
it "should read and write hashes" do
|
69
|
+
@cache.write('foo', {:a => "b"})
|
70
|
+
@cache.read('foo').should == {:a => "b"}
|
71
|
+
end
|
72
|
+
|
73
|
+
it "should read and write integers" do
|
74
|
+
@cache.write('foo', 1)
|
75
|
+
@cache.read('foo').should == 1
|
76
|
+
end
|
77
|
+
|
78
|
+
it "should read and write nil" do
|
79
|
+
@cache.write('foo', nil)
|
80
|
+
@cache.read('foo').should be_nil
|
81
|
+
end
|
82
|
+
|
83
|
+
it "should return the stored value when fetching on hit" do
|
84
|
+
@cache.write('foo', 'bar')
|
85
|
+
@cache.fetch('foo'){'baz'}.should == 'bar'
|
86
|
+
end
|
87
|
+
|
88
|
+
it "should return the default value when fetching on miss" do
|
89
|
+
@cache.fetch('foo'){'baz'}.should == 'baz'
|
90
|
+
end
|
91
|
+
|
92
|
+
it "should return the default value when forcing a miss" do
|
93
|
+
@cache.fetch('foo', :force => true){'bar'}.should == 'bar'
|
94
|
+
end
|
95
|
+
|
96
|
+
it "should increment an integer value in the cache" do
|
97
|
+
@cache.write('foo', 1, :raw => true)
|
98
|
+
@cache.read('foo', :raw => true).to_i.should == 1
|
99
|
+
@cache.increment('foo')
|
100
|
+
@cache.read('foo', :raw => true).to_i.should == 2
|
101
|
+
end
|
102
|
+
|
103
|
+
it "should decrement an integer value in the cache" do
|
104
|
+
@cache.write('foo', 1, :raw => true)
|
105
|
+
@cache.read('foo', :raw => true).to_i.should == 1
|
106
|
+
@cache.decrement('foo')
|
107
|
+
@cache.read('foo', :raw => true).to_i.should == 0
|
108
|
+
end
|
109
|
+
|
110
|
+
it "should detect if a value exists in the cache" do
|
111
|
+
@cache.write('foo', 'bar')
|
112
|
+
@cache.exist?('foo').should be_true
|
113
|
+
end
|
114
|
+
|
115
|
+
it "should delete matching keys from the cache" do
|
116
|
+
@cache.write('foo', 'bar')
|
117
|
+
@cache.write('green', 'thumb')
|
118
|
+
@cache.delete_matched(/foo/)
|
119
|
+
@cache.read('foo').should be_nil
|
120
|
+
@cache.read('green').should == 'thumb'
|
121
|
+
end
|
122
|
+
|
123
|
+
it "should delete a single key from the cache" do
|
124
|
+
@cache.write('foo', 'bar')
|
125
|
+
@cache.read('foo').should == 'bar'
|
126
|
+
@cache.delete('foo')
|
127
|
+
@cache.read('foo').should be_nil
|
128
|
+
end
|
129
|
+
end
|