blodsband 0.0.2

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.
@@ -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