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,50 @@
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::Link do
17
+ describe "parsing a link header" do
18
+ it "should create Link objects from the data" do
19
+ result = Riak::Link.parse('</raw/foo/bar>; rel="tag", </raw/foo>; rel="up"')
20
+ result.should be_kind_of(Array)
21
+ result.should be_all {|i| Riak::Link === i }
22
+ end
23
+
24
+ it "should set the url and rel parameters properly" do
25
+ result = Riak::Link.parse('</raw/foo/bar>; riaktag="tag", </raw/foo>; rel="up"')
26
+ result[0].url.should == "/raw/foo/bar"
27
+ result[0].rel.should == "tag"
28
+ result[1].url.should == "/raw/foo"
29
+ result[1].rel.should == "up"
30
+ end
31
+ end
32
+
33
+ it "should convert to a string appropriate for use in the Link header" do
34
+ Riak::Link.new("/raw/foo", "up").to_s.should == '</raw/foo>; riaktag="up"'
35
+ Riak::Link.new("/raw/foo/bar", "next").to_s.should == '</raw/foo/bar>; riaktag="next"'
36
+ end
37
+
38
+ it "should convert to a walk spec when pointing to an object" do
39
+ Riak::Link.new("/raw/foo/bar", "next").to_walk_spec.to_s.should == "foo,next,_"
40
+ lambda { Riak::Link.new("/raw/foo", "up").to_walk_spec }.should raise_error
41
+ end
42
+
43
+ it "should be equivalent to a link with the same url and rel" do
44
+ one = Riak::Link.new("/raw/foo/bar", "next")
45
+ two = Riak::Link.new("/raw/foo/bar", "next")
46
+ one.should == two
47
+ [one].should include(two)
48
+ [two].should include(one)
49
+ end
50
+ end
@@ -0,0 +1,347 @@
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::MapReduce do
17
+ before :each do
18
+ @client = Riak::Client.new
19
+ @http = mock("HTTPBackend")
20
+ @client.stub!(:http).and_return(@http)
21
+ @mr = Riak::MapReduce.new(@client)
22
+ end
23
+
24
+ it "should require a client" do
25
+ lambda { Riak::MapReduce.new }.should raise_error
26
+ lambda { Riak::MapReduce.new(@client) }.should_not raise_error
27
+ end
28
+
29
+ it "should initialize the inputs and query to empty arrays" do
30
+ @mr.inputs.should == []
31
+ @mr.query.should == []
32
+ end
33
+
34
+ it "should yield itself when given a block on initializing" do
35
+ @mr2 = nil
36
+ @mr = Riak::MapReduce.new(@client) do |mr|
37
+ @mr2 = mr
38
+ end
39
+ @mr2.should == @mr
40
+ end
41
+
42
+ describe "adding inputs" do
43
+ it "should return self for chaining" do
44
+ @mr.add("foo", "bar").should == @mr
45
+ end
46
+
47
+ it "should add bucket/key pairs to the inputs" do
48
+ @mr.add("foo","bar")
49
+ @mr.inputs.should == [["foo","bar"]]
50
+ end
51
+
52
+ it "should add an array containing a bucket/key pair to the inputs" do
53
+ @mr.add(["foo","bar"])
54
+ @mr.inputs.should == [["foo","bar"]]
55
+ end
56
+
57
+ it "should add an object to the inputs by its bucket and key" do
58
+ bucket = Riak::Bucket.new(@client, "foo")
59
+ obj = Riak::RObject.new(bucket, "bar")
60
+ @mr.add(obj)
61
+ @mr.inputs.should == [["foo", "bar"]]
62
+ end
63
+
64
+ it "should add an array containing a bucket/key/key-data triple to the inputs" do
65
+ @mr.add(["foo","bar",1000])
66
+ @mr.inputs.should == [["foo","bar",1000]]
67
+ end
68
+
69
+ it "should use a bucket name as the single input" do
70
+ @mr.add(Riak::Bucket.new(@client, "foo"))
71
+ @mr.inputs.should == "foo"
72
+ @mr.add("docs")
73
+ @mr.inputs.should == "docs"
74
+ end
75
+ end
76
+
77
+ [:map, :reduce].each do |type|
78
+ describe "adding #{type} phases" do
79
+ it "should return self for chaining" do
80
+ @mr.send(type, "function(){}").should == @mr
81
+ end
82
+
83
+ it "should accept a function string" do
84
+ @mr.send(type, "function(){}")
85
+ @mr.query.should have(1).items
86
+ phase = @mr.query.first
87
+ phase.function.should == "function(){}"
88
+ phase.type.should == type
89
+ end
90
+
91
+ it "should accept a function and options" do
92
+ @mr.send(type, "function(){}", :keep => true)
93
+ @mr.query.should have(1).items
94
+ phase = @mr.query.first
95
+ phase.function.should == "function(){}"
96
+ phase.type.should == type
97
+ phase.keep.should be_true
98
+ end
99
+
100
+ it "should accept a module/function pair" do
101
+ @mr.send(type, ["riak","mapsomething"])
102
+ @mr.query.should have(1).items
103
+ phase = @mr.query.first
104
+ phase.function.should == ["riak", "mapsomething"]
105
+ phase.type.should == type
106
+ phase.language.should == "erlang"
107
+ end
108
+
109
+ it "should accept a module/function pair with extra options" do
110
+ @mr.send(type, ["riak", "mapsomething"], :arg => [1000])
111
+ @mr.query.should have(1).items
112
+ phase = @mr.query.first
113
+ phase.function.should == ["riak", "mapsomething"]
114
+ phase.type.should == type
115
+ phase.language.should == "erlang"
116
+ phase.arg.should == [1000]
117
+ end
118
+ end
119
+ end
120
+
121
+ describe "adding link phases" do
122
+ it "should return self for chaining" do
123
+ @mr.link({}).should == @mr
124
+ end
125
+
126
+ it "should accept a WalkSpec" do
127
+ @mr.link(Riak::WalkSpec.new(:tag => "next"))
128
+ @mr.query.should have(1).items
129
+ phase = @mr.query.first
130
+ phase.type.should == :link
131
+ phase.function.should be_kind_of(Riak::WalkSpec)
132
+ phase.function.tag.should == "next"
133
+ end
134
+
135
+ it "should accept a WalkSpec and a hash of options" do
136
+ @mr.link(Riak::WalkSpec.new(:bucket => "foo"), :keep => true)
137
+ @mr.query.should have(1).items
138
+ phase = @mr.query.first
139
+ phase.type.should == :link
140
+ phase.function.should be_kind_of(Riak::WalkSpec)
141
+ phase.function.bucket.should == "foo"
142
+ phase.keep.should be_true
143
+ end
144
+
145
+ it "should accept a hash of options intermingled with the walk spec options" do
146
+ @mr.link(:tag => "snakes", :arg => [1000])
147
+ @mr.query.should have(1).items
148
+ phase = @mr.query.first
149
+ phase.arg.should == [1000]
150
+ phase.function.should be_kind_of(Riak::WalkSpec)
151
+ phase.function.tag.should == "snakes"
152
+ end
153
+ end
154
+
155
+ describe "converting to JSON for the job" do
156
+ it "should include the inputs and query keys" do
157
+ @mr.to_json.should =~ /"inputs":/
158
+ end
159
+
160
+ it "should map phases to their JSON equivalents" do
161
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => "function(){}")
162
+ @mr.query << phase
163
+ @mr.to_json.should include('"source":"function(){}"')
164
+ @mr.to_json.should include('"query":[{"map":{')
165
+ end
166
+
167
+ it "should emit only the bucket name when the input is the whole bucket" do
168
+ @mr.add("foo")
169
+ @mr.to_json.should include('"inputs":"foo"')
170
+ end
171
+
172
+ it "should emit an array of inputs when there are multiple inputs" do
173
+ @mr.add("foo","bar",1000).add("foo","baz")
174
+ @mr.to_json.should include('"inputs":[["foo","bar",1000],["foo","baz"]]')
175
+ end
176
+ end
177
+
178
+ describe "executing the map reduce job" do
179
+ it "should issue POST request to the mapred endpoint" do
180
+ @http.should_receive(:post).with(200, "/mapred", @mr.to_json, hash_including("Content-Type" => "application/json")).and_return({:headers => {'content-type' => ["application/json"]}, :body => "{}"})
181
+ @mr.run
182
+ end
183
+
184
+ it "should vivify JSON responses" do
185
+ @http.stub!(:post).and_return(:headers => {'content-type' => ["application/json"]}, :body => '{"key":"value"}')
186
+ @mr.run.should == {"key" => "value"}
187
+ end
188
+
189
+ it "should return the full response hash for non-JSON responses" do
190
+ response = {:code => 200, :headers => {'content-type' => ["text/plain"]}, :body => 'This is some text.'}
191
+ @http.stub!(:post).and_return(response)
192
+ @mr.run.should == response
193
+ end
194
+
195
+ it "should interpret failed requests with JSON content-types as map reduce errors" do
196
+ @http.stub!(:post).and_raise(Riak::FailedRequest.new(:post, 200, 500, {"content-type" => ["application/json"]}, '{"error":"syntax error"}'))
197
+ lambda { @mr.run }.should raise_error(Riak::MapReduceError)
198
+ begin
199
+ @mr.run
200
+ rescue Riak::MapReduceError => mre
201
+ mre.message.should == '{"error":"syntax error"}'
202
+ else
203
+ fail "No exception raised!"
204
+ end
205
+ end
206
+
207
+ it "should re-raise non-JSON error responses" do
208
+ @http.stub!(:post).and_raise(Riak::FailedRequest.new(:post, 200, 500, {"content-type" => ["text/plain"]}, 'Oops, you bwoke it.'))
209
+ lambda { @mr.run }.should raise_error(Riak::FailedRequest)
210
+ end
211
+ end
212
+ end
213
+
214
+ describe Riak::MapReduce::Phase do
215
+ before :each do
216
+ @fun = "function(v,_,_){ return v['values'][0]['data']; }"
217
+ end
218
+
219
+ it "should initialize with a type and a function" do
220
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => @fun, :language => "javascript")
221
+ phase.type.should == :map
222
+ phase.function.should == @fun
223
+ phase.language.should == "javascript"
224
+ end
225
+
226
+ it "should initialize with a type and an MF" do
227
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => ["module", "function"], :language => "erlang")
228
+ phase.type.should == :map
229
+ phase.function.should == ["module", "function"]
230
+ phase.language.should == "erlang"
231
+ end
232
+
233
+ it "should initialize with a type and a bucket/key" do
234
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => {:bucket => "funs", :key => "awesome_map"}, :language => "javascript")
235
+ phase.type.should == :map
236
+ phase.function.should == {:bucket => "funs", :key => "awesome_map"}
237
+ phase.language.should == "javascript"
238
+ end
239
+
240
+ it "should assume the language is erlang when the function is an array" do
241
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => ["module", "function"])
242
+ phase.language.should == "erlang"
243
+ end
244
+
245
+ it "should assume the language is javascript when the function is a string" do
246
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => @fun)
247
+ phase.language.should == "javascript"
248
+ end
249
+
250
+ it "should assume the language is javascript when the function is a hash" do
251
+ phase = Riak::MapReduce::Phase.new(:type => :map, :function => {:bucket => "jobs", :key => "awesome_map"})
252
+ phase.language.should == "javascript"
253
+ end
254
+
255
+ it "should accept a WalkSpec for the function when a link phase" do
256
+ phase = Riak::MapReduce::Phase.new(:type => :link, :function => Riak::WalkSpec.new({}))
257
+ phase.function.should be_kind_of(Riak::WalkSpec)
258
+ end
259
+
260
+ it "should raise an error if a WalkSpec is given for a phase type other than :link" do
261
+ lambda { Riak::MapReduce::Phase.new(:type => :map, :function => Riak::WalkSpec.new({})) }.should raise_error(ArgumentError)
262
+ end
263
+
264
+ describe "converting to JSON for the job" do
265
+ before :each do
266
+ @phase = Riak::MapReduce::Phase.new(:type => :map, :function => "")
267
+ end
268
+
269
+ [:map, :reduce].each do |type|
270
+ describe "when a #{type} phase" do
271
+ before :each do
272
+ @phase.type = type
273
+ end
274
+
275
+ it "should be an object with a single key of '#{type}'" do
276
+ @phase.to_json.should =~ /^\{"#{type}":/
277
+ end
278
+
279
+ it "should include the language" do
280
+ @phase.to_json.should =~ /"language":/
281
+ end
282
+
283
+ it "should include the keep value" do
284
+ @phase.to_json.should =~ /"keep":false/
285
+ @phase.keep = true
286
+ @phase.to_json.should =~ /"keep":true/
287
+ end
288
+
289
+ it "should include the function source when the function is a source string" do
290
+ @phase.function = "function(v,_,_){ return v; }"
291
+ @phase.to_json.should include(@phase.function)
292
+ @phase.to_json.should =~ /"source":/
293
+ end
294
+
295
+ it "should include the function name when the function is not a lambda" do
296
+ @phase.function = "Riak.mapValues"
297
+ @phase.to_json.should include('"name":"Riak.mapValues"')
298
+ @phase.to_json.should_not include('"source"')
299
+ end
300
+
301
+ it "should include the bucket and key when referring to a stored function" do
302
+ @phase.function = {:bucket => "design", :key => "wordcount_map"}
303
+ @phase.to_json.should include('"bucket":"design"')
304
+ @phase.to_json.should include('"key":"wordcount_map"')
305
+ end
306
+
307
+ it "should include the module and function when invoking an Erlang function" do
308
+ @phase.function = ["riak_mapreduce", "mapreduce_fun"]
309
+ @phase.to_json.should include('"module":"riak_mapreduce"')
310
+ @phase.to_json.should include('"function":"mapreduce_fun"')
311
+ end
312
+ end
313
+ end
314
+
315
+ describe "when a link phase" do
316
+ before :each do
317
+ @phase.type = :link
318
+ @phase.function = {}
319
+ end
320
+
321
+ it "should be an object of a single key 'link'" do
322
+ @phase.to_json.should =~ /^\{"link":/
323
+ end
324
+
325
+ it "should include the bucket" do
326
+ @phase.to_json.should =~ /"bucket":"_"/
327
+ @phase.function[:bucket] = "foo"
328
+ @phase.to_json.should =~ /"bucket":"foo"/
329
+ end
330
+
331
+ it "should include the tag" do
332
+ @phase.to_json.should =~ /"tag":"_"/
333
+ @phase.function[:tag] = "parent"
334
+ @phase.to_json.should =~ /"tag":"parent"/
335
+ end
336
+
337
+ it "should include the keep value" do
338
+ @phase.to_json.should =~ /"keep":false/
339
+ @phase.keep = true
340
+ @phase.to_json.should =~ /"keep":true/
341
+ @phase.keep = false
342
+ @phase.function[:keep] = true
343
+ @phase.to_json.should =~ /"keep":true/
344
+ end
345
+ end
346
+ end
347
+ end
@@ -0,0 +1,36 @@
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::Util::Multipart do
17
+ it "should extract the boundary string from a header value" do
18
+ Riak::Util::Multipart.extract_boundary("multipart/mixed; boundary=123446677890").should == "123446677890"
19
+ end
20
+
21
+ it "should parse an empty multipart body into empty arrays" do
22
+ data = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-blank.txt"))
23
+ Riak::Util::Multipart.parse(data, "73NmmA8dJxSB5nL2dVerpFIi8ze").should == [[]]
24
+ end
25
+
26
+ it "should parse multipart body into nested arrays with response-like results" do
27
+ data = File.read(File.expand_path("#{File.dirname(__FILE__)}/../fixtures/multipart-with-body.txt"))
28
+ results = Riak::Util::Multipart.parse(data, "5EiMOjuGavQ2IbXAqsJPLLfJNlA")
29
+ results.should be_kind_of(Array)
30
+ results.first.should be_kind_of(Array)
31
+ obj = results.first.first
32
+ obj.should be_kind_of(Hash)
33
+ obj.should have_key(:headers)
34
+ obj.should have_key(:body)
35
+ end
36
+ end
@@ -0,0 +1,28 @@
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::Client::NetHTTPBackend do
17
+ before :each do
18
+ @client = Riak::Client.new
19
+ @backend = Riak::Client::NetHTTPBackend.new(@client)
20
+ FakeWeb.allow_net_connect = false
21
+ end
22
+
23
+ def setup_http_mock(method, uri, options={})
24
+ FakeWeb.register_uri(method, uri, options)
25
+ end
26
+
27
+ it_should_behave_like "HTTP backend"
28
+ end