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,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