akdubya-cushion 0.5.2 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
data/README.rdoc CHANGED
@@ -1,226 +1,146 @@
1
1
  = Cushion: A slim CouchDB client
2
2
 
3
- Cushion is a minimalist Ruby client for accessing CouchDB servers. Originally
4
- extracted from CouchRest (http://github.com/jchrist/couchrest), Cushion strips
5
- out the extras while supporting more native CouchDB commands.
3
+ Cushion is a Ruby client for accessing CouchDB servers that's light on the
4
+ extras and heavy on the syntatical sugar.
5
+
6
+ Credit goes to CouchRest (http://github.com/jchris/couchrest/tree/master) for
7
+ the inspiration.
6
8
 
7
9
  == Getting Started
8
10
 
9
11
  # myapp.rb
10
12
  require 'cushion'
11
13
 
12
- db = Cushion.db!('http://127.0.0.1:5984/mydb')
13
- db.save_doc("_id" => "abc", "foo" => "bar")
14
- db.open_doc("abc")
15
-
16
- == Servers
17
-
18
- The server location defaults to http://127.0.0.1:5984, so you may omit the host
19
- when initializing a server if you are satisfied with the default.
20
-
21
- Cushion.server
22
- Cushion.server('http://myhost:6969')
23
-
24
- Display the server welcome message:
25
-
26
- server.info
27
-
28
- Get a list of all databases on the server:
14
+ db = Cushion!(mydb)
29
15
 
30
- server.all_dbs
16
+ == Document Basics
31
17
 
32
- Display any running tasks (such as replication):
18
+ Storing documents is simple. Just supply a hash of attributes. Leave out the
19
+ <tt>_id</tt> attribute if you want to use an auto-generated UUID. Set the
20
+ <tt>_rev</tt> attribute to update a document:
33
21
 
34
- server.active_tasks
22
+ db.store("baz" => "bat")
23
+ # => {"ok"=>true, "id"=> "bdd39a3f9a1e5894d3d1283aa0d0f53f", "rev"=> "2699240268"}
35
24
 
36
- Get the server configuration hash:
25
+ response = db.store("_id" => "mydoc", "foo" => "bar")
26
+ # => {"ok"=>true, "id"=>"mydoc", "rev"=>"882534292"}
37
27
 
38
- server.config
28
+ db.store("_id" => "mydoc", "foo" => "baz", "_rev" => response['rev'])
29
+ # => {"ok"=>true, "id"=>"mydoc", "rev"=>"3617508982"}
39
30
 
40
- Set a configuration option:
31
+ Fetching documents is just as simple.
41
32
 
42
- server.set_config("mysection", "myoption", "myvalue")
43
-
44
- Restart the server:
45
-
46
- server.restart
47
-
48
- Obtain server stats:
49
-
50
- server.stats
51
-
52
- Replicate from a source database to a target database:
53
-
54
- server.replicate("db1", "db2")
55
- server.replicate("db1", "http://host2:5984/bar")
33
+ db.key?("mydoc") # => true
34
+ db.fetch("mydoc") # => {"_id"=>"mydoc", "_rev"=>"3617508982", "foo"=>"baz"}
35
+ db.fetch("bogusid") # => raises RestClient::ResourceNotFound
56
36
 
57
- Return a <tt>Cushion::Database</tt> without first creating it:
37
+ # Use etags to perform a conditional GET
38
+ db.fetch("mydoc", :etag => cur_rev) # => raises RestClient::NotModified
58
39
 
59
- server.db("mydb")
60
- server[:mydb]
40
+ Store attachments inline or via CouchDB's standalone attachment API. The standalone
41
+ method accepts both IO objects and strings:
61
42
 
62
- Create a database and return a <tt>Cushion::Database</tt>:
63
-
64
- server.db!("mydb")
65
-
66
- == Databases
67
-
68
- Create a database directly from a url, or just return the database instance if
69
- it already exists:
70
-
71
- Cushion.db!('http://127.0.0.1:5984/mydb')
72
-
73
- Compact a database, eliminating old document revisions:
74
-
75
- db.compact
76
-
77
- Create and delete a database from an instance of <tt>Cushion::Database</tt>
78
-
79
- db.create
80
- db.delete
81
-
82
- Open a document:
83
-
84
- db.open_doc("mydoc")
43
+ # Inline
44
+ db.store("_id" => "hasfiles", ..., "_attachments" => {
45
+ "foo.txt" => {
46
+ "content_type" => "text/plain",
47
+ "data" => "Hello World!"
48
+ }})
85
49
 
86
- Fetch all documents from a database:
50
+ # Standalone
51
+ res = @db.store("_id" => "savemefirst", ...)
52
+ db.attach("savemefirst", "foo.txt", "Hello World!", :rev => res['rev'],
53
+ :content_type => "text/plain")
54
+ # => Foo now contains an attachment hash similar to the above example
87
55
 
88
- db.all_docs
56
+ # Fetch attachment data
57
+ db.fetch("savemefirst", "foo.txt") # => "Hello World!"
89
58
 
90
- Save a document. The +_id+ may be omitted on document creation:
59
+ Cushion returns parsed JSON by default. In those cases where you need the raw
60
+ JSON string, simply set an accept header:
91
61
 
92
- db.save_doc("_id" => "abc", .. attributes ..)
93
- db.save_doc(.. attributes ..)
94
- ab.save_doc("_id" => "exists", "_rev" => "1234", .. attributes ..)
62
+ db.fetch("mydoc", :headers => { :accept => "text/plain" })
63
+ # => "{\"_id\":\"mydoc\",\"_rev\":\"3617508982\",\"foo\":\"baz\"}\n"
95
64
 
96
- Delete a document. Must include both an +id+ and a +rev+:
65
+ The technique above works for nearly every <tt>Cushion</tt> request method.
97
66
 
98
- db.delete_doc("mydoc", "1234")
67
+ == Document Macros
99
68
 
100
- Perform a bulk save/update/delete operation:
69
+ You can use CouchDB's bulk docs feature to create, update and delete many
70
+ documents at once:
101
71
 
102
72
  docs = [
103
- { "_id" => "0", "_rev" => "123456", "_deleted" => true }, #=> Delete this doc
104
- { "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update this doc
105
- { "_id" => "2", "baz" => "bat" } #=> Create this doc
73
+ { "_id" => "0", "_rev" => "123456", "_deleted" => true }, #=> Delete
74
+ { "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update
75
+ { "_id" => "2", "baz" => "bat" } #=> Create
106
76
  ]
107
77
 
108
- db.bulk_docs(docs)
78
+ db.bulk(docs) # => returns a hash of updated documents
79
+ db.bulk(docs, :delete => true) # => deletes the selected docs
109
80
 
110
- Perform a bulk delete operation. This is a convenience method:
81
+ Documents and attachments may also be copied or moved within the same database.
82
+ Some useful examples:
111
83
 
112
- db.bulk_delete(some_docs)
84
+ # Documents
85
+ db.copy("mydoc", "new_doc")
86
+ # => {"ok"=>true, "id"=>"new_doc", "rev"=> ...}
87
+ db.move("mydoc", "existing_doc", :rev => ..., :dest_rev => ...)
88
+ # => overwrites the existing doc
113
89
 
114
- Purge document revisions from the database, effectively erasing history:
90
+ # Attachments
91
+ db.copy_attachment("mydoc", "foo.txt", "bar.txt")
92
+ # => copies an attachment within the same doc
93
+ db.move_attachment("mydoc", "foo.txt", "bar.txt")
94
+ # => renames an attachment; aliased at #rename_attachment
115
95
 
116
- doc_revs = {
117
- "1" => ["12345", "42836"],
118
- "2" => ["572654"],
119
- "3" => ["34462"]
120
- }
121
-
122
- db.purge(doc_revs)
123
-
124
- Copy or move a document to a new +id+. Specify a +dest_rev+ to overwrite an existing
125
- document:
126
-
127
- db.copy_doc("doc1", "doc2")
128
- db.copy_doc("doc1", "doc2", :dest_rev => "36452")
129
- db.move_doc("doc1", "1234", "doc2")
130
- db.move_doc("doc1", "1234", "doc2", :dest_rev => "37462")
131
-
132
- CouchDB can attach multiple files directly to documents, either as inline Base64
133
- encoded data or via a standalone attachment API.
134
-
135
- Open a document attachment. This will return an <tt>IO</tt> object:
136
-
137
- temp = db.open_attachment("mydoc", "foo.txt")
138
- temp.read
139
-
140
- Save an inline attachment to a document:
141
-
142
- db.save_doc("_id" => "mydoc", "_attachments" => {
143
- "foo.txt" => {
144
- "content_type" => "text/plain",
145
- "data" => "Hello World!"
146
- }
147
- })
148
-
149
- Save a standalone attachment to a saved document. Content type defaults to
150
- "application/octet-stream":
151
-
152
- db.save_attachment("mydoc", "1234", "bar.txt", "somedata", :content_type => "text/plain")
153
-
154
- Delete a document attachment:
155
-
156
- db.delete_attachment("mydoc", "1234", "foo.txt")
157
-
158
- Rename an attachment:
159
-
160
- db.rename_attachment("mydoc", "1234", "baz.txt", "bat.txt")
96
+ See the database specs for more copy and move examples.
161
97
 
162
98
  == Views
163
99
 
164
- Create a temporary view and run a query against it:
100
+ - creating (design docs)
101
+ - querying
102
+ - temp views
165
103
 
166
- temp_view = { :map => "function(doc){emit(doc.status,null)}" }
167
- db.temp_view(temp_view, :key => "verified")
104
+ == Servers and Databases
168
105
 
169
- Run a query against a saved view:
170
-
171
- db.view("people/by_first_name", :key => "gomer")
172
-
173
- Show and list functions:
174
-
175
- db.show("examples", "people", "mydoc", :format => "xml")
176
- db.list("examples", "browse-people", "people-by-name", :startkey => ["a"], :limit => 10)
106
+ The server location defaults to http://127.0.0.1:5984, so you may omit the host
107
+ when initializing a server if you are satisfied with the default.
177
108
 
178
- See http://wiki.apache.org/couchdb/Formatting_with_Show_and_List
109
+ NOTE: Some cruft needs cleaning up. Use <tt>Cushion!(dbname, opts)</tt> for
110
+ now.
179
111
 
180
- == External Processes
112
+ Display various bits of server metadata as follows:
181
113
 
182
- Issue a request to a CouchDB external process, such as a full text indexing
183
- engine:
114
+ server.info #=> welcome message
115
+ server.all_dbs #=> all dbs available
116
+ server.active_tasks #=> running tasks (e.g., replication or compaction)
117
+ server.config #=> server config
118
+ server.restart #=> restart the server
119
+ server.stats #=> detailed runtime stats
184
120
 
185
- db.external(:get, 'search/foo', :query => { 'foo' => 'bar' })
121
+ <tt>Cushion::Server</tt> can also replicate local and remote databases:
186
122
 
187
- == Content Negotiation
123
+ server.replicate("db1", "db2")
124
+ server.replicate("db1", "http://host2:5984/bar")
188
125
 
189
- All commands except for +show+ and +list+ return parsed JSON by default, but in some
190
- cases it is useful to return the raw response from CouchDB. Simply set the correct
191
- HTTP headers to request an alternate format:
126
+ Creating database instances:
192
127
 
193
- db.all_docs :headers => { :accept => "text/plain" }
128
+ - top level methods
129
+ - options
194
130
 
195
- Some commands accept a headers hash as the last parameter, allowing you to omit
196
- the explicit headers key like so:
131
+ DB Operations:
197
132
 
198
- db.info :accept => "text/plain"
133
+ db.create
134
+ db.delete
135
+ db.compact
199
136
 
200
137
  == Cushion::Document
201
138
 
202
- <tt>Cushion::Document</tt> provides a light wrapper for working directly with
203
- individual documents.
204
-
205
- Create a document:
206
-
207
- Cushion::Document.use_database(db)
208
- doc = Cushion::Document.new("foo" => "bar")
209
- doc.save
210
-
211
- Open a document and return an instance of <tt>Cushion::Document</tt>:
212
-
213
- db.doc("mydoc")
214
- doc["_foo"] = "bar"
215
- doc.save
216
-
217
- Copy a document:
218
-
219
- doc.copy("mydoc2")
139
+ == Tricks
220
140
 
221
- Work with document attachments:
141
+ Use the #headers method to obtain response headers on any request:
222
142
 
223
- doc.open_attachment("foo.txt")
224
- doc.save_attachment("bar.txt", "somedata", :content_type => "text/plain")
225
- doc.delete_attachment("bar.txt")
226
- doc.rename_attachment("foo.txt", "newfoo.txt")
143
+ db.fetch("mydoc").headers
144
+ # => {:server=>"CouchDB/0.9.0a (Erlang OTP/R12B)", :etag=>"\"3617508982\"",
145
+ :date=>"Fri, 06 Mar 2009 10:21:59 GMT", :content_type=>"application/json",
146
+ :content_length=>"48", :cache_control=>"must-revalidate"}
data/lib/cushion.rb CHANGED
@@ -12,14 +12,22 @@ require dir + 'design'
12
12
 
13
13
  DEFAULT_COUCH_HOST = "http://127.0.0.1:5984"
14
14
 
15
+ def Cushion(dbname, opts = {})
16
+ Cushion.server(opts).db(dbname)
17
+ end
18
+
19
+ def Cushion!(dbname, opts = {})
20
+ Cushion.server(opts).db!(dbname)
21
+ end
22
+
15
23
  module Cushion
16
- VERSION = '0.5.2'
24
+ VERSION = '0.6.0'
17
25
 
18
26
  class << self
19
27
 
20
28
  # Returns a Cushion::Server instance.
21
- def server(*args)
22
- Server.new(*args)
29
+ def server(options = {})
30
+ Server.new(options)
23
31
  end
24
32
  alias_method :new, :server
25
33
 
@@ -27,7 +35,7 @@ module Cushion
27
35
  # to create the database on the server.
28
36
  def db(url)
29
37
  parsed = parse(url)
30
- server = Cushion.new(parsed[:host])
38
+ server = Cushion.new(:uri => parsed[:host])
31
39
  server.db(parsed[:db])
32
40
  end
33
41
  alias_method :database, :db
@@ -36,7 +44,7 @@ module Cushion
36
44
  # instance.
37
45
  def db!(url)
38
46
  parsed = parse(url)
39
- server = Cushion.new(parsed[:host])
47
+ server = Cushion.new(:uri => parsed[:host])
40
48
  server.db!(parsed[:db])
41
49
  end
42
50
  alias_method :database!, :db!
@@ -45,7 +53,7 @@ module Cushion
45
53
  # instance.
46
54
  def recreate(url)
47
55
  parsed = parse(url)
48
- server = Cushion.new(parsed[:host])
56
+ server = Cushion.new(:uri => parsed[:host])
49
57
  server.recreate(parsed[:db])
50
58
  end
51
59
 
@@ -61,9 +69,13 @@ module Cushion
61
69
  url
62
70
  end
63
71
 
64
- # Handles document id escaping.
65
- def escape_docid(id)
66
- /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
72
+ # Handles key escaping.
73
+ def escape_key(id, filename = nil)
74
+ if filename
75
+ "#{escape_docid(id)}/#{CGI.escape(filename)}"
76
+ else
77
+ escape_docid(id)
78
+ end
67
79
  end
68
80
 
69
81
  # Base64 encodes inline attachments.
@@ -78,7 +90,7 @@ module Cushion
78
90
  def base64(data)
79
91
  Base64.encode64(data).gsub(/\s/,'')
80
92
  end
81
-
93
+
82
94
  # Sets the RestClient proxy.
83
95
  def proxy(url)
84
96
  RestClient.proxy = url
@@ -86,6 +98,10 @@ module Cushion
86
98
 
87
99
  private
88
100
 
101
+ def escape_docid(id)
102
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
103
+ end
104
+
89
105
  def parse(url)
90
106
  parsed = URI.parse(url)
91
107
  {
@@ -94,4 +110,32 @@ module Cushion
94
110
  }
95
111
  end
96
112
  end # class << self
113
+
114
+ # Provides a clean method of adding metadata (e.g., headers) to JSON parsed
115
+ # responses. Inspired by <tt>OpenURI::Meta</tt>.
116
+ module Meta
117
+ def Meta.init(obj, src=nil)
118
+ obj.extend Meta
119
+ obj.instance_eval {
120
+ @headers = {}
121
+ }
122
+ if src
123
+ src.headers.each {|name, value|
124
+ obj.add_header_field(name, value)
125
+ }
126
+ end
127
+ obj
128
+ end
129
+
130
+ attr_accessor :headers
131
+
132
+ def add_header_field(name, value)
133
+ #value.gsub!('"', '') if name == :etag ==> CouchDB seems to require the xtra "'s
134
+ @headers[name] = value
135
+ end
136
+
137
+ def etag
138
+ @headers[:etag]
139
+ end
140
+ end
97
141
  end
@@ -4,92 +4,157 @@ require 'base64'
4
4
  module Cushion
5
5
  class Database
6
6
  attr_reader :server, :name
7
+ attr_accessor :use_tempfiles
7
8
 
8
- def initialize(server, name)
9
- raise ArgumentError, "server must be an a Cushion::Server" unless server.kind_of?(Cushion::Server)
9
+ # Initializes a Cushion::Database instance.
10
+ def initialize(name, options = {})
11
+ unless options[:server]
12
+ raise ArgumentError, "server option must be provided"
13
+ end
14
+ unless name
15
+ raise ArgumentError, "name must be provided"
16
+ end
10
17
  # TODO: CouchDB has strict db naming requirements. Should validate.
11
18
  @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
- post("_compact", nil, headers)
23
- end
24
-
25
- # Creates this database on the server.
26
- def create(headers = {})
27
- server.put(@name, nil, headers)
19
+ @server = options[:server]
20
+ @use_tempfiles = options[:use_tempfiles]
28
21
  end
29
22
 
30
- # Deletes this database from the server.
31
- def drop(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.
23
+ # Retrieves a single document or attachment by key. Returns attachments as
24
+ # an <tt>OpenURI</tt> <tt>IO</tt> object if the +use_tempfiles+ database
25
+ # option has been set. Set the +headers+ option to pass custom request headers.
26
+ # Examples:
38
27
  #
39
- def all_docs(params = {})
40
- keys = params.delete(:keys)
41
- opts = params.delete(:headers) || {}
42
- path = Cushion.paramify_url("_all_docs", params)
43
- if keys
44
- post(path, {:keys => keys}, opts)
45
- else
46
- get(path, opts)
28
+ # db.fetch("mydoc")
29
+ # db.fetch("my/doc") # Forward slashes are automatically escaped...
30
+ # db.fetch("_design/foo") # ...except in the case of design docs
31
+ #
32
+ # Attachments can be fetched by supplying the id and filename as follows:
33
+ #
34
+ # db.fetch("mydoc", "foo.txt")
35
+ # db.fetch("mydoc", "path/to/foo.txt") # Virtual file path
36
+ #
37
+ # Cushion requests "application/json" by default, and response bodies will
38
+ # automatically be parsed as such. To request data from CouchDB in another
39
+ # (unparsed) format, simply set the +accept+ header:
40
+ #
41
+ # db.fetch("mydoc", :headers => { :accept => "text/plain" })
42
+ #
43
+ # Set <code>:head => true</code> to return the headers rather than the
44
+ # content body. Set <code>:etag => some_value</code> to perform a
45
+ # simple if-none-match conditional GET.
46
+ #
47
+ def fetch(*args)
48
+ options = args.last.is_a?(Hash) ? args.pop : {}
49
+ id, file = args
50
+ slug = Cushion.escape_key(id, file)
51
+ headers = options.delete(:headers) || {}
52
+ if etag = options.delete(:etag)
53
+ headers.merge!(:if_none_match => "\"#{etag}\"")
47
54
  end
55
+ head = options.delete(:head)
56
+ path = Cushion.paramify_url("#{slug}", options)
57
+ if head
58
+ return head(path, headers)
59
+ elsif file && @use_tempfiles
60
+ return server.open_attachment("#{@name}/#{path}")
61
+ end
62
+ get(path, headers)
48
63
  end
49
64
 
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("#{slug}", params)
56
- get(path, opts)
65
+ # Returns true if a document key exists in this database. See #fetch.
66
+ def key?(*args)
67
+ options = args.last.is_a?(Hash) ? args.pop : {}
68
+ id, file = args
69
+ !!fetch(id, file, options.merge(:head => true))
70
+ rescue RestClient::ResourceNotFound
71
+ false
57
72
  end
73
+ alias_method :has_key?, :key?
58
74
 
59
75
  # Retrieves a single document by +id+ and returns a <tt>Cushion::Document</tt>
60
76
  # linked to this database.
61
77
  #
62
- def doc(id, params = {})
63
- result = open_doc(id, params)
64
- ndoc = Cushion::Document.new(result)
78
+ def doc(id, options = {})
79
+ result = fetch(id.to_s, options)
80
+
81
+ ndoc = if /^_design/ =~ result["_id"]
82
+ Design.new(result)
83
+ else
84
+ Document.new(result)
85
+ end
86
+
65
87
  ndoc.database = self
66
88
  ndoc
67
89
  end
68
90
  alias_method :document, :doc
69
91
 
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.
92
+ # Stores a document to the database. Pass a hash with the desired attributes.
93
+ # If an +_id+ attribute is not provided one will be generated automatically
94
+ # from the server UUID cache. Examples:
95
+ #
96
+ # db.store("foo" => "bar") #=> Creates a doc with an auto-generated key
97
+ # db.store("_id" => "def") #=> Creates a doc with key "def"
98
+ #
99
+ # To update a document, set the +_rev+ attribute on the document hash:
74
100
  #
75
- def save_doc(params = {})
76
- opts = params.delete(:headers) || {}
77
- if params['_attachments']
78
- params['_attachments'] = Cushion.encode_attachments(params['_attachments'])
101
+ # db.store("_id" => "ghi", "_rev" => "1234")
102
+ #
103
+ # Inline attachments are automatically encoded within the document.
104
+ #
105
+ def store(doc, options = {})
106
+ headers = options.delete(:headers) || {}
107
+ if doc['_attachments']
108
+ doc['_attachments'] = Cushion.encode_attachments(doc['_attachments'])
79
109
  end
80
- if params['_id']
81
- slug = Cushion.escape_docid(params['_id'])
82
- put("#{slug}", params, opts)
110
+ res = if doc['_id']
111
+ slug = Cushion.escape_key(doc['_id'])
112
+ put("#{slug}", doc, headers)
83
113
  else
84
- slug = params['_id'] = server.next_uuid
85
- put("#{slug}", params, opts)
114
+ slug = doc['_id'] = server.next_uuid
115
+ put("#{slug}", doc, headers)
86
116
  end
117
+ if res['ok']
118
+ doc['_id'] = res['id']
119
+ doc['_rev'] = res['rev']
120
+ end
121
+ res
87
122
  end
88
123
 
89
- # Deletes a single document by +id+ and +rev+.
90
- def delete_doc(id, rev, headers = {})
91
- slug = Cushion.escape_docid(id)
92
- delete("#{slug}?rev=#{rev}", headers)
124
+ # Save a file attachment under an existing document, or create a new document
125
+ # containing an attachment. +id+ is the document id. +filename+ is the name
126
+ # of the attachment (extensions may be omitted). Set the +rev+ option to store
127
+ # the attachment under an existing document. Example:
128
+ #
129
+ # db.attach("docid", "foo.txt", somedata, :rev => "1234")
130
+ # #=> Stores an attachment under docid
131
+ #
132
+ # db.attach("anotherid", "bar.jpg", :content_type => "image/jpeg")
133
+ # #=> Creates a new document containing the attachment. Also sets the
134
+ # content type.
135
+ #
136
+ # +data+ may be a <tt>String</tt>, or any object that responds to read.
137
+ #
138
+ # Content type defaults to 'application/octet-stream'.
139
+ #
140
+ def attach(id, filename, data, options = {})
141
+ headers = options.delete(:headers) || {}
142
+ headers[:content_type] = options.delete(:content_type) || "application/octet-stream"
143
+ rev = options.delete(:rev)
144
+ slug = Cushion.escape_key(id, filename)
145
+ rev_slug = "?rev=#{rev}" if rev
146
+ data = data.respond_to?(:read) ? data.read : data
147
+ put("#{slug}#{rev_slug}", data, headers)
148
+ end
149
+
150
+ # Deletes a single document or attachment by key. The +rev+ option must be
151
+ # provided.
152
+ def destroy(*args)
153
+ options = args.last.is_a?(Hash) ? args.pop : {}
154
+ headers = options.delete(:headers) || {}
155
+ id, file = args
156
+ slug = Cushion.escape_key(id, file)
157
+ delete("#{slug}?rev=#{options[:rev]}", headers)
93
158
  end
94
159
 
95
160
  # Creates, updates and deletes multiple documents in this database according
@@ -102,10 +167,10 @@ module Cushion
102
167
  # { "_id" => "1", "_rev" => "32486671", "foo" => "bar" }, #=> Update this doc
103
168
  # { "_id" => "2", "baz" => "bat" } #=> Create this doc
104
169
  # ]
105
- #
106
- # db.bulk_docs(docs)
107
170
  #
108
- # +bulk_docs+ returns a hash of updated doc ids and revs, as follows:
171
+ # db.bulk(docs)
172
+ #
173
+ # <tt>bulk</tt> returns a hash of updated doc ids and revs, as follows:
109
174
  #
110
175
  # {
111
176
  # "ok" => true,
@@ -117,12 +182,19 @@ module Cushion
117
182
  # }
118
183
  #
119
184
  # Set the +use_uuids+ option to generate UUIDs from the server cache before
120
- # saveing. Set the +headers+ option to pass custom request headers.
185
+ # saving. Set the +headers+ option to pass custom request headers.
121
186
  #
122
- def bulk_docs(docs, params = {})
123
- headers = params.delete(:headers) || {}
124
- opts = { :use_uuids => true }.merge(params)
125
- if (opts[:use_uuids])
187
+ def bulk(docs, options = {})
188
+ delete_all = options.delete(:delete)
189
+ headers = options.delete(:headers) || {}
190
+ use_uuids = options.delete(:use_uuids) || true
191
+ if delete_all
192
+ updates = docs.map { |doc|
193
+ { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true } }
194
+ else
195
+ updates = docs
196
+ end
197
+ if (use_uuids)
126
198
  ids, noids = docs.partition{|d|d['_id']}
127
199
  uuid_count = [noids.length, server.uuid_batch_count].max
128
200
  noids.each do |doc|
@@ -130,114 +202,144 @@ module Cushion
130
202
  doc['_id'] = nextid if nextid
131
203
  end
132
204
  end
133
- post("_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))
205
+ post("_bulk_docs", {:docs => updates}, headers)
143
206
  end
144
207
 
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
- # }
208
+ # Copies the document at +source+ to a new or existing location at +destination+.
209
+ # Set the +dest_rev+ option to overwrite an existing document. Set the
210
+ # +headers+ option to pass custom request headers. Example:
154
211
  #
155
- # db.purge(doc_revs)
212
+ # db.copy("doc1", "doc2")
213
+ # #=> Copies to a new location
214
+ # db.copy("doc1", "doc3", :dest_rev => "4567")
215
+ # #=> Overwrites an existing doc
156
216
  #
157
- def purge(doc_revs, headers = {})
158
- post("_purge", doc_revs, headers)
217
+ def copy(source, destination, options = {})
218
+ headers = options.delete(:headers) || {}
219
+ dest_rev = options.delete(:dest_rev)
220
+ slug = Cushion.escape_key(source)
221
+ dest = if dest_rev
222
+ "#{destination}?rev=#{dest_rev}"
223
+ else
224
+ destination
225
+ end
226
+ server.copy("#{@name}/#{slug}", dest, headers)
227
+ end
228
+
229
+ # Copies an attachment to a new location. This is presently experimental.
230
+ def copy_attachment(id, old_filename, new_filename, options = {})
231
+ headers = options.delete(:headers) || {}
232
+ src = fetch(id, old_filename)
233
+ headers[:content_type] = src.headers[:content_type]
234
+ dest_id = options[:dest_id] || id
235
+ opts = options[:dest_rev] ? { :rev => options[:dest_rev] } : {}
236
+ if opts.empty? && (id == dest_id)
237
+ opts = { :rev => src.headers[:etag] }
238
+ end
239
+ res = attach(dest_id, new_filename, src, { :headers => headers }.merge(opts))
240
+ if id == dest_id
241
+ res.merge("src_rev" => res["rev"])
242
+ else
243
+ res.merge("src_rev" => src.headers[:etag])
244
+ end
159
245
  end
160
246
 
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.
247
+ # Moves the document at +source+ to a new or existing location at +destination+.
248
+ # Set the +dest_rev+ option to overwrite an existing document. Set the
249
+ # +headers+ option to pass custom request headers. Example:
164
250
  #
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}"
251
+ # db.move("doc1", "doc2", :rev => "1234")
252
+ # #=> Moves to a new location
253
+ # db.copy("doc1", "doc3", :rev => "1234", :dest_rev => "4567")
254
+ # #=> Overwrites an existing doc
255
+ #
256
+ def move(source, destination, options = {})
257
+ headers = options.delete(:headers) || {}
258
+ dest_rev = options.delete(:dest_rev)
259
+ slug = Cushion.escape_key(source)
260
+ path = Cushion.paramify_url("#{slug}", options)
261
+ dest = if dest_rev
262
+ "#{destination}?rev=#{dest_rev}"
171
263
  else
172
- dest_id
264
+ destination
173
265
  end
174
- copy("#{slug}", destination, headers)
266
+ server.move("#{@name}/#{path}", dest, headers)
175
267
  end
176
268
 
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}"
269
+ # Moves an attachment to a new location. This is presently experimental.
270
+ def move_attachment(id, old_filename, new_filename, options = {})
271
+ res = copy_attachment(id, old_filename, new_filename, options)
272
+ if res["ok"]
273
+ destroy(id, old_filename, :rev => res["src_rev"])
188
274
  else
189
- dest_id
275
+ res
190
276
  end
191
- move("#{slug}?rev=#{src_rev}", destination, headers)
192
277
  end
278
+ alias_method :rename_attachment, :move_attachment
193
279
 
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:
280
+ # Query a named view. Set +name+ to :all to query CouchDB's all_docs view.
281
+ # Set +name+ to :temp to query a temp_view. Set the +keys+ option to perform
282
+ # a key-based multi-document fetch against any view. Examples
197
283
  #
198
284
  # temp_view = { :map => "function(doc){emit(doc.status,null)}" }
199
- # db.temp_view(temp_view, :key => "verified")
285
+ # db.view(:temp, :funcs => temp_view, :key => "verified")
286
+ # db.view("people/by_first_name", :key => "gomer")
287
+ # db.view("foo/bar", :keys => ["a", "b", "c"])
288
+ # db.view(:all)
200
289
  #
201
- # Set the +headers+ option to pass custom request headers.
290
+ # Set the +headers+ option to pass custom request headers. Set
291
+ # <code>:etag => some_value</code> to perform a simple if-none-match
292
+ # conditional GET.
202
293
  #
203
294
  # 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("_temp_view", params)
211
- 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:
295
+ # during development. Whenever possible you should query against saved
296
+ # views.
216
297
  #
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("_view/#{view}", params)
225
- if keys
226
- post(path, {:keys => keys}, headers)
298
+ def view(name, options = {})
299
+ keys = options.delete(:keys)
300
+ headers = options.delete(:headers) || {}
301
+ if etag = options.delete(:etag)
302
+ headers.merge!(:if_none_match => "\"#{etag}\"".squeeze("\""))
303
+ end
304
+ body = keys ? {:keys => keys} : {}
305
+ if name == :all
306
+ slug = "_all_docs"
307
+ elsif name == :temp
308
+ slug = "_temp_view"
309
+ body.merge!(options.delete(:funcs))
310
+ else
311
+ slug = "_view/#{name}"
312
+ end
313
+ path = Cushion.paramify_url(slug, options)
314
+
315
+ if body.any?
316
+ post(path, body, headers)
227
317
  else
228
318
  get(path, headers)
229
319
  end
230
320
  end
231
321
 
322
+ # Convenience method for querying the all_docs view. See #view for more
323
+ # information.
324
+ def all(options = {})
325
+ view(:all, options)
326
+ end
327
+
328
+ # Convenience method for querying temporary views. See #view for more
329
+ # information.
330
+ def temp(funcs, options = {})
331
+ view(:temp, options.merge(:funcs => funcs))
332
+ end
333
+
232
334
  # Query the show template identified by +design+, +show_template+ and +id+.
233
335
  # The results are not parsed by default. Set the +headers+ option to pass
234
336
  # custom request headers.
235
337
  #
236
- def show(design, show_template, id, params = {})
338
+ def show(design, show_template, id, options = {})
237
339
  defaults = { :accept => "text/html;text/plain;*/*" }
238
- headers = params.delete(:headers) || {}
239
- slug = Cushion.escape_docid(id)
240
- path = Cushion.paramify_url("_show/#{design}/#{show_template}/#{slug}", params)
340
+ headers = options.delete(:headers) || {}
341
+ slug = Cushion.escape_key(id)
342
+ path = Cushion.paramify_url("_show/#{design}/#{show_template}/#{slug}", options)
241
343
  get(path, defaults.merge(headers))
242
344
  end
243
345
 
@@ -245,65 +347,66 @@ module Cushion
245
347
  # The results are not parsed by default. Set the +headers+ option to pass
246
348
  # custom request headers.
247
349
  #
248
- def list(design, list_template, view, params = {})
350
+ def list(design, list_template, view, options = {})
249
351
  defaults = { :accept => "text/html;text/plain;*/*" }
250
- headers = params.delete(:headers) || {}
251
- path = Cushion.paramify_url("_list/#{design}/#{list_template}/#{view}", params)
352
+ headers = options.delete(:headers) || {}
353
+ path = Cushion.paramify_url("_list/#{design}/#{list_template}/#{view}", options)
252
354
  get(path, defaults.merge(headers))
253
355
  end
254
356
 
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
- request(verb, path, :body => params[:body], :headers => params[:headers])
357
+ # Retrieves information about this database.
358
+ def info(headers = {})
359
+ server.get(@name, headers)
262
360
  end
263
361
 
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)
362
+ # Compacts this database.
363
+ def compact(headers = {})
364
+ post("_compact", nil, headers)
272
365
  end
273
366
 
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
- put("#{slug}/#{fname}?rev=#{rev}", data, defaults.merge(headers))
367
+ # Creates this database on the server.
368
+ def create(headers = {})
369
+ server.put(@name, nil, headers)
284
370
  end
285
371
 
286
- # Deletes an attachment under the document identified by +id+ and +rev+ with
287
- # the supplied +filename+.
372
+ # Deletes this database from the server.
373
+ def drop(headers = {})
374
+ server.delete(@name, headers)
375
+ end
376
+
377
+ # Deletes and recreates this database.
378
+ def recreate
379
+ begin
380
+ drop
381
+ rescue RestClient::ResourceNotFound
382
+ nil
383
+ end
384
+ create
385
+ end
386
+
387
+ # Completely purges the supplied document revisions from the database.
388
+ # +doc_revs+ is a hash of document ids, each containing an array of revisions
389
+ # to be deleted. Example:
390
+ #
391
+ # doc_revs = {
392
+ # "1" => ["12345", "42836"],
393
+ # "2" => ["572654"],
394
+ # "3" => ["34462"]
395
+ # }
288
396
  #
289
- def delete_attachment(id, rev, filename, headers = {})
290
- slug = Cushion.escape_docid(id)
291
- fname = CGI.escape(filename)
292
- delete("#{slug}/#{fname}?rev=#{rev}", headers)
397
+ # db.purge(doc_revs)
398
+ #
399
+ def purge(doc_revs, options = {})
400
+ post("_purge", doc_revs, options)
293
401
  end
294
402
 
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.
403
+ # Issues a request to the CouchDB external server identified by +process+.
404
+ # Calls to external must include a request +verb+. Set the +headers+ option
405
+ # to pass custom request headers.
298
406
  #
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
407
+ def external(verb, process_path, params = {})
408
+ path = Cushion.paramify_url("_#{process_path}", params[:query])
409
+ request(verb, path, :body => params[:body], :headers => params[:headers])
307
410
  end
308
411
 
309
412
  # Issues a HEAD request to this database. See Cushion::Server#head.
@@ -331,22 +434,10 @@ module Cushion
331
434
  server.delete("#{@name}/#{path}", headers)
332
435
  end
333
436
 
334
- # Issues a COPY request to this database. See Cushion::Server#copy.
335
- def copy(source, destination, headers = {})
336
- server.copy("#{@name}/#{source}", destination, headers)
337
- end
338
-
339
- # Issues a MOVE request to this database. See Cushion::Server#move.
340
- def move(source, destination, headers = {})
341
- server.move("#{@name}/#{source}", destination, headers)
342
- end
343
-
344
437
  # Issues a generic request to this database. See Cushion::Server#request.
345
438
  def request(verb, path, params)
346
439
  server.request(verb, "#{@name}/#{path}", params)
347
440
  end
348
441
 
349
- ## EXPERIMENTAL ##
350
-
351
442
  end
352
443
  end