akdubya-cushion 0.5.1

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