derfred-couchrest 0.12.6
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/LICENSE +176 -0
- data/README.md +68 -0
- data/Rakefile +66 -0
- data/THANKS.md +18 -0
- data/examples/model/example.rb +138 -0
- data/examples/word_count/markov +38 -0
- data/examples/word_count/views/books/chunked-map.js +3 -0
- data/examples/word_count/views/books/united-map.js +1 -0
- data/examples/word_count/views/markov/chain-map.js +6 -0
- data/examples/word_count/views/markov/chain-reduce.js +7 -0
- data/examples/word_count/views/word_count/count-map.js +6 -0
- data/examples/word_count/views/word_count/count-reduce.js +3 -0
- data/examples/word_count/word_count.rb +46 -0
- data/examples/word_count/word_count_query.rb +40 -0
- data/examples/word_count/word_count_views.rb +26 -0
- data/lib/couchrest/commands/generate.rb +71 -0
- data/lib/couchrest/commands/push.rb +103 -0
- data/lib/couchrest/core/database.rb +314 -0
- data/lib/couchrest/core/design.rb +89 -0
- data/lib/couchrest/core/document.rb +101 -0
- data/lib/couchrest/core/model.rb +615 -0
- data/lib/couchrest/core/server.rb +88 -0
- data/lib/couchrest/core/view.rb +4 -0
- data/lib/couchrest/helper/pager.rb +103 -0
- data/lib/couchrest/helper/streamer.rb +44 -0
- data/lib/couchrest/monkeypatches.rb +99 -0
- data/lib/couchrest.rb +161 -0
- data/spec/couchrest/core/couchrest_spec.rb +201 -0
- data/spec/couchrest/core/database_spec.rb +740 -0
- data/spec/couchrest/core/design_spec.rb +131 -0
- data/spec/couchrest/core/document_spec.rb +311 -0
- data/spec/couchrest/core/model_spec.rb +855 -0
- data/spec/couchrest/helpers/pager_spec.rb +122 -0
- data/spec/couchrest/helpers/streamer_spec.rb +23 -0
- data/spec/fixtures/attachments/README +3 -0
- data/spec/fixtures/attachments/couchdb.png +0 -0
- data/spec/fixtures/attachments/test.html +11 -0
- data/spec/fixtures/views/lib.js +3 -0
- data/spec/fixtures/views/test_view/lib.js +3 -0
- data/spec/fixtures/views/test_view/only-map.js +4 -0
- data/spec/fixtures/views/test_view/test-map.js +3 -0
- data/spec/fixtures/views/test_view/test-reduce.js +3 -0
- data/spec/spec.opts +6 -0
- data/spec/spec_helper.rb +21 -0
- data/utils/remap.rb +27 -0
- data/utils/subset.rb +30 -0
- metadata +143 -0
@@ -0,0 +1,88 @@
|
|
1
|
+
module CouchRest
|
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 "available" databases.
|
10
|
+
# An available database, is a database that was specified
|
11
|
+
# as avaiable by your code.
|
12
|
+
# It allows to define common databases to use and reuse in your code
|
13
|
+
def available_databases
|
14
|
+
@available_databases ||= {}
|
15
|
+
end
|
16
|
+
|
17
|
+
# Adds a new available database and create it unless it already exists
|
18
|
+
#
|
19
|
+
# Example:
|
20
|
+
#
|
21
|
+
# @couch = CouchRest::Server.new
|
22
|
+
# @couch.define_available_database(:default, "tech-blog")
|
23
|
+
#
|
24
|
+
def define_available_database(reference, db_name, create_unless_exists = true)
|
25
|
+
available_databases[reference.to_sym] = create_unless_exists ? database!(db_name) : database(db_name)
|
26
|
+
end
|
27
|
+
|
28
|
+
# Checks that a database is set as available
|
29
|
+
#
|
30
|
+
# Example:
|
31
|
+
#
|
32
|
+
# @couch.available_database?(:default)
|
33
|
+
#
|
34
|
+
def available_database?(ref_or_name)
|
35
|
+
ref_or_name.is_a?(Symbol) ? available_databases.keys.include?(ref_or_name) : available_databases.values.map{|db| db.name}.include?(ref_or_name)
|
36
|
+
end
|
37
|
+
|
38
|
+
def default_database=(name, create_unless_exists = true)
|
39
|
+
define_available_database(:default, name, create_unless_exists = true)
|
40
|
+
end
|
41
|
+
|
42
|
+
def default_database
|
43
|
+
available_databases[:default]
|
44
|
+
end
|
45
|
+
|
46
|
+
# Lists all databases on the server
|
47
|
+
def databases
|
48
|
+
CouchRest.get "#{@uri}/_all_dbs"
|
49
|
+
end
|
50
|
+
|
51
|
+
# Returns a CouchRest::Database for the given name
|
52
|
+
def database(name)
|
53
|
+
CouchRest::Database.new(self, name)
|
54
|
+
end
|
55
|
+
|
56
|
+
# Creates the database if it doesn't exist
|
57
|
+
def database!(name)
|
58
|
+
create_db(name) rescue nil
|
59
|
+
database(name)
|
60
|
+
end
|
61
|
+
|
62
|
+
# GET the welcome message
|
63
|
+
def info
|
64
|
+
CouchRest.get "#{@uri}/"
|
65
|
+
end
|
66
|
+
|
67
|
+
# Create a database
|
68
|
+
def create_db(name)
|
69
|
+
CouchRest.put "#{@uri}/#{name}"
|
70
|
+
database(name)
|
71
|
+
end
|
72
|
+
|
73
|
+
# Restart the CouchDB instance
|
74
|
+
def restart!
|
75
|
+
CouchRest.post "#{@uri}/_restart"
|
76
|
+
end
|
77
|
+
|
78
|
+
# Retrive an unused UUID from CouchDB. Server instances manage caching a list of unused UUIDs.
|
79
|
+
def next_uuid(count = @uuid_batch_count)
|
80
|
+
@uuids ||= []
|
81
|
+
if @uuids.empty?
|
82
|
+
@uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"]
|
83
|
+
end
|
84
|
+
@uuids.pop
|
85
|
+
end
|
86
|
+
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,103 @@
|
|
1
|
+
module CouchRest
|
2
|
+
class Pager
|
3
|
+
attr_accessor :db
|
4
|
+
def initialize db
|
5
|
+
@db = db
|
6
|
+
end
|
7
|
+
|
8
|
+
def all_docs(limit=100, &block)
|
9
|
+
startkey = nil
|
10
|
+
oldend = nil
|
11
|
+
|
12
|
+
while docrows = request_all_docs(limit+1, startkey)
|
13
|
+
startkey = docrows.last['key']
|
14
|
+
docrows.pop if docrows.length > limit
|
15
|
+
if oldend == startkey
|
16
|
+
break
|
17
|
+
end
|
18
|
+
yield(docrows)
|
19
|
+
oldend = startkey
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
def key_reduce(view, limit=2000, firstkey = nil, lastkey = nil, &block)
|
24
|
+
# start with no keys
|
25
|
+
startkey = firstkey
|
26
|
+
# lastprocessedkey = nil
|
27
|
+
keepgoing = true
|
28
|
+
|
29
|
+
while keepgoing && viewrows = request_view(view, limit, startkey)
|
30
|
+
startkey = viewrows.first['key']
|
31
|
+
endkey = viewrows.last['key']
|
32
|
+
|
33
|
+
if (startkey == endkey)
|
34
|
+
# we need to rerequest to get a bigger page
|
35
|
+
# so we know we have all the rows for that key
|
36
|
+
viewrows = @db.view(view, :key => startkey)['rows']
|
37
|
+
# we need to do an offset thing to find the next startkey
|
38
|
+
# otherwise we just get stuck
|
39
|
+
lastdocid = viewrows.last['id']
|
40
|
+
fornextloop = @db.view(view, :startkey => startkey, :startkey_docid => lastdocid, :limit => 2)['rows']
|
41
|
+
|
42
|
+
newendkey = fornextloop.last['key']
|
43
|
+
if (newendkey == endkey)
|
44
|
+
keepgoing = false
|
45
|
+
else
|
46
|
+
startkey = newendkey
|
47
|
+
end
|
48
|
+
rows = viewrows
|
49
|
+
else
|
50
|
+
rows = []
|
51
|
+
for r in viewrows
|
52
|
+
if (lastkey && r['key'] == lastkey)
|
53
|
+
keepgoing = false
|
54
|
+
break
|
55
|
+
end
|
56
|
+
break if (r['key'] == endkey)
|
57
|
+
rows << r
|
58
|
+
end
|
59
|
+
startkey = endkey
|
60
|
+
end
|
61
|
+
|
62
|
+
key = :begin
|
63
|
+
values = []
|
64
|
+
|
65
|
+
rows.each do |r|
|
66
|
+
if key != r['key']
|
67
|
+
# we're on a new key, yield the old first and then reset
|
68
|
+
yield(key, values) if key != :begin
|
69
|
+
key = r['key']
|
70
|
+
values = []
|
71
|
+
end
|
72
|
+
# keep accumulating
|
73
|
+
values << r['value']
|
74
|
+
end
|
75
|
+
yield(key, values)
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
private
|
81
|
+
|
82
|
+
def request_all_docs limit, startkey = nil
|
83
|
+
opts = {}
|
84
|
+
opts[:limit] = limit if limit
|
85
|
+
opts[:startkey] = startkey if startkey
|
86
|
+
results = @db.documents(opts)
|
87
|
+
rows = results['rows']
|
88
|
+
rows unless rows.length == 0
|
89
|
+
end
|
90
|
+
|
91
|
+
def request_view view, limit = nil, startkey = nil, endkey = nil
|
92
|
+
opts = {}
|
93
|
+
opts[:limit] = limit if limit
|
94
|
+
opts[:startkey] = startkey if startkey
|
95
|
+
opts[:endkey] = endkey if endkey
|
96
|
+
|
97
|
+
results = @db.view(view, opts)
|
98
|
+
rows = results['rows']
|
99
|
+
rows unless rows.length == 0
|
100
|
+
end
|
101
|
+
|
102
|
+
end
|
103
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module CouchRest
|
2
|
+
class Streamer
|
3
|
+
attr_accessor :db
|
4
|
+
def initialize db
|
5
|
+
@db = db
|
6
|
+
end
|
7
|
+
|
8
|
+
# Stream a view, yielding one row at a time. Shells out to <tt>curl</tt> to keep RAM usage low when you have millions of rows.
|
9
|
+
def view name, params = nil, &block
|
10
|
+
urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}"
|
11
|
+
url = CouchRest.paramify_url urlst, params
|
12
|
+
# puts "stream #{url}"
|
13
|
+
first = nil
|
14
|
+
IO.popen("curl --silent #{url}") do |view|
|
15
|
+
first = view.gets # discard header
|
16
|
+
while line = view.gets
|
17
|
+
row = parse_line(line)
|
18
|
+
block.call row
|
19
|
+
end
|
20
|
+
end
|
21
|
+
parse_first(first)
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def parse_line line
|
27
|
+
return nil unless line
|
28
|
+
if /(\{.*\}),?/.match(line.chomp)
|
29
|
+
JSON.parse($1)
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
def parse_first first
|
34
|
+
return nil unless first
|
35
|
+
parts = first.split(',')
|
36
|
+
parts.pop
|
37
|
+
line = parts.join(',')
|
38
|
+
JSON.parse("#{line}}")
|
39
|
+
rescue
|
40
|
+
nil
|
41
|
+
end
|
42
|
+
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,99 @@
|
|
1
|
+
# This file must be loaded after the JSON gem and any other library that beats up the Time class.
|
2
|
+
class Time
|
3
|
+
# This date format sorts lexicographically
|
4
|
+
# and is compatible with Javascript's <tt>new Date(time_string)</tt> constructor.
|
5
|
+
# Note this this format stores all dates in UTC so that collation
|
6
|
+
# order is preserved. (There's no longer a need to set <tt>ENV['TZ'] = 'UTC'</tt>
|
7
|
+
# in your application.)
|
8
|
+
|
9
|
+
def to_json(options = nil)
|
10
|
+
u = self.utc
|
11
|
+
%("#{u.strftime("%Y/%m/%d %H:%M:%S +0000")}")
|
12
|
+
end
|
13
|
+
|
14
|
+
# Decodes the JSON time format to a UTC time.
|
15
|
+
# Based on Time.parse from ActiveSupport. ActiveSupport's version
|
16
|
+
# is more complete, returning a time in your current timezone,
|
17
|
+
# rather than keeping the time in UTC. YMMV.
|
18
|
+
# def self.parse string, fallback=nil
|
19
|
+
# d = DateTime.parse(string).new_offset
|
20
|
+
# self.utc(d.year, d.month, d.day, d.hour, d.min, d.sec)
|
21
|
+
# rescue
|
22
|
+
# fallback
|
23
|
+
# end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Monkey patch for faster net/http io
|
27
|
+
if RUBY_VERSION.to_f < 1.9
|
28
|
+
class Net::BufferedIO #:nodoc:
|
29
|
+
alias :old_rbuf_fill :rbuf_fill
|
30
|
+
def rbuf_fill
|
31
|
+
begin
|
32
|
+
@rbuf << @io.read_nonblock(65536)
|
33
|
+
rescue Errno::EWOULDBLOCK
|
34
|
+
if IO.select([@io], nil, nil, @read_timeout)
|
35
|
+
@rbuf << @io.read_nonblock(65536)
|
36
|
+
else
|
37
|
+
raise Timeout::TimeoutError
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
module RestClient
|
45
|
+
def self.copy(url, headers={})
|
46
|
+
Request.execute(:method => :copy,
|
47
|
+
:url => url,
|
48
|
+
:headers => headers)
|
49
|
+
end
|
50
|
+
|
51
|
+
def self.move(url, headers={})
|
52
|
+
Request.execute(:method => :move,
|
53
|
+
:url => url,
|
54
|
+
:headers => headers)
|
55
|
+
end
|
56
|
+
|
57
|
+
class Request
|
58
|
+
def transmit(uri, req, payload)
|
59
|
+
setup_credentials(req)
|
60
|
+
|
61
|
+
Thread.current[:host] ||= uri.host
|
62
|
+
Thread.current[:port] ||= uri.port
|
63
|
+
|
64
|
+
net = net_http_class.new(uri.host, uri.port)
|
65
|
+
|
66
|
+
if Thread.current[:connection].nil? || Thread.current[:host] != uri.host
|
67
|
+
Thread.current[:connection].finish if (Thread.current[:connection] && Thread.current[:connection].started?)
|
68
|
+
net.use_ssl = uri.is_a?(URI::HTTPS)
|
69
|
+
net.verify_mode = OpenSSL::SSL::VERIFY_NONE
|
70
|
+
Thread.current[:connection] = net
|
71
|
+
Thread.current[:connection].start
|
72
|
+
end
|
73
|
+
|
74
|
+
display_log request_log
|
75
|
+
http = Thread.current[:connection]
|
76
|
+
|
77
|
+
http.read_timeout = @timeout if @timeout
|
78
|
+
begin
|
79
|
+
res = http.request(req, payload)
|
80
|
+
rescue
|
81
|
+
# p "Net::HTTP connection failed, reconnecting"
|
82
|
+
Thread.current[:connection].finish
|
83
|
+
http = Thread.current[:connection] = net
|
84
|
+
Thread.current[:connection].start
|
85
|
+
res = http.request(req, payload)
|
86
|
+
display_log response_log(res)
|
87
|
+
process_result res
|
88
|
+
else
|
89
|
+
display_log response_log(res)
|
90
|
+
process_result res
|
91
|
+
end
|
92
|
+
|
93
|
+
rescue EOFError
|
94
|
+
raise RestClient::ServerBrokeConnection
|
95
|
+
rescue Timeout::Error
|
96
|
+
raise RestClient::RequestTimeout
|
97
|
+
end
|
98
|
+
end
|
99
|
+
end
|
data/lib/couchrest.rb
ADDED
@@ -0,0 +1,161 @@
|
|
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 "rubygems"
|
16
|
+
require 'json'
|
17
|
+
require 'rest_client'
|
18
|
+
|
19
|
+
$:.unshift File.dirname(__FILE__) unless
|
20
|
+
$:.include?(File.dirname(__FILE__)) ||
|
21
|
+
$:.include?(File.expand_path(File.dirname(__FILE__)))
|
22
|
+
|
23
|
+
|
24
|
+
require 'couchrest/monkeypatches'
|
25
|
+
|
26
|
+
# = CouchDB, close to the metal
|
27
|
+
module CouchRest
|
28
|
+
VERSION = '0.12.6'
|
29
|
+
|
30
|
+
autoload :Server, 'couchrest/core/server'
|
31
|
+
autoload :Database, 'couchrest/core/database'
|
32
|
+
autoload :Document, 'couchrest/core/document'
|
33
|
+
autoload :Design, 'couchrest/core/design'
|
34
|
+
autoload :View, 'couchrest/core/view'
|
35
|
+
autoload :Model, 'couchrest/core/model'
|
36
|
+
autoload :Pager, 'couchrest/helper/pager'
|
37
|
+
autoload :FileManager, 'couchrest/helper/file_manager'
|
38
|
+
autoload :Streamer, 'couchrest/helper/streamer'
|
39
|
+
|
40
|
+
autoload :ExtendedDocument, 'couchrest/more/extended_document'
|
41
|
+
|
42
|
+
require File.join(File.dirname(__FILE__), 'couchrest', 'mixins')
|
43
|
+
|
44
|
+
# The CouchRest module methods handle the basic JSON serialization
|
45
|
+
# and deserialization, as well as query parameters. The module also includes
|
46
|
+
# some helpers for tasks like instantiating a new Database or Server instance.
|
47
|
+
class << self
|
48
|
+
|
49
|
+
# todo, make this parse the url and instantiate a Server or Database instance
|
50
|
+
# depending on the specificity.
|
51
|
+
def new(*opts)
|
52
|
+
Server.new(*opts)
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse url
|
56
|
+
case url
|
57
|
+
when /^http:\/\/(.*)\/(.*)\/(.*)/
|
58
|
+
host = $1
|
59
|
+
db = $2
|
60
|
+
docid = $3
|
61
|
+
when /^http:\/\/(.*)\/(.*)/
|
62
|
+
host = $1
|
63
|
+
db = $2
|
64
|
+
when /^http:\/\/(.*)/
|
65
|
+
host = $1
|
66
|
+
when /(.*)\/(.*)\/(.*)/
|
67
|
+
host = $1
|
68
|
+
db = $2
|
69
|
+
docid = $3
|
70
|
+
when /(.*)\/(.*)/
|
71
|
+
host = $1
|
72
|
+
db = $2
|
73
|
+
else
|
74
|
+
db = url
|
75
|
+
end
|
76
|
+
|
77
|
+
db = nil if db && db.empty?
|
78
|
+
|
79
|
+
{
|
80
|
+
:host => host || "127.0.0.1:5984",
|
81
|
+
:database => db,
|
82
|
+
:doc => docid
|
83
|
+
}
|
84
|
+
end
|
85
|
+
|
86
|
+
# set proxy for RestClient to use
|
87
|
+
def proxy url
|
88
|
+
RestClient.proxy = url
|
89
|
+
end
|
90
|
+
|
91
|
+
# ensure that a database exists
|
92
|
+
# creates it if it isn't already there
|
93
|
+
# returns it after it's been created
|
94
|
+
def database! url
|
95
|
+
parsed = parse url
|
96
|
+
cr = CouchRest.new(parsed[:host])
|
97
|
+
cr.database!(parsed[:database])
|
98
|
+
end
|
99
|
+
|
100
|
+
def database url
|
101
|
+
parsed = parse url
|
102
|
+
cr = CouchRest.new(parsed[:host])
|
103
|
+
cr.database(parsed[:database])
|
104
|
+
end
|
105
|
+
|
106
|
+
def put_raw(uri, doc=nil)
|
107
|
+
payload = doc.to_json if doc
|
108
|
+
RestClient.put(uri, payload)
|
109
|
+
end
|
110
|
+
|
111
|
+
def get_raw(uri)
|
112
|
+
RestClient.get(uri)
|
113
|
+
end
|
114
|
+
|
115
|
+
def post_raw(uri, doc=nil)
|
116
|
+
payload = doc.to_json if doc
|
117
|
+
RestClient.post(uri, payload)
|
118
|
+
end
|
119
|
+
|
120
|
+
def delete_raw(uri)
|
121
|
+
RestClient.delete(uri)
|
122
|
+
end
|
123
|
+
|
124
|
+
|
125
|
+
|
126
|
+
def put uri, doc = nil
|
127
|
+
JSON.parse(put_raw(uri, doc))
|
128
|
+
end
|
129
|
+
|
130
|
+
def get uri
|
131
|
+
JSON.parse(get_raw(uri), :max_nesting => false)
|
132
|
+
end
|
133
|
+
|
134
|
+
def post uri, doc = nil
|
135
|
+
JSON.parse(post_raw(uri, doc))
|
136
|
+
end
|
137
|
+
|
138
|
+
def delete uri
|
139
|
+
JSON.parse(delete_raw(uri))
|
140
|
+
end
|
141
|
+
|
142
|
+
def copy uri, destination
|
143
|
+
JSON.parse(RestClient.copy(uri, {'Destination' => destination}))
|
144
|
+
end
|
145
|
+
|
146
|
+
def move uri, destination
|
147
|
+
JSON.parse(RestClient.move(uri, {'Destination' => destination}))
|
148
|
+
end
|
149
|
+
|
150
|
+
def paramify_url url, params = {}
|
151
|
+
if params && !params.empty?
|
152
|
+
query = params.collect do |k,v|
|
153
|
+
v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
|
154
|
+
"#{k}=#{CGI.escape(v.to_s)}"
|
155
|
+
end.join("&")
|
156
|
+
url = "#{url}?#{query}"
|
157
|
+
end
|
158
|
+
url
|
159
|
+
end
|
160
|
+
end # class << self
|
161
|
+
end
|