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,82 @@
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
+ begin
17
+ require 'fiber'
18
+ rescue LoadError
19
+ require 'riak/util/fiber1.8'
20
+ end
21
+
22
+ module Riak
23
+ class Client
24
+ # An HTTP backend for Riak::Client that uses the 'curb' library/gem.
25
+ # If the 'curb' library is present, this backend will be preferred to
26
+ # the backend based on Net::HTTP.
27
+ # Conforms to the Riak::Client::HTTPBackend interface.
28
+ class CurbBackend < HTTPBackend
29
+ private
30
+ def perform(method, uri, headers, expect, data=nil)
31
+ # Setup
32
+ curl.headers = headers
33
+ curl.url = uri.to_s
34
+ response_headers.initialize_http_header(nil)
35
+ if block_given?
36
+ _curl = curl
37
+ Fiber.new {
38
+ f = Fiber.current
39
+ _curl.on_body {|chunk| f.resume(chunk); chunk.size }
40
+ loop do
41
+ yield Fiber.yield
42
+ end
43
+ }.resume
44
+ else
45
+ curl.on_body
46
+ end
47
+ # Perform
48
+ case method
49
+ when :put, :post
50
+ curl.send("http_#{method}", data)
51
+ else
52
+ curl.send("http_#{method}")
53
+ end
54
+
55
+ # Verify
56
+ if valid_response?(expect, curl.response_code)
57
+ result = { :headers => response_headers.to_hash, :code => curl.response_code.to_i }
58
+ if return_body?(method, curl.response_code, block_given?)
59
+ result[:body] = curl.body_str
60
+ end
61
+ result
62
+ else
63
+ raise FailedRequest.new(method, expect, curl.response_code, response_headers.to_hash, curl.body_str)
64
+ end
65
+ end
66
+
67
+ def curl
68
+ Thread.current[:curl_easy_handle] ||= Curl::Easy.new.tap do |c|
69
+ c.follow_location = false
70
+ c.on_header do |header_line|
71
+ response_headers.parse(header_line)
72
+ header_line.size
73
+ end
74
+ end
75
+ end
76
+
77
+ def response_headers
78
+ Thread.current[:response_headers] ||= Riak::Util::Headers.new
79
+ end
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,209 @@
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
+ class Client
18
+ class HTTPBackend
19
+ include Util::Translation
20
+ # The Riak::Client that uses this backend
21
+ attr_reader :client
22
+
23
+ # Create an HTTPBackend for the Riak::Client.
24
+ # @param [Client] client the client
25
+ def initialize(client)
26
+ raise ArgumentError, t("client_type", :client => client) unless Client === client
27
+ @client = client
28
+ end
29
+
30
+ # Default header hash sent with every request, based on settings in the client
31
+ # @return [Hash] headers that will be merged with user-specified headers on every request
32
+ def default_headers
33
+ {
34
+ "Accept" => "multipart/mixed, application/json;q=0.7, */*;q=0.5",
35
+ "X-Riak-ClientId" => @client.client_id
36
+ }
37
+ end
38
+
39
+ # Performs a HEAD request to the specified resource on the Riak server.
40
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
41
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
42
+ # @overload head(expect, *resource)
43
+ # @overload head(expect, *resource, headers)
44
+ # Send the request with custom headers
45
+ # @param [Hash] headers custom headers to send with the request
46
+ # @return [Hash] response data, containing only the :headers and :code keys
47
+ # @raise [FailedRequest] if the response code doesn't match the expected response
48
+ def head(expect, *resource)
49
+ headers = default_headers.merge(resource.extract_options!)
50
+ verify_path!(resource)
51
+ perform(:head, path(*resource), headers, expect)
52
+ end
53
+
54
+ # Performs a GET request to the specified resource on the Riak server.
55
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
56
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
57
+ # @overload get(expect, *resource)
58
+ # @overload get(expect, *resource, headers)
59
+ # Send the request with custom headers
60
+ # @param [Hash] headers custom headers to send with the request
61
+ # @overload get(expect, *resource, headers={})
62
+ # Stream the response body through the supplied block
63
+ # @param [Hash] headers custom headers to send with the request
64
+ # @yield [chunk] yields successive chunks of the response body as strings
65
+ # @return [Hash] response data, containing only the :headers and :code keys
66
+ # @return [Hash] response data, containing :headers, :body, and :code keys
67
+ # @raise [FailedRequest] if the response code doesn't match the expected response
68
+ def get(expect, *resource, &block)
69
+ headers = default_headers.merge(resource.extract_options!)
70
+ verify_path!(resource)
71
+ perform(:get, path(*resource), headers, expect, &block)
72
+ end
73
+
74
+ # Performs a PUT request to the specified resource on the Riak server.
75
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
76
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
77
+ # @param [String] body the request body to send to the server
78
+ # @overload put(expect, *resource, body)
79
+ # @overload put(expect, *resource, body, headers)
80
+ # Send the request with custom headers
81
+ # @param [Hash] headers custom headers to send with the request
82
+ # @overload put(expect, *resource, body, headers={})
83
+ # Stream the response body through the supplied block
84
+ # @param [Hash] headers custom headers to send with the request
85
+ # @yield [chunk] yields successive chunks of the response body as strings
86
+ # @return [Hash] response data, containing only the :headers and :code keys
87
+ # @return [Hash] response data, containing :headers, :code, and :body keys
88
+ # @raise [FailedRequest] if the response code doesn't match the expected response
89
+ def put(expect, *resource, &block)
90
+ headers = default_headers.merge(resource.extract_options!)
91
+ uri, data = verify_path_and_body!(resource)
92
+ perform(:put, path(*uri), headers, expect, data, &block)
93
+ end
94
+
95
+ # Performs a POST request to the specified resource on the Riak server.
96
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
97
+ # @param [String, Array<String>] resource a relative path or array of path segments that will be joined to the root URI
98
+ # @param [String] body the request body to send to the server
99
+ # @overload post(expect, *resource, body)
100
+ # @overload post(expect, *resource, body, headers)
101
+ # Send the request with custom headers
102
+ # @param [Hash] headers custom headers to send with the request
103
+ # @overload post(expect, *resource, body, headers={})
104
+ # Stream the response body through the supplied block
105
+ # @param [Hash] headers custom headers to send with the request
106
+ # @yield [chunk] yields successive chunks of the response body as strings
107
+ # @return [Hash] response data, containing only the :headers and :code keys
108
+ # @return [Hash] response data, containing :headers, :code and :body keys
109
+ # @raise [FailedRequest] if the response code doesn't match the expected response
110
+ def post(expect, *resource, &block)
111
+ headers = default_headers.merge(resource.extract_options!)
112
+ uri, data = verify_path_and_body!(resource)
113
+ perform(:post, path(*uri), headers, expect, data, &block)
114
+ end
115
+
116
+ # Performs a DELETE request to the specified resource on the Riak server.
117
+ # @param [Fixnum, Array] expect the expected HTTP response code(s) from Riak
118
+ # @param [String, Array<String,Hash>] resource a relative path or array of path segments and optional query params Hash that will be joined to the root URI
119
+ # @overload delete(expect, *resource)
120
+ # @overload delete(expect, *resource, headers)
121
+ # Send the request with custom headers
122
+ # @param [Hash] headers custom headers to send with the request
123
+ # @overload delete(expect, *resource, headers={})
124
+ # Stream the response body through the supplied block
125
+ # @param [Hash] headers custom headers to send with the request
126
+ # @yield [chunk] yields successive chunks of the response body as strings
127
+ # @return [Hash] response data, containing only the :headers and :code keys
128
+ # @return [Hash] response data, containing :headers, :code and :body keys
129
+ # @raise [FailedRequest] if the response code doesn't match the expected response
130
+ def delete(expect, *resource, &block)
131
+ headers = default_headers.merge(resource.extract_options!)
132
+ verify_path!(resource)
133
+ perform(:delete, path(*resource), headers, expect, &block)
134
+ end
135
+
136
+ # @return [URI] The calculated root URI for the Riak HTTP endpoint
137
+ def root_uri
138
+ URI.parse("http://#{@client.host}:#{@client.port}")
139
+ end
140
+
141
+ # Calculates an absolute URI from a relative path specification
142
+ # @param [Array<String,Hash>] segments a relative path or sequence of path segments and optional query params Hash that will be joined to the root URI
143
+ # @return [URI] an absolute URI for the resource
144
+ def path(*segments)
145
+ query = segments.extract_options!.to_param
146
+ root_uri.merge(segments.join("/").gsub(/\/+/, "/").sub(/^\//, '')).tap do |uri|
147
+ uri.query = query if query.present?
148
+ end
149
+ end
150
+
151
+ # Verifies that both a resource path and body are present in the arguments
152
+ # @param [Array] args the arguments to verify
153
+ # @raise [ArgumentError] if the body or resource is missing, or if the body is not a String
154
+ def verify_path_and_body!(args)
155
+ body = args.pop
156
+ begin
157
+ verify_path!(args)
158
+ rescue ArgumentError
159
+ raise ArgumentError, t("path_and_body_required")
160
+ end
161
+
162
+ raise ArgumentError, t("request_body_type") unless String === body || IO === body
163
+ [args, body]
164
+ end
165
+
166
+ # Verifies that the specified resource is valid
167
+ # @param [String, Array] resource the resource specification
168
+ # @raise [ArgumentError] if the resource path is too short
169
+ def verify_path!(resource)
170
+ resource = Array(resource).flatten
171
+ raise ArgumentError, t("resource_path_short") unless resource.length > 1 || resource.include?(@client.mapred)
172
+ end
173
+
174
+ # Checks the expected response codes against the actual response code. Use internally when
175
+ # implementing {#perform}.
176
+ # @param [String, Fixnum, Array<String,Fixnum>] expected the expected response code(s)
177
+ # @param [String, Fixnum] actual the received response code
178
+ # @return [Boolean] whether the actual response code is acceptable given the expectations
179
+ def valid_response?(expected, actual)
180
+ Array(expected).map(&:to_i).include?(actual.to_i)
181
+ end
182
+
183
+ # Checks whether a combination of the HTTP method, response code, and block should
184
+ # result in returning the :body in the response hash. Use internally when implementing {#perform}.
185
+ # @param [Symbol] method the HTTP method
186
+ # @param [String, Fixnum] code the received response code
187
+ # @param [Boolean] has_block whether a streaming block was passed to {#perform}. Pass block_given? to this parameter.
188
+ # @return [Boolean] whether to return the body in the response hash
189
+ def return_body?(method, code, has_block)
190
+ method != :head && !valid_response?([204,205,304], code) && !has_block
191
+ end
192
+
193
+ # Executes requests according to the underlying HTTP client library semantics.
194
+ # @abstract Subclasses must implement this internal method to perform HTTP requests
195
+ # according to the API of their HTTP libraries.
196
+ # @param [Symbol] method one of :head, :get, :post, :put, :delete
197
+ # @param [URI] uri the HTTP URI to request
198
+ # @param [Hash] headers headers to send along with the request
199
+ # @param [Fixnum, Array] expect the expected response code(s)
200
+ # @param [String, IO] body the PUT or POST request body
201
+ # @return [Hash] response data, containing :headers, :code and :body keys. Only :headers and :code should be present when the body is streamed or the method is :head.
202
+ # @yield [chunk] if the method is not :head, successive chunks of the response body will be yielded as strings
203
+ # @raise [NotImplementedError] if a subclass does not implement this method
204
+ def perform(method, uri, headers, expect, body=nil)
205
+ raise NotImplementedError
206
+ end
207
+ end
208
+ end
209
+ end
@@ -0,0 +1,49 @@
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
+ class Client
18
+ # Uses the Ruby standard library Net::HTTP to connect to Riak.
19
+ # We recommend using the CurbBackend, which will
20
+ # be preferred when the 'curb' library is available.
21
+ # Conforms to the Riak::Client::HTTPBackend interface.
22
+ class NetHTTPBackend < HTTPBackend
23
+ private
24
+ def perform(method, uri, headers, expect, data=nil) #:nodoc:
25
+ Net::HTTP.start(uri.host, uri.port) do |http|
26
+ request = Net::HTTP.const_get(method.to_s.camelize).new(uri.request_uri, headers)
27
+ case data
28
+ when String
29
+ request.body = data
30
+ when IO
31
+ request.body_stream = data
32
+ end
33
+ response = http.request(request)
34
+
35
+ if valid_response?(expect, response.code)
36
+ result = {:headers => response.to_hash, :code => response.code.to_i}
37
+ response.read_body {|chunk| yield chunk } if block_given?
38
+ if return_body?(method, response.code, block_given?)
39
+ result[:body] = response.body
40
+ end
41
+ result
42
+ else
43
+ raise FailedRequest.new(method, expect, response.code, response.to_hash, response.body)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,37 @@
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
+ # Exception raised when the expected response code from Riak
18
+ # fails to match the actual response code.
19
+ class FailedRequest < StandardError
20
+ include Util::Translation
21
+ # @return [Symbol] the HTTP method, one of :head, :get, :post, :put, :delete
22
+ attr_reader :method
23
+ # @return [Fixnum] the expected response code
24
+ attr_reader :expected
25
+ # @return [Fixnum] the received response code
26
+ attr_reader :code
27
+ # @return [Hash] the response headers
28
+ attr_reader :headers
29
+ # @return [String] the response body, if present
30
+ attr_reader :body
31
+
32
+ def initialize(method, expected_code, received_code, headers, body)
33
+ @method, @expected, @code, @headers, @body = method, expected_code, received_code, headers, body
34
+ super t("failed_request", :expected => @expected.inspect, :code => @code, :body => @body)
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,20 @@
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
+ begin
15
+ require 'active_support/i18n'
16
+ rescue LoadError
17
+ require 'i18n' # support ActiveSupport < 3
18
+ end
19
+
20
+ I18n.load_path << File.expand_path("../locale/en.yml", __FILE__)
@@ -0,0 +1,25 @@
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
+ # Raised when Riak returns a response that is in an unexpected format
18
+ class InvalidResponse < StandardError
19
+ def initialize(expected, received, extra="")
20
+ expected = expected.inspect if Hash === expected
21
+ received = received.inspect if Hash === received
22
+ super "Expected #{expected} but received #{received} from Riak #{extra}"
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,73 @@
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
+ # Represents a link from one object to another in Riak
18
+ class Link
19
+ include Util::Translation
20
+ # @return [String] the URL (relative or absolute) of the related resource
21
+ attr_accessor :url
22
+
23
+ # @return [String] the relationship ("rel") of the other resource to this one
24
+ attr_accessor :rel
25
+ alias :tag :rel
26
+ alias :tag= :rel=
27
+
28
+ # @param [String] header_string the string value of the Link: HTTP header from a Riak response
29
+ # @return [Array<Link>] an array of Riak::Link structs parsed from the header
30
+ def self.parse(header_string)
31
+ header_string.scan(%r{<([^>]+)>\s*;\s*(?:rel|riaktag)=\"([^\"]+)\"}).map do |match|
32
+ new(match[0], match[1])
33
+ end
34
+ end
35
+
36
+ def initialize(url, rel)
37
+ @url, @rel = url, rel
38
+ end
39
+
40
+ # @return [String] bucket_name, if the Link url is a known Riak link ("/riak/<bucket>/<key>")
41
+ def bucket
42
+ URI.unescape($1) if url =~ %r{^/[^/]+/([^/]+)/?}
43
+ end
44
+
45
+ # @return [String] key, if the Link url is a known Riak link ("/riak/<bucket>/<key>")
46
+ def key
47
+ URI.unescape($1) if url =~ %r{^/[^/]+/[^/]+/([^/]+)/?}
48
+ end
49
+
50
+ def inspect; to_s; end
51
+
52
+ def to_s
53
+ %Q[<#{@url}>; riaktag="#{@rel}"]
54
+ end
55
+
56
+ def hash
57
+ self.to_s.hash
58
+ end
59
+
60
+ def eql?(other)
61
+ self == other
62
+ end
63
+
64
+ def ==(other)
65
+ other.is_a?(Link) && url == other.url && rel == other.rel
66
+ end
67
+
68
+ def to_walk_spec
69
+ raise t("bucket_link_conversion") if @rel == "up" || key.nil?
70
+ WalkSpec.new(:bucket => bucket, :tag => @rel)
71
+ end
72
+ end
73
+ end