ripple 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (75) hide show
  1. data/.document +5 -0
  2. data/.gitignore +26 -0
  3. data/LICENSE +13 -0
  4. data/README.textile +126 -0
  5. data/RELEASE_NOTES.textile +24 -0
  6. data/Rakefile +61 -0
  7. data/VERSION +1 -0
  8. data/lib/riak.rb +45 -0
  9. data/lib/riak/bucket.rb +105 -0
  10. data/lib/riak/client.rb +138 -0
  11. data/lib/riak/client/curb_backend.rb +63 -0
  12. data/lib/riak/client/http_backend.rb +209 -0
  13. data/lib/riak/client/net_http_backend.rb +49 -0
  14. data/lib/riak/failed_request.rb +37 -0
  15. data/lib/riak/i18n.rb +15 -0
  16. data/lib/riak/invalid_response.rb +25 -0
  17. data/lib/riak/link.rb +54 -0
  18. data/lib/riak/locale/en.yml +37 -0
  19. data/lib/riak/map_reduce.rb +240 -0
  20. data/lib/riak/map_reduce_error.rb +20 -0
  21. data/lib/riak/robject.rb +234 -0
  22. data/lib/riak/util/headers.rb +44 -0
  23. data/lib/riak/util/multipart.rb +52 -0
  24. data/lib/riak/util/translation.rb +29 -0
  25. data/lib/riak/walk_spec.rb +113 -0
  26. data/lib/ripple.rb +48 -0
  27. data/lib/ripple/core_ext/casting.rb +96 -0
  28. data/lib/ripple/document.rb +60 -0
  29. data/lib/ripple/document/attribute_methods.rb +111 -0
  30. data/lib/ripple/document/attribute_methods/dirty.rb +52 -0
  31. data/lib/ripple/document/attribute_methods/query.rb +49 -0
  32. data/lib/ripple/document/attribute_methods/read.rb +38 -0
  33. data/lib/ripple/document/attribute_methods/write.rb +36 -0
  34. data/lib/ripple/document/bucket_access.rb +38 -0
  35. data/lib/ripple/document/finders.rb +84 -0
  36. data/lib/ripple/document/persistence.rb +93 -0
  37. data/lib/ripple/document/persistence/callbacks.rb +48 -0
  38. data/lib/ripple/document/properties.rb +85 -0
  39. data/lib/ripple/document/validations.rb +44 -0
  40. data/lib/ripple/embedded_document.rb +38 -0
  41. data/lib/ripple/embedded_document/persistence.rb +46 -0
  42. data/lib/ripple/i18n.rb +15 -0
  43. data/lib/ripple/locale/en.yml +16 -0
  44. data/lib/ripple/property_type_mismatch.rb +23 -0
  45. data/lib/ripple/translation.rb +24 -0
  46. data/ripple.gemspec +159 -0
  47. data/spec/fixtures/cat.jpg +0 -0
  48. data/spec/fixtures/multipart-blank.txt +7 -0
  49. data/spec/fixtures/multipart-with-body.txt +16 -0
  50. data/spec/riak/bucket_spec.rb +141 -0
  51. data/spec/riak/client_spec.rb +169 -0
  52. data/spec/riak/curb_backend_spec.rb +50 -0
  53. data/spec/riak/headers_spec.rb +34 -0
  54. data/spec/riak/http_backend_spec.rb +136 -0
  55. data/spec/riak/link_spec.rb +50 -0
  56. data/spec/riak/map_reduce_spec.rb +347 -0
  57. data/spec/riak/multipart_spec.rb +36 -0
  58. data/spec/riak/net_http_backend_spec.rb +28 -0
  59. data/spec/riak/object_spec.rb +444 -0
  60. data/spec/riak/walk_spec_spec.rb +208 -0
  61. data/spec/ripple/attribute_methods_spec.rb +149 -0
  62. data/spec/ripple/bucket_access_spec.rb +48 -0
  63. data/spec/ripple/callbacks_spec.rb +86 -0
  64. data/spec/ripple/document_spec.rb +35 -0
  65. data/spec/ripple/embedded_document_spec.rb +52 -0
  66. data/spec/ripple/finders_spec.rb +146 -0
  67. data/spec/ripple/persistence_spec.rb +89 -0
  68. data/spec/ripple/properties_spec.rb +195 -0
  69. data/spec/ripple/ripple_spec.rb +43 -0
  70. data/spec/ripple/validations_spec.rb +64 -0
  71. data/spec/spec.opts +1 -0
  72. data/spec/spec_helper.rb +32 -0
  73. data/spec/support/http_backend_implementation_examples.rb +215 -0
  74. data/spec/support/mock_server.rb +58 -0
  75. metadata +221 -0
@@ -0,0 +1,63 @@
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
+ # An HTTP backend for Riak::Client that uses the 'curb' library/gem.
19
+ # If the 'curb' library is present, this backend will be preferred to
20
+ # the backend based on Net::HTTP.
21
+ # Conforms to the Riak::Client::HTTPBackend interface.
22
+ class CurbBackend < HTTPBackend
23
+ # @private
24
+ def initialize(client)
25
+ super
26
+ @curl = Curl::Easy.new
27
+ @curl.follow_location = false
28
+ @curl.on_header do |header_line|
29
+ @response_headers.parse(header_line)
30
+ header_line.size
31
+ end
32
+ end
33
+
34
+ private
35
+ def perform(method, uri, headers, expect, data=nil)
36
+ # Setup
37
+ @curl.headers = headers
38
+ @curl.url = uri.to_s
39
+ @response_headers = Riak::Util::Headers.new
40
+ @curl.on_body {|chunk| yield chunk; chunk.size } if block_given?
41
+
42
+ # Perform
43
+ case method
44
+ when :put, :post
45
+ @curl.send("http_#{method}", data)
46
+ else
47
+ @curl.send("http_#{method}")
48
+ end
49
+
50
+ # Verify
51
+ if valid_response?(expect, @curl.response_code)
52
+ result = { :headers => @response_headers.to_hash, :code => @curl.response_code.to_i }
53
+ if return_body?(method, @curl.response_code, block_given?)
54
+ result[:body] = @curl.body_str
55
+ end
56
+ result
57
+ else
58
+ raise FailedRequest.new(method, expect, @curl.response_code, @response_headers.to_hash, @curl.body_str)
59
+ end
60
+ end
61
+ end
62
+ end
63
+ 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(URI.escape(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,15 @@
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 'active_support/i18n'
15
+ 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,54 @@
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
+
26
+ # @param [String] header_string the string value of the Link: HTTP header from a Riak response
27
+ # @return [Array<Link>] an array of Riak::Link structs parsed from the header
28
+ def self.parse(header_string)
29
+ header_string.scan(%r{<([^>]+)>\s*;\s*(?:rel|riaktag)=\"([^\"]+)\"}).map do |match|
30
+ new(match[0], match[1])
31
+ end
32
+ end
33
+
34
+ def initialize(url, rel)
35
+ @url, @rel = url, rel
36
+ end
37
+
38
+ def inspect; to_s; end
39
+
40
+ def to_s
41
+ %Q[<#{@url}>; riaktag="#{@rel}"]
42
+ end
43
+
44
+ def ==(other)
45
+ other.is_a?(Link) && url == other.url && rel == other.rel
46
+ end
47
+
48
+ def to_walk_spec
49
+ bucket, object = $1, $2 if @url =~ %r{/raw/([^/]+)/([^/]+)/?}
50
+ raise t("bucket_link_conversion") if @rel == "up" || object.nil?
51
+ WalkSpec.new(:bucket => bucket, :tag => @rel)
52
+ end
53
+ end
54
+ end