akdubya-cushion 0.5.1
Sign up to get free protection for your applications and to get access to all the features.
- data/LICENSE +176 -0
- data/README.rdoc +226 -0
- data/Rakefile +65 -0
- data/lib/cushion.rb +97 -0
- data/lib/cushion/core_ext.rb +27 -0
- data/lib/cushion/database.rb +310 -0
- data/lib/cushion/design.rb +33 -0
- data/lib/cushion/document.rb +117 -0
- data/lib/cushion/server.rb +208 -0
- data/spec/cushion_spec.rb +9 -0
- data/spec/database_spec.rb +223 -0
- data/spec/helpers.rb +2 -0
- data/spec/server_spec.rb +119 -0
- metadata +75 -0
data/lib/cushion.rb
ADDED
@@ -0,0 +1,97 @@
|
|
1
|
+
require 'json'
|
2
|
+
require 'restclient'
|
3
|
+
require 'uri'
|
4
|
+
|
5
|
+
dir = File.dirname(__FILE__) + '/cushion/'
|
6
|
+
|
7
|
+
require dir + 'core_ext'
|
8
|
+
require dir + 'server'
|
9
|
+
require dir + 'database'
|
10
|
+
require dir + 'document'
|
11
|
+
require dir + 'design'
|
12
|
+
|
13
|
+
DEFAULT_COUCH_HOST = "http://127.0.0.1:5984"
|
14
|
+
|
15
|
+
module Cushion
|
16
|
+
VERSION = '0.5.1'
|
17
|
+
|
18
|
+
class << self
|
19
|
+
|
20
|
+
# Returns a Cushion::Server instance.
|
21
|
+
def server(*args)
|
22
|
+
Server.new(*args)
|
23
|
+
end
|
24
|
+
alias_method :new, :server
|
25
|
+
|
26
|
+
# Returns a Cushion::Database instance rooted at +url+. Does not attempt
|
27
|
+
# to create the database on the server.
|
28
|
+
def db(url)
|
29
|
+
parsed = parse(url)
|
30
|
+
server = Cushion.new(parsed[:host])
|
31
|
+
server.db(parsed[:db])
|
32
|
+
end
|
33
|
+
alias_method :database, :db
|
34
|
+
|
35
|
+
# Creates a database at +url+ if it does not exist. Returns a Cushion::Database
|
36
|
+
# instance.
|
37
|
+
def db!(url)
|
38
|
+
parsed = parse(url)
|
39
|
+
server = Cushion.new(parsed[:host])
|
40
|
+
server.db!(parsed[:db])
|
41
|
+
end
|
42
|
+
alias_method :database!, :db!
|
43
|
+
|
44
|
+
# Drops and recreates a database at +url+, returning a Cushion::Database
|
45
|
+
# instance.
|
46
|
+
def recreate(url)
|
47
|
+
parsed = parse(url)
|
48
|
+
server = Cushion.new(parsed[:host])
|
49
|
+
server.recreate(parsed[:db])
|
50
|
+
end
|
51
|
+
|
52
|
+
# Utility method for converting parameters into url query strings.
|
53
|
+
def paramify_url(url, params = {})
|
54
|
+
if params && !params.empty?
|
55
|
+
query = params.collect do |k,v|
|
56
|
+
v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
|
57
|
+
"#{k}=#{CGI.escape(v.to_s)}"
|
58
|
+
end.join("&")
|
59
|
+
url = "#{url}?#{query}"
|
60
|
+
end
|
61
|
+
url
|
62
|
+
end
|
63
|
+
|
64
|
+
# Handles document id escaping.
|
65
|
+
def escape_docid(id)
|
66
|
+
/^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
|
67
|
+
end
|
68
|
+
|
69
|
+
# Base64 encodes inline attachments.
|
70
|
+
def encode_attachments(attachments)
|
71
|
+
attachments.each do |k,v|
|
72
|
+
next if v['stub']
|
73
|
+
v['data'] = base64(v['data'])
|
74
|
+
end
|
75
|
+
attachments
|
76
|
+
end
|
77
|
+
|
78
|
+
def base64(data)
|
79
|
+
Base64.encode64(data).gsub(/\s/,'')
|
80
|
+
end
|
81
|
+
|
82
|
+
# Sets the RestClient proxy.
|
83
|
+
def proxy(url)
|
84
|
+
RestClient.proxy = url
|
85
|
+
end
|
86
|
+
|
87
|
+
private
|
88
|
+
|
89
|
+
def parse(url)
|
90
|
+
parsed = URI.parse(url)
|
91
|
+
{
|
92
|
+
:host => "#{parsed.scheme}://#{parsed.host}:#{parsed.port}",
|
93
|
+
:db => parsed.path[1, parsed.path.length] # strip off the leading slash
|
94
|
+
}
|
95
|
+
end
|
96
|
+
end # class << self
|
97
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# This file must be loaded after the JSON gem and any other library that beats up the Time class.
|
2
|
+
class Time
|
3
|
+
# This date format sorts lexicographically
|
4
|
+
# and is compatible with Javascript's <tt>new Date(time_string)</tt> constructor.
|
5
|
+
# Note this this format stores all dates in UTC so that collation
|
6
|
+
# order is preserved. (There's no longer a need to set <tt>ENV['TZ'] = 'UTC'</tt>
|
7
|
+
# in your application.)
|
8
|
+
|
9
|
+
def to_json(options = nil)
|
10
|
+
u = self.utc
|
11
|
+
%("#{u.strftime("%Y/%m/%d %H:%M:%S +0000")}")
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
module RestClient
|
16
|
+
def self.copy(url, headers={})
|
17
|
+
Request.execute(:method => :copy,
|
18
|
+
:url => url,
|
19
|
+
:headers => headers)
|
20
|
+
end
|
21
|
+
|
22
|
+
def self.move(url, headers={})
|
23
|
+
Request.execute(:method => :move,
|
24
|
+
:url => url,
|
25
|
+
:headers => headers)
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,310 @@
|
|
1
|
+
require 'cgi'
|
2
|
+
require 'base64'
|
3
|
+
|
4
|
+
module Cushion
|
5
|
+
class Database
|
6
|
+
attr_reader :server, :name
|
7
|
+
|
8
|
+
def initialize(server, name)
|
9
|
+
raise ArgumentError, "server must be an a Cushion::Server" unless server.kind_of?(Cushion::Server)
|
10
|
+
# TODO: CouchDB has strict db naming requirements. Should validate.
|
11
|
+
@name = CGI.escape(name.to_s)
|
12
|
+
@server = server
|
13
|
+
end
|
14
|
+
|
15
|
+
# Retrieves information about this database.
|
16
|
+
def info(headers = {})
|
17
|
+
server.get(@name, headers)
|
18
|
+
end
|
19
|
+
|
20
|
+
# Compacts this database.
|
21
|
+
def compact(headers = {})
|
22
|
+
server.post("#{@name}/_compact", nil, headers)
|
23
|
+
end
|
24
|
+
|
25
|
+
# Creates this database on the server.
|
26
|
+
def create(headers = {})
|
27
|
+
server.put(@name, nil, headers)
|
28
|
+
end
|
29
|
+
|
30
|
+
# Deletes this database from the server.
|
31
|
+
def delete(headers = {})
|
32
|
+
server.delete(@name, headers)
|
33
|
+
end
|
34
|
+
|
35
|
+
# Query the default +all_docs+ view. Set the +keys+ option to perform a
|
36
|
+
# key-based multi-document fetch. Set the +headers+ option to pass
|
37
|
+
# custom request headers.
|
38
|
+
#
|
39
|
+
def all_docs(params = {})
|
40
|
+
keys = params.delete(:keys)
|
41
|
+
opts = params.delete(:headers) || {}
|
42
|
+
path = Cushion.paramify_url("#{@name}/_all_docs", params)
|
43
|
+
if keys
|
44
|
+
server.post(path, {:keys => keys}, opts)
|
45
|
+
else
|
46
|
+
server.get(path, opts)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Retrieves a single document by +id+. Set the +headers+ option to pass
|
51
|
+
# custom request headers.
|
52
|
+
def open_doc(id, params = {})
|
53
|
+
opts = params.delete(:headers) || {}
|
54
|
+
slug = Cushion.escape_docid(id)
|
55
|
+
path = Cushion.paramify_url("#{@name}/#{slug}", params)
|
56
|
+
server.get(path, opts)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Retrieves a single document by +id+ and returns a <tt>Cushion::Document</tt>
|
60
|
+
# linked to this database.
|
61
|
+
#
|
62
|
+
def doc(id, params = {})
|
63
|
+
result = open_doc(id, params)
|
64
|
+
ndoc = Cushion::Document.new(result)
|
65
|
+
ndoc.database = self
|
66
|
+
ndoc
|
67
|
+
end
|
68
|
+
alias_method :document, :doc
|
69
|
+
|
70
|
+
# Saves the params hash to the database as a document, encoding inline
|
71
|
+
# attachments and generating a UUID if no +_id+ is supplied. A +_rev+ attribute
|
72
|
+
# must be supplied to update an existing document. Set the +headers+ option
|
73
|
+
# to pass custom request headers.
|
74
|
+
#
|
75
|
+
def save_doc(params = {})
|
76
|
+
opts = params.delete(:headers) || {}
|
77
|
+
if params['_attachments']
|
78
|
+
params['_attachments'] = Cushion.encode_attachments(params['_attachments'])
|
79
|
+
end
|
80
|
+
if params['_id']
|
81
|
+
slug = Cushion.escape_docid(params['_id'])
|
82
|
+
server.put("#{@name}/#{slug}", params, opts)
|
83
|
+
else
|
84
|
+
slug = params['_id'] = server.next_uuid
|
85
|
+
server.put("#{@name}/#{slug}", params, opts)
|
86
|
+
end
|
87
|
+
end
|
88
|
+
|
89
|
+
# Deletes a single document by +id+ and +rev+.
|
90
|
+
def delete_doc(id, rev, headers = {})
|
91
|
+
slug = Cushion.escape_docid(id)
|
92
|
+
server.delete("#{@name}/#{slug}?rev=#{rev}", headers)
|
93
|
+
end
|
94
|
+
|
95
|
+
# Creates, updates and deletes multiple documents in this database according
|
96
|
+
# to the supplied +docs+ array. Set the +_deleted+ attribute to true to delete
|
97
|
+
# an individual doc. Set the +_rev+ attribute to update an individual doc. Leave
|
98
|
+
# out the +_rev+ attribute to create a new doc. Example:
|
99
|
+
#
|
100
|
+
# docs = [
|
101
|
+
# { "_id" => "0", "_rev" => "123456", "_deleted" => true }, #=> Delete this doc
|
102
|
+
# { "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update this doc
|
103
|
+
# { "_id" => "2", "baz" => "bat" } #=> Create this doc
|
104
|
+
# ]
|
105
|
+
#
|
106
|
+
# db.bulk_docs(docs)
|
107
|
+
#
|
108
|
+
# +bulk_docs+ returns a hash of updated doc ids and revs, as follows:
|
109
|
+
#
|
110
|
+
# {
|
111
|
+
# "ok" => true,
|
112
|
+
# "new_revs" => [
|
113
|
+
# { "id" => "0", "rev" => "3682408536" },
|
114
|
+
# { "id" => "1", "rev" => "3206753266" },
|
115
|
+
# { "id" => "2", "rev" => "426742535" }
|
116
|
+
# ]
|
117
|
+
# }
|
118
|
+
#
|
119
|
+
# Set the +use_uuids+ option to generate UUIDs from the server cache before
|
120
|
+
# saveing. Set the +headers+ option to pass custom request headers.
|
121
|
+
#
|
122
|
+
def bulk_docs(docs, params = {})
|
123
|
+
headers = params.delete(:headers) || {}
|
124
|
+
opts = { :use_uuids => true }.merge(params)
|
125
|
+
if (opts[:use_uuids])
|
126
|
+
ids, noids = docs.partition{|d|d['_id']}
|
127
|
+
uuid_count = [noids.length, server.uuid_batch_count].max
|
128
|
+
noids.each do |doc|
|
129
|
+
nextid = server.next_uuid(uuid_count) rescue nil
|
130
|
+
doc['_id'] = nextid if nextid
|
131
|
+
end
|
132
|
+
end
|
133
|
+
server.post("#{@name}/_bulk_docs", {:docs => docs}, headers)
|
134
|
+
end
|
135
|
+
|
136
|
+
# A convenience method for deleting multiple documents at once. Just pass in
|
137
|
+
# an array of saved +docs+ and the correct deletion params are
|
138
|
+
# automatically set. (See #bulk_docs)
|
139
|
+
#
|
140
|
+
def bulk_delete(docs, params = {})
|
141
|
+
deletions = docs.map { |doc| { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true } }
|
142
|
+
bulk_docs(deletions, params.merge(:use_uuids => false))
|
143
|
+
end
|
144
|
+
|
145
|
+
# Completely purges the supplied document revisions from the database.
|
146
|
+
# +doc_revs+ is a hash of document ids, each containing an array of revisions
|
147
|
+
# to be deleted. Example:
|
148
|
+
#
|
149
|
+
# doc_revs = {
|
150
|
+
# "1" => ["12345", "42836"],
|
151
|
+
# "2" => ["572654"],
|
152
|
+
# "3" => ["34462"]
|
153
|
+
# }
|
154
|
+
#
|
155
|
+
# db.purge(doc_revs)
|
156
|
+
#
|
157
|
+
def purge(doc_revs, headers = {})
|
158
|
+
server.post("#{@name}/_purge", doc_revs, headers)
|
159
|
+
end
|
160
|
+
|
161
|
+
# Copies the document identified by +source_id+ to a new or existing document
|
162
|
+
# at +destination_id+. Set the +dest_rev+ option to overwrite an existing doc.
|
163
|
+
# Set the +headers+ option to pass custom request headers.
|
164
|
+
#
|
165
|
+
def copy_doc(source_id, dest_id, params = {})
|
166
|
+
headers = params.delete(:headers) || {}
|
167
|
+
dest_rev = params[:dest_rev]
|
168
|
+
slug = Cushion.escape_docid(source_id)
|
169
|
+
destination = if dest_rev
|
170
|
+
"#{dest_id}?rev=#{dest_rev}"
|
171
|
+
else
|
172
|
+
dest_id
|
173
|
+
end
|
174
|
+
server.copy("#{@name}/#{slug}", destination, headers)
|
175
|
+
end
|
176
|
+
|
177
|
+
# Moves the document identified by +source_id+ and +src_rev+ to a new or
|
178
|
+
# existing document at +destination_id+. Set the +dest_rev+ option to
|
179
|
+
# overwrite an existing doc. Set the +headers+ option to pass custom
|
180
|
+
# request headers.
|
181
|
+
#
|
182
|
+
def move_doc(source_id, dest_id, src_rev, params = {})
|
183
|
+
headers = params.delete(:headers) || {}
|
184
|
+
dest_rev = params[:dest_rev]
|
185
|
+
slug = Cushion.escape_docid(source_id)
|
186
|
+
destination = if dest_rev
|
187
|
+
"#{dest_id}?rev=#{dest_rev}"
|
188
|
+
else
|
189
|
+
dest_id
|
190
|
+
end
|
191
|
+
server.move("#{@name}/#{slug}?rev=#{src_rev}", destination, headers)
|
192
|
+
end
|
193
|
+
|
194
|
+
# Supply +funcs+ to create a temporary view and run a query against it. Set
|
195
|
+
# the +keys+ option to perform a key-based multi-document fetch against the
|
196
|
+
# temp view. Example:
|
197
|
+
#
|
198
|
+
# temp_view = { :map => "function(doc){emit(doc.status,null)}" }
|
199
|
+
# db.temp_view(temp_view, :key => "verified")
|
200
|
+
#
|
201
|
+
# Set the +headers+ option to pass custom request headers.
|
202
|
+
#
|
203
|
+
# Temp view queries are slow, so they should only be used as a convenience
|
204
|
+
# during development.
|
205
|
+
#
|
206
|
+
def temp_view(funcs, params = {})
|
207
|
+
keys = params.delete(:keys)
|
208
|
+
headers = params.delete(:headers) || {}
|
209
|
+
funcs = funcs.merge({:keys => keys}) if keys
|
210
|
+
path = Cushion.paramify_url("#{@name}/_temp_view", params)
|
211
|
+
server.post(path, funcs, headers)
|
212
|
+
end
|
213
|
+
|
214
|
+
# Query a saved view identified by +view+. Set the +keys+ param to perform a
|
215
|
+
# key-based multi-document fetch. Example:
|
216
|
+
#
|
217
|
+
# db.view("people/by_first_name", :key => "gomer")
|
218
|
+
#
|
219
|
+
# Set the +headers+ option to pass custom request headers.
|
220
|
+
#
|
221
|
+
def view(view, params = {})
|
222
|
+
keys = params.delete(:keys)
|
223
|
+
headers = params.delete(:headers) || {}
|
224
|
+
path = Cushion.paramify_url("#{@name}/_view/#{view}", params)
|
225
|
+
if keys
|
226
|
+
server.post(path, {:keys => keys}, headers)
|
227
|
+
else
|
228
|
+
server.get(path, headers)
|
229
|
+
end
|
230
|
+
end
|
231
|
+
|
232
|
+
# Query the show template identified by +design+, +show_template+ and +id+.
|
233
|
+
# The results are not parsed by default. Set the +headers+ option to pass
|
234
|
+
# custom request headers.
|
235
|
+
#
|
236
|
+
def show(design, show_template, id, params = {})
|
237
|
+
defaults = { :accept => "text/html;text/plain;*/*" }
|
238
|
+
headers = params.delete(:headers) || {}
|
239
|
+
slug = Cushion.escape_docid(id)
|
240
|
+
path = Cushion.paramify_url("#{@name}/_show/#{design}/#{show_template}/#{slug}", params)
|
241
|
+
server.get(path, defaults.merge(headers))
|
242
|
+
end
|
243
|
+
|
244
|
+
# Query the list template identified by +design+, +list_template+ and +view+.
|
245
|
+
# The results are not parsed by default. Set the +headers+ option to pass
|
246
|
+
# custom request headers.
|
247
|
+
#
|
248
|
+
def list(design, list_template, view, params = {})
|
249
|
+
defaults = { :accept => "text/html;text/plain;*/*" }
|
250
|
+
headers = params.delete(:headers) || {}
|
251
|
+
path = Cushion.paramify_url("#{@name}/_list/#{design}/#{list_template}/#{view}", params)
|
252
|
+
server.get(path, defaults.merge(headers))
|
253
|
+
end
|
254
|
+
|
255
|
+
# Issues a request to the CouchDB external server identified by +process+.
|
256
|
+
# Calls to external must include a request +verb+. Set the +headers+ option
|
257
|
+
# to pass custom request headers.
|
258
|
+
#
|
259
|
+
def external(verb, process_path, params = {})
|
260
|
+
path = Cushion.paramify_url("_#{process_path}", params[:query])
|
261
|
+
server.request(verb, path, :body => params[:body], :headers => params[:headers])
|
262
|
+
end
|
263
|
+
|
264
|
+
# Uses open-uri to open the attachment identified by +id+ and +filename+.
|
265
|
+
# Returns a Tempfile.
|
266
|
+
#
|
267
|
+
def open_attachment(id, filename, params = {})
|
268
|
+
slug = Cushion.escape_docid(id)
|
269
|
+
fname = CGI.escape(filename)
|
270
|
+
path = Cushion.paramify_url("#{@name}/#{slug}/#{fname}", params)
|
271
|
+
server.open_attachment(path)
|
272
|
+
end
|
273
|
+
|
274
|
+
# Saves an attachment under the document identified by +id+ and +rev+ with
|
275
|
+
# the supplied +filename+ and +data+. Content type defaults to
|
276
|
+
# 'application/octet-stream'. Change it by passing <code>:content_type => "foo/bar"</code>
|
277
|
+
# as the last param.
|
278
|
+
#
|
279
|
+
def save_attachment(id, rev, filename, data, headers = {})
|
280
|
+
defaults = { :content_type => "application/octet-stream" }
|
281
|
+
slug = Cushion.escape_docid(id)
|
282
|
+
fname = CGI.escape(filename)
|
283
|
+
server.put("#{@name}/#{slug}/#{fname}?rev=#{rev}", data, defaults.merge(headers))
|
284
|
+
end
|
285
|
+
|
286
|
+
# Deletes an attachment under the document identified by +id+ and +rev+ with
|
287
|
+
# the supplied +filename+.
|
288
|
+
#
|
289
|
+
def delete_attachment(id, rev, filename, headers = {})
|
290
|
+
slug = Cushion.escape_docid(id)
|
291
|
+
fname = CGI.escape(filename)
|
292
|
+
server.delete("#{@name}/#{slug}/#{fname}?rev=#{rev}", headers)
|
293
|
+
end
|
294
|
+
|
295
|
+
# Renames an attachment under the document identified by +id+ and +rev+.
|
296
|
+
# +oldname+ is the name of the existing attachment and +newname+ is the
|
297
|
+
# desired name.
|
298
|
+
#
|
299
|
+
def rename_attachment(id, rev, oldname, newname)
|
300
|
+
temp = open_attachment(id, oldname)
|
301
|
+
res = save_attachment(id, rev, newname, temp.read, :content_type => temp.content_type)
|
302
|
+
if res["ok"] == true
|
303
|
+
res = delete_attachment(id, res["rev"], oldname)
|
304
|
+
else
|
305
|
+
res
|
306
|
+
end
|
307
|
+
end
|
308
|
+
|
309
|
+
end
|
310
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Cushion
|
2
|
+
class Design < Document
|
3
|
+
def view(view_name, query={})
|
4
|
+
view_name = view_name.to_s
|
5
|
+
view_slug = "#{name}/#{view_name}"
|
6
|
+
defaults = (self['views'][view_name] && self['views'][view_name]["couchrest-defaults"]) || {}
|
7
|
+
database.view(view_slug, defaults.merge(query))
|
8
|
+
end
|
9
|
+
|
10
|
+
def name
|
11
|
+
id.sub('_design/','') if id
|
12
|
+
end
|
13
|
+
|
14
|
+
def name=(newname)
|
15
|
+
self['_id'] = "_design/#{newname}"
|
16
|
+
end
|
17
|
+
|
18
|
+
def language
|
19
|
+
self['language']
|
20
|
+
end
|
21
|
+
|
22
|
+
# Sets this design document's language.
|
23
|
+
def language=(lang)
|
24
|
+
self['language'] = lang
|
25
|
+
end
|
26
|
+
|
27
|
+
def save
|
28
|
+
raise ArgumentError, "_design docs require a name" unless name && name.length > 0
|
29
|
+
self.language = "javascript" unless language
|
30
|
+
super
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|