sova 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
data/README.md ADDED
@@ -0,0 +1,4 @@
1
+ # Sova is a CouchRest fork
2
+
3
+ * Removed a lot of bulk
4
+ * Added HTTPI support
@@ -0,0 +1,349 @@
1
+ require 'cgi'
2
+ require "base64"
3
+
4
+ module Sova
5
+ class Database
6
+ attr_reader :server, :host, :name, :root, :uri
7
+ attr_accessor :bulk_save_cache_limit
8
+
9
+ # Create a Sova::Database adapter for the supplied Sova::Server
10
+ # and database name.
11
+ #
12
+ # ==== Parameters
13
+ # server<Sova::Server>:: database host
14
+ # name<String>:: database name
15
+ #
16
+ def initialize(server, name)
17
+ @name = name
18
+ @server = server
19
+ @host = server.uri
20
+ @uri = "/#{name.gsub('/','%2F')}"
21
+ @root = host + uri
22
+ @bulk_save_cache = []
23
+ @bulk_save_cache_limit = 500 # must be smaller than the uuid count
24
+ end
25
+
26
+ # returns the database's uri
27
+ def to_s
28
+ @root
29
+ end
30
+
31
+ # GET the database info from CouchDB
32
+ def info
33
+ HTTP.get @root
34
+ end
35
+
36
+ # Query the <tt>_all_docs</tt> view. Accepts all the same arguments as view.
37
+ def documents(params = {})
38
+ keys = params.delete(:keys)
39
+ url = Sova.paramify_url "#{@root}/_all_docs", params
40
+ if keys
41
+ HTTP.post(url, {:keys => keys})
42
+ else
43
+ HTTP.get url
44
+ end
45
+ end
46
+
47
+ # load a set of documents by passing an array of ids
48
+ def get_bulk(ids)
49
+ documents(:keys => ids, :include_docs => true)
50
+ end
51
+ alias :bulk_load :get_bulk
52
+
53
+ # POST a temporary view function to CouchDB for querying. This is not
54
+ # recommended, as you don't get any performance benefit from CouchDB's
55
+ # materialized views. Can be quite slow on large databases.
56
+ def slow_view(funcs, params = {})
57
+ keys = params.delete(:keys)
58
+ funcs = funcs.merge({:keys => keys}) if keys
59
+ url = Sova.paramify_url "#{@root}/_temp_view", params
60
+ HTTP.post(url, funcs)
61
+ end
62
+
63
+ # backwards compatibility is a plus
64
+ alias :temp_view :slow_view
65
+
66
+ # Query a CouchDB view as defined by a <tt>_design</tt> document. Accepts
67
+ # paramaters as described in http://wiki.apache.org/couchdb/HttpViewApi
68
+ def view(name, params = {}, &block)
69
+ keys = params.delete(:keys)
70
+ name = name.split('/') # I think this will always be length == 2, but maybe not...
71
+ dname = name.shift
72
+ vname = name.join('/')
73
+ url = Sova.paramify_url "#{@root}/_design/#{dname}/_view/#{vname}", params
74
+ if keys
75
+ HTTP.post(url, {:keys => keys})
76
+ else
77
+ HTTP.get url
78
+ end
79
+ end
80
+
81
+ # GET a document from CouchDB, by id. Returns a Ruby Hash.
82
+ def get(id, params = {})
83
+ slug = escape_docid(id)
84
+ url = Sova.paramify_url("#{@root}/#{slug}", params)
85
+ result = HTTP.get(url)
86
+ end
87
+
88
+ # GET an attachment directly from CouchDB
89
+ def fetch_attachment(doc, name)
90
+ uri = url_for_attachment(doc, name)
91
+ HTTP.request(:get, uri).body
92
+ end
93
+
94
+ # PUT an attachment directly to CouchDB
95
+ def put_attachment(doc, name, file, options = {})
96
+ docid = escape_docid(doc['_id'])
97
+ uri = url_for_attachment(doc, name)
98
+ response = HTTP.request(:put, uri, file, options)
99
+ JSON.parse(response.body)
100
+ end
101
+
102
+ # DELETE an attachment directly from CouchDB
103
+ def delete_attachment(doc, name, force=false)
104
+ uri = url_for_attachment(doc, name)
105
+ # this needs a rev
106
+ begin
107
+ HTTP.delete(uri)
108
+ rescue Exception => error
109
+ if force
110
+ # get over a 409
111
+ doc = get(doc['_id'])
112
+ uri = url_for_attachment(doc, name)
113
+ HTTP.delete(uri)
114
+ else
115
+ raise error
116
+ end
117
+ end
118
+ end
119
+
120
+ # Save a document to CouchDB. This will use the <tt>_id</tt> field from
121
+ # the document as the id for PUT, or request a new UUID from CouchDB, if
122
+ # no <tt>_id</tt> is present on the document. IDs are attached to
123
+ # documents on the client side because POST has the curious property of
124
+ # being automatically retried by proxies in the event of network
125
+ # segmentation and lost responses.
126
+ #
127
+ # If <tt>bulk</tt> is true (false by default) the document is cached for bulk-saving later.
128
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
129
+ #
130
+ # If <tt>batch</tt> is true (false by default) the document is saved in
131
+ # batch mode, "used to achieve higher throughput at the cost of lower
132
+ # guarantees. When [...] sent using this option, it is not immediately
133
+ # written to disk. Instead it is stored in memory on a per-user basis for a
134
+ # second or so (or the number of docs in memory reaches a certain point).
135
+ # After the threshold has passed, the docs are committed to disk. Instead
136
+ # of waiting for the doc to be written to disk before responding, CouchDB
137
+ # sends an HTTP 202 Accepted response immediately. batch=ok is not suitable
138
+ # for crucial data, but it ideal for applications like logging which can
139
+ # accept the risk that a small proportion of updates could be lost due to a
140
+ # crash."
141
+ def save_doc(doc, bulk = false, batch = false)
142
+ if doc['_attachments']
143
+ doc['_attachments'] = encode_attachments(doc['_attachments'])
144
+ end
145
+ if bulk
146
+ @bulk_save_cache << doc
147
+ bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
148
+ return {"ok" => true} # Compatibility with Document#save
149
+ elsif !bulk && @bulk_save_cache.length > 0
150
+ bulk_save
151
+ end
152
+ result = if doc['_id']
153
+ slug = escape_docid(doc['_id'])
154
+ begin
155
+ uri = "#{@root}/#{slug}"
156
+ uri << "?batch=ok" if batch
157
+ HTTP.put uri, doc
158
+ rescue Sova::NotFound
159
+ p "resource not found when saving even tho an id was passed"
160
+ slug = doc['_id'] = @server.next_uuid
161
+ HTTP.put "#{@root}/#{slug}", doc
162
+ end
163
+ else
164
+ begin
165
+ slug = doc['_id'] = @server.next_uuid
166
+ HTTP.put "#{@root}/#{slug}", doc
167
+ rescue #old version of couchdb
168
+ HTTP.post @root, doc
169
+ end
170
+ end
171
+ if result['ok']
172
+ doc['_id'] = result['id']
173
+ doc['_rev'] = result['rev']
174
+ doc.database = self if doc.respond_to?(:database=)
175
+ end
176
+ result
177
+ end
178
+
179
+ # Save a document to CouchDB in bulk mode. See #save_doc's +bulk+ argument.
180
+ def bulk_save_doc(doc)
181
+ save_doc(doc, true)
182
+ end
183
+
184
+ # Save a document to CouchDB in batch mode. See #save_doc's +batch+ argument.
185
+ def batch_save_doc(doc)
186
+ save_doc(doc, false, true)
187
+ end
188
+
189
+ # POST an array of documents to CouchDB. If any of the documents are
190
+ # missing ids, supply one from the uuid cache.
191
+ #
192
+ # If called with no arguments, bulk saves the cache of documents to be bulk saved.
193
+ def bulk_save(docs = nil, use_uuids = true)
194
+ if docs.nil?
195
+ docs = @bulk_save_cache
196
+ @bulk_save_cache = []
197
+ end
198
+ if (use_uuids)
199
+ ids, noids = docs.partition{|d|d['_id']}
200
+ uuid_count = [noids.length, @server.uuid_batch_count].max
201
+ noids.each do |doc|
202
+ nextid = @server.next_uuid(uuid_count) rescue nil
203
+ doc['_id'] = nextid if nextid
204
+ end
205
+ end
206
+ HTTP.post "#{@root}/_bulk_docs", {:docs => docs}
207
+ end
208
+ alias :bulk_delete :bulk_save
209
+
210
+ # DELETE the document from CouchDB that has the given <tt>_id</tt> and
211
+ # <tt>_rev</tt>.
212
+ #
213
+ # If <tt>bulk</tt> is true (false by default) the deletion is recorded for bulk-saving (bulk-deletion :) later.
214
+ # Bulk saving happens automatically when #bulk_save_cache limit is exceded, or on the next non bulk save.
215
+ def delete_doc(doc, bulk = false)
216
+ raise ArgumentError, "_id and _rev required for deleting" unless doc['_id'] && doc['_rev']
217
+ if bulk
218
+ @bulk_save_cache << { '_id' => doc['_id'], '_rev' => doc['_rev'], '_deleted' => true }
219
+ return bulk_save if @bulk_save_cache.length >= @bulk_save_cache_limit
220
+ return { "ok" => true } # Mimic the non-deferred version
221
+ end
222
+ slug = escape_docid(doc['_id'])
223
+ HTTP.delete "#{@root}/#{slug}?rev=#{doc['_rev']}"
224
+ end
225
+
226
+ # COPY an existing document to a new id. If the destination id currently exists, a rev must be provided.
227
+ # <tt>dest</tt> can take one of two forms if overwriting: "id_to_overwrite?rev=revision" or the actual doc
228
+ # hash with a '_rev' key
229
+ def copy_doc(doc, dest)
230
+ raise ArgumentError, "_id is required for copying" unless doc['_id']
231
+ slug = escape_docid(doc['_id'])
232
+ destination = if dest.respond_to?(:has_key?) && dest['_id'] && dest['_rev']
233
+ "#{dest['_id']}?rev=#{dest['_rev']}"
234
+ else
235
+ dest
236
+ end
237
+ HTTP.copy "#{@root}/#{slug}", destination
238
+ end
239
+
240
+ # Updates the given doc by yielding the current state of the doc
241
+ # and trying to update update_limit times. Returns the new doc
242
+ # if the doc was successfully updated without hitting the limit
243
+ def update_doc(doc_id, params = {}, update_limit=10)
244
+ resp = {'ok' => false}
245
+ new_doc = nil
246
+ last_fail = nil
247
+
248
+ until resp['ok'] or update_limit <= 0
249
+ doc = self.get(doc_id, params) # grab the doc
250
+ new_doc = yield doc # give it to the caller to be updated
251
+ begin
252
+ resp = self.save_doc new_doc # try to PUT the updated doc into the db
253
+ rescue Sova::Conflict => e
254
+ update_limit -= 1
255
+ last_fail = e
256
+ end
257
+ end
258
+
259
+ raise last_fail unless resp['ok']
260
+ new_doc
261
+ end
262
+
263
+ # Compact the database, removing old document revisions and optimizing space use.
264
+ def compact!
265
+ HTTP.post "#{@root}/_compact"
266
+ end
267
+
268
+ # Create the database
269
+ def create!
270
+ bool = server.create_db(@name) rescue false
271
+ bool && true
272
+ end
273
+
274
+ # Delete and re create the database
275
+ def recreate!
276
+ delete!
277
+ create!
278
+ rescue Sova::NotFound
279
+ ensure
280
+ create!
281
+ end
282
+
283
+ # Replicates via "pulling" from another database to this database. Makes no attempt to deal with conflicts.
284
+ def replicate_from(other_db, continuous = false, create_target = false)
285
+ replicate(other_db, continuous, :target => name, :create_target => create_target)
286
+ end
287
+
288
+ # Replicates via "pushing" to another database. Makes no attempt to deal with conflicts.
289
+ def replicate_to(other_db, continuous = false, create_target = false)
290
+ replicate(other_db, continuous, :source => name, :create_target => create_target)
291
+ end
292
+
293
+ # DELETE the database itself. This is not undoable and could be rather
294
+ # catastrophic. Use with care!
295
+ def delete!
296
+ HTTP.delete @root
297
+ end
298
+
299
+ private
300
+
301
+ def replicate(other_db, continuous, options)
302
+ raise ArgumentError, "must provide a Sova::Database" unless other_db.kind_of?(Sova::Database)
303
+ raise ArgumentError, "must provide a target or source option" unless (options.key?(:target) || options.key?(:source))
304
+ payload = options
305
+ if options.has_key?(:target)
306
+ payload[:source] = other_db.root
307
+ else
308
+ payload[:target] = other_db.root
309
+ end
310
+ payload[:continuous] = continuous
311
+ HTTP.post "#{@host}/_replicate", payload
312
+ end
313
+
314
+ def uri_for_attachment(doc, name)
315
+ if doc.is_a?(String)
316
+ puts "Sova::Database#fetch_attachment will eventually require a doc as the first argument, not a doc.id"
317
+ docid = doc
318
+ rev = nil
319
+ else
320
+ docid = doc['_id']
321
+ rev = doc['_rev']
322
+ end
323
+ docid = escape_docid(docid)
324
+ name = CGI.escape(name)
325
+ rev = "?rev=#{doc['_rev']}" if rev
326
+ "/#{docid}/#{name}#{rev}"
327
+ end
328
+
329
+ def url_for_attachment(doc, name)
330
+ @root + uri_for_attachment(doc, name)
331
+ end
332
+
333
+ def escape_docid id
334
+ /^_design\/(.*)/ =~ id ? "_design/#{CGI.escape($1)}" : CGI.escape(id)
335
+ end
336
+
337
+ def encode_attachments(attachments)
338
+ attachments.each do |k,v|
339
+ next if v['stub']
340
+ v['data'] = base64(v['data'])
341
+ end
342
+ attachments
343
+ end
344
+
345
+ def base64(data)
346
+ Base64.encode64(data).gsub(/\s/,'')
347
+ end
348
+ end
349
+ end
data/lib/sova/http.rb ADDED
@@ -0,0 +1,81 @@
1
+ require "httpi"
2
+ require "forwardable"
3
+
4
+ module Sova
5
+ class HTTPError < StandardError
6
+ extend Forwardable
7
+
8
+ def_delegators :@response, :code, :headers, :body
9
+
10
+ def initialize response
11
+ @response = response
12
+ end
13
+ end
14
+ class NotFound < HTTPError; end
15
+ class Conflict < HTTPError; end
16
+ class Invalid < HTTPError; end
17
+
18
+ module HTTP
19
+ extend self
20
+
21
+ attr_accessor :adapter
22
+
23
+ def request method, uri, doc=nil, headers={}
24
+ request = HTTPI::Request.new
25
+ request.url = uri
26
+ request.proxy = Sova.proxy if Sova.proxy
27
+ request.body = doc if doc
28
+ request.headers = {
29
+ "Content-Type" => "application/json",
30
+ "Accept" => "application/json"
31
+ }.merge(headers)
32
+
33
+ response = HTTPI.request(method, request, adapter)
34
+ raise http_error(response) if response.error?
35
+ response
36
+ end
37
+
38
+ def put(uri, doc=nil, headers={})
39
+ doc = doc.to_json if doc
40
+ response = request(:put, uri, doc, headers)
41
+ JSON.parse(response.body, :max_nesting => false)
42
+ end
43
+
44
+ def get(uri)
45
+ response = request(:get, uri)
46
+ JSON.parse(response.body, :max_nesting => false)
47
+ end
48
+
49
+ def post(uri, doc=nil)
50
+ doc = doc.to_json if doc
51
+ response = request(:post, uri, doc)
52
+ JSON.parse(response.body, :max_nesting => false)
53
+ end
54
+
55
+ def delete(uri)
56
+ response = request(:delete, uri)
57
+ JSON.parse(response.body, :max_nesting => false)
58
+ end
59
+
60
+ def copy(uri, destination)
61
+ headers = {'X-HTTP-Method-Override' => 'COPY', 'Destination' => destination}
62
+ response = request(:post, uri, nil, headers)
63
+ JSON.parse(response.body, :max_nesting => false)
64
+ end
65
+
66
+ private
67
+
68
+ def http_error response
69
+ klass =
70
+ case response.code
71
+ when 404 then
72
+ NotFound
73
+ when 409 then
74
+ Conflict
75
+ else
76
+ HTTPError
77
+ end
78
+ klass.new(response)
79
+ end
80
+ end
81
+ end
@@ -0,0 +1,50 @@
1
+ module Sova
2
+ class Server
3
+ attr_accessor :uri, :uuid_batch_count, :available_databases
4
+ def initialize(server = 'http://127.0.0.1:5984', uuid_batch_count = 1000)
5
+ @uri = server
6
+ @uuid_batch_count = uuid_batch_count
7
+ end
8
+
9
+ # Lists all databases on the server
10
+ def databases
11
+ HTTP.get "#{@uri}/_all_dbs"
12
+ end
13
+
14
+ # Returns a Sova::Database for the given name
15
+ def database(name)
16
+ Sova::Database.new(self, name)
17
+ end
18
+
19
+ # Creates the database if it doesn't exist
20
+ def database!(name)
21
+ create_db(name) rescue nil
22
+ database(name)
23
+ end
24
+
25
+ # GET the welcome message
26
+ def info
27
+ HTTP.get "#{@uri}/"
28
+ end
29
+
30
+ # Create a database
31
+ def create_db(name)
32
+ HTTP.put "#{@uri}/#{name}"
33
+ database(name)
34
+ end
35
+
36
+ # Restart the CouchDB instance
37
+ def restart!
38
+ HTTP.post "#{@uri}/_restart"
39
+ end
40
+
41
+ # Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
42
+ def next_uuid(count = @uuid_batch_count)
43
+ @uuids ||= []
44
+ if @uuids.empty?
45
+ @uuids = HTTP.get("#{@uri}/_uuids?count=#{count}")["uuids"]
46
+ end
47
+ @uuids.pop
48
+ end
49
+ end
50
+ end
data/lib/sova.rb ADDED
@@ -0,0 +1,100 @@
1
+ # Copyright 2008 J. Chris Anderson
2
+ #
3
+ # Licensed under the Apache License, Version 2.0 (the "License");
4
+ # you may not use this file except in compliance with the License.
5
+ # You may obtain a copy of the License at
6
+ #
7
+ # http://www.apache.org/licenses/LICENSE-2.0
8
+ #
9
+ # Unless required by applicable law or agreed to in writing, software
10
+ # distributed under the License is distributed on an "AS IS" BASIS,
11
+ # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
+ # See the License for the specific language governing permissions and
13
+ # limitations under the License.
14
+
15
+ require 'json'
16
+
17
+ require 'sova/http'
18
+ require 'sova/server'
19
+ require 'sova/database'
20
+
21
+ Sova::HTTP.adapter = :net_http
22
+
23
+ # = CouchDB, close to the metal
24
+ module Sova
25
+ VERSION = '1.0.1'
26
+
27
+ # The Sova module methods handle the basic JSON serialization
28
+ # and deserialization, as well as query parameters. The module also includes
29
+ # some helpers for tasks like instantiating a new Database or Server instance.
30
+ class << self
31
+
32
+ # todo, make this parse the url and instantiate a Server or Database instance
33
+ # depending on the specificity.
34
+ def new(*opts)
35
+ Server.new(*opts)
36
+ end
37
+
38
+ def parse url
39
+ case url
40
+ when /^(https?:\/\/)(.*)\/(.*)\/(.*)/
41
+ scheme = $1
42
+ host = $2
43
+ db = $3
44
+ docid = $4
45
+ when /^(https?:\/\/)(.*)\/(.*)/
46
+ scheme = $1
47
+ host = $2
48
+ db = $3
49
+ when /^(https?:\/\/)(.*)/
50
+ scheme = $1
51
+ host = $2
52
+ when /(.*)\/(.*)\/(.*)/
53
+ host = $1
54
+ db = $2
55
+ docid = $3
56
+ when /(.*)\/(.*)/
57
+ host = $1
58
+ db = $2
59
+ else
60
+ db = url
61
+ end
62
+
63
+ db = nil if db && db.empty?
64
+
65
+ {
66
+ :host => (scheme || "http://") + (host || "127.0.0.1:5984"),
67
+ :database => db,
68
+ :doc => docid
69
+ }
70
+ end
71
+
72
+ attr_accessor :proxy
73
+
74
+ # ensure that a database exists
75
+ # creates it if it isn't already there
76
+ # returns it after it's been created
77
+ def database! url
78
+ parsed = parse url
79
+ cr = Sova.new(parsed[:host])
80
+ cr.database!(parsed[:database])
81
+ end
82
+
83
+ def database url
84
+ parsed = parse url
85
+ cr = Sova.new(parsed[:host])
86
+ cr.database(parsed[:database])
87
+ end
88
+
89
+ def paramify_url url, params = {}
90
+ if params && !params.empty?
91
+ query = params.collect do |k,v|
92
+ v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
93
+ "#{k}=#{CGI.escape(v.to_s)}"
94
+ end.join("&")
95
+ url = "#{url}?#{query}"
96
+ end
97
+ url
98
+ end
99
+ end # class << self
100
+ end
data/sova.gemspec ADDED
@@ -0,0 +1,19 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = "sova"
3
+ s.version = "0.0.1"
4
+ s.date = "2011-02-16"
5
+ s.summary = "CouchDB library"
6
+ s.email = "harry@vangberg.name"
7
+ s.homepage = "http://github.com/ichverstehe/sova"
8
+ s.has_rdoc = true
9
+ s.authors = ["Harry Vangberg"]
10
+ s.files = [
11
+ "README.md",
12
+ "sova.gemspec",
13
+ "lib/sova.rb",
14
+ "lib/sova/database.rb",
15
+ "lib/sova/http.rb",
16
+ "lib/sova/server.rb"
17
+ ]
18
+ end
19
+
metadata ADDED
@@ -0,0 +1,69 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: sova
3
+ version: !ruby/object:Gem::Version
4
+ prerelease: false
5
+ segments:
6
+ - 0
7
+ - 0
8
+ - 1
9
+ version: 0.0.1
10
+ platform: ruby
11
+ authors:
12
+ - Harry Vangberg
13
+ autorequire:
14
+ bindir: bin
15
+ cert_chain: []
16
+
17
+ date: 2011-02-16 00:00:00 -03:00
18
+ default_executable:
19
+ dependencies: []
20
+
21
+ description:
22
+ email: harry@vangberg.name
23
+ executables: []
24
+
25
+ extensions: []
26
+
27
+ extra_rdoc_files: []
28
+
29
+ files:
30
+ - README.md
31
+ - sova.gemspec
32
+ - lib/sova.rb
33
+ - lib/sova/database.rb
34
+ - lib/sova/http.rb
35
+ - lib/sova/server.rb
36
+ has_rdoc: true
37
+ homepage: http://github.com/ichverstehe/sova
38
+ licenses: []
39
+
40
+ post_install_message:
41
+ rdoc_options: []
42
+
43
+ require_paths:
44
+ - lib
45
+ required_ruby_version: !ruby/object:Gem::Requirement
46
+ none: false
47
+ requirements:
48
+ - - ">="
49
+ - !ruby/object:Gem::Version
50
+ segments:
51
+ - 0
52
+ version: "0"
53
+ required_rubygems_version: !ruby/object:Gem::Requirement
54
+ none: false
55
+ requirements:
56
+ - - ">="
57
+ - !ruby/object:Gem::Version
58
+ segments:
59
+ - 0
60
+ version: "0"
61
+ requirements: []
62
+
63
+ rubyforge_project:
64
+ rubygems_version: 1.3.7
65
+ signing_key:
66
+ specification_version: 3
67
+ summary: CouchDB library
68
+ test_files: []
69
+