ripple 0.5.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 (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