akdubya-cushion 0.5.1

Sign up to get free protection for your applications and to get access to all the features.
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