sova 0.0.1

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