rufus-jig 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,533 @@
1
+ #--
2
+ # Copyright (c) 2009-2009, John Mettraux, jmettraux@gmail.com
3
+ #
4
+ # Permission is hereby granted, free of charge, to any person obtaining a copy
5
+ # of this software and associated documentation files (the "Software"), to deal
6
+ # in the Software without restriction, including without limitation the rights
7
+ # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8
+ # copies of the Software, and to permit persons to whom the Software is
9
+ # furnished to do so, subject to the following conditions:
10
+ #
11
+ # The above copyright notice and this permission notice shall be included in
12
+ # all copies or substantial portions of the Software.
13
+ #
14
+ # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16
+ # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17
+ # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18
+ # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19
+ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
20
+ # THE SOFTWARE.
21
+ #
22
+ # Made in Japan.
23
+ #++
24
+
25
+
26
+ module Rufus::Jig
27
+
28
+ #
29
+ # An error class for the couch stuff.
30
+ #
31
+ # Has a #status and an #original methods.
32
+ #
33
+ class CouchError < HttpError
34
+
35
+ # the original error hash
36
+ #
37
+ attr_reader :original
38
+
39
+ def initialize (status, message)
40
+
41
+ @original = (Rufus::Jig::Json.decode(message) rescue nil) || message
42
+
43
+ if @original.is_a?(String)
44
+ super(status, @original)
45
+ else
46
+ super(status, "#{@original['error']}: #{@original['reason']}")
47
+ end
48
+ end
49
+ end
50
+
51
+ #
52
+ # The parent class of Rufus::Jig::Couch CouchDatabase and CouchDocument.
53
+ #
54
+ class CouchResource
55
+
56
+ # the jig client
57
+ #
58
+ attr_reader :http
59
+
60
+ # the path for this couch resource
61
+ #
62
+ attr_reader :path
63
+
64
+ # nil for a Couch instance, the Couch instance for a CouchDatabase or
65
+ # the CouchDatabase for a CouchDocument.
66
+ #
67
+ attr_reader :parent
68
+
69
+ def initialize (parent_or_http, path)
70
+
71
+ @path = path
72
+
73
+ path = path.split('/').select { |e| e != '' }
74
+
75
+ @parent, @http = if parent_or_http.is_a?(Rufus::Jig::Http)
76
+
77
+ parent = if path.length == 0
78
+ nil
79
+ elsif path.length == 1
80
+ Couch.new(parent_or_http)
81
+ else
82
+ CouchDatabase.new(parent_or_http, path.first)
83
+ end
84
+ [ parent, parent_or_http ]
85
+
86
+ else
87
+
88
+ [ parent_or_http, parent_or_http.http ]
89
+ end
90
+
91
+ @http.options[:error_class] = CouchError
92
+ end
93
+
94
+ # Returns the Rufus::Jig::Couch instance holding this couch resource.
95
+ #
96
+ def couch
97
+
98
+ @parent == nil ? self : @parent.couch
99
+ end
100
+
101
+ # Returns the Rufus::Jig::CouchDatabase instance holding this couch
102
+ # resource (or nil if this resource is a Rufus::Jig::Couch instance).
103
+ #
104
+ def db
105
+
106
+ return nil if @parent.nil?
107
+ return self if self.is_a?(CouchDatabase)
108
+ @parent # self is a document
109
+ end
110
+
111
+ # GET, relatively to this resource.
112
+ #
113
+ def get (path, opts={})
114
+ @http.get(adjust(path), opts)
115
+ end
116
+
117
+ # POST, relatively to this resource.
118
+ #
119
+ def post (path, data, opts={})
120
+ @http.post(adjust(path), data, opts)
121
+ end
122
+
123
+ # DELETE, relatively to this resource.
124
+ #
125
+ def delete (path, opts={})
126
+ @http.delete(adjust(path), opts)
127
+ end
128
+
129
+ # PUT, relatively to this resource.
130
+ #
131
+ def put (path, data, opts={})
132
+ @http.put(adjust(path), data, opts)
133
+ end
134
+
135
+ # Returns an array of 1 or more UUIDs generated by CouchDB.
136
+ #
137
+ def get_uuids (count=1)
138
+
139
+ @http.get("/_uuids?count=#{count}")['uuids']
140
+ end
141
+
142
+ # Returns the list of all database [names] in this couch.
143
+ #
144
+ def get_databases
145
+
146
+ @http.get('/_all_dbs')
147
+ end
148
+
149
+ protected
150
+
151
+ def adjust (path)
152
+
153
+ case path
154
+ when '.' then @path
155
+ when /^\// then path
156
+ else Rufus::Jig::Path.join(@path, path)
157
+ end
158
+ end
159
+
160
+ # Fetches etag from http cache
161
+ #
162
+ def etag (path)
163
+
164
+ r = @http.cache[path]
165
+
166
+ r ? r.first : nil
167
+ end
168
+ end
169
+
170
+ #
171
+ # Wrapping info about a Couch server.
172
+ #
173
+ #
174
+ # Also provides a set of class methods for interacting directly with couch
175
+ # resources.
176
+ #
177
+ # * get_couch
178
+ # * get_db
179
+ # * put_db
180
+ # * delete_db
181
+ # * get_doc
182
+ # * put_doc
183
+ # * delete_doc
184
+ #
185
+ # The first one is very important
186
+ #
187
+ class Couch < CouchResource
188
+
189
+ # Never call this method directly.
190
+ #
191
+ # Do
192
+ #
193
+ # couch = Rufus::Jig::Couch.get_couch('127.0.0.1', 5984)
194
+ #
195
+ # instead.
196
+ #
197
+ def initialize (parent_or_http)
198
+
199
+ super(parent_or_http, '/')
200
+ end
201
+
202
+ # Returns a CouchDatabase instance or nil if the database doesn't
203
+ # exist in this couch.
204
+ #
205
+ # couch = Rufus::Jig::Couch.get_couch('127.0.0.1', 5984)
206
+ # db = couch.get_db('hr_documents')
207
+ #
208
+ def get_db (name)
209
+
210
+ return nil if get(name).nil?
211
+
212
+ CouchDatabase.new(couch, name)
213
+ end
214
+
215
+ # Creates a database and returns the new CouchDatabase instance.
216
+ #
217
+ # Will raise a Rufus::Jig::CouchError if the db already exists.
218
+ #
219
+ # couch = Rufus::Jig::Couch.get_couch('127.0.0.1', 5984)
220
+ # db = couch.put_db('financial_results')
221
+ #
222
+ def put_db (name)
223
+
224
+ d = CouchDatabase.new(couch, name)
225
+ d.put('.', '')
226
+
227
+ d
228
+ end
229
+
230
+ # Deletes a database, given its name.
231
+ #
232
+ # Will raise a Rufus::Jig::CouchError if the db doesn't exist.
233
+ #
234
+ # couch = Rufus::Jig::Couch.get_couch('127.0.0.1', 5984)
235
+ # db = couch.delete_db('financial_results')
236
+ #
237
+ def delete_db (name)
238
+
239
+ raise(CouchError.new(404, "no db named '#{name}'")) if get(name).nil?
240
+
241
+ delete(name)
242
+ end
243
+
244
+ #--
245
+ # handy class methods
246
+ #++
247
+
248
+ # Returns a Rufus::Jig::Couch instance.
249
+ #
250
+ # couch = Rufus::Jig::Couch.get_couch('http://127.0.0.1:5984')
251
+ # # or
252
+ # couch = Rufus::Jig::Couch.get_couch('127.0.0.1', 5984)
253
+ #
254
+ # Will raise a Rufus::Jig::CouchError in case of trouble.
255
+ #
256
+ def self.get_couch (*args)
257
+
258
+ ht, pt, pl, op = extract_http(false, *args)
259
+
260
+ Couch.new(ht)
261
+ end
262
+
263
+ # Returns a CouchDatabase instance or nil if the db doesn't exist.
264
+ #
265
+ # db = Rufus::Jig::Couch.get_db('127.0.0.1', 5984, 'my_database')
266
+ # # or
267
+ # db = Rufus::Jig::Couch.get_db('http://127.0.0.1:5984/my_database')
268
+ #
269
+ def self.get_db (*args)
270
+
271
+ ht, pt, pl, op = extract_http(false, *args)
272
+
273
+ return nil unless ht.get(pt)
274
+
275
+ CouchDatabase.new(ht, Rufus::Jig::Path.to_name(pt))
276
+ end
277
+
278
+ # Creates a database and returns a CouchDatabase instance.
279
+ #
280
+ # db = Rufus::Jig::Couch.put_db('127.0.0.1', 5984, 'my_database')
281
+ # # or
282
+ # db = Rufus::Jig::Couch.put_db('http://127.0.0.1:5984/my_database')
283
+ #
284
+ # Will raise a Rufus::Jig::CouchError if the db already exists.
285
+ #
286
+ def self.put_db (*args)
287
+
288
+ ht, pt, pl, op = extract_http(false, *args)
289
+
290
+ ht.put(pt, '')
291
+
292
+ CouchDatabase.new(ht, Rufus::Jig::Path.to_name(pt))
293
+ end
294
+
295
+ # Deletes a database.
296
+ #
297
+ # Rufus::Jig::Couch.delete_db('127.0.0.1', 5984, 'my_database')
298
+ # # or
299
+ # Rufus::Jig::Couch.delete_db('http://127.0.0.1:5984/my_database')
300
+ #
301
+ # Will raise a Rufus::Jig::CouchError if the db doesn't exist.
302
+ #
303
+ def self.delete_db (*args)
304
+
305
+ ht, pt, pl, op = extract_http(false, *args)
306
+
307
+ ht.delete(pt)
308
+ end
309
+
310
+ # Fetches a document. Returns nil if not found or a CouchDocument instance.
311
+ #
312
+ # Rufus::Jig::Couch.get_doc('127.0.0.1', 5984, 'my_database/doc0')
313
+ # # or
314
+ # Rufus::Jig::Couch.get_doc('http://127.0.0.1:5984/my_database/doc0')
315
+ #
316
+ def self.get_doc (*args)
317
+
318
+ ht, pt, pl, op = extract_http(false, *args)
319
+
320
+ doc = ht.get(pt)
321
+
322
+ doc ? CouchDocument.new(ht, pt, doc) : nil
323
+ end
324
+
325
+ # Puts (creates) a document
326
+ #
327
+ # Rufus::Jig::Couch.put_doc(
328
+ # '127.0.0.1', 5984, 'my_database/doc0', { 'a' => 'b' })
329
+ # # or
330
+ # Rufus::Jig::Couch.put_doc(
331
+ # 'http://127.0.0.1:5984/my_database/doc0', { 'x' => 'y' })
332
+ #
333
+ # To update a doc, get it first, then change its content and put it
334
+ # via its put method.
335
+ #
336
+ def self.put_doc (*args)
337
+
338
+ ht, pt, pl, op = extract_http(true, *args)
339
+
340
+ info = ht.put(pt, pl, :content_type => :json, :cache => false)
341
+
342
+ CouchDocument.new(ht, pt, Rufus::Jig.marshal_copy(pl), info)
343
+ end
344
+
345
+ # Deletes a document.
346
+ #
347
+ # Rufus::Jig::Couch.delete_doc('127.0.0.1', 5984, 'my_database/doc0')
348
+ # # or
349
+ # Rufus::Jig::Couch.delete_doc('http://127.0.0.1:5984/my_database/doc0')
350
+ #
351
+ # Will raise a Rufus::Jig::CouchError if the doc doesn't exist.
352
+ #
353
+ def self.delete_doc (*args)
354
+
355
+ ht, pt, pl, op = extract_http(false, *args)
356
+
357
+ ht.delete(pt)
358
+ end
359
+
360
+ # This method is used from get_couch, get_db, put_db and co...
361
+ #
362
+ # Never used directly.
363
+ #
364
+ def self.extract_http (payload_expected, *args)
365
+
366
+ a = Rufus::Jig::Http.extract_http(payload_expected, *args)
367
+
368
+ a.first.error_class = Rufus::Jig::CouchError
369
+
370
+ a
371
+ end
372
+ end
373
+
374
+ #
375
+ # Wrapping info about a Couch database.
376
+ #
377
+ # You usually grab an instance of it like that :
378
+ #
379
+ # db = Rufus::Jig::Couch.get_db('127.0.0.1', 5984, 'my_database')
380
+ # # or
381
+ # db = Rufus::Jig::Couch.get_db('http://127.0.0.1:5984/my_database')
382
+ #
383
+ # # or
384
+ # couch = Rufus::Jig::Couch.get_couch('127.0.0.1', 5984)
385
+ # db = Rufus::Jig::Couch.get_db('my_database')
386
+ #
387
+ class CouchDatabase < CouchResource
388
+
389
+ attr_reader :name
390
+
391
+ # Usually called via Couch#get_database(name)
392
+ #
393
+ def initialize (parent_or_http, name)
394
+
395
+ @name = name
396
+
397
+ super(parent_or_http, Rufus::Jig::Path.to_path(@name))
398
+ end
399
+
400
+ # Given an id and an JSONable hash, puts the doc to the database
401
+ # and returns a CouchDocument instance wrapping it.
402
+ #
403
+ # db.put_doc('doc0', { 'item' => 'car', 'brand' => 'bmw' })
404
+ #
405
+ def put_doc (doc_id, doc)
406
+
407
+ info = put(doc_id, doc, :content_type => :json, :cache => false)
408
+
409
+ CouchDocument.new(
410
+ self,
411
+ Rufus::Jig::Path.join(@name, doc_id),
412
+ Rufus::Jig.marshal_copy(doc), info)
413
+ end
414
+
415
+ # Gets a document, given its id.
416
+ # (conditional GET).
417
+ #
418
+ # db.get_doc('doc0')
419
+ #
420
+ def get_doc (doc_id)
421
+
422
+ path = Rufus::Jig::Path.join(@path, doc_id)
423
+ opts = {}
424
+
425
+ if et = etag(path)
426
+ opts[:etag] = et
427
+ end
428
+
429
+ doc = get(path, opts)
430
+
431
+ doc ?
432
+ CouchDocument.new(self, Rufus::Jig::Path.join(@name, doc_id), doc) :
433
+ nil
434
+ end
435
+
436
+ # Deletes a document, you have to provide the current revision.
437
+ #
438
+ # db.delete_doc('doc0')
439
+ #
440
+ def delete_doc (doc_id, rev)
441
+
442
+ raise(ArgumentError.new("no doc '#{name}'")) if get(doc_id).nil?
443
+
444
+ delete(Rufus::Jig::Path.add_params(doc_id, :rev => rev))
445
+ end
446
+ end
447
+
448
+ #
449
+ # Wrapping a couch document.
450
+ #
451
+ # Responds to [] and []=
452
+ #
453
+ class CouchDocument < CouchResource
454
+
455
+ attr_reader :payload
456
+
457
+ # Don't call this method directly, use one of the get_doc or put_doc
458
+ # methods.
459
+ #
460
+ def initialize (parent_or_http, path, doc, put_result=nil)
461
+
462
+ super(parent_or_http, path)
463
+ @payload = doc
464
+
465
+ if put_result
466
+ @payload['_id'] = put_result['id']
467
+ @payload['_rev'] = put_result['rev']
468
+ end
469
+ end
470
+
471
+ # Gets a value.
472
+ #
473
+ def [] (k)
474
+ @payload[k]
475
+ end
476
+
477
+ # Sets a value.
478
+ #
479
+ def []= (k, v)
480
+ @payload[k] = v
481
+ end
482
+
483
+ # Returns to CouchDB id of the document.
484
+ #
485
+ def _id
486
+ @payload['_id']
487
+ end
488
+
489
+ # Returns the revision string for this copy of the document.
490
+ #
491
+ def _rev
492
+ @payload['_rev']
493
+ end
494
+
495
+ # Re-gets this document (updating its _rev and content if necessary).
496
+ #
497
+ def get
498
+
499
+ opts = {}
500
+
501
+ if @payload && rev = @payload['_rev']
502
+ opts[:etag] = "\"#{rev}\""
503
+ end
504
+
505
+ h = super(@path, opts)
506
+
507
+ raise(CouchError.new(410, 'probably gone')) unless h
508
+
509
+ @payload = h
510
+
511
+ self
512
+ end
513
+
514
+ # Deletes this document (from Couch).
515
+ #
516
+ def delete
517
+
518
+ super(Rufus::Jig::Path.add_params(@path, :rev => _rev))
519
+ end
520
+
521
+ # Puts this document (assumes you have updated it).
522
+ #
523
+ def put
524
+
525
+ h = super(
526
+ @path, @payload,
527
+ :content_type => :json, :etag => "\"#{@payload['_rev']}\"")
528
+
529
+ @payload['_rev'] = h['rev']
530
+ end
531
+ end
532
+ end
533
+