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 +2 -0
- data/MIT_LICENSE +8 -0
- data/README.rdoc +7 -0
- data/lib/couch_resource.rb +25 -0
- data/lib/couch_resource/base.rb +705 -0
- data/lib/couch_resource/callbacks.rb +103 -0
- data/lib/couch_resource/connection.rb +194 -0
- data/lib/couch_resource/error.rb +3 -0
- data/lib/couch_resource/struct.rb +340 -0
- data/lib/couch_resource/validations.rb +520 -0
- data/lib/couch_resource/view.rb +228 -0
- data/test/test_base.rb +384 -0
- data/test/test_callbacks.rb +115 -0
- data/test/test_connection.rb +39 -0
- data/test/test_struct.rb +332 -0
- data/test/test_validations.rb +186 -0
- metadata +70 -0
data/LICENSE
ADDED
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,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
|