rufus-jig 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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
+