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.
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