akdubya-cushion 0.5.2 → 0.6.0

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/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