blodsband 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,27 @@
1
+
2
+ require 'uri'
3
+ require 'em-synchrony'
4
+ require 'em-synchrony/em-http'
5
+ require 'em-http/middleware/json_response'
6
+ require 'yajl'
7
+ require 'mail'
8
+ require 'date'
9
+ require 'cgi'
10
+ require 'rexml/document'
11
+ require 'digest/sha1'
12
+
13
+ $LOAD_PATH.unshift(File.expand_path('lib'))
14
+
15
+ require 'blodsband/future'
16
+ require 'blodsband/multi'
17
+ require 'blodsband/error'
18
+ require 'blodsband/riak'
19
+ require 'blodsband/riak/mr'
20
+ require 'blodsband/riak/response'
21
+ require 'blodsband/riak/lock'
22
+ require 'blodsband/riak/list'
23
+ require 'blodsband/riak/map'
24
+ require 'blodsband/riak/sset'
25
+ require 'blodsband/riak/bucket'
26
+ require 'blodsband/riak/counter'
27
+ require 'blodsband/riak/search'
@@ -0,0 +1,18 @@
1
+
2
+ module Blodsband
3
+
4
+ #
5
+ # A class that collects a single HTTP based error.
6
+ #
7
+ class Error < RuntimeError
8
+ attr_reader :response, :status
9
+ def initialize(http)
10
+ @status = http.response_header.status
11
+ @response = http.response
12
+ end
13
+ def to_s
14
+ "#{@response}: #{@status}"
15
+ end
16
+ end
17
+
18
+ end
@@ -0,0 +1,31 @@
1
+
2
+ module Blodsband
3
+
4
+ #
5
+ # A class that encapsulates an asynchronous operation.
6
+ #
7
+ # Used to avoid the callback carbonara usually produced when relying heavily on asynchronous code.
8
+ #
9
+ class Future
10
+
11
+ #
12
+ # Create a future that will run a block upon request.
13
+ #
14
+ # @param [block] block the block to run when {#get} is called.
15
+ #
16
+ def initialize(&block)
17
+ @block = block
18
+ end
19
+
20
+ #
21
+ # Get the result from the asynchronous operation.
22
+ #
23
+ # @return [Object] whatever the block given in {#initialize} produced.
24
+ #
25
+ def get
26
+ @value ||= @block.call
27
+ end
28
+
29
+ end
30
+
31
+ end
@@ -0,0 +1,37 @@
1
+
2
+ module Blodsband
3
+
4
+ class Multi < EM::Synchrony::Multi
5
+
6
+ #
7
+ # An error that collects all errors in a single {Blodsband::Multi} and renders them in a readable way.
8
+ #
9
+ class Error < RuntimeError
10
+ def initialize(multi)
11
+ @bad = {}
12
+ @bad.merge!(multi.responses[:errback])
13
+ multi.responses[:callback].each do |key, value|
14
+ @bad[key] = value unless [200,204].include?(value.response_header.status)
15
+ end
16
+ end
17
+ def to_s
18
+ @bad.values.inject([]) do |sum, http|
19
+ if http.response_header.status == 0
20
+ sum + ["Connection timed out: #{http.req.uri}"]
21
+ else
22
+ sum + ["#{http.response_header.status}: #{http.response}"]
23
+ end
24
+ end.join("\n")
25
+ end
26
+ end
27
+
28
+ #
29
+ # Does exactly what {::EventMachine::Synchrony::Multi} does, but keeps waiting until it is really REALLY finished (and not just until it just happens to get scheduled again).
30
+ #
31
+ def really_perform
32
+ perform while !finished?
33
+ end
34
+
35
+ end
36
+
37
+ end
@@ -0,0 +1,48 @@
1
+ module Blodsband
2
+
3
+ class Riak
4
+
5
+ #
6
+ # Initialize a Riak client.
7
+ #
8
+ # @param [URI] url a url pointing to the HTML port of a Riak node.
9
+ #
10
+ def initialize(url)
11
+ @url = url
12
+ end
13
+
14
+ #
15
+ # Get a {Blodsband::Riak::Bucket}.
16
+ #
17
+ # @param [String] name see {Blodsband::Riak::Bucket#initialize}
18
+ # @param [Hash] options see {Blodsband::Riak::Bucket#initialize}
19
+ #
20
+ # @return [Blodsband::Riak::Bucket] a Bucket with the provided configuration and the {::URI} of this {Blodsband::Riak}.
21
+ #
22
+ def bucket(name, options = {})
23
+ Bucket.new(@url, name, options)
24
+ end
25
+
26
+ #
27
+ # Get a {Blodsband::Riak::Search}
28
+ #
29
+ # @param [URI] url a url pointing to the HTML port of a Riak node.
30
+ #
31
+ # @return [Blodsband::Riak::Search] a search instance initialized with the {::URI} of this {Blodsband::Riak}.
32
+ #
33
+ def search
34
+ Search.new(@url)
35
+ end
36
+
37
+ #
38
+ # Get a map/reduce helper instance.
39
+ #
40
+ # @return [Blodsband::Riak::Mr] a {Blodsband::Riak::Mr} with the {::URI} of this {Blodsband::Riak}.
41
+ #
42
+ def mr
43
+ Mr.new(@url)
44
+ end
45
+
46
+ end
47
+
48
+ end
@@ -0,0 +1,1169 @@
1
+
2
+ module Blodsband
3
+
4
+ class Riak
5
+
6
+ #
7
+ # Encapsulates functionality of Riak buckets.
8
+ #
9
+ class Bucket
10
+
11
+ #
12
+ # [String] The name of the Riak bucket this {Blodsband::Riak::Bucket} refers to.
13
+ #
14
+ attr_reader :name
15
+ #
16
+ # [Hash<Symbol, Object>] The default options for this {Blodsband::Riak::Bucket}
17
+ #
18
+ attr_reader :defaults
19
+
20
+ #
21
+ # Create a {Blodsband::Riak::Bucket}.
22
+ #
23
+ # @param [::URI] url a url pointing to the HTML port of a Riak node.
24
+ # @param [String] name the name of the bucket.
25
+ # @param [Hash<Symbol, Object>] options
26
+ # :client_id:: [String] <code>client_id</code> to use when communicating with Riak. Will default to a random 256 bit string.
27
+ # :unique:: [true, false] if this bucket should always default to <code>:unique => true</code> when doing {Blodsband::Riak::Bucket#get}. Will default to <code>false</code>.
28
+ #
29
+ def initialize(url, name, options = {})
30
+ @url = url
31
+ @name = name
32
+ @defaults = options.merge(:client_id => rand(1 << 256).to_s(36))
33
+ raise "Unknown options to #{self.class}#initialize: #{options}" unless options.empty?
34
+ end
35
+
36
+ #
37
+ # Sets the {Blodsband::Riak::Bucket} property <code>:allow_mult</code> to <code>true</code>
38
+ # and the default option <code>:unique</code> to <code>true</code>.
39
+ #
40
+ # @return [Blodsband::Riak::Bucket] this same bucket with the new options set.
41
+ #
42
+ def unique
43
+ @defaults[:unique] = true
44
+ self.props = {:allow_mult => true}
45
+ self
46
+ end
47
+
48
+ #
49
+ # Sets the {Blodsband::Riak::Bucket} property <code>:search</code> to <code>true</code>.
50
+ #
51
+ # @return [Blodsband::Riak::Bucket] this same bucket.
52
+ #
53
+ def indexed
54
+ self.props = {:search => true}
55
+ self
56
+ end
57
+
58
+ #
59
+ # @return [Hash<Symbol, Object>] the properties of this Riak bucket.
60
+ #
61
+ def props
62
+ Yajl::Parser.parse(EM::HttpRequest.new(uri).get.response)['props']
63
+ end
64
+
65
+ #
66
+ # Sets the properties of this Riak bucket.
67
+ # Will merge in the given properties with the existing ones, so a complete property {::Hash} is not necessary.
68
+ #
69
+ # @param [Hash<Symbol, Object>] p the new properties.
70
+ #
71
+ def props=(p)
72
+ EM::HttpRequest.new(uri).put(:head => {"Content-Type" => "application/json"},
73
+ :body => Yajl::Encoder.encode(:props => p))
74
+ end
75
+
76
+ #
77
+ # {include:Bucket#delete}
78
+ #
79
+ # @param (see #delete)
80
+ #
81
+ # @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request.
82
+ #
83
+ def adelete(key, options = {})
84
+ riak_params = options.delete(:riak_params)
85
+ m = Multi.new
86
+ url = uri(key)
87
+ url += "?#{riak_params.collect do |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" end.join("&")}" if riak_params
88
+ m.add(:resp, EM::HttpRequest.new(url).adelete)
89
+ return(Future.new do
90
+ m.really_perform
91
+ Response.parse(m)
92
+ end)
93
+ end
94
+
95
+ #
96
+ # Delete a key in Riak.
97
+ #
98
+ # @param [String] key the key to delete.
99
+ # @param [Hash<Symbol, Object>] options
100
+ # :riak_params:: [Hash<String, String>] <code>param_name => param_value</code> of extra parameters to send to Riak when doing the HTTP request.
101
+ #
102
+ # @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request.
103
+ #
104
+ def delete(key, options = {})
105
+ adelete(key, options).get
106
+ end
107
+
108
+ #
109
+ # {include:Bucket#put}
110
+ #
111
+ # @param [String] key the key to put the value under.
112
+ # @param [Object] value the value to put under the key.
113
+ #
114
+ # @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request.
115
+ #
116
+ def []=(key, value)
117
+ self.put(key, value)
118
+ end
119
+
120
+ #
121
+ # {include:Bucket#put}
122
+ #
123
+ # @param (see #put)
124
+ #
125
+ # @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request.
126
+ #
127
+ def aput(key, value, options = {})
128
+ links = value.links if value.respond_to?(:links)
129
+ links = (links || {}).merge(options.delete(:links)) if options.include?(:links)
130
+
131
+ index_rels = value.index_rels if value.respond_to?(:index_rels)
132
+ index_rels = (index_rels || {}).merge(options.delete(:index_rels)) if options.include?(:index_rels)
133
+
134
+ riak_params = options.delete(:riak_params)
135
+
136
+ vclock = value.vclock if value.respond_to?(:vclock)
137
+ vclock = options.delete(:vclock) if options.include?(:vclock)
138
+
139
+ client_id = options.delete(:client_id) || @defaults[:client_id]
140
+
141
+ meta = value.meta if value.respond_to?(:meta)
142
+ meta = (meta || {}).merge(options.delete(:meta)) if options.include?(:meta)
143
+ meta ||= {}
144
+
145
+ raise "Unknown options to #{self.class}#put(#{key}, #{value}, ...): #{options.inspect}" unless options.empty?
146
+ head = {
147
+ "Content-Type" => "application/json",
148
+ "Accept" => "multipart/mixed, application/json"
149
+ }
150
+ meta.each do |k, v|
151
+ head["X-Riak-Meta-#{k}"] = v.to_s
152
+ end
153
+ head["X-Riak-ClientId"] = client_id if client_id
154
+ head["Link"] = create_link_header(links) if links
155
+ head["X-Riak-Vclock"] = vclock if vclock
156
+ index_rel_futures = []
157
+ if index_rels
158
+ index_rels.each do |names, relations|
159
+ relations.each do |rel|
160
+ index_rel_futures << aadd_index_rel(key, names, rel)
161
+ end
162
+ end
163
+ end
164
+ url = uri(key)
165
+ url += "?#{riak_params.collect do |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" end.join("&")}" if riak_params
166
+ m = Multi.new
167
+ m.add(:resp,
168
+ EM::HttpRequest.new(url).apost(:body => Yajl::Encoder.encode(value),
169
+ :head => head))
170
+ return(Future.new do
171
+ m.really_perform
172
+ index_rel_futures.each do |f|
173
+ f.get
174
+ end
175
+ Response.parse(m, :bucket => @name, :key => key)
176
+ end)
177
+ end
178
+
179
+ #
180
+ # Put a value in Riak.
181
+ #
182
+ # @param [String] key the key to put the value under.
183
+ # @param [Object] value the value to insert under the key. If this {::Object} responds to <code>:vclock</code>, <code>:links</code>, <code>:index_rels</code> or <code>:meta</code> the return values from those methods will be used as options in the resulting {#put}.
184
+ # @param [Hash<Symbol, Object>] options
185
+ # :links:: [Hash<Symbol, Array<Array<String, String>>>] <code>tagname => [[bucket, key], [bucket, key], ...]</code>. Will default to the <code>#links</code> of the <code>value</code> if available.
186
+ # :index_rels:: [Hash<Array<Symbol, Symbol>, Array<Array<String, String>>>] <code>[i_am_to_relation, relation_is_to_me] => [[bucket, key], [bucket, key], ...]</code>. Example: <code>bucket.put(artist.key, artist, :links_rels => {[:artists, :tracks] => artist.track_refs})</code>. Will default to the <code>#index_rels</code> of the <code>value</code> if available.
187
+ # :riak_params:: [Hash<String, String>] <code>param_name => param_value</code> of extra parameters to send to Riak when doing the HTTP request.
188
+ # :vclock:: [String] the <code>vclock</code> returned when the object was originally fetched. Will default to the <code>#vclock</code> of the <code>value</code> if available.
189
+ # :client_id:: [String] the <code>client_id</code> to use when communicating with Riak. Will default to the <code>client_id</code> of the {Blodsband::Riak::Bucket}.
190
+ # :meta:: [Hash<String, String>] <code>meta_name => meta_value</code> of Riak meta data to save with the object, that will be returned when fetching the <code>value</code> next time. Will default to the <code>#meta</code> of the <code>value</code> if available.
191
+ #
192
+ # @return [Blodsband::Riak::Response] the response from riak for the resulting HTTP request.
193
+ #
194
+ def put(key, value, options = {})
195
+ aput(key, value, options).get
196
+ end
197
+
198
+ #
199
+ # Fetch a value from Riak.
200
+ #
201
+ # @param [String] key the key for the value.
202
+ #
203
+ # @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
204
+ #
205
+ def [](key)
206
+ self.get(key)
207
+ end
208
+
209
+ #
210
+ # Efficiently check what keys correspond to existing values in Riak.
211
+ #
212
+ # @param [Array<String>] keys an {::Array} of {::String} containing the keys for which to check if values exist.
213
+ #
214
+ # @return [Set<String>] the keys having values.
215
+ #
216
+ def has_many?(keys)
217
+ ahas_many?(keys).get
218
+ end
219
+
220
+ #
221
+ # {include:Bucket#has_many?}
222
+ #
223
+ # @param (see #has_many?)
224
+ #
225
+ # @return [Blodsband::Future<Set<String>>] a Set eventually containing the keys for which values exist.
226
+ #
227
+ def ahas_many?(keys)
228
+ future = Mr.new(@url).
229
+ inputs(keys.collect do |key| [@name, key] end).
230
+ map({
231
+ :keep => false,
232
+ :language => "javascript",
233
+ :source => <<EOF
234
+ function(v,k,a) {
235
+ if (v.values != null) {
236
+ return [v];
237
+ } else {
238
+ return [];
239
+ }
240
+ }
241
+ EOF
242
+ }).
243
+ reduce({
244
+ :keep => true,
245
+ :language => "javascript",
246
+ :source => <<EOF
247
+ function(v) {
248
+ rval = [];
249
+ for (var i = 0; i < v.length; i++) {
250
+ if (v[i].values != null) {
251
+ rval.push(v[i].key);
252
+ }
253
+ }
254
+ return rval;
255
+ }
256
+ EOF
257
+ }).arun
258
+ return(Future.new do
259
+ Set.new(future.get)
260
+ end)
261
+ end
262
+
263
+ #
264
+ # Efficiently fetch values for many keys in Riak.
265
+ #
266
+ # @param [Array] keys an {::Array} of {::String} containing the keys for which to fetch the values.
267
+ #
268
+ # @return [Array<Blodsband::Riak::Response>] the response from Riak for the resulting HTTP request (which will also be ie a {::String}s or a {::Hash}es.
269
+ #
270
+ def get_many(keys)
271
+ aget_many(keys).get
272
+ end
273
+
274
+ #
275
+ # {include:Bucket#get_many}
276
+ #
277
+ # @param (see #get_many)
278
+ #
279
+ # @return [Blodsband::Future<Array<Blodsband::Riak::Response>>] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String}s or a {::Hash}es.
280
+ #
281
+ def aget_many(keys)
282
+ Mr.new(@url).
283
+ inputs(keys.collect do |key| [@name, key] end).
284
+ map({
285
+ :keep => false,
286
+ :language => "javascript",
287
+ :source => <<EOF
288
+ function(v,k,a) {
289
+ if (v.values != null) {
290
+ return [v];
291
+ } else {
292
+ return [];
293
+ }
294
+ }
295
+ EOF
296
+ }).
297
+ reduce({
298
+ :keep => true,
299
+ :language => "javascript",
300
+ :source => <<EOF
301
+ function(v) {
302
+ rval = [];
303
+ for (var i = 0; i < v.length; i++) {
304
+ if (v[i].values != null) {
305
+ rval.push(JSON.parse(v[i].values[0].data));
306
+ }
307
+ }
308
+ return rval;
309
+ }
310
+ EOF
311
+ }).arun
312
+ end
313
+
314
+ #
315
+ # {include:Bucket#get}
316
+ #
317
+ # @param (see #get)
318
+ #
319
+ # @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
320
+ #
321
+ def aget(key, options = {})
322
+ links = options.delete(:links)
323
+ index_rels = options.delete(:index_rels)
324
+ riak_params = options.delete(:riak_params)
325
+ unique = options.include?(:unique) ? options.delete(:unique) : @defaults[:unique]
326
+ ary = options.delete(:ary) || @defaults[:ary]
327
+ raise "Unknown options to #{self.class}#get(#{key}, ...): #{options.inspect}" unless options.empty?
328
+ u = uri(key)
329
+ m = Multi.new
330
+ if links
331
+ links.each do |linkchain|
332
+ m.add(linkchain, EM::HttpRequest.new("#{u}#{create_link_walker(linkchain)}").aget)
333
+ end
334
+ end
335
+ mrs = []
336
+ if index_rels
337
+ index_rels.each do |names|
338
+ mrs << aindex_rels(key, names)
339
+ end
340
+ end
341
+ u += "?#{riak_params.collect do |k,v| "#{CGI.escape(k.to_s)}=#{CGI.escape(v.to_s)}" end.join("&")}" if riak_params
342
+ m.add(:resp, EM::HttpRequest.new(u).aget(:head => {"Accept" => "multipart/mixed, application/json"}))
343
+ return(Future.new do
344
+ m.really_perform
345
+ resp = Response.parse(m, :links => links, :bucket => @name, :key => key)
346
+ resp = find_winner(key, resp) if unique && resp && resp.status == 300
347
+ resp = arify(resp) if ary
348
+ rval = resp
349
+ if mrs.size > 0
350
+ unless resp.is_a?(Hash)
351
+ rval = [resp]
352
+ end
353
+ class << resp
354
+ attr_reader :index_rels
355
+ end
356
+ mrs.each_with_index do |mr, index|
357
+ resp.instance_eval do
358
+ @index_rels ||= {}
359
+ @index_rels[index_rels[index]] = mr.get.collect do |hash| [hash["bucket"], hash["key"]] end
360
+ end
361
+ if resp.is_a?(Hash)
362
+ resp[index_rels[index][1].to_s] = mr.get.collect do |hash| extended_index_rel(hash) end
363
+ else
364
+ rval << mr.get.collect do |hash| extended_index_rel(hash) end
365
+ end
366
+ end
367
+ end
368
+ rval
369
+ end)
370
+ end
371
+
372
+ #
373
+ # {include:Bucket#[]}
374
+ #
375
+ # @param [String] key the key from which to fetch the value.
376
+ # @param [Hash<Symbol, Object>] options
377
+ # :links:: [Array<Array<String>>] an {::Array} of Riak link chains to fetch for the key. Example: <code>artist.get(key, :links => [["aliases"], ["friends", "friends"]])</code>
378
+ # :index_rels:: [Array<Array<Symbol>>] an {::Array} of relationships stored as index relations to fetch for the key. Example: <code>artist.get(key, :index_rels => [[:artists, :tracks], [:artists, :followers]])</code>
379
+ # :unique:: [true, false] if <code>true</code>, any siblings returned from Riak will be reduced to one using a method that will return the currently likely winner in an ongoing {#put_if_missing} scenario.
380
+ # :ary:: [true, false] if <code>true</code>, will be guaranteed to return an {::Array} containing zero or more values depending on the number of siblings and deleted values.
381
+ #
382
+ # @return (see #[])
383
+ #
384
+ def get(key, options = {})
385
+ aget(key, options).get
386
+ end
387
+
388
+ #
389
+ # Get a concurrent {Blodsband::Riak::Tree} in this {Blodsband::Riak::Bucket}.
390
+ #
391
+ # @param [String] key the key for this lock.
392
+ #
393
+ # @return [Blodsband::Riak::Lock] a lock supporting concurrent synchronization over a Riak cluster.
394
+ #
395
+ def lock(key)
396
+ return Lock.new(self, key)
397
+ end
398
+
399
+ #
400
+ # Get a concurrent {Blodsband::Riak::List} in this {Blodsband::Riak::Bucket}.
401
+ #
402
+ # @param [String] key the key for this list.
403
+ #
404
+ # @return [Blodsband::Riak::List] a list supporting concurrent update and reads.
405
+ #
406
+ def list(key)
407
+ return List.new(self, key)
408
+ end
409
+
410
+ #
411
+ # Get a concurrent {Blodsband::Riak::Map} in this {Blodsband::Riak::Bucket}.
412
+ #
413
+ # @param [String] key the key for this map.
414
+ #
415
+ # @return [Blodsband::Riak::Map] a map supporting concurrent updates and reads.
416
+ #
417
+ def map(key)
418
+ return Map.new(self, key)
419
+ end
420
+
421
+ #
422
+ # Get a concurrent {Blodsband::Riak::Sset} in this {Blodsband::Riak::Bucket}.
423
+ #
424
+ # @param [String] key the key for this set.
425
+ #
426
+ # @return [Blodsband::Riak::Sset] a set supporting concurrent updates and reads.
427
+ #
428
+ def sset(key)
429
+ return Sset.new(self, key)
430
+ end
431
+
432
+ #
433
+ # Get a concurrent {Blodsband::Riak::Counter} in this {Blodsband::Riak::Bucket}.
434
+ #
435
+ # @param [String] key the key for this counter.
436
+ #
437
+ # @return [Blodsband::Riak::Counter] a counter supporting concurrent update and reads.
438
+ #
439
+ def counter(key)
440
+ return Counter.new(self, key)
441
+ end
442
+
443
+ #
444
+ # Put a value in the bucket if the key is missing.
445
+ #
446
+ # @param [String] key the key to put the value under (if it doesn't already exist).
447
+ # @param [Object] value the value to put under the key (if the key doesn't already exist).
448
+ # @param [Hash<Symbol, Object>] options
449
+ # :client_id:: [String] <code>client_id</code> to use when communicating with Riak for this operation. Will default to the <code>client_id</code> of this {Blodsband::Riak::Bucket}.
450
+ #
451
+ # @return [Object] whatever this call succeeded in inserting under <code>key</code>, or <code>nil</code>.
452
+ #
453
+ def put_if_missing(key, value, options = {})
454
+ aput_if_missing(key, value, options).get
455
+ end
456
+
457
+ #
458
+ # {include:Bucket#put_if_missing}
459
+ #
460
+ # @param (see #put_if_missing)
461
+ #
462
+ # @return [Blodsband::Future<Object>] whatever this call eventually succeeded in inserting under <code>key</code>, or <code>nil</code>.
463
+ #
464
+ def aput_if_missing(key, value, options = {})
465
+ acas(key, value, nil, options)
466
+ end
467
+
468
+ #
469
+ # Compare the value of a key to an expected vclock (or nil) and uniquely set a new value if the
470
+ # current value matches the expectations.
471
+ #
472
+ # @param [String] key the key to look at.
473
+ # @param [Object] value the value to put under the key.
474
+ # @param [String] expected_vclock the vclock to replace, or <code>nil</code> if only set if the value is missing.
475
+ # @param [Hash<Symbol, Object>] options
476
+ # :client_id:: [String] <code>client_id</code> to use when communicating with Riak for this operation. Will default to the <code>client_id</code> of this {Blodsband::Riak::Bucket}.
477
+ #
478
+ # @return [Object] whatever this call succeeded in inserting under <code>key</code>, or <code>nil</code>.
479
+ #
480
+ def cas(key, value, expected_vclock, options = {})
481
+ acas(key, value, expected_vclock, options).get
482
+ end
483
+
484
+ #
485
+ # {include:Bucket#cas}
486
+ #
487
+ # @param (see #cas)
488
+ #
489
+ # @return [Blodsband::Future<Object>] whatever this call eventually succeeded in inserting under <code>key</code>, or <code>nil</code>.
490
+ #
491
+ def acas(key, value, expected_vclock, options = {})
492
+ #
493
+ # While result is :nothing we will continue working.
494
+ #
495
+ result = :nothing
496
+ #
497
+ # Remember the fiber that started it all
498
+ #
499
+ parent = Fiber.current
500
+ #
501
+ # Create a fiber that does our dirty work for us
502
+ #
503
+ Fiber.new do
504
+ post_check_sleep = options.delete(:post_check_sleep)
505
+ post_write_sleep = options.delete(:post_write_sleep)
506
+ client_id = @defaults[:client_id] || rand(1 << 256).to_s(36)
507
+ cas_id = rand(1 << 256).to_s(36)
508
+
509
+ current = get(key)
510
+ sleep(post_check_sleep) if post_check_sleep
511
+
512
+ #
513
+ # While we lack a result
514
+ #
515
+ while result == :nothing do
516
+ if (expected_vclock.nil? && current.nil?) || (current.respond_to?(:vclock) && current.vclock == expected_vclock)
517
+ #
518
+ # The current value is what we expected, write our value and repeat with the response
519
+ #
520
+ current = put(key,
521
+ value,
522
+ :riak_params => {:w => :all, :returnbody => true},
523
+ :client_id => client_id,
524
+ :vclock => current.respond_to?(:vclock) ? current.vclock : nil,
525
+ :meta => {
526
+ "cas-state" => "prospect",
527
+ "cas-voter" => cas_id,
528
+ "cas-id" => cas_id
529
+ })
530
+ sleep(post_write_sleep) if post_write_sleep
531
+ else
532
+ #
533
+ # The current value was not what we expected, check what is the winner.
534
+ #
535
+ winner = find_winner(key, current, options.merge(:client_id => client_id,
536
+ :cas_voter => cas_id))
537
+ if winner.nil?
538
+ #
539
+ # No winner, we tried to overwrite an existing value that didn't exist.
540
+ #
541
+ result = nil
542
+ elsif winner.meta["cas-id"] == cas_id
543
+ #
544
+ # We are winners!
545
+ #
546
+ result = winner
547
+ else
548
+ #
549
+ # We lost :/
550
+ #
551
+ result = nil
552
+ end
553
+ end
554
+ end
555
+ #
556
+ # Resume the parent if necessary.
557
+ #
558
+ parent.resume if parent.alive? && parent != Fiber.current
559
+ end.resume
560
+ return(Future.new do
561
+ rval = nil
562
+ Fiber.yield while result == :nothing
563
+ result
564
+ end)
565
+ end
566
+
567
+ #
568
+ # {include:Bucket#delete_index_rel}
569
+ #
570
+ # @param (see #delete_index_rel)
571
+ #
572
+ # @return [Blodsband::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
573
+ #
574
+ def adelete_index_rel(key, names, rel)
575
+ Bucket.new(@url, names.sort.join("-")).adelete(index_rel_key(key, names, rel))
576
+ end
577
+
578
+ #
579
+ # Remove an index stored relation from Riak.
580
+ #
581
+ # @param [String] key the key from which to remove the relation.
582
+ # @param [Array<Symbol, Symbol>] names the specification of the relationship type from which to remove a relation.
583
+ # @param [Array<String, String>] rel the bucket and key to remove from the relationship type.
584
+ #
585
+ # @return [Blodsband::Riak::Response] the eventual response from Riak for the resulting HTTP request (which will also be ie a {::String} or a {::Hash}.
586
+ #
587
+ def delete_index_rel(key, names, rel)
588
+ adelete_index_rel(key, names, rel).get
589
+ end
590
+
591
+ #
592
+ # {include:Bucket#add_index_rel}
593
+ #
594
+ # @note (see #add_index_rel)
595
+ #
596
+ # @param (see #add_index_rel)
597
+ #
598
+ # @return [Blodsband::Riak::Future<Blodsband::Riak::Response>] the eventual response from Riak for the resulting HTTP request.
599
+ #
600
+ def aadd_index_rel(key, names, rel)
601
+ Bucket.new(@url, names.sort.join("-")).aput(index_rel_key(key, names, rel), index_rel_document(names, key, rel))
602
+ end
603
+
604
+ #
605
+ # Add an index stored relation to Riak.
606
+ #
607
+ # Right now this has proven meaningful up to relation sizes of about 10000 due to Riak Search limitations
608
+ # when it comes to how it counts and fetches documents. After that a clear linear scaling is observable.
609
+ #
610
+ # @note The <code>rel</code> parameter can have a {::Hash} as a third element containing extra
611
+ # attributes for the relationship. These attributes will be stored in the relationship document
612
+ # and can be used to filter queries on this relationship type. This is also usable from the {#put}
613
+ # method and will have the same effect there.
614
+ #
615
+ # @param [String] key the key for which to add the relation.
616
+ # @param [Array<Symbol, Symbol>] names the specification of the relationship type for which to add a relation.
617
+ # @param [Array<String, String>] rel the bucket and key to add to the relationship type.
618
+ #
619
+ # @return [Blodsband::Riak::Response] the response from Riak for the resulting HTTP request.
620
+ #
621
+ def add_index_rel(key, names, rel)
622
+ aadd_index_rel(key, names, rel).get
623
+ end
624
+
625
+ #
626
+ # {include:Bucket#count_index_rels}
627
+ #
628
+ # @note (see #add_index_rel)
629
+ #
630
+ # @param (see #count_index_rels)
631
+ #
632
+ # @return [Blodsband::Future<Integer>] the number of matching relations eventually returned.
633
+ #
634
+ def acount_index_rels(key, names)
635
+ future = Search.new(@url).asearch(create_search_expression(key, names), :index => names.sort.join("-"), :page => 1, :per_page => 1)
636
+ return(Future.new do
637
+ future.get[:total]
638
+ end)
639
+ end
640
+
641
+ #
642
+ # Count the number of relations of a given type.
643
+ #
644
+ # @note (see #add_index_rel)
645
+ #
646
+ # @param [String] key the key for which to count relations.
647
+ # @param [Array<Symbol, Symbol>] names the specification of the relationship type to count.
648
+ #
649
+ # @return [Integer] the number of matching relations.
650
+ #
651
+ def count_index_rels(key, names)
652
+ acount_index_rels(key, names).get
653
+ end
654
+
655
+ #
656
+ # {include:Bucket#has_index_rel?}
657
+ #
658
+ # @param (see #has_index_rel?)
659
+ #
660
+ # @return [Blodsband::Future<true,false>] the eventual response to whether the relation exists.
661
+ #
662
+ def ahas_index_rel?(key, names, rel)
663
+ future = Bucket.new(@url, names.sort.join("-")).aget(index_rel_key(key, names, rel))
664
+ return(Future.new do
665
+ !future.get.nil?
666
+ end)
667
+ end
668
+
669
+ #
670
+ # Check whether a given relation exists.
671
+ #
672
+ # @param [String] key the key for which to check the relations.
673
+ # @param [Array<Symbol, Symbol>] names the specification of the relationship type in which to search.
674
+ # @param [Array<String, String>] rel the bucket and key to check if it exists in the relation type.
675
+ #
676
+ # @return [true, false] whether the relation exists.
677
+ #
678
+ def has_index_rel?(key, names, rel)
679
+ ahas_index_rel?(key, names, rel).get
680
+ end
681
+
682
+ #
683
+ # {include:Bucket#index_rels}
684
+ #
685
+ # @note (see #index_rels)
686
+ #
687
+ # @param (see #index_rels)
688
+ #
689
+ # @return [Blodsband::Riak::Future<Array<Blodsband::Riak::Response>>] a future containing an {::Array} containing the eventually retrieved {Blodsband::Riak::Response}s stored in the index as these relations.
690
+ #
691
+ def aindex_rels(key, names)
692
+ index_rel_mr(key, names).arun
693
+ end
694
+
695
+ #
696
+ # {include:Bucket#recip_index_rels}
697
+ #
698
+ # @note (see #index_rels)
699
+ #
700
+ # @param (see #index_rels)
701
+ #
702
+ # @return [Blodsband::Riak::Future<Array<Blodsband::Riak::Response>>] a future containing an {::Array} containing the retrieved {Blodsband::Riak:Response}s stored in the index as these relations.
703
+ #
704
+ def arecip_index_rels(key, names)
705
+ recip_index_rel_mr(key, names).arun
706
+ end
707
+
708
+ #
709
+ # Retrieve a set of relations having the same relation to us as we to them.
710
+ #
711
+ # @note (see #index_rels)
712
+ #
713
+ # @param [String] key the key to which the relations are connected.
714
+ # @param [Array<Symbol, Symbol>] names the specification of the relationship type to fetch.
715
+ #
716
+ # @return [Array<Blodsband::Riak::Response>] an {::Array} containing the retrieved {Blodsband::Riak:Response}s stored in the index as these relations.
717
+ #
718
+ def recip_index_rels(key, names)
719
+ arecip_index_rels(key, names).get
720
+ end
721
+
722
+ #
723
+ # Retrieve a set of relations stored in the Riak index.
724
+ #
725
+ # @note The <code>names</code> parameter can have a {::Hash} as third element containing a filter definition
726
+ # <code>:field => :required_value</code> that will be used to filter the relations returned. This
727
+ # is also usable from the {#get} method and will have the same effect there.
728
+ #
729
+ # @param [String] key the key to which the relations are connected.
730
+ # @param [Array<Symbol, Symbol>] names the specification of the relationship type to fetch.
731
+ #
732
+ # @return [Array<Blodsband::Riak::Response>] a future containing an {::Array} containing the retrieved {Blodsband::Riak::Response}s stored in the index as these relations.
733
+ #
734
+ def index_rels(key, names)
735
+ aindex_rels(key, names).get
736
+ end
737
+
738
+ private
739
+
740
+ def arify(resp)
741
+ if resp.nil?
742
+ []
743
+ elsif resp.status == 300
744
+ resp.compact!
745
+ resp
746
+ else
747
+ resp.copy_to([resp])
748
+ end
749
+ end
750
+
751
+ def vote_winner(current)
752
+ if current.status == 300
753
+ #
754
+ # Multiple alternatives
755
+ #
756
+ winner = nil
757
+ current.compact.each do |alternative|
758
+ #
759
+ # Find the one already considered a winner, OR the one with the highest cas-id and cas-voter
760
+ #
761
+ if winner.nil?
762
+ winner = alternative
763
+ elsif winner.meta["cas-state"] != "winner"
764
+ if (alternative.meta["cas-state"] == "winner" ||
765
+ alternative.meta["cas-id"].to_s > winner.meta["cas-id"].to_s ||
766
+ (alternative.meta["cas-id"].to_s == winner.meta["cas-id"].to_s &&
767
+ alternative.meta["cas-voter"].to_s > winner.meta["cas-voter"].to_s))
768
+ winner = alternative
769
+ end
770
+ end
771
+ end
772
+ #
773
+ # Make the winner look like it came directly from a #get
774
+ #
775
+ current.copy_to(winner, :except => [:status])
776
+ else
777
+ current
778
+ end
779
+ end
780
+
781
+ def find_winner(key, current, options = {})
782
+ client_id = options.delete(:client_id) || @defaults[:client_id]
783
+ cas_voter = options.delete(:cas_voter)
784
+ post_check_sleep = options.delete(:post_check_sleep)
785
+ post_write_sleep = options.delete(:post_write_sleep)
786
+ unique_wait_threshold = options.delete(:unique_wait_threshold) || @defaults[:unique_wait_threshold] || 3
787
+ raise "Unknown options to #{self}.find_winner(...): #{options.inspect}" unless options.empty?
788
+ #STDERR.puts("#{$$} setting read without to 0 from #{caller.join("\n")}")
789
+ reads_without_winner = 0
790
+
791
+ cas_id = nil
792
+ result = :nothing
793
+ while result == :nothing
794
+ if current.nil?
795
+ #
796
+ # If there is no document, then the empty document is the winner
797
+ #
798
+ result = nil
799
+ else
800
+ #STDERR.puts("#{$$} trying to find a winner among #{current.inspect}")
801
+ status = current.status
802
+ winner = vote_winner(current)
803
+ if winner.meta["cas-state"] == "winner"
804
+ #
805
+ # If the winner is already marked as winner.
806
+ #
807
+ if cas_voter == winner.meta["cas-voter"]
808
+ #
809
+ # If the winner has our cas_voter.
810
+ #
811
+ if status == 300
812
+ #
813
+ # If this was a winner in a sibling horde, overwrite the sibling horde with the winner.
814
+ #
815
+ #STDERR.puts("#{$$} #{winner.inspect} was our winner, but it's not alone so overwriting the sibling horde")
816
+ current = put(key,
817
+ winner,
818
+ :riak_params => {:w => :all, :returnbody => true},
819
+ :client_id => client_id)
820
+ sleep(post_write_sleep) if post_write_sleep
821
+ else
822
+ #
823
+ # Else just set result as current winner.
824
+ #
825
+ result = winner
826
+ end
827
+ else
828
+ #
829
+ # If the winner doesn't have our cas_voter, just set it as the result.
830
+ #
831
+ result = winner
832
+ end
833
+ else
834
+ if cas_voter.nil?
835
+ #
836
+ # If we have not yet voted.
837
+ #
838
+ if reads_without_winner < unique_wait_threshold
839
+ #
840
+ # If we still have patience.
841
+ #
842
+ #STDERR.puts("#{$$} has only tried to re-read #{reads_without_winner} times, hanging on")
843
+ EM::Synchrony.sleep(0.1)
844
+ current = get(key, :unique => false)
845
+ reads_without_winner += 1
846
+ else
847
+ #
848
+ # We need to DO something! Lets select this winner as our own and write it,
849
+ # and see what happens.
850
+ #
851
+ cas_voter = rand(1 << 256).to_s(36)
852
+ winner.meta["cas-voter"] = cas_voter
853
+ #STDERR.puts("#{$$} we have no horse, selecting #{winner.inspect}")
854
+ current = put(key,
855
+ winner,
856
+ :riak_params => {:w => :all, :returnbody => true},
857
+ :client_id => client_id)
858
+ end
859
+ else
860
+ if status == 300
861
+ #
862
+ # We got multiple results
863
+ #
864
+ if cas_voter == winner.meta["cas-voter"]
865
+ #
866
+ # The winner has our cas_voter, try to replace the horde with the winner
867
+ #
868
+ #STDERR.puts("#{$$} #{winner} was our winner, but unmarked and in a horde. overwriting siblings")
869
+ current = put(key,
870
+ winner,
871
+ :riak_params => {:w => :all, :returnbody => true},
872
+ :client_id => client_id)
873
+ else
874
+ #
875
+ # The winner doesn't have our cas_voter, but it is still there after we made at least one write,
876
+ # so just give up and accept it as a winner.
877
+ #
878
+ result = winner
879
+ end
880
+ else
881
+ #
882
+ # We only got one result after at least one write.
883
+ #
884
+ if cas_voter == winner.meta["cas-voter"]
885
+ #
886
+ # It has our cas_voter, so lets mark it as a winner.
887
+ #
888
+ winner.meta["cas-state"] = "winner"
889
+ #STDERR.puts("#{$$} #{winner} was our winner, but unmarked. marking it")
890
+ current = put(key,
891
+ winner,
892
+ :riak_params => {:w => :all, :returnbody => true},
893
+ :client_id => client_id)
894
+ else
895
+ #
896
+ # It doesn't have our id. Lets just set it as the result.
897
+ #
898
+ result = winner
899
+ end
900
+ end
901
+ end
902
+ end
903
+ end
904
+ end
905
+ #STDERR.puts("#{$$} returning #{result.inspect}")
906
+ result
907
+ end
908
+
909
+ def create_link_walker(ary)
910
+ if ary.nil?
911
+ ""
912
+ else
913
+ rval = []
914
+ ary.each do |link|
915
+ rval << "_,#{link},1"
916
+ end
917
+ "/#{rval.join("/")}"
918
+ end
919
+ end
920
+
921
+ def create_link_header(hash)
922
+ rval = []
923
+ hash.each do |type, links|
924
+ links.each do |link|
925
+ rval << "</#{key_uri(link[0], link[1])}>; riaktag=\"#{type}\""
926
+ end
927
+ end
928
+ return rval.join(", ")
929
+ end
930
+
931
+ def mergehash(master, stuff)
932
+ stuff.each do |k, v|
933
+ if master.include?(k)
934
+ current = master[k]
935
+ if current.respond_to?(:<<)
936
+ v.each do |e|
937
+ current << e
938
+ end
939
+ elsif current.is_a?(Hash)
940
+ mergehash(current, v)
941
+ else
942
+ master[k] = v
943
+ end
944
+ else
945
+ master[k] = v
946
+ end
947
+ end
948
+ end
949
+
950
+ def index_rel_subkey(bucket, key)
951
+ "<#{key}@#{bucket}>"
952
+ end
953
+
954
+ def index_rel_key(key, names, rel)
955
+ n = names[0...2]
956
+ if n.sort == n
957
+ "#{index_rel_subkey(name, key)}-#{index_rel_subkey(rel[0], rel[1])}"
958
+ else
959
+ "#{index_rel_subkey(rel[0], rel[1])}-#{index_rel_subkey(name, key)}"
960
+ end
961
+ end
962
+
963
+ def extended_index_rel(hash)
964
+ rval = hash["document"]
965
+ class << rval
966
+ attr_reader :key
967
+ attr_reader :bucket
968
+ end
969
+ rval.instance_eval do
970
+ @key = hash["key"]
971
+ @bucket = hash["bucket"]
972
+ end
973
+ rval
974
+ end
975
+
976
+ def index_rel_document(names, key, rel)
977
+ rval = if rel.size == 3
978
+ rel[2]
979
+ elsif rel.size == 2
980
+ {}
981
+ else
982
+ raise "Illegal argument to #index_rel_document(#{names}, #{key}, #{rel}): Third argument must be Array of size 2 or 3"
983
+ end
984
+ rval.merge("#{names[0]}_key" => index_rel_subkey(name, key),
985
+ "#{names[1]}_key" => index_rel_subkey(rel[0], rel[1]))
986
+ end
987
+
988
+ def index_rel_extra_search(hash)
989
+ hash.collect do |k, v|
990
+ "#{k}:\"#{v}\""
991
+ end.join(" AND ")
992
+ end
993
+
994
+ def create_search_expression(key, names)
995
+ #
996
+ # Create a search expression that finds all relations where we are on one side of the relation.
997
+ # Optionally with extra search criteria.
998
+ #
999
+ search = if names.size == 3
1000
+ extra = names.delete_at(2)
1001
+ "#{names[0]}_key:\"#{index_rel_subkey(name, key)}\" AND (#{index_rel_extra_search(extra)})"
1002
+ elsif names.size == 2
1003
+ "#{names[0]}_key:\"#{index_rel_subkey(name, key)}\""
1004
+ else
1005
+ raise "Illegal argument to #index_rel_mr(#{key}, #{names}): Second argument must be Array of size 2 or 3"
1006
+ end
1007
+ end
1008
+
1009
+ def recip_index_rel_mr(key, names)
1010
+ search = create_search_expression(key, names)
1011
+ Mr.new(@url).
1012
+ #
1013
+ # Create a new Mr where the input is the result of above search.
1014
+ #
1015
+ inputs(:module => "riak_search",
1016
+ :function => "mapred_search",
1017
+ :arg => [names.sort.join("-"),
1018
+ search]).
1019
+ #
1020
+ # Redirect the Mr to the reverse relation (where left and right side switch places).
1021
+ #
1022
+ map({
1023
+ :keep => false,
1024
+ :language => "javascript",
1025
+ :source => <<EOF
1026
+ function(v,k,a) {
1027
+ var key_match = /^(<.*@.*>)-(<.*@.*>)$/.exec(v["key"]);
1028
+ return [[v["bucket"], key_match[2] + "-" + key_match[1]]];
1029
+ }
1030
+ EOF
1031
+ }).
1032
+ #
1033
+ # Redirect the Mr to the endpoint of the reverse relation (that is not us, ie that is what we started looking
1034
+ # at before we reversed it), filtering out the deleted ones (where .values == null)
1035
+ #
1036
+ map({
1037
+ :keep => false,
1038
+ :language => "javascript",
1039
+ :source => <<EOF
1040
+ function(v,k,a) {
1041
+ var rel_key = JSON.parse(v.values[0].data).#{names[0]}_key;
1042
+ if ((match = /^<(.*)@(.*)>$/.exec(rel_key)) != null) {
1043
+ return [[match[2], match[1]]];
1044
+ } else {
1045
+ return [];
1046
+ }
1047
+ }
1048
+ EOF
1049
+ }).
1050
+ #
1051
+ # Collect the documents on the other side of those bucket/key combos,
1052
+ # filtering out those that are deleted (have .values == null)
1053
+ #
1054
+ map({
1055
+ :keep => false,
1056
+ :language => "javascript",
1057
+ :source => <<EOF
1058
+ function(v,k,a) {
1059
+ if (v.values != null) {
1060
+ return [v];
1061
+ } else {
1062
+ return [];
1063
+ }
1064
+ }
1065
+ EOF
1066
+ }).
1067
+ #
1068
+ # Filter out the documents that actually exist (have .values != null)
1069
+ #
1070
+ reduce({
1071
+ :keep => true,
1072
+ :language => "javascript",
1073
+ :source => <<EOF
1074
+ function(v) {
1075
+ rval = [];
1076
+ for (var i = 0; i < v.length; i++) {
1077
+ if (v[i].values != null) {
1078
+ rval.push({"document": JSON.parse(v[i].values[0].data), "bucket": v[i].bucket, "key": v[i].key});
1079
+ }
1080
+ }
1081
+ return rval;
1082
+ }
1083
+ EOF
1084
+ })
1085
+ end
1086
+
1087
+ def index_rel_mr(key, names)
1088
+ search = create_search_expression(key, names)
1089
+ Mr.new(@url).
1090
+ #
1091
+ # Create a new Mr where the input is the result of above search.
1092
+ #
1093
+ inputs(:module => "riak_search",
1094
+ :function => "mapred_search",
1095
+ :arg => [names.sort.join("-"),
1096
+ search]).
1097
+ #
1098
+ # Redirect the Mr to the bucket/key combo on the other side of the relation.
1099
+ #
1100
+ map({
1101
+ :keep => false,
1102
+ :language => "javascript",
1103
+ :source => <<EOF
1104
+ function(v,k,a) {
1105
+ var rel_key = JSON.parse(v.values[0].data).#{names[1]}_key;
1106
+ if ((match = /^<(.*)@(.*)>$/.exec(rel_key)) != null) {
1107
+ return [[match[2], match[1]]];
1108
+ } else {
1109
+ return [];
1110
+ }
1111
+ }
1112
+ EOF
1113
+ }).
1114
+ #
1115
+ # Collect the documents on the other side of those bucket/key combos,
1116
+ # filtering out those that are deleted (have .values == null)
1117
+ #
1118
+ map({
1119
+ :keep => false,
1120
+ :language => "javascript",
1121
+ :source => <<EOF
1122
+ function(v,k,a) {
1123
+ if (v.values != null) {
1124
+ return [v];
1125
+ } else {
1126
+ return [];
1127
+ }
1128
+ }
1129
+ EOF
1130
+ }).
1131
+ #
1132
+ # Filter out the documents that actually exist (have .values != null)
1133
+ #
1134
+ reduce({
1135
+ :keep => true,
1136
+ :language => "javascript",
1137
+ :source => <<EOF
1138
+ function(v) {
1139
+ rval = [];
1140
+ for (var i = 0; i < v.length; i++) {
1141
+ if (v[i].values != null) {
1142
+ rval.push({"document": JSON.parse(v[i].values[0].data), "bucket": v[i].bucket, "key": v[i].key});
1143
+ }
1144
+ }
1145
+ return rval;
1146
+ }
1147
+ EOF
1148
+ })
1149
+ end
1150
+
1151
+ def key_uri(bucket, key)
1152
+ "buckets/#{CGI.escape(bucket)}/keys/#{CGI.escape(key)}"
1153
+ end
1154
+
1155
+ def uri(*args)
1156
+ if args.size == 0
1157
+ URI.join(@url.to_s, "riak/#{CGI.escape(name)}")
1158
+ elsif args.size == 1
1159
+ URI.join(@url.to_s, key_uri(name, args[0]))
1160
+ elsif args.size == 2
1161
+ URI.join(@url.to_s, key_uri(args[0], args[1]))
1162
+ end
1163
+ end
1164
+
1165
+ end
1166
+
1167
+ end
1168
+
1169
+ end