yssk22-couch_resource 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/LICENSE ADDED
@@ -0,0 +1,2 @@
1
+ CouchResource is published under MIT license.
2
+ see MIT_LICENSE file in detail.
data/MIT_LICENSE ADDED
@@ -0,0 +1,8 @@
1
+ Copyright (c) 2008-2009 Yohei Sasaki http://www.yssk22.info/
2
+ All rights reserved.
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom then Software is furnished to do so, subject to the following conditions:
5
+
6
+ The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
7
+
8
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.rdoc ADDED
@@ -0,0 +1,7 @@
1
+ = couch_resource
2
+
3
+ Description goes here.
4
+
5
+ == Copyright
6
+
7
+ Copyright (c) 2009 Yohei Sasaki. See LICENSE for details.
@@ -0,0 +1,25 @@
1
+ require File.join(File.dirname(__FILE__), 'couch_resource/struct')
2
+ require File.join(File.dirname(__FILE__), 'couch_resource/validations')
3
+ require File.join(File.dirname(__FILE__), 'couch_resource/callbacks')
4
+ require File.join(File.dirname(__FILE__), 'couch_resource/view')
5
+ require File.join(File.dirname(__FILE__), 'couch_resource/base')
6
+
7
+ module CouchResource
8
+ Base.class_eval do
9
+ include Struct
10
+ include Validations
11
+ include View
12
+ alias_method_chain :save, :validation
13
+ alias_method_chain :save!, :validation
14
+
15
+ include Callbacks
16
+ # validation method chain
17
+ end
18
+ end
19
+
20
+ module CouchResource
21
+ class SubResource
22
+ include Struct
23
+ include Validations
24
+ end
25
+ end
@@ -0,0 +1,705 @@
1
+ require 'rubygems'
2
+ require 'json'
3
+ require File.join(File.dirname(__FILE__), 'error')
4
+ require File.join(File.dirname(__FILE__), 'connection')
5
+
6
+ module CouchResource
7
+ class Base
8
+ cattr_accessor :logger, :instance_writer => false
9
+ cattr_accessor :check_design_revision_every_time, :instance_writer => false
10
+
11
+ class << self
12
+ # Get the URI of the CouchDB database to map for this class
13
+ def database
14
+ if defined?(@database)
15
+ @database
16
+ elsif superclass != Object && superclass.database
17
+ superclass.database.dup.freeze
18
+ end
19
+ end
20
+
21
+ # Set the URI of the CouchDB database to map for this class
22
+ def database=(uri)
23
+ @connection = nil
24
+ if uri.nil?
25
+ @database = nil
26
+ else
27
+ @database = uri.is_a?(URI) ? uri.dup : URI.parse(uri)
28
+ @user = URI.decode(@database.user) if @database.user
29
+ @password = URI.decode(@database.password) if @database.password
30
+ end
31
+ end
32
+ alias :set_database :database=
33
+
34
+ # Gets the user for REST HTTP authentication.
35
+ def user
36
+ # Not using superclass_delegating_reader. See +site+ for explanation
37
+ if defined?(@user)
38
+ @user
39
+ elsif superclass != Object && superclass.user
40
+ superclass.user.dup.freeze
41
+ end
42
+ end
43
+
44
+ # Sets the user for REST HTTP authentication.
45
+ def user=(user)
46
+ @connection = nil
47
+ @user = user
48
+ end
49
+
50
+ # Gets the password for REST HTTP authentication.
51
+ def password
52
+ # Not using superclass_delegating_reader. See +site+ for explanation
53
+ if defined?(@password)
54
+ @password
55
+ elsif superclass != Object && superclass.password
56
+ superclass.password.dup.freeze
57
+ end
58
+ end
59
+
60
+ # Sets the password for REST HTTP authentication.
61
+ def password=(password)
62
+ @connection = nil
63
+ @password = password
64
+ end
65
+
66
+ # Sets the number of seconds after which requests to the REST API should time out.
67
+ def timeout=(timeout)
68
+ @connection = nil
69
+ @timeout = timeout
70
+ end
71
+
72
+ # Gets tthe number of seconds after which requests to the REST API should time out.
73
+ def timeout
74
+ if defined?(@timeout)
75
+ @timeout
76
+ elsif superclass != Object && superclass.timeout
77
+ superclass.timeout
78
+ end
79
+ end
80
+
81
+ # An instance of CouchResource::Connection that is the base connection to the remote service.
82
+ # The +refresh+ parameter toggles whether or not the connection is refreshed at every request
83
+ # or not (defaults to <tt>false</tt>).
84
+ def connection(reflesh = false)
85
+ if defined?(@connection) || superclass == Object
86
+ @connection = Connection.new(database) if reflesh || @connection.nil?
87
+ @connection.user = user if user
88
+ @connection.password = password if password
89
+ @connection.timeout = timeout if timeout
90
+ @connection
91
+ else
92
+ superclass.connection
93
+ end
94
+ end
95
+
96
+ # Get the document path specified by the document id
97
+ def document_path(id, query_options=nil)
98
+ "#{File.join(database.path, id)}#{query_string(query_options)}"
99
+ end
100
+
101
+ # Returns the path of _all_docs API
102
+ def all_docs_path(query_options=nil)
103
+ document_path("_all_docs", query_options)
104
+ end
105
+
106
+ # Returns the path of _bulk_docs
107
+ def bulk_docs_path
108
+ document_path("_bulk_docs")
109
+ end
110
+
111
+ def query_string(query_options=nil)
112
+ # compatibility :count and :limit
113
+ if query_options.nil? || query_options.empty?
114
+ nil
115
+ else
116
+ q = query_options.dup
117
+ q[:limit] = q.delete(:count) if q.has_key?(:count)
118
+ "?#{q.to_query}"
119
+ end
120
+
121
+ end
122
+
123
+ # Get whether the document specified by <tt>id</tt> exists or not
124
+ # options are
125
+ # * <tt>:rev</tt> - An string value determining the revision of the document.
126
+ def exists?(id, options=nil)
127
+ if id
128
+ headers = {}
129
+ headers["If-Match"] = options[:rev].to_json if options[:rev]
130
+ path = document_path(id, options)
131
+ result = connection.head(path, headers)
132
+ end
133
+ end
134
+
135
+ # Get the document revisions specified by <tt>id</ttd>
136
+ # This method returns an array including revision numbers.
137
+ # If the second argument, <tt>detailed</tt>, is true, each element in the result array is
138
+ # a Hash which contains 2 key, "status" and "revision".
139
+ # If false, each element is a string which represents revision number.
140
+ def get_revs(id, detailed = false)
141
+ if id
142
+ if detailed
143
+ path = document_path(id, { :revs_info => true })
144
+ result = connection.get(path)
145
+ result[:_revs_info].map { |r| r.symbolize_keys! }
146
+ else
147
+ path = document_path(id, { :revs => true })
148
+ result = connection.get(path)
149
+ result[:_revs]
150
+ end
151
+ end
152
+ end
153
+
154
+ #
155
+ # Find the existant document and returns the document object mapped to this class.
156
+ # ==== Examples
157
+ # Document.find(1)
158
+ # Document.find("abceef")
159
+ # Document.find(1, 2, 3)
160
+ # Document.find([1,2,3])
161
+ # Document.find(1,:rev => 123456)
162
+ #
163
+ # Note that when the first argument is one of :first, :last or :all,
164
+ # this finder method requests a view name to retrieve documents.
165
+ # In this case options can be the same as CouchDB's querying options (http://wiki.apache.org/couchdb/HttpViewApi).
166
+ # * <tt>key</tt> - an object value determining key parameter.
167
+ # * <tt>startkey</tt> - an object value determining startkey parameter.
168
+ # * <tt>startkey_docid</tt> - a string value determiming startkey_docid parameter.
169
+ # * <tt>endkey</tt> - an object value determining endkey parameter.
170
+ # * <tt>endkey_docid</tt> - a string value determiming startkey_docid parameter.
171
+ # * <tt>count</tt> - an integer value determining count parameter.
172
+ # * <tt>descending</tt> - a boolean value determining descending parameter.
173
+ # * <tt>skip</tt> - an integer value determining skip parameter.
174
+ # * <tt>group</tt> - an boolean value determining group parameter.
175
+ # * <tt>group_level</tt> - an integer value determining gropu_level parameter.
176
+ #
177
+ # Common Options
178
+ # * return_raw_hash - if set true, finder method returns row hash of the response
179
+ #
180
+ # ==== Examples
181
+ # Document.find(:first, "design_name", "view_name")
182
+ # Document.find(:first, "design_name", "view_name", :key => "abcd")
183
+ # Document.find(:first, "design_name", "view_name",
184
+ # :startkey => ["abcd"], :endkey => ["abcd", "ZZZZ"],
185
+ # :descending => true)
186
+ # Document.find(:last, "design_name", "view_name")
187
+ # Document.find(:all, "design_name", "view_name")
188
+ def find(*args)
189
+ options = args.extract_options!
190
+ case args.first
191
+ when :first, :last, :all
192
+ raise ArgumentError.new("Design name must be specified. ") unless args[1]
193
+ raise ArgumentError.new("View name must be specified. ") unless args[2]
194
+ send("find_#{args.first}", args[1], args[2], options)
195
+ else
196
+ find_from_ids(*(args << options))
197
+ end
198
+ end
199
+
200
+ # Returns human readable attribute name
201
+ def human_attribute_name(attribute_key_name) #:nodoc:
202
+ attribute_key_name.humanize
203
+ end
204
+
205
+ # Execute bulk_docs transaction.
206
+ # Each element in the <tt>array</tt> is passed to the "docs" member in the request body.
207
+ #
208
+ # This method returns the raw hash of JSON from CouchDB.
209
+ #
210
+ # Examples)
211
+ # bulk_docs([1,2,3]) --->
212
+ # POST /{db}/_bulk_docs
213
+ #
214
+ # { "docs" : [1,2,3] }
215
+ #
216
+ # bulk_docs([{ :_id => "1", :_rev => "1234", :foo => "bar" },
217
+ # { :_id => "1", :_rev => "5678", :_deleted => true }]) --->
218
+ # POST /{db}/_bulk_docs
219
+ #
220
+ # { "docs" : [
221
+ # { "_id" : "1" , "_rev" : "1234", "foo" : "bar" },
222
+ # { "_id" : "2" , "_rev" : "5678", "_deleted" : true },
223
+ # ] }
224
+ #
225
+ def bulk_docs(array = [])
226
+ document = { :docs => array }
227
+ logger.debug "CouchResource::Connection#post #{bulk_docs_path}"
228
+ logger.debug document.to_json
229
+ result = connection.post(bulk_docs_path, document.to_json)
230
+ logger.debug result.inspect
231
+ result
232
+ end
233
+
234
+ def default
235
+ obj = self.new
236
+ (self.read_inheritable_attribute(:attribute_members) || {}).each do |name, option|
237
+ if option.has_key?(:default)
238
+ default = option[:default]
239
+ if default.is_a?(Proc)
240
+ obj.set_attribute(name, default.call())
241
+ else
242
+ obj.set_attribute(name, default)
243
+ end
244
+ end
245
+ end
246
+ obj
247
+ end
248
+
249
+ private
250
+ def find_first(design, view, options)
251
+ path = view_path(design, view, options)
252
+ logger.debug "CouchResource::Connection#get #{path}"
253
+ result = self.connection.get(path)
254
+ logger.debug result.to_json
255
+ first = result["rows"].first
256
+ if first
257
+ if options[:return_raw_hash]
258
+ result
259
+ else
260
+ obj = new(first["value"])
261
+ # invoke explicit callback
262
+ obj.send(:after_find) rescue NoMethodError
263
+ obj
264
+ end
265
+ else
266
+ nil
267
+ end
268
+ end
269
+
270
+ def find_last(design, view, options)
271
+ path = view_path(design, view, options)
272
+ logger.debug "CouchResource::Connection#get #{path}"
273
+ result = self.connection.get(path)
274
+ logger.debug result.to_json
275
+ last = result["rows"].last
276
+ if last
277
+ if options[:return_raw_hash]
278
+ result
279
+ else
280
+ obj = new(last["value"])
281
+ # invoke explicit callback
282
+ obj.send(:after_find) rescue NoMethodError
283
+ obj
284
+ end
285
+ else
286
+ nil
287
+ end
288
+ end
289
+
290
+ def find_all(design, view, options)
291
+ #
292
+ # paginate options
293
+ # direction :
294
+ # expected_offset : when paginating
295
+ #
296
+ # About expected_offset :
297
+ # Sometimes paginate feature returns unexpected option because the offset is incorrect.
298
+ # It seems to be caused by CouchDB BUG (Couch-135)
299
+ # keep watching on http://issues.apache.org/jira/browse/COUCHDB-135
300
+ #
301
+ # The offset parameter CouchDB returns is not reliable so that we can use expected_offset to calculate the correct offset.
302
+ #
303
+ # ** /NOTES FOR THE CURRENT IMPLEMENTATION **
304
+ #
305
+ paginate_options = {}
306
+ [:direction, :expected_offset, :initial_startkey, :initial_endkey].each do |key|
307
+ paginate_options[key] = options.delete(key) if options.has_key?(key)
308
+ end
309
+
310
+ # process requset for view
311
+ result = get_view_result(design, view, options)
312
+ # check if design document has reduce function or not
313
+ view_def = self.get_view_definition(design, view)
314
+ unless view_def.has_key?(:reduce)
315
+ _prev,_next = calculate_pagenate_option(options, paginate_options, result)
316
+ result[:previous] = _prev
317
+ result[:next] = _next
318
+ end
319
+ if options[:return_raw_hash]
320
+ result
321
+ else
322
+ value_or_doc = options[:include_docs] ? "doc" : "value"
323
+ {
324
+ :next => result[:next],
325
+ :previous => result[:previous],
326
+ :total_rows => result[:total_rows],
327
+ :offset => result[:offset],
328
+ :rows => result[:rows].map { |row|
329
+ obj = new(row[value_or_doc])
330
+ # invoke explicit callback
331
+ obj.send(:after_find) rescue NoMethodError
332
+ obj
333
+ }
334
+ }
335
+ end
336
+ end
337
+
338
+ def get_view_result(design, view, options)
339
+ path = view_path(design, view, options)
340
+ result = nil
341
+ if options.has_key?(:keys)
342
+ logger.debug "CouchResource::Connection#post #{path}"
343
+ result = self.connection.post(path, {:keys => options[:keys]}.to_json)
344
+ else
345
+ logger.debug "CouchResource::Connection#get #{path}"
346
+ result = self.connection.get(path)
347
+ end
348
+ logger.debug result.to_json
349
+ result
350
+ end
351
+
352
+ def calculate_pagenate_option(request_options, paginate_options, result)
353
+ total_count = result[:total_rows]
354
+ offset = result[:offset]
355
+ row_count = result[:rows].length
356
+ if row_count < total_count
357
+ request_count = request_options[:count]
358
+ request_desc = request_options.has_key?(:descending) && request_options[:descending]
359
+ #
360
+ # calculate pagination if request_count is set and total_count has te value
361
+ # note: total_count should be nil if reduce is executed
362
+ #
363
+ if !request_count.nil? && !total_count.nil?
364
+ # calculate paginate option
365
+ logger.debug " *** CouchResource Pagination Calculation *** "
366
+ logger.debug " total_count : #{total_count}"
367
+ logger.debug " request_count : #{request_count}"
368
+ logger.debug " row_count : #{row_count}"
369
+ logger.debug " offset : #{offset}"
370
+ logger.debug " direction : #{paginate_options[:direction]}"
371
+ logger.debug " initial_startkey : #{paginate_options[:initial_endkey]}"
372
+ logger.debug " initial_endkey : #{paginate_options[:initial_endkey]}"
373
+ next_option = {}
374
+ previous_option = {}
375
+ if paginate_options.has_key?(:expected_offset) && request_options.has_key?(:skip)
376
+ old = offset
377
+ logger.debug " CouchDB offset bug(COUCH-135) workaround : Offset change from #{old} to offset in view point of #{paginate_options[:direction]}."
378
+ offset = paginate_options[:expected_offset].to_i
379
+ end
380
+ # Direction handling to calculate next and previous expected offset.
381
+ if paginate_options.has_key?(:direction)
382
+ case paginate_options[:direction].to_s
383
+ when "previous"
384
+ result[:rows].reverse!
385
+ # On previous pagination the original desc option is the reverse to that of requested.
386
+ request_desc = !request_desc
387
+ if row_count < request_count
388
+ logger.debug "CouchResource : The count of fetched rows is shorter than that of requested"
389
+ previous_option[:expected_offset] = -1
390
+ if row_count == 0
391
+ logger.debug "CouchResource : reached over in the previous operation"
392
+ # this may be the same option as first request. (:skip is not supported)
393
+ next_option[:startkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_endkey)
394
+ next_option[:endkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_startkey)
395
+ next_option[:descending] = request_desc
396
+ next_option[:count] = request_count
397
+ else
398
+ # this code may not be reached ... if the client keeps pagination options correctly.
399
+ next_option[:startkey] = result[:rows].last["key"]
400
+ next_option[:startkey_docid] = result[:rows].last["id"]
401
+ next_option[:endkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_endkey)
402
+ next_option[:descending] = request_desc
403
+ next_option[:count] = request_count
404
+ next_option[:skip] = 1
405
+ next_option[:expected_offset] = total_count - offset
406
+ end
407
+ else
408
+ # [previous_option]
409
+ previous_option[:startkey] = result[:rows].first["key"]
410
+ previous_option[:startkey_doc_id] = result[:rows].first["id"]
411
+ previous_option[:endkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_startkey)
412
+ previous_option[:descending] = !request_desc
413
+ previous_option[:count] = request_count
414
+ previous_option[:skip] = 1
415
+ previous_option[:expected_offset] = offset + previous_option[:count]
416
+ # [next_option] :
417
+ # CouchDB options
418
+ next_option[:startkey] = result[:rows].last["key"]
419
+ next_option[:startkey_docid] = result[:rows].last["id"]
420
+ next_option[:endkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_endkey)
421
+ next_option[:descending] = request_desc
422
+ next_option[:count] = request_count
423
+ next_option[:skip] = 1
424
+ next_option[:expected_offset] = total_count - offset
425
+ end
426
+ when "next"
427
+ if row_count < request_count
428
+ logger.debug "CouchResource : The count of fetched rows is shorter than that of requested"
429
+ # [previous_option]
430
+ if row_count == 0
431
+ logger.debug "CouchResource : reached over in the next operation"
432
+ previous_option[:startkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_endkey)
433
+ previous_option[:endkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_startkey)
434
+ previous_option[:descending] = !request_desc
435
+ previous_option[:count] = request_count
436
+ else
437
+ previous_option[:startkey] = result[:rows].first["key"]
438
+ previous_option[:startkey_doc_id] = result[:rows].first["id"]
439
+ previous_option[:endkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_startkey)
440
+ previous_option[:descending] = !request_desc
441
+ previous_option[:count] = request_count
442
+ previous_option[:skip] = 1
443
+ previous_option[:expected_offset] = total_count - offset
444
+ end
445
+ # [next_option] :
446
+ # just set paginate options (no more next docs)
447
+ next_option[:expected_offset] = -1
448
+ else
449
+ previous_option[:startkey] = result[:rows].first["key"]
450
+ previous_option[:startkey_doc_id] = result[:rows].first["id"]
451
+ previous_option[:endkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_startkey)
452
+ previous_option[:descending] = !request_desc
453
+ previous_option[:count] = request_count
454
+ previous_option[:skip] = 1
455
+ previous_option[:expected_offset] = total_count - offset
456
+ # [next_option] :
457
+ # CouchDB options
458
+ next_option[:startkey] = result[:rows].last["key"]
459
+ next_option[:startkey_docid] = result[:rows].last["id"]
460
+ next_option[:endkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_endkey)
461
+ next_option[:descending] = request_desc
462
+ next_option[:count] = request_count
463
+ next_option[:skip] = 1
464
+ # Paginate options
465
+ next_option[:expected_offset] = offset + next_option[:count]
466
+ end
467
+ else
468
+ raise ArgumentError.new("paginate_options :direction should be 'previous' or 'next'.")
469
+ end
470
+ # common Paginate options
471
+ previous_option[:direction] = "previous"
472
+ next_option[:direction] = "next"
473
+ # keep initial_state
474
+ previous_option[:initial_startkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_startkey)
475
+ previous_option[:initial_endkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_endkey)
476
+ next_option[:initial_startkey] = paginate_options[:initial_startkey] if paginate_options.has_key?(:initial_startkey)
477
+ next_option[:initial_endkey] = paginate_options[:initial_endkey] if paginate_options.has_key?(:initial_endkey)
478
+ else
479
+ #
480
+ # first request for pagination
481
+ #
482
+ logger.debug "CouchResource : first request for pagination"
483
+ previous_option[:direction] = "previous"
484
+ previous_option[:expected_offset] = -1
485
+ previous_option[:initial_startkey] = request_options[:startkey] if request_options.has_key?(:startkey)
486
+ previous_option[:initial_endkey] = request_options[:endkey] if request_options.has_key?(:endkey)
487
+
488
+ if row_count < request_count
489
+ logger.debug "CouchResource : The count of fetched rows is shorter than that of requested"
490
+ # [next_option] :
491
+ # just set paginate options
492
+ next_option[:direction] = "next"
493
+ next_option[:expected_offset] = -1
494
+ next_option[:initial_startkey] = request_options[:startkey] if request_options.has_key?(:startkey)
495
+ next_option[:initial_endkey] = request_options[:endkey] if request_options.has_key?(:endkey)
496
+ else
497
+ # [next_option] :
498
+ # CouchDB options
499
+ next_option[:startkey] = result[:rows].last["key"]
500
+ next_option[:startkey_docid] = result[:rows].last["id"]
501
+ next_option[:endkey] = request_options[:endkey] if request_options.has_key?(:endkey)
502
+ next_option[:count] = request_count
503
+ next_option[:skip] = 1
504
+ next_option[:descending] = request_desc
505
+ # Paginate options
506
+ next_option[:direction] = "next"
507
+ next_option[:expected_offset] = offset + next_option[:count]
508
+ next_option[:initial_startkey] = request_options[:startkey] if request_options.has_key?(:startkey)
509
+ next_option[:initial_endkey] = request_options[:endkey] if request_options.has_key?(:endkey)
510
+ end
511
+ end
512
+ [previous_option, next_option]
513
+ end
514
+ end
515
+ end
516
+
517
+ def find_from_ids(*args)
518
+ options = args.extract_options!
519
+ ids = args.flatten
520
+ query_option = {}
521
+ headers = {}
522
+ query_option[:rev] = options[:rev] if options[:rev]
523
+ if ids.length > 1 # two or more ids
524
+ path = all_docs_path((query_option || {}).update({:include_docs => true}))
525
+ post = { :keys => ids }
526
+ logger.debug "CouchResource::Connection#post #{path}"
527
+ docs = connection.post(path, post.to_json)["rows"]
528
+ logger.debug docs.to_json
529
+ docs.map { |doc|
530
+ obj = new(doc["doc"])
531
+ obj.send(:after_find) rescue NoMethodError
532
+ obj
533
+ }
534
+ else
535
+ # return one document.
536
+ path = document_path(ids, query_option)
537
+ logger.debug "CouchResource::Connection#get #{path}"
538
+ result = connection.get(path, headers)
539
+ logger.debug result.to_json
540
+ obj = new(result)
541
+ obj.send(:after_find) rescue NoMethodError
542
+ obj
543
+ end
544
+ end
545
+ end
546
+
547
+ def initialize(attributes = nil)
548
+ if attributes
549
+ (self.class.read_inheritable_attribute(:attribute_members) || {}).each do |name, option|
550
+ self.set_attribute(name, attributes[name.to_sym])
551
+ end
552
+ @id = attributes[:_id] ? attributes[:_id] : nil
553
+ @rev = attributes[:_rev] ? attributes[:_rev] : nil
554
+ end
555
+ # invoke explicit callback
556
+ self.after_initialize rescue NoMethodError
557
+ end
558
+
559
+ def new?
560
+ rev.nil?
561
+ end
562
+ alias :new_record? :new?
563
+
564
+ def id
565
+ @id
566
+ end
567
+ alias :_id :id
568
+
569
+ def id=(id)
570
+ @id=id
571
+ end
572
+ alias :_id= :id=
573
+
574
+ def rev
575
+ @rev
576
+ end
577
+ alias :_rev :rev
578
+
579
+ def revs(detailed = false)
580
+ new? ? [] : self.class.get_revs(self.id, detailed)
581
+ end
582
+
583
+ def save
584
+ result = create_or_update
585
+ end
586
+
587
+ def save!
588
+ save || raise(RecordNotSaved)
589
+ end
590
+
591
+ def update_attribute(name, value)
592
+ send("#{name.to_s}=", value)
593
+ save
594
+ end
595
+
596
+ def destroy
597
+ if self.new_record?
598
+ false
599
+ else
600
+ # [TODO] This does not work on couchdb 0.9.0 (returns 400 'Document rev and etag have different values.')
601
+ # connection.delete(document_path, {"If-Match" => rev })
602
+ connection.delete(document_path)
603
+ @rev = nil
604
+ true
605
+ end
606
+ end
607
+
608
+ def exists?
609
+ if rev
610
+ !new? && self.class.exists?(id, { :rev => rev })
611
+ else
612
+ !new? && self.class.exists?(id)
613
+ end
614
+ end
615
+
616
+ # Get the path of the object.
617
+ def document_path
618
+ if rev
619
+ self.class.document_path(id, { :rev => rev })
620
+ else
621
+ self.class.document_path(id)
622
+ end
623
+ end
624
+
625
+ # Get the json representation of the object.
626
+ def to_json
627
+ h = self.to_hash
628
+ h[:id] = self.id if self.id
629
+ h[:rev] = self.rev if self.rev
630
+ h.to_json
631
+ end
632
+
633
+ # Get the xml representation of the object, whose root is the class name.
634
+ def to_xml
635
+ h = self.to_hash
636
+ h[:id] = self.id if self.id
637
+ h[:rev] = self.rev if self.rev
638
+ h.to_xml(:root => self.class.to_s)
639
+ end
640
+
641
+ protected
642
+ def connection(reflesh = false)
643
+ self.class.connection(reflesh)
644
+ end
645
+
646
+ def create_or_update
647
+ new? ? create : update
648
+ end
649
+
650
+ def create
651
+ set_magic_attribute_values_on_create
652
+ result = if id
653
+ document = { "_id" => id }.update(to_hash)
654
+ logger.debug "CouchResource::Connection#put #{self.document_path}"
655
+ logger.debug document.to_json
656
+ connection.put(self.document_path, document.to_json)
657
+ else
658
+ document = to_hash
659
+ logger.debug "CouchResource::Connection#post #{self.class.database.path}"
660
+ logger.debug document.to_json
661
+ connection.post(self.class.database.path, document.to_json)
662
+ end
663
+ if result
664
+ set_id_and_rev(result[:id], result[:rev])
665
+ true
666
+ else
667
+ false
668
+ end
669
+ end
670
+
671
+ def update
672
+ set_magic_attribute_values_on_update
673
+ # [TODO] This does not work on couchdb 0.9.0 (returns 400 'Document rev and etag have different values.')
674
+ # document = { "_id" => self.id, "_rev" => self.rev }.update(to_hash)
675
+ document = { "_id" => self.id }.update(to_hash)
676
+ logger.debug "CouchResource::Connection#put #{self.document_path}"
677
+ logger.debug "If-Match: #{rev.to_json}"
678
+ logger.debug document.to_json
679
+ result = connection.put(self.document_path, document.to_json, {"If-Match" => rev.to_json})
680
+ if result
681
+ set_id_and_rev(result[:id], result[:rev])
682
+ true
683
+ else
684
+ false
685
+ end
686
+ end
687
+
688
+ def set_magic_attribute_values_on_create
689
+ self.created_at = Time.now if get_attribute_option(:created_at)
690
+ self.created_on = Time.today if get_attribute_option(:created_on)
691
+ self.updated_at = Time.now if get_attribute_option(:updated_at)
692
+ self.updated_on = Time.today if get_attribute_option(:updated_on)
693
+ end
694
+
695
+ def set_magic_attribute_values_on_update
696
+ self.updated_at = Time.now if get_attribute_option(:updated_at)
697
+ self.updated_on = Time.today if get_attribute_option(:updated_on)
698
+ end
699
+
700
+ def set_id_and_rev(id, rev)
701
+ @id = id
702
+ @rev = rev
703
+ end
704
+ end
705
+ end