riak-client 0.7.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (43) hide show
  1. data/Rakefile +74 -0
  2. data/lib/riak.rb +49 -0
  3. data/lib/riak/bucket.rb +176 -0
  4. data/lib/riak/cache_store.rb +82 -0
  5. data/lib/riak/client.rb +139 -0
  6. data/lib/riak/client/curb_backend.rb +82 -0
  7. data/lib/riak/client/http_backend.rb +209 -0
  8. data/lib/riak/client/net_http_backend.rb +49 -0
  9. data/lib/riak/failed_request.rb +37 -0
  10. data/lib/riak/i18n.rb +20 -0
  11. data/lib/riak/invalid_response.rb +25 -0
  12. data/lib/riak/link.rb +73 -0
  13. data/lib/riak/locale/en.yml +37 -0
  14. data/lib/riak/map_reduce.rb +248 -0
  15. data/lib/riak/map_reduce_error.rb +20 -0
  16. data/lib/riak/robject.rb +267 -0
  17. data/lib/riak/util/escape.rb +12 -0
  18. data/lib/riak/util/fiber1.8.rb +48 -0
  19. data/lib/riak/util/headers.rb +44 -0
  20. data/lib/riak/util/multipart.rb +52 -0
  21. data/lib/riak/util/translation.rb +29 -0
  22. data/lib/riak/walk_spec.rb +117 -0
  23. data/spec/fixtures/cat.jpg +0 -0
  24. data/spec/fixtures/multipart-blank.txt +7 -0
  25. data/spec/fixtures/multipart-with-body.txt +16 -0
  26. data/spec/integration/riak/cache_store_spec.rb +129 -0
  27. data/spec/riak/bucket_spec.rb +247 -0
  28. data/spec/riak/client_spec.rb +174 -0
  29. data/spec/riak/curb_backend_spec.rb +53 -0
  30. data/spec/riak/escape_spec.rb +21 -0
  31. data/spec/riak/headers_spec.rb +34 -0
  32. data/spec/riak/http_backend_spec.rb +131 -0
  33. data/spec/riak/link_spec.rb +82 -0
  34. data/spec/riak/map_reduce_spec.rb +352 -0
  35. data/spec/riak/multipart_spec.rb +36 -0
  36. data/spec/riak/net_http_backend_spec.rb +28 -0
  37. data/spec/riak/object_spec.rb +538 -0
  38. data/spec/riak/walk_spec_spec.rb +208 -0
  39. data/spec/spec_helper.rb +30 -0
  40. data/spec/support/http_backend_implementation_examples.rb +215 -0
  41. data/spec/support/mock_server.rb +61 -0
  42. data/spec/support/mocks.rb +3 -0
  43. 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,7 @@
1
+
2
+ --73NmmA8dJxSB5nL2dVerpFIi8ze
3
+ Content-Type: multipart/mixed; boundary=8fPXq9XfV15txMoV1IbA3hovEij
4
+
5
+ --8fPXq9XfV15txMoV1IbA3hovEij--
6
+
7
+ --73NmmA8dJxSB5nL2dVerpFIi8ze--
@@ -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