jchris-couchrest 0.9.1 → 0.9.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.markdown +51 -0
- data/lib/couch_rest.rb +42 -8
- data/lib/couchrest.rb +1 -0
- data/lib/database.rb +19 -5
- data/lib/pager.rb +0 -22
- data/lib/streamer.rb +29 -0
- data/spec/couchrest_spec.rb +25 -4
- data/spec/database_spec.rb +32 -2
- data/spec/streamer_spec.rb +23 -0
- metadata +4 -2
- data/README +0 -39
data/README.markdown
ADDED
@@ -0,0 +1,51 @@
|
|
1
|
+
## CouchRest - CouchDB, close to the metal
|
2
|
+
|
3
|
+
CouchRest is based on [CouchDB's couch.js test library](http://svn.apache.org/repos/asf/incubator/couchdb/trunk/share/www/script/couch.js), which I find to be concise, clear, and well designed. CouchRest lightly wraps CouchDB's HTTP API, managing JSON serialization, and remembering the URI-paths to CouchDB's API endpoints so you don't have to.
|
4
|
+
|
5
|
+
CouchRest's lighweight is designed to make a simple base for application and framework-specific object oriented APIs.
|
6
|
+
|
7
|
+
### Easy Install
|
8
|
+
|
9
|
+
`sudo gem install jchris-couchrest -s http://gems.github.com`
|
10
|
+
|
11
|
+
### Relax, it's RESTful
|
12
|
+
|
13
|
+
The core of Couchrest is Heroku’s excellent REST Client Ruby HTTP wrapper. REST Client takes all the nastyness of Net::HTTP and gives is a pretty face, while still giving you more control than Open-URI. I recommend it anytime you’re interfacing with a well-defined web service.
|
14
|
+
|
15
|
+
### Running the Specs
|
16
|
+
|
17
|
+
The most complete documentation is the spec/ directory. To validate your CouchRest install, from the project root directory run `rake`, or `autotest` (requires RSpec and optionally ZenTest for autotest support).
|
18
|
+
|
19
|
+
### Examples
|
20
|
+
|
21
|
+
Quick Start:
|
22
|
+
|
23
|
+
# with !, it creates the database if it doesn't already exist
|
24
|
+
@db = CouchRest.database!("http://localhost:5984/couchrest-test")
|
25
|
+
response = @db.save({:key => 'value', 'another key' => 'another value'})
|
26
|
+
doc = @db.get(response['id'])
|
27
|
+
puts doc.inspect
|
28
|
+
|
29
|
+
Bulk Save:
|
30
|
+
|
31
|
+
@db.bulk_save([
|
32
|
+
{"wild" => "and random"},
|
33
|
+
{"mild" => "yet local"},
|
34
|
+
{"another" => ["set","of","keys"]}
|
35
|
+
])
|
36
|
+
# returns ids and revs of the current docs
|
37
|
+
puts @db.documents.inspect
|
38
|
+
|
39
|
+
Creating and Querying Views:
|
40
|
+
|
41
|
+
@db.save({
|
42
|
+
"_id" => "_design/first",
|
43
|
+
:views => {
|
44
|
+
:test => {
|
45
|
+
:map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}"
|
46
|
+
}
|
47
|
+
}
|
48
|
+
})
|
49
|
+
puts @db.view('first/test')['rows'].inspect
|
50
|
+
|
51
|
+
|
data/lib/couch_rest.rb
CHANGED
@@ -1,7 +1,27 @@
|
|
1
1
|
class CouchRest
|
2
|
-
attr_accessor :uri
|
3
|
-
def initialize server = 'http://localhost:5984'
|
2
|
+
attr_accessor :uri, :uuid_batch_count
|
3
|
+
def initialize server = 'http://localhost:5984', uuid_batch_count = 1000
|
4
4
|
@uri = server
|
5
|
+
@uuid_batch_count = uuid_batch_count
|
6
|
+
end
|
7
|
+
|
8
|
+
# ensure that a database exists
|
9
|
+
# creates it if it isn't already there
|
10
|
+
# returns it after it's been created
|
11
|
+
def self.database! url
|
12
|
+
uri = URI.parse url
|
13
|
+
path = uri.path
|
14
|
+
uri.path = ''
|
15
|
+
cr = CouchRest.new(uri.to_s)
|
16
|
+
cr.database!(path)
|
17
|
+
end
|
18
|
+
|
19
|
+
def self.database url
|
20
|
+
uri = URI.parse url
|
21
|
+
path = uri.path
|
22
|
+
uri.path = ''
|
23
|
+
cr = CouchRest.new(uri.to_s)
|
24
|
+
cr.database(path)
|
5
25
|
end
|
6
26
|
|
7
27
|
# list all databases on the server
|
@@ -10,7 +30,13 @@ class CouchRest
|
|
10
30
|
end
|
11
31
|
|
12
32
|
def database name
|
13
|
-
CouchRest::Database.new(
|
33
|
+
CouchRest::Database.new(self, name)
|
34
|
+
end
|
35
|
+
|
36
|
+
# creates the database if it doesn't exist
|
37
|
+
def database! name
|
38
|
+
create_db(path) rescue nil
|
39
|
+
database name
|
14
40
|
end
|
15
41
|
|
16
42
|
# get the welcome message
|
@@ -18,17 +44,25 @@ class CouchRest
|
|
18
44
|
CouchRest.get "#{@uri}/"
|
19
45
|
end
|
20
46
|
|
21
|
-
# restart the couchdb instance
|
22
|
-
def restart!
|
23
|
-
CouchRest.post "#{@uri}/_restart"
|
24
|
-
end
|
25
|
-
|
26
47
|
# create a database
|
27
48
|
def create_db name
|
28
49
|
CouchRest.put "#{@uri}/#{name}"
|
29
50
|
database name
|
30
51
|
end
|
31
52
|
|
53
|
+
# restart the couchdb instance
|
54
|
+
def restart!
|
55
|
+
CouchRest.post "#{@uri}/_restart"
|
56
|
+
end
|
57
|
+
|
58
|
+
def next_uuid count = @uuid_batch_count
|
59
|
+
@uuids ||= []
|
60
|
+
if @uuids.empty?
|
61
|
+
@uuids = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"]
|
62
|
+
end
|
63
|
+
@uuids.pop
|
64
|
+
end
|
65
|
+
|
32
66
|
class << self
|
33
67
|
def put uri, doc = nil
|
34
68
|
payload = doc.to_json if doc
|
data/lib/couchrest.rb
CHANGED
data/lib/database.rb
CHANGED
@@ -3,10 +3,12 @@ require "base64"
|
|
3
3
|
|
4
4
|
class CouchRest
|
5
5
|
class Database
|
6
|
-
|
7
|
-
|
6
|
+
attr_reader :server, :host, :name, :root
|
7
|
+
|
8
|
+
def initialize server, name
|
8
9
|
@name = name
|
9
|
-
@
|
10
|
+
@server = server
|
11
|
+
@host = server.uri
|
10
12
|
@root = "#{host}/#{name}"
|
11
13
|
end
|
12
14
|
|
@@ -14,6 +16,10 @@ class CouchRest
|
|
14
16
|
@root
|
15
17
|
end
|
16
18
|
|
19
|
+
def info
|
20
|
+
CouchRest.get @root
|
21
|
+
end
|
22
|
+
|
17
23
|
def documents params = nil
|
18
24
|
url = CouchRest.paramify_url "#{@root}/_all_docs", params
|
19
25
|
CouchRest.get url
|
@@ -58,13 +64,18 @@ class CouchRest
|
|
58
64
|
end
|
59
65
|
if doc['_id']
|
60
66
|
slug = CGI.escape(doc['_id'])
|
61
|
-
CouchRest.put "#{@root}/#{slug}", doc
|
62
67
|
else
|
63
|
-
|
68
|
+
slug = doc['_id'] = @server.next_uuid
|
64
69
|
end
|
70
|
+
CouchRest.put "#{@root}/#{slug}", doc
|
65
71
|
end
|
66
72
|
|
67
73
|
def bulk_save docs
|
74
|
+
ids, noids = docs.partition{|d|d['_id']}
|
75
|
+
uuid_count = [noids.length, @server.uuid_batch_count].max
|
76
|
+
noids.each do |doc|
|
77
|
+
doc['_id'] = @server.next_uuid(uuid_count)
|
78
|
+
end
|
68
79
|
CouchRest.post "#{@root}/_bulk_docs", {:docs => docs}
|
69
80
|
end
|
70
81
|
|
@@ -76,7 +87,9 @@ class CouchRest
|
|
76
87
|
def delete!
|
77
88
|
CouchRest.delete @root
|
78
89
|
end
|
90
|
+
|
79
91
|
private
|
92
|
+
|
80
93
|
def encode_attachments attachments
|
81
94
|
attachments.each do |k,v|
|
82
95
|
next if v['stub']
|
@@ -84,6 +97,7 @@ class CouchRest
|
|
84
97
|
end
|
85
98
|
attachments
|
86
99
|
end
|
100
|
+
|
87
101
|
def base64 data
|
88
102
|
Base64.encode64(data).gsub(/\s/,'')
|
89
103
|
end
|
data/lib/pager.rb
CHANGED
@@ -1,19 +1,3 @@
|
|
1
|
-
module Enumerable
|
2
|
-
def group_by
|
3
|
-
inject({}) do |grouped, element|
|
4
|
-
(grouped[yield(element)] ||= []) << element
|
5
|
-
grouped
|
6
|
-
end
|
7
|
-
end unless [].respond_to?(:group_by)
|
8
|
-
|
9
|
-
def group_by_fast
|
10
|
-
inject({}) do |grouped, element|
|
11
|
-
(grouped[yield(element)] ||= []) << element
|
12
|
-
grouped
|
13
|
-
end
|
14
|
-
end
|
15
|
-
end
|
16
|
-
|
17
1
|
class CouchRest
|
18
2
|
class Pager
|
19
3
|
attr_accessor :db
|
@@ -74,12 +58,6 @@ class CouchRest
|
|
74
58
|
end
|
75
59
|
startkey = endkey
|
76
60
|
end
|
77
|
-
|
78
|
-
# grouped = rows.group_by{|r|r['key']}
|
79
|
-
# grouped.each do |k, rs|
|
80
|
-
# vs = rs.collect{|r|r['value']}
|
81
|
-
# yield(k,vs)
|
82
|
-
# end
|
83
61
|
|
84
62
|
key = :begin
|
85
63
|
values = []
|
data/lib/streamer.rb
ADDED
@@ -0,0 +1,29 @@
|
|
1
|
+
class CouchRest
|
2
|
+
class Streamer
|
3
|
+
attr_accessor :db
|
4
|
+
def initialize db
|
5
|
+
@db = db
|
6
|
+
end
|
7
|
+
|
8
|
+
def view name, params = nil
|
9
|
+
urlst = /^_/.match(name) ? "#{@db.root}/#{name}" : "#{@db.root}/_view/#{name}"
|
10
|
+
url = CouchRest.paramify_url urlst, params
|
11
|
+
IO.popen("curl --silent #{url}") do |view|
|
12
|
+
view.gets # discard header
|
13
|
+
while row = parse_line(view.gets)
|
14
|
+
yield row
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def parse_line line
|
22
|
+
return nil unless line
|
23
|
+
if /(\{.*\}),?/.match(line.chomp)
|
24
|
+
JSON.parse($1)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
end
|
29
|
+
end
|
data/spec/couchrest_spec.rb
CHANGED
@@ -26,10 +26,12 @@ describe CouchRest do
|
|
26
26
|
end
|
27
27
|
end
|
28
28
|
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
29
|
+
it "should restart" do
|
30
|
+
@cr.restart!
|
31
|
+
end
|
32
|
+
|
33
|
+
it "should provide one-time access to uuids" do
|
34
|
+
@cr.next_uuid.should_not be_nil
|
33
35
|
end
|
34
36
|
|
35
37
|
describe "initializing a database" do
|
@@ -40,6 +42,25 @@ describe CouchRest do
|
|
40
42
|
end
|
41
43
|
end
|
42
44
|
|
45
|
+
describe "easy initializing a database adapter" do
|
46
|
+
it "should be possible without an explicit CouchRest instantiation" do
|
47
|
+
db = CouchRest.database "http://localhost:5984/couchrest-test"
|
48
|
+
db.should be_an_instance_of(CouchRest::Database)
|
49
|
+
db.host.should == "http://localhost:5984"
|
50
|
+
end
|
51
|
+
it "should not create the database automatically" do
|
52
|
+
db = CouchRest.database "http://localhost:5984/couchrest-test"
|
53
|
+
lambda{db.info}.should raise_error(RestClient::ResourceNotFound)
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
describe "ensuring the db exists" do
|
58
|
+
it "should be super easy" do
|
59
|
+
db = CouchRest.database! "http://localhost:5984/couchrest-test-2"
|
60
|
+
db.info["db_name"].should == 'couchrest-test-2'
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
43
64
|
describe "successfully creating a database" do
|
44
65
|
it "should start without a database" do
|
45
66
|
@cr.databases.should_not include(TESTDB)
|
data/spec/database_spec.rb
CHANGED
@@ -137,6 +137,17 @@ describe CouchRest::Database do
|
|
137
137
|
@db.get(r['id'])
|
138
138
|
end
|
139
139
|
end
|
140
|
+
|
141
|
+
it "should use uuids when ids aren't provided" do
|
142
|
+
@db.server.stub!(:next_uuid).and_return('asdf6sgadkfhgsdfusdf')
|
143
|
+
|
144
|
+
docs = [{'key' => 'value'}, {'_id' => 'totally-uniq'}]
|
145
|
+
id_docs = [{'key' => 'value', '_id' => 'asdf6sgadkfhgsdfusdf'}, {'_id' => 'totally-uniq'}]
|
146
|
+
CouchRest.should_receive(:post).with("http://localhost:5984/couchrest-test/_bulk_docs", {:docs => id_docs})
|
147
|
+
|
148
|
+
@db.bulk_save(docs)
|
149
|
+
end
|
150
|
+
|
140
151
|
it "should add them with uniq ids" do
|
141
152
|
rs = @db.bulk_save([
|
142
153
|
{"_id" => "oneB", "wild" => "and random"},
|
@@ -147,6 +158,7 @@ describe CouchRest::Database do
|
|
147
158
|
@db.get(r['id'])
|
148
159
|
end
|
149
160
|
end
|
161
|
+
|
150
162
|
it "in the case of an id conflict should not insert anything" do
|
151
163
|
@r = @db.save({'lemons' => 'from texas', 'and' => 'how', "_id" => "oneB"})
|
152
164
|
|
@@ -159,12 +171,25 @@ describe CouchRest::Database do
|
|
159
171
|
end.should raise_error(RestClient::RequestFailed)
|
160
172
|
|
161
173
|
lambda do
|
162
|
-
@db.get('twoB')
|
174
|
+
@db.get('twoB')
|
163
175
|
end.should raise_error(RestClient::ResourceNotFound)
|
164
176
|
end
|
177
|
+
it "should raise an error that is useful for recovery" do
|
178
|
+
@r = @db.save({"_id" => "taken", "field" => "stuff"})
|
179
|
+
begin
|
180
|
+
rs = @db.bulk_save([
|
181
|
+
{"_id" => "taken", "wild" => "and random"},
|
182
|
+
{"_id" => "free", "mild" => "yet local"},
|
183
|
+
{"another" => ["set","of","keys"]}
|
184
|
+
])
|
185
|
+
rescue RestClient::RequestFailed => e
|
186
|
+
# soon CouchDB will provide _which_ docs conflicted
|
187
|
+
JSON.parse(e.response.body)['error'].should == 'conflict'
|
188
|
+
end
|
189
|
+
end
|
165
190
|
end
|
166
191
|
|
167
|
-
describe "
|
192
|
+
describe "new document without an id" do
|
168
193
|
it "should start empty" do
|
169
194
|
@db.documents["total_rows"].should == 0
|
170
195
|
end
|
@@ -173,6 +198,11 @@ describe CouchRest::Database do
|
|
173
198
|
r2 = @db.get(r['id'])
|
174
199
|
r2["lemons"].should == "from texas"
|
175
200
|
end
|
201
|
+
it "should use PUT with UUIDs" do
|
202
|
+
CouchRest.should_receive(:put)
|
203
|
+
r = @db.save({'just' => ['another document']})
|
204
|
+
end
|
205
|
+
|
176
206
|
end
|
177
207
|
|
178
208
|
describe "PUT document with attachment" do
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require File.dirname(__FILE__) + '/spec_helper'
|
2
|
+
|
3
|
+
describe CouchRest::Streamer do
|
4
|
+
before(:all) do
|
5
|
+
@cr = CouchRest.new(COUCHHOST)
|
6
|
+
@db = @cr.database(TESTDB)
|
7
|
+
@db.delete! rescue nil
|
8
|
+
@db = @cr.create_db(TESTDB) rescue nil
|
9
|
+
@streamer = CouchRest::Streamer.new(@db)
|
10
|
+
@docs = (1..1000).collect{|i| {:integer => i, :string => i.to_s}}
|
11
|
+
@db.bulk_save(@docs)
|
12
|
+
end
|
13
|
+
|
14
|
+
it "should yield each row in a view" do
|
15
|
+
count = 0
|
16
|
+
sum = 0
|
17
|
+
@streamer.view("_all_docs") do |row|
|
18
|
+
count += 1
|
19
|
+
end
|
20
|
+
count.should == 1000
|
21
|
+
end
|
22
|
+
|
23
|
+
end
|
metadata
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: jchris-couchrest
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.9.
|
4
|
+
version: 0.9.2
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- J. Chris Anderson
|
@@ -46,14 +46,16 @@ files:
|
|
46
46
|
- lib/database.rb
|
47
47
|
- lib/pager.rb
|
48
48
|
- lib/file_manager.rb
|
49
|
+
- lib/streamer.rb
|
49
50
|
- Rakefile
|
50
|
-
- README
|
51
|
+
- README.markdown
|
51
52
|
- bin/couchdir
|
52
53
|
- bin/couchview
|
53
54
|
- spec/couchrest_spec.rb
|
54
55
|
- spec/database_spec.rb
|
55
56
|
- spec/pager_spec.rb
|
56
57
|
- spec/file_manager_spec.rb
|
58
|
+
- spec/streamer_spec.rb
|
57
59
|
- spec/spec_helper.rb
|
58
60
|
has_rdoc: false
|
59
61
|
homepage: http://github.com/jchris/couchrest
|
data/README
DELETED
@@ -1,39 +0,0 @@
|
|
1
|
-
Couchrest is a loose port of the couch.js library from CouchDB’s Futon admin interface, which I find to be concise, clear, and well designed.
|
2
|
-
|
3
|
-
I prefer to stay close to the metal, especially when the metal is as clean and simple as CouchDB’s HTTP API. The main thing Couchrest does for you is manage the Ruby <=> JSON serialization, and point your actions (database creation, document CRUD, view creation and queries, etc) at the appropriate API endpoints.
|
4
|
-
|
5
|
-
The core of Couchrest is Heroku’s excellent REST Client Ruby HTTP wrapper. REST Client takes all the nastyness of Net::HTTP and gives is a pretty face, while still giving you more control than Open-URI. I recommend it anytime you’re interfacing with a well-defined API. (We use it for Grabb.it’s Tumblr integration, and it works like a charm.)
|
6
|
-
|
7
|
-
The most complete source of documentation are the spec files. To ensure that your environment is setup for successful CouchRest operation, from the project root directory run `autotest` or `spec spec` (requires RSpec and optionally ZenTest for autotest support).
|
8
|
-
|
9
|
-
Quick Start:
|
10
|
-
|
11
|
-
@cr = CouchRest.new("http://localhost:5984")
|
12
|
-
@db = @cr.create_db('couchrest-test')
|
13
|
-
response = @db.save({:key => 'value', 'another key' => 'another value'})
|
14
|
-
doc = @db.get(response['id'])
|
15
|
-
puts doc.inspect
|
16
|
-
|
17
|
-
|
18
|
-
Bulk Save:
|
19
|
-
|
20
|
-
@db.bulk_save([
|
21
|
-
{"wild" => "and random"},
|
22
|
-
{"mild" => "yet local"},
|
23
|
-
{"another" => ["set","of","keys"]}
|
24
|
-
])
|
25
|
-
puts @db.documents.inspect # returns ids and revs of the current docs
|
26
|
-
|
27
|
-
Creating and Querying Views:
|
28
|
-
|
29
|
-
@db.save({
|
30
|
-
"_id" => "_design/first",
|
31
|
-
:views => {
|
32
|
-
:test => {
|
33
|
-
:map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}"
|
34
|
-
}
|
35
|
-
}
|
36
|
-
})
|
37
|
-
puts @db.view('first/test')['rows'].inspect
|
38
|
-
|
39
|
-
|