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,444 @@
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::RObject do
17
+ before :each do
18
+ @client = Riak::Client.new
19
+ @bucket = Riak::Bucket.new(@client, "foo")
20
+ end
21
+
22
+ describe "initialization" do
23
+ it "should set the bucket" do
24
+ @object = Riak::RObject.new(@bucket)
25
+ @object.bucket.should == @bucket
26
+ end
27
+
28
+ it "should set the key" do
29
+ @object = Riak::RObject.new(@bucket, "bar")
30
+ @object.key.should == "bar"
31
+ end
32
+
33
+ it "should initialize the links to an empty array" do
34
+ @object = Riak::RObject.new(@bucket, "bar")
35
+ @object.links.should == []
36
+ end
37
+
38
+ it "should initialize the meta to an empty hash" do
39
+ @object = Riak::RObject.new(@bucket, "bar")
40
+ @object.meta.should == {}
41
+ end
42
+ end
43
+
44
+ describe "serialization" do
45
+ before :each do
46
+ @object = Riak::RObject.new(@bucket, "bar")
47
+ end
48
+
49
+ it "should change the data into a string by default when serializing" do
50
+ @object.serialize("foo").should == "foo"
51
+ @object.serialize(2).should == "2"
52
+ end
53
+
54
+ it "should not change the data when it is an IO" do
55
+ file = File.open("#{File.dirname(__FILE__)}/../fixtures/cat.jpg", "r")
56
+ file.should_not_receive(:to_s)
57
+ @object.serialize(file).should == file
58
+ end
59
+
60
+ it "should not modify the data by default when deserializing" do
61
+ @object.deserialize("foo").should == "foo"
62
+ end
63
+
64
+ describe "when the content type is YAML" do
65
+ before :each do
66
+ @object.content_type = "text/x-yaml"
67
+ end
68
+
69
+ it "should serialize into a YAML stream" do
70
+ @object.serialize({"foo" => "bar"}).should == "--- \nfoo: bar\n"
71
+ end
72
+
73
+ it "should deserialize a YAML stream" do
74
+ @object.deserialize("--- \nfoo: bar\n").should == {"foo" => "bar"}
75
+ end
76
+ end
77
+
78
+ describe "when the content type is JSON" do
79
+ before :each do
80
+ @object.content_type = "application/json"
81
+ end
82
+
83
+ it "should serialize into a JSON blob" do
84
+ @object.serialize({"foo" => "bar"}).should == '{"foo":"bar"}'
85
+ @object.serialize(2).should == "2"
86
+ @object.serialize("Some text").should == '"Some text"'
87
+ @object.serialize([1,2,3]).should == "[1,2,3]"
88
+ end
89
+
90
+ it "should deserialize a JSON blob" do
91
+ @object.deserialize('{"foo":"bar"}').should == {"foo" => "bar"}
92
+ @object.deserialize("2").should == 2
93
+ @object.deserialize('"Some text"').should == "Some text"
94
+ @object.deserialize('[1,2,3]').should == [1,2,3]
95
+ end
96
+ end
97
+
98
+ describe "when the content type is an octet-stream" do
99
+ before :each do
100
+ @object.content_type = "application/octet-stream"
101
+ end
102
+
103
+ describe "if the ruby-serialization meta field is set to Marshal" do
104
+ before :each do
105
+ @object.meta['ruby-serialization'] = "Marshal"
106
+ @payload = Marshal.dump({"foo" => "bar"})
107
+ end
108
+
109
+ it "should dump via Marshal" do
110
+ @object.serialize({"foo" => "bar"}).should == @payload
111
+ end
112
+
113
+ it "should load from Marshal" do
114
+ @object.deserialize(@payload).should == {"foo" => "bar"}
115
+ end
116
+ end
117
+
118
+ describe "if the ruby-serialization meta field is not set to Marshal" do
119
+ before :each do
120
+ @object.meta.delete("ruby-serialization")
121
+ end
122
+
123
+ it "should dump to a string" do
124
+ @object.serialize(2).should == "2"
125
+ @object.serialize("foo").should == "foo"
126
+ end
127
+
128
+ it "should load the body unmodified" do
129
+ @object.deserialize("foo").should == "foo"
130
+ end
131
+ end
132
+ end
133
+ end
134
+
135
+ describe "loading data from the response" do
136
+ before :each do
137
+ @object = Riak::RObject.new(@bucket, "bar")
138
+ end
139
+
140
+ it "should load the content type" do
141
+ @object.load({:headers => {"content-type" => ["application/json"]}})
142
+ @object.content_type.should == "application/json"
143
+ end
144
+
145
+ it "should load the body data" do
146
+ @object.load({:headers => {"content-type" => ["application/json"]}, :body => '{"foo":"bar"}'})
147
+ @object.data.should be_present
148
+ end
149
+
150
+ it "should deserialize the body data" do
151
+ @object.should_receive(:deserialize).with("{}").and_return({})
152
+ @object.load({:headers => {"content-type" => ["application/json"]}, :body => "{}"})
153
+ @object.data.should == {}
154
+ end
155
+
156
+ it "should leave the object data unchanged if the response body is blank" do
157
+ @object.data = "Original data"
158
+ @object.load({:headers => {"content-type" => ["application/json"]}, :body => ""})
159
+ @object.data.should == "Original data"
160
+ end
161
+
162
+ it "should load the vclock from the headers" do
163
+ @object.load({:headers => {"content-type" => ["application/json"], 'x-riak-vclock' => ["somereallylongbase64string=="]}, :body => "{}"})
164
+ @object.vclock.should == "somereallylongbase64string=="
165
+ end
166
+
167
+ it "should load links from the headers" do
168
+ @object.load({:headers => {"content-type" => ["application/json"], "link" => ['</raw/bar>; rel="up"']}, :body => "{}"})
169
+ @object.links.should have(1).item
170
+ @object.links.first.url.should == "/raw/bar"
171
+ @object.links.first.rel.should == "up"
172
+ end
173
+
174
+ it "should load the ETag from the headers" do
175
+ @object.load({:headers => {"content-type" => ["application/json"], "etag" => ["32748nvas83572934"]}, :body => "{}"})
176
+ @object.etag.should == "32748nvas83572934"
177
+ end
178
+
179
+ it "should load the modified date from the headers" do
180
+ time = Time.now
181
+ @object.load({:headers => {"content-type" => ["application/json"], "last-modified" => [time.httpdate]}, :body => "{}"})
182
+ @object.last_modified.to_s.should == time.to_s # bah, times are not equivalent unless equal
183
+ end
184
+
185
+ it "should load meta information from the headers" do
186
+ @object.load({:headers => {"content-type" => ["application/json"], "x-riak-meta-some-kind-of-robot" => ["for AWESOME"]}, :body => "{}"})
187
+ @object.meta["some-kind-of-robot"].should == ["for AWESOME"]
188
+ end
189
+
190
+ it "should parse the location header into the key when present" do
191
+ @object.load({:headers => {"content-type" => ["application/json"], "location" => ["/raw/foo/baz"]}})
192
+ @object.key.should == "baz"
193
+ end
194
+ end
195
+
196
+ describe "headers used for storing the object" do
197
+ before :each do
198
+ @object = Riak::RObject.new(@bucket, "bar")
199
+ end
200
+
201
+ it "should include the content type" do
202
+ @object.content_type = "application/json"
203
+ @object.store_headers["Content-Type"].should == "application/json"
204
+ end
205
+
206
+ it "should include the vclock when present" do
207
+ @object.vclock = "123445678990"
208
+ @object.store_headers["X-Riak-Vclock"].should == "123445678990"
209
+ end
210
+
211
+ it "should exclude the vclock when nil" do
212
+ @object.vclock = nil
213
+ @object.store_headers.should_not have_key("X-Riak-Vclock")
214
+ end
215
+
216
+ describe "when links are defined" do
217
+ before :each do
218
+ @object.links = [Riak::Link.new("/raw/foo/baz", "next")]
219
+ end
220
+
221
+ it "should include a Link header with references to other objects" do
222
+ @object.store_headers.should have_key("Link")
223
+ @object.store_headers["Link"].should include('</raw/foo/baz>; riaktag="next"')
224
+ end
225
+
226
+ it "should exclude the 'up' link to the bucket from the header" do
227
+ @object.links << Riak::Link.new("/raw/foo", "up")
228
+ @object.store_headers.should have_key("Link")
229
+ @object.store_headers["Link"].should_not include('riaktag="up"')
230
+ end
231
+ end
232
+
233
+ it "should exclude the Link header when no links are present" do
234
+ @object.links = []
235
+ @object.store_headers.should_not have_key("Link")
236
+ end
237
+
238
+ describe "when meta fields are present" do
239
+ before :each do
240
+ @object.meta = {"some-kind-of-robot" => true, "powers" => "for awesome", "cold-ones" => 10}
241
+ end
242
+
243
+ it "should include X-Riak-Meta-* headers for each meta key" do
244
+ @object.store_headers.should have_key("X-Riak-Meta-some-kind-of-robot")
245
+ @object.store_headers.should have_key("X-Riak-Meta-cold-ones")
246
+ @object.store_headers.should have_key("X-Riak-Meta-powers")
247
+ end
248
+
249
+ it "should turn non-string meta values into strings" do
250
+ @object.store_headers["X-Riak-Meta-some-kind-of-robot"].should == "true"
251
+ @object.store_headers["X-Riak-Meta-cold-ones"].should == "10"
252
+ end
253
+
254
+ it "should leave string meta values unchanged in the header" do
255
+ @object.store_headers["X-Riak-Meta-powers"].should == "for awesome"
256
+ end
257
+ end
258
+ end
259
+
260
+ describe "headers used for reloading the object" do
261
+ before :each do
262
+ @object = Riak::RObject.new(@bucket, "bar")
263
+ end
264
+
265
+ it "should be blank when the etag and last_modified properties are blank" do
266
+ @object.etag.should be_blank
267
+ @object.last_modified.should be_blank
268
+ @object.reload_headers.should be_blank
269
+ end
270
+
271
+ it "should include the If-None-Match key when the etag is present" do
272
+ @object.etag = "etag!"
273
+ @object.reload_headers['If-None-Match'].should == "etag!"
274
+ end
275
+
276
+ it "should include the If-Modified-Since header when the last_modified time is present" do
277
+ time = Time.now
278
+ @object.last_modified = time
279
+ @object.reload_headers['If-Modified-Since'].should == time.httpdate
280
+ end
281
+ end
282
+
283
+ describe "when storing the object normally" do
284
+ before :each do
285
+ @http = mock("HTTPBackend")
286
+ @client.stub!(:http).and_return(@http)
287
+ @object = Riak::RObject.new(@bucket)
288
+ @object.content_type = "text/plain"
289
+ @object.data = "This is some text."
290
+ @headers = @object.store_headers
291
+ end
292
+
293
+ it "should raise an error when the content_type is blank" do
294
+ lambda { @object.content_type = nil; @object.store }.should raise_error(ArgumentError)
295
+ lambda { @object.content_type = " "; @object.store }.should raise_error(ArgumentError)
296
+ end
297
+
298
+ describe "when the object has no key" do
299
+ it "should issue a POST request to the bucket, and update the object properties (returning the body by default)" do
300
+ @http.should_receive(:post).with(201, "/raw/", "foo", {:returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/raw/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 201})
301
+ @object.store
302
+ @object.key.should == "somereallylongstring"
303
+ @object.vclock.should == "areallylonghashvalue"
304
+ end
305
+
306
+ it "should include persistence-tuning parameters in the query string" do
307
+ @http.should_receive(:post).with(201, "/raw/", "foo", {:dw => 2, :returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/raw/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 201})
308
+ @object.store(:dw => 2)
309
+ end
310
+ end
311
+
312
+ describe "when the object has a key" do
313
+ before :each do
314
+ @object.key = "bar"
315
+ end
316
+
317
+ it "should issue a PUT request to the bucket, and update the object properties (returning the body by default)" do
318
+ @http.should_receive(:put).with([200,204], "/raw/", "foo/bar", {:returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/raw/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 204})
319
+ @object.store
320
+ @object.key.should == "somereallylongstring"
321
+ @object.vclock.should == "areallylonghashvalue"
322
+ end
323
+
324
+ it "should include persistence-tuning parameters in the query string" do
325
+ @http.should_receive(:put).with([200,204], "/raw/", "foo/bar", {:dw => 2, :returnbody => true}, "This is some text.", @headers).and_return({:headers => {'location' => ["/raw/foo/somereallylongstring"], "x-riak-vclock" => ["areallylonghashvalue"]}, :code => 204})
326
+ @object.store(:dw => 2)
327
+ end
328
+ end
329
+ end
330
+
331
+ describe "when reloading the object" do
332
+ before :each do
333
+ @http = mock("HTTPBackend")
334
+ @client.stub!(:http).and_return(@http)
335
+ @object = Riak::RObject.new(@bucket, "bar")
336
+ @object.vclock = "somereallylongstring"
337
+ @object.stub!(:reload_headers).and_return({})
338
+ end
339
+
340
+ it "should return without requesting if the key is blank" do
341
+ @object.key = nil
342
+ @http.should_not_receive(:get)
343
+ @object.reload
344
+ end
345
+
346
+ it "should return without requesting if the vclock is blank" do
347
+ @object.vclock = nil
348
+ @http.should_not_receive(:get)
349
+ @object.reload
350
+ end
351
+
352
+ it "should make the request if the key is present and the :force option is given" do
353
+ @http.should_receive(:get).and_return({:headers => {}, :code => 304})
354
+ @object.reload :force => true
355
+ end
356
+
357
+ it "should pass along the reload_headers" do
358
+ @headers = {"If-None-Match" => "etag"}
359
+ @object.should_receive(:reload_headers).and_return(@headers)
360
+ @http.should_receive(:get).with([200,304], "/raw/", "foo", "bar", {}, @headers).and_return({:code => 304})
361
+ @object.reload
362
+ end
363
+
364
+ it "should return without modifying the object if the response is 304 Not Modified" do
365
+ @http.should_receive(:get).and_return({:code => 304})
366
+ @object.should_not_receive(:load)
367
+ @object.reload
368
+ end
369
+
370
+ it "should raise an exception when the response code is not 200 or 304" do
371
+ @http.should_receive(:get).and_raise(Riak::FailedRequest.new(:get, 200, 500, {}, ''))
372
+ @object.should_not_receive(:load)
373
+ lambda { @object.reload }.should raise_error(Riak::FailedRequest)
374
+ end
375
+ end
376
+
377
+ describe "walking from the object to linked objects" do
378
+ before :each do
379
+ @http = mock("HTTPBackend")
380
+ @client.stub!(:http).and_return(@http)
381
+ @client.stub!(:bucket).and_return(@bucket)
382
+ @object = Riak::RObject.new(@bucket, "bar")
383
+ @body = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-with-body.txt"))
384
+ end
385
+
386
+ it "should issue a GET request to the given walk spec" do
387
+ @http.should_receive(:get).with(200, "/raw/", "foo", "bar", "_,next,1").and_return(:headers => {"content-type" => ["multipart/mixed; boundary=12345"]}, :body => "\n--12345\nContent-Type: multipart/mixed; boundary=09876\n\n--09876--\n\n--12345--\n")
388
+ @object.walk(nil,"next",true)
389
+ end
390
+
391
+ it "should parse the results into arrays of objects" do
392
+ @http.stub!(:get).and_return(:headers => {"content-type" => ["multipart/mixed; boundary=5EiMOjuGavQ2IbXAqsJPLLfJNlA"]}, :body => @body)
393
+ results = @object.walk(nil,"next",true)
394
+ results.should be_kind_of(Array)
395
+ results.first.should be_kind_of(Array)
396
+ obj = results.first.first
397
+ obj.should be_kind_of(Riak::RObject)
398
+ obj.content_type.should == "text/plain"
399
+ obj.key.should == "baz"
400
+ obj.bucket.should == @bucket
401
+ end
402
+
403
+ it "should assign the bucket for newly parsed objects" do
404
+ @http.stub!(:get).and_return(:headers => {"content-type" => ["multipart/mixed; boundary=5EiMOjuGavQ2IbXAqsJPLLfJNlA"]}, :body => @body)
405
+ @client.should_receive(:bucket).with("foo", :keys => false).and_return(@bucket)
406
+ @object.walk(nil,"next",true)
407
+ end
408
+ end
409
+
410
+ describe "when deleting" do
411
+ before :each do
412
+ @http = mock("HTTPBackend")
413
+ @client.stub!(:http).and_return(@http)
414
+ @object = Riak::RObject.new(@bucket, "bar")
415
+ end
416
+
417
+ it "should make a DELETE request to the Riak server and freeze the object" do
418
+ @http.should_receive(:delete).with([204,404], "/raw/", "foo", "bar").and_return({:code => 204, :headers => {}})
419
+ @object.delete
420
+ @object.should be_frozen
421
+ end
422
+
423
+ it "should do nothing when the key is blank" do
424
+ @http.should_not_receive(:delete)
425
+ @object.key = nil
426
+ @object.delete
427
+ end
428
+
429
+ it "should pass through a failed request exception" do
430
+ @http.should_receive(:delete).and_raise(Riak::FailedRequest.new(:delete, [204,404], 500, {}, ""))
431
+ lambda { @object.delete }.should raise_error(Riak::FailedRequest)
432
+ end
433
+ end
434
+
435
+ it "should convert to a link having the same url and an empty tag" do
436
+ @object = Riak::RObject.new(@bucket, "bar")
437
+ @object.to_link.should == Riak::Link.new("/raw/foo/bar", nil)
438
+ end
439
+
440
+ it "should convert to a link having the same url and a supplied tag" do
441
+ @object = Riak::RObject.new(@bucket, "bar")
442
+ @object.to_link("next").should == Riak::Link.new("/raw/foo/bar", "next")
443
+ end
444
+ end
@@ -0,0 +1,208 @@
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::WalkSpec do
17
+ describe "initializing" do
18
+ describe "with a hash" do
19
+ it "should be empty by default" do
20
+ spec = Riak::WalkSpec.new({})
21
+ spec.bucket.should == "_"
22
+ spec.tag.should == "_"
23
+ spec.keep.should be_false
24
+ end
25
+
26
+ it "should extract the bucket" do
27
+ spec = Riak::WalkSpec.new({:bucket => "foo"})
28
+ spec.bucket.should == "foo"
29
+ spec.tag.should == "_"
30
+ spec.keep.should be_false
31
+ end
32
+
33
+ it "should extract the tag" do
34
+ spec = Riak::WalkSpec.new({:tag => "foo"})
35
+ spec.bucket.should == "_"
36
+ spec.tag.should == "foo"
37
+ spec.keep.should be_false
38
+ end
39
+
40
+ it "should extract the keep" do
41
+ spec = Riak::WalkSpec.new({:keep => true})
42
+ spec.bucket.should == "_"
43
+ spec.tag.should == "_"
44
+ spec.keep.should be_true
45
+ end
46
+ end
47
+
48
+ describe "with three arguments for bucket, tag, and keep" do
49
+ it "should assign the bucket, tag, and keep" do
50
+ spec = Riak::WalkSpec.new("foo", "next", false)
51
+ spec.bucket.should == "foo"
52
+ spec.tag.should == "next"
53
+ spec.keep.should be_false
54
+ end
55
+
56
+ it "should make the bucket '_' when false or nil" do
57
+ spec = Riak::WalkSpec.new(nil, "next", false)
58
+ spec.bucket.should == "_"
59
+ spec = Riak::WalkSpec.new(false, "next", false)
60
+ spec.bucket.should == "_"
61
+ end
62
+
63
+ it "should make the tag '_' when false or nil" do
64
+ spec = Riak::WalkSpec.new("foo", nil, false)
65
+ spec.tag.should == "_"
66
+ spec = Riak::WalkSpec.new("foo", false, false)
67
+ spec.tag.should == "_"
68
+ end
69
+
70
+ it "should make the keep false when false or nil" do
71
+ spec = Riak::WalkSpec.new(nil, nil, nil)
72
+ spec.keep.should be_false
73
+ spec = Riak::WalkSpec.new(nil, nil, false)
74
+ spec.keep.should be_false
75
+ end
76
+ end
77
+
78
+ it "should raise an ArgumentError for invalid arguments" do
79
+ lambda { Riak::WalkSpec.new }.should raise_error(ArgumentError)
80
+ lambda { Riak::WalkSpec.new("foo") }.should raise_error(ArgumentError)
81
+ lambda { Riak::WalkSpec.new("foo","bar") }.should raise_error(ArgumentError)
82
+ end
83
+ end
84
+
85
+ describe "converting to a string" do
86
+ before :each do
87
+ @spec = Riak::WalkSpec.new({})
88
+ end
89
+
90
+ it "should be the empty spec by default" do
91
+ @spec.to_s.should == "_,_,_"
92
+ end
93
+
94
+ it "should include the bucket when set" do
95
+ @spec.bucket = "foo"
96
+ @spec.to_s.should == "foo,_,_"
97
+ end
98
+
99
+ it "should include the tag when set" do
100
+ @spec.tag = "next"
101
+ @spec.to_s.should == "_,next,_"
102
+ end
103
+
104
+ it "should include the keep when true" do
105
+ @spec.keep = true
106
+ @spec.to_s.should == "_,_,1"
107
+ end
108
+ end
109
+
110
+ describe "creating from a list of parameters" do
111
+ it "should detect hashes and WalkSpecs interleaved with other parameters" do
112
+ specs = Riak::WalkSpec.normalize(nil,"next",nil,{:bucket => "foo"},Riak::WalkSpec.new({:tag => "child", :keep => true}))
113
+ specs.should have(3).items
114
+ specs.should be_all {|s| s.kind_of?(Riak::WalkSpec) }
115
+ specs.join("/").should == "_,next,_/foo,_,_/_,child,1"
116
+ end
117
+ end
118
+
119
+ describe "matching other objects with ===" do
120
+ before :each do
121
+ @spec = Riak::WalkSpec.new({})
122
+ end
123
+
124
+ it "should not match objects that aren't links or walk specs" do
125
+ @spec.should_not === "foo"
126
+ end
127
+
128
+ describe "matching links" do
129
+ before :each do
130
+ @link = Riak::Link.new("/raw/foo/bar", "next")
131
+ end
132
+
133
+ it "should match a link when the bucket and tag are not specified" do
134
+ @spec.should === @link
135
+ end
136
+
137
+ it "should match a link when the bucket is the same" do
138
+ @spec.bucket = "foo"
139
+ @spec.should === @link
140
+ end
141
+
142
+ it "should not match a link when the bucket is different" do
143
+ @spec.bucket = "bar"
144
+ @spec.should_not === @link
145
+ end
146
+
147
+ it "should match a link when the tag is the same" do
148
+ @spec.tag = "next"
149
+ @spec.should === @link
150
+ end
151
+
152
+ it "should not match a link when the tag is different" do
153
+ @spec.tag = "previous"
154
+ @spec.should_not === @link
155
+ end
156
+
157
+ it "should match a link when the bucket and tag are the same" do
158
+ @spec.bucket = "foo"
159
+ @spec.should === @link
160
+ end
161
+ end
162
+
163
+ describe "matching walk specs" do
164
+ before :each do
165
+ @other = Riak::WalkSpec.new({})
166
+ end
167
+
168
+ it "should match a walk spec that is equivalent" do
169
+ @spec.should === @other
170
+ end
171
+
172
+ it "should not match a walk spec that has a different keep value" do
173
+ @other.keep = true
174
+ @spec.should_not === @other
175
+ end
176
+
177
+ it "should match a walk spec with a more specific bucket" do
178
+ @other.bucket = "foo"
179
+ @spec.should === @other
180
+ end
181
+
182
+ it "should match a walk spec with the same bucket" do
183
+ @other.bucket = "foo"; @spec.bucket = "foo"
184
+ @spec.should === @other
185
+ end
186
+
187
+ it "should not match a walk spec with a different bucket" do
188
+ @other.bucket = "foo"; @spec.bucket = "bar"
189
+ @spec.should_not === @other
190
+ end
191
+
192
+ it "should match a walk spec with a more specific tag" do
193
+ @other.tag = "next"
194
+ @spec.should === @other
195
+ end
196
+
197
+ it "should match a walk spec with the same tag" do
198
+ @other.tag = "next"; @spec.tag = "next"
199
+ @spec.should === @other
200
+ end
201
+
202
+ it "should not match a walk spec with a different tag" do
203
+ @other.tag = "next"; @spec.tag = "previous"
204
+ @spec.should_not === @other
205
+ end
206
+ end
207
+ end
208
+ end