jgre-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.
Files changed (47) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +68 -0
  3. data/Rakefile +66 -0
  4. data/THANKS.md +18 -0
  5. data/examples/model/example.rb +138 -0
  6. data/examples/word_count/markov +38 -0
  7. data/examples/word_count/views/books/chunked-map.js +3 -0
  8. data/examples/word_count/views/books/united-map.js +1 -0
  9. data/examples/word_count/views/markov/chain-map.js +6 -0
  10. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  11. data/examples/word_count/views/word_count/count-map.js +6 -0
  12. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  13. data/examples/word_count/word_count.rb +46 -0
  14. data/examples/word_count/word_count_query.rb +40 -0
  15. data/examples/word_count/word_count_views.rb +26 -0
  16. data/lib/couchrest.rb +139 -0
  17. data/lib/couchrest/commands/generate.rb +71 -0
  18. data/lib/couchrest/commands/push.rb +103 -0
  19. data/lib/couchrest/core/database.rb +241 -0
  20. data/lib/couchrest/core/design.rb +89 -0
  21. data/lib/couchrest/core/document.rb +94 -0
  22. data/lib/couchrest/core/model.rb +613 -0
  23. data/lib/couchrest/core/server.rb +51 -0
  24. data/lib/couchrest/core/view.rb +4 -0
  25. data/lib/couchrest/helper/pager.rb +103 -0
  26. data/lib/couchrest/helper/streamer.rb +44 -0
  27. data/lib/couchrest/monkeypatches.rb +38 -0
  28. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  29. data/spec/couchrest/core/database_spec.rb +629 -0
  30. data/spec/couchrest/core/design_spec.rb +131 -0
  31. data/spec/couchrest/core/document_spec.rb +213 -0
  32. data/spec/couchrest/core/model_spec.rb +859 -0
  33. data/spec/couchrest/helpers/pager_spec.rb +122 -0
  34. data/spec/couchrest/helpers/streamer_spec.rb +23 -0
  35. data/spec/fixtures/attachments/README +3 -0
  36. data/spec/fixtures/attachments/couchdb.png +0 -0
  37. data/spec/fixtures/attachments/test.html +11 -0
  38. data/spec/fixtures/views/lib.js +3 -0
  39. data/spec/fixtures/views/test_view/lib.js +3 -0
  40. data/spec/fixtures/views/test_view/only-map.js +4 -0
  41. data/spec/fixtures/views/test_view/test-map.js +3 -0
  42. data/spec/fixtures/views/test_view/test-reduce.js +3 -0
  43. data/spec/spec.opts +6 -0
  44. data/spec/spec_helper.rb +20 -0
  45. data/utils/remap.rb +27 -0
  46. data/utils/subset.rb +30 -0
  47. metadata +143 -0
@@ -0,0 +1,51 @@
1
+ module CouchRest
2
+ class Server
3
+ attr_accessor :uri, :uuid_batch_count
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
+ # List all databases on the server
10
+ def databases
11
+ CouchRest.get "#{@uri}/_all_dbs"
12
+ end
13
+
14
+ # Returns a CouchRest::Database for the given name
15
+ def database name
16
+ CouchRest::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
+ CouchRest.get "#{@uri}/"
28
+ end
29
+
30
+ # Create a database
31
+ def create_db name
32
+ CouchRest.put "#{@uri}/#{name}"
33
+ database name
34
+ end
35
+
36
+ # Restart the CouchDB instance
37
+ def restart!
38
+ CouchRest.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 = CouchRest.post("#{@uri}/_uuids?count=#{count}")["uuids"]
46
+ end
47
+ @uuids.pop
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,4 @@
1
+ module CouchRest
2
+ class View
3
+ end
4
+ 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,38 @@
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
+ module RestClient
27
+ def self.copy(url, headers={})
28
+ Request.execute(:method => :copy,
29
+ :url => url,
30
+ :headers => headers)
31
+ end
32
+
33
+ def self.move(url, headers={})
34
+ Request.execute(:method => :move,
35
+ :url => url,
36
+ :headers => headers)
37
+ end
38
+ end
@@ -0,0 +1,201 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe CouchRest do
4
+
5
+ before(:each) do
6
+ @cr = CouchRest.new(COUCHHOST)
7
+ begin
8
+ @db = @cr.database(TESTDB)
9
+ @db.delete! rescue nil
10
+ end
11
+ end
12
+
13
+ after(:each) do
14
+ begin
15
+ @db.delete! rescue nil
16
+ end
17
+ end
18
+
19
+ describe "getting info" do
20
+ it "should list databases" do
21
+ @cr.databases.should be_an_instance_of(Array)
22
+ end
23
+ it "should get info" do
24
+ @cr.info["couchdb"].should == "Welcome"
25
+ @cr.info.class.should == Hash
26
+ end
27
+ end
28
+
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
35
+ end
36
+
37
+ describe "initializing a database" do
38
+ it "should return a db" do
39
+ db = @cr.database(TESTDB)
40
+ db.should be_an_instance_of(CouchRest::Database)
41
+ db.host.should == @cr.uri
42
+ end
43
+ end
44
+
45
+ describe "parsing urls" do
46
+ it "should parse just a dbname" do
47
+ db = CouchRest.parse "my-db"
48
+ db[:database].should == "my-db"
49
+ db[:host].should == "127.0.0.1:5984"
50
+ end
51
+ it "should parse a host and db" do
52
+ db = CouchRest.parse "127.0.0.1/my-db"
53
+ db[:database].should == "my-db"
54
+ db[:host].should == "127.0.0.1"
55
+ end
56
+ it "should parse a host and db with http" do
57
+ db = CouchRest.parse "http://127.0.0.1/my-db"
58
+ db[:database].should == "my-db"
59
+ db[:host].should == "127.0.0.1"
60
+ end
61
+ it "should parse a host with a port and db" do
62
+ db = CouchRest.parse "127.0.0.1:5555/my-db"
63
+ db[:database].should == "my-db"
64
+ db[:host].should == "127.0.0.1:5555"
65
+ end
66
+ it "should parse a host with a port and db with http" do
67
+ db = CouchRest.parse "http://127.0.0.1:5555/my-db"
68
+ db[:database].should == "my-db"
69
+ db[:host].should == "127.0.0.1:5555"
70
+ end
71
+ it "should parse just a host" do
72
+ db = CouchRest.parse "http://127.0.0.1:5555/"
73
+ db[:database].should be_nil
74
+ db[:host].should == "127.0.0.1:5555"
75
+ end
76
+ it "should parse just a host no slash" do
77
+ db = CouchRest.parse "http://127.0.0.1:5555"
78
+ db[:host].should == "127.0.0.1:5555"
79
+ db[:database].should be_nil
80
+ end
81
+ it "should get docid" do
82
+ db = CouchRest.parse "127.0.0.1:5555/my-db/my-doc"
83
+ db[:database].should == "my-db"
84
+ db[:host].should == "127.0.0.1:5555"
85
+ db[:doc].should == "my-doc"
86
+ end
87
+ it "should get docid with http" do
88
+ db = CouchRest.parse "http://127.0.0.1:5555/my-db/my-doc"
89
+ db[:database].should == "my-db"
90
+ db[:host].should == "127.0.0.1:5555"
91
+ db[:doc].should == "my-doc"
92
+ end
93
+
94
+ it "should parse a host and db" do
95
+ db = CouchRest.parse "127.0.0.1/my-db"
96
+ db[:database].should == "my-db"
97
+ db[:host].should == "127.0.0.1"
98
+ end
99
+ it "should parse a host and db with http" do
100
+ db = CouchRest.parse "http://127.0.0.1/my-db"
101
+ db[:database].should == "my-db"
102
+ db[:host].should == "127.0.0.1"
103
+ end
104
+ it "should parse a host with a port and db" do
105
+ db = CouchRest.parse "127.0.0.1:5555/my-db"
106
+ db[:database].should == "my-db"
107
+ db[:host].should == "127.0.0.1:5555"
108
+ end
109
+ it "should parse a host with a port and db with http" do
110
+ db = CouchRest.parse "http://127.0.0.1:5555/my-db"
111
+ db[:database].should == "my-db"
112
+ db[:host].should == "127.0.0.1:5555"
113
+ end
114
+ it "should parse just a host" do
115
+ db = CouchRest.parse "http://127.0.0.1:5555/"
116
+ db[:database].should be_nil
117
+ db[:host].should == "127.0.0.1:5555"
118
+ end
119
+ it "should parse just a host no slash" do
120
+ db = CouchRest.parse "http://127.0.0.1:5555"
121
+ db[:host].should == "127.0.0.1:5555"
122
+ db[:database].should be_nil
123
+ end
124
+ it "should get docid" do
125
+ db = CouchRest.parse "127.0.0.1:5555/my-db/my-doc"
126
+ db[:database].should == "my-db"
127
+ db[:host].should == "127.0.0.1:5555"
128
+ db[:doc].should == "my-doc"
129
+ end
130
+ it "should get docid with http" do
131
+ db = CouchRest.parse "http://127.0.0.1:5555/my-db/my-doc"
132
+ db[:database].should == "my-db"
133
+ db[:host].should == "127.0.0.1:5555"
134
+ db[:doc].should == "my-doc"
135
+ end
136
+ end
137
+
138
+ describe "easy initializing a database adapter" do
139
+ it "should be possible without an explicit CouchRest instantiation" do
140
+ db = CouchRest.database "http://127.0.0.1:5984/couchrest-test"
141
+ db.should be_an_instance_of(CouchRest::Database)
142
+ db.host.should == "127.0.0.1:5984"
143
+ end
144
+ # TODO add https support (need test environment...)
145
+ # it "should work with https" # do
146
+ # db = CouchRest.database "https://127.0.0.1:5984/couchrest-test"
147
+ # db.host.should == "https://127.0.0.1:5984"
148
+ # end
149
+ it "should not create the database automatically" do
150
+ db = CouchRest.database "http://127.0.0.1:5984/couchrest-test"
151
+ lambda{db.info}.should raise_error(RestClient::ResourceNotFound)
152
+ end
153
+ end
154
+
155
+ describe "ensuring the db exists" do
156
+ it "should be super easy" do
157
+ db = CouchRest.database! "http://127.0.0.1:5984/couchrest-test-2"
158
+ db.name.should == 'couchrest-test-2'
159
+ db.info["db_name"].should == 'couchrest-test-2'
160
+ end
161
+ end
162
+
163
+ describe "successfully creating a database" do
164
+ it "should start without a database" do
165
+ @cr.databases.should_not include(TESTDB)
166
+ end
167
+ it "should return the created database" do
168
+ db = @cr.create_db(TESTDB)
169
+ db.should be_an_instance_of(CouchRest::Database)
170
+ end
171
+ it "should create the database" do
172
+ db = @cr.create_db(TESTDB)
173
+ @cr.databases.should include(TESTDB)
174
+ end
175
+ end
176
+
177
+ describe "failing to create a database because the name is taken" do
178
+ before(:each) do
179
+ db = @cr.create_db(TESTDB)
180
+ end
181
+ it "should start with the test database" do
182
+ @cr.databases.should include(TESTDB)
183
+ end
184
+ it "should PUT the database and raise an error" do
185
+ lambda{
186
+ @cr.create_db(TESTDB)
187
+ }.should raise_error(RestClient::Request::RequestFailed)
188
+ end
189
+ end
190
+
191
+ describe "using a proxy for RestClient connections" do
192
+ it "should set proxy url for RestClient" do
193
+ CouchRest.proxy 'http://localhost:8888/'
194
+ proxy_uri = URI.parse(RestClient.proxy)
195
+ proxy_uri.host.should eql( 'localhost' )
196
+ proxy_uri.port.should eql( 8888 )
197
+ CouchRest.proxy nil
198
+ end
199
+ end
200
+
201
+ end
@@ -0,0 +1,629 @@
1
+ require File.dirname(__FILE__) + '/../../spec_helper'
2
+
3
+ describe CouchRest::Database do
4
+ before(:each) 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
+ end
10
+
11
+ describe "map query with _temp_view in Javascript" do
12
+ before(:each) do
13
+ @db.bulk_save([
14
+ {"wild" => "and random"},
15
+ {"mild" => "yet local"},
16
+ {"another" => ["set","of","keys"]}
17
+ ])
18
+ @temp_view = {:map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}"}
19
+ end
20
+ it "should return the result of the temporary function" do
21
+ rs = @db.temp_view(@temp_view)
22
+ rs['rows'].select{|r|r['key'] == 'wild' && r['value'] == 'and random'}.length.should == 1
23
+ end
24
+ it "should work with a range" do
25
+ rs = @db.temp_view(@temp_view, :startkey => "b", :endkey => "z")
26
+ rs['rows'].length.should == 2
27
+ end
28
+ it "should work with a key" do
29
+ rs = @db.temp_view(@temp_view, :key => "wild")
30
+ rs['rows'].length.should == 1
31
+ end
32
+ it "should work with a limit" do
33
+ rs = @db.temp_view(@temp_view, :limit => 1)
34
+ rs['rows'].length.should == 1
35
+ end
36
+ it "should work with multi-keys" do
37
+ rs = @db.temp_view(@temp_view, :keys => ["another", "wild"])
38
+ rs['rows'].length.should == 2
39
+ end
40
+ end
41
+
42
+ describe "map/reduce query with _temp_view in Javascript" do
43
+ before(:each) do
44
+ @db.bulk_save([
45
+ {"beverage" => "beer", :count => 4},
46
+ {"beverage" => "beer", :count => 2},
47
+ {"beverage" => "tea", :count => 3}
48
+ ])
49
+ end
50
+ it "should return the result of the temporary function" do
51
+ rs = @db.temp_view(:map => "function(doc){emit(doc.beverage, doc.count)}", :reduce => "function(beverage,counts){return sum(counts)}")
52
+ # rs.should == 'x'
53
+ rs['rows'][0]['value'].should == 9
54
+ end
55
+ end
56
+
57
+ describe "saving a view" do
58
+ before(:each) do
59
+ @view = {'test' => {'map' => 'function(doc) {
60
+ if (doc.word && !/\W/.test(doc.word)) {
61
+ emit(doc.word,null);
62
+ }
63
+ }'}}
64
+ @db.save({
65
+ "_id" => "_design/test",
66
+ :views => @view
67
+ })
68
+ end
69
+ it "should work properly" do
70
+ @db.bulk_save([
71
+ {"word" => "once"},
72
+ {"word" => "and again"}
73
+ ])
74
+ @db.view('test/test')['total_rows'].should == 1
75
+ end
76
+ it "should round trip" do
77
+ @db.get("_design/test")['views'].should == @view
78
+ end
79
+ end
80
+
81
+ describe "select from an existing view" do
82
+ before(:each) do
83
+ r = @db.save({
84
+ "_id" => "_design/first",
85
+ :views => {
86
+ :test => {
87
+ :map => "function(doc){for(var w in doc){ if(!w.match(/^_/))emit(w,doc[w])}}"
88
+ }
89
+ }
90
+ })
91
+ @db.bulk_save([
92
+ {"wild" => "and random"},
93
+ {"mild" => "yet local"},
94
+ {"another" => ["set","of","keys"]}
95
+ ])
96
+ end
97
+ it "should have the view" do
98
+ @db.get('_design/first')['views']['test']['map'].should include("for(var w in doc)")
99
+ end
100
+ it "should list from the view" do
101
+ rs = @db.view('first/test')
102
+ rs['rows'].select{|r|r['key'] == 'wild' && r['value'] == 'and random'}.length.should == 1
103
+ end
104
+ it "should work with a range" do
105
+ rs = @db.view('first/test', :startkey => "b", :endkey => "z")
106
+ rs['rows'].length.should == 2
107
+ end
108
+ it "should work with a key" do
109
+ rs = @db.view('first/test', :key => "wild")
110
+ rs['rows'].length.should == 1
111
+ end
112
+ it "should work with a limit" do
113
+ rs = @db.view('first/test', :limit => 1)
114
+ rs['rows'].length.should == 1
115
+ end
116
+ it "should work with multi-keys" do
117
+ rs = @db.view('first/test', :keys => ["another", "wild"])
118
+ rs['rows'].length.should == 2
119
+ end
120
+ it "should accept a block" do
121
+ rows = []
122
+ rs = @db.view('first/test', :include_docs => true) do |row|
123
+ rows << row
124
+ end
125
+ rows.length.should == 4
126
+ rs["total_rows"].should == 3
127
+ end
128
+ end
129
+
130
+ describe "GET (document by id) when the doc exists" do
131
+ before(:each) do
132
+ @r = @db.save({'lemons' => 'from texas', 'and' => 'spain'})
133
+ @docid = "http://example.com/stuff.cgi?things=and%20stuff"
134
+ @db.save({'_id' => @docid, 'will-exist' => 'here'})
135
+ end
136
+ it "should get the document" do
137
+ doc = @db.get(@r['id'])
138
+ doc['lemons'].should == 'from texas'
139
+ end
140
+ it "should work with a funky id" do
141
+ @db.get(@docid)['will-exist'].should == 'here'
142
+ end
143
+ end
144
+
145
+ describe "POST (adding bulk documents)" do
146
+ it "should add them without ids" do
147
+ rs = @db.bulk_save([
148
+ {"wild" => "and random"},
149
+ {"mild" => "yet local"},
150
+ {"another" => ["set","of","keys"]}
151
+ ])
152
+ rs['new_revs'].each do |r|
153
+ @db.get(r['id'])
154
+ end
155
+ end
156
+
157
+ it "should use uuids when ids aren't provided" do
158
+ @db.server.stub!(:next_uuid).and_return('asdf6sgadkfhgsdfusdf')
159
+
160
+ docs = [{'key' => 'value'}, {'_id' => 'totally-uniq'}]
161
+ id_docs = [{'key' => 'value', '_id' => 'asdf6sgadkfhgsdfusdf'}, {'_id' => 'totally-uniq'}]
162
+ CouchRest.should_receive(:post).with("http://127.0.0.1:5984/couchrest-test/_bulk_docs", {:docs => id_docs})
163
+
164
+ @db.bulk_save(docs)
165
+ end
166
+
167
+ it "should add them with uniq ids" do
168
+ rs = @db.bulk_save([
169
+ {"_id" => "oneB", "wild" => "and random"},
170
+ {"_id" => "twoB", "mild" => "yet local"},
171
+ {"another" => ["set","of","keys"]}
172
+ ])
173
+ rs['new_revs'].each do |r|
174
+ @db.get(r['id'])
175
+ end
176
+ end
177
+
178
+ it "in the case of an id conflict should not insert anything" do
179
+ @r = @db.save({'lemons' => 'from texas', 'and' => 'how', "_id" => "oneB"})
180
+
181
+ lambda do
182
+ rs = @db.bulk_save([
183
+ {"_id" => "oneB", "wild" => "and random"},
184
+ {"_id" => "twoB", "mild" => "yet local"},
185
+ {"another" => ["set","of","keys"]}
186
+ ])
187
+ end.should raise_error(RestClient::RequestFailed)
188
+
189
+ lambda do
190
+ @db.get('twoB')
191
+ end.should raise_error(RestClient::ResourceNotFound)
192
+ end
193
+
194
+ it "should empty the bulk save cache if no documents are given" do
195
+ @db.save({"_id" => "bulk_cache_1", "val" => "test"}, true)
196
+ lambda do
197
+ @db.get('bulk_cache_1')
198
+ end.should raise_error(RestClient::ResourceNotFound)
199
+ @db.bulk_save
200
+ @db.get("bulk_cache_1")["val"].should == "test"
201
+ end
202
+
203
+ it "should raise an error that is useful for recovery" do
204
+ @r = @db.save({"_id" => "taken", "field" => "stuff"})
205
+ begin
206
+ rs = @db.bulk_save([
207
+ {"_id" => "taken", "wild" => "and random"},
208
+ {"_id" => "free", "mild" => "yet local"},
209
+ {"another" => ["set","of","keys"]}
210
+ ])
211
+ rescue RestClient::RequestFailed => e
212
+ # soon CouchDB will provide _which_ docs conflicted
213
+ JSON.parse(e.response.body)['error'].should == 'conflict'
214
+ end
215
+ end
216
+ end
217
+
218
+ describe "new document without an id" do
219
+ it "should start empty" do
220
+ @db.documents["total_rows"].should == 0
221
+ end
222
+ it "should create the document and return the id" do
223
+ r = @db.save({'lemons' => 'from texas', 'and' => 'spain'})
224
+ r2 = @db.get(r['id'])
225
+ r2["lemons"].should == "from texas"
226
+ end
227
+ it "should use PUT with UUIDs" do
228
+ CouchRest.should_receive(:put).and_return({"ok" => true, "id" => "100", "rev" => "55"})
229
+ r = @db.save({'just' => ['another document']})
230
+ end
231
+
232
+ end
233
+
234
+ describe "PUT attachment from file" do
235
+ before(:each) do
236
+ filename = FIXTURE_PATH + '/attachments/couchdb.png'
237
+ @file = File.open(filename)
238
+ end
239
+ after(:each) do
240
+ @file.close
241
+ end
242
+ it "should save the attachment to a new doc" do
243
+ r = @db.put_attachment({'_id' => 'attach-this'}, 'couchdb.png', image = @file.read, {:content_type => 'image/png'})
244
+ r['ok'].should == true
245
+ attachment = @db.fetch_attachment("attach-this","couchdb.png")
246
+ attachment.should == image
247
+ end
248
+ end
249
+
250
+ describe "PUT document with attachment" do
251
+ before(:each) do
252
+ @attach = "<html><head><title>My Doc</title></head><body><p>Has words.</p></body></html>"
253
+ @doc = {
254
+ "_id" => "mydocwithattachment",
255
+ "field" => ["some value"],
256
+ "_attachments" => {
257
+ "test.html" => {
258
+ "type" => "text/html",
259
+ "data" => @attach
260
+ }
261
+ }
262
+ }
263
+ @db.save(@doc)
264
+ end
265
+ it "should save and be indicated" do
266
+ doc = @db.get("mydocwithattachment")
267
+ doc['_attachments']['test.html']['length'].should == @attach.length
268
+ end
269
+ it "should be there" do
270
+ attachment = @db.fetch_attachment("mydocwithattachment","test.html")
271
+ attachment.should == @attach
272
+ end
273
+ end
274
+
275
+ describe "PUT document with attachment stub" do
276
+ before(:each) do
277
+ @attach = "<html><head><title>My Doc</title></head><body><p>Has words.</p></body></html>"
278
+ doc = {
279
+ '_id' => 'mydocwithattachment',
280
+ 'field' => ['some_value'],
281
+ '_attachments' => {
282
+ 'test.html' => {
283
+ 'type' => 'text/html', 'data' => @attach
284
+ }
285
+ }
286
+ }
287
+ @db.save(doc)
288
+ doc = @db.get('mydocwithattachment')
289
+ doc['field'] << 'another value'
290
+ @db.save(doc)
291
+ end
292
+
293
+ it 'should be there' do
294
+ attachment = @db.fetch_attachment('mydocwithattachment', 'test.html')
295
+ attachment.should == @attach
296
+ end
297
+ end
298
+
299
+ describe "PUT document with multiple attachments" do
300
+ before(:each) do
301
+ @attach = "<html><head><title>My Doc</title></head><body><p>Has words.</p></body></html>"
302
+ @attach2 = "<html><head><title>Other Doc</title></head><body><p>Has more words.</p></body></html>"
303
+ @doc = {
304
+ "_id" => "mydocwithattachment",
305
+ "field" => ["some value"],
306
+ "_attachments" => {
307
+ "test.html" => {
308
+ "type" => "text/html",
309
+ "data" => @attach
310
+ },
311
+ "other.html" => {
312
+ "type" => "text/html",
313
+ "data" => @attach2
314
+ }
315
+ }
316
+ }
317
+ @db.save(@doc)
318
+ end
319
+ it "should save and be indicated" do
320
+ doc = @db.get("mydocwithattachment")
321
+ doc['_attachments']['test.html']['length'].should == @attach.length
322
+ doc['_attachments']['other.html']['length'].should == @attach2.length
323
+ end
324
+ it "should be there" do
325
+ attachment = @db.fetch_attachment("mydocwithattachment","test.html")
326
+ attachment.should == @attach
327
+ end
328
+ it "should be there" do
329
+ attachment = @db.fetch_attachment("mydocwithattachment","other.html")
330
+ attachment.should == @attach2
331
+ end
332
+ end
333
+
334
+ describe "POST document with attachment (with funky name)" do
335
+ before(:each) do
336
+ @attach = "<html><head><title>My Funky Doc</title></head><body><p>Has words.</p></body></html>"
337
+ @doc = {
338
+ "field" => ["some other value"],
339
+ "_attachments" => {
340
+ "http://example.com/stuff.cgi?things=and%20stuff" => {
341
+ "type" => "text/html",
342
+ "data" => @attach
343
+ }
344
+ }
345
+ }
346
+ @docid = @db.save(@doc)['id']
347
+ end
348
+ it "should save and be indicated" do
349
+ doc = @db.get(@docid)
350
+ doc['_attachments']['http://example.com/stuff.cgi?things=and%20stuff']['length'].should == @attach.length
351
+ end
352
+ it "should be there" do
353
+ attachment = @db.fetch_attachment(@docid,"http://example.com/stuff.cgi?things=and%20stuff")
354
+ attachment.should == @attach
355
+ end
356
+ end
357
+
358
+ describe "PUT (new document with url id)" do
359
+ it "should create the document" do
360
+ @docid = "http://example.com/stuff.cgi?things=and%20stuff"
361
+ @db.save({'_id' => @docid, 'will-exist' => 'here'})
362
+ lambda{@db.save({'_id' => @docid})}.should raise_error(RestClient::Request::RequestFailed)
363
+ @db.get(@docid)['will-exist'].should == 'here'
364
+ end
365
+ end
366
+
367
+ describe "PUT (new document with id)" do
368
+ it "should start without the document" do
369
+ # r = @db.save({'lemons' => 'from texas', 'and' => 'spain'})
370
+ @db.documents['rows'].each do |doc|
371
+ doc['id'].should_not == 'my-doc'
372
+ end
373
+ # should_not include({'_id' => 'my-doc'})
374
+ # this needs to be a loop over docs on content with the post
375
+ # or instead make it return something with a fancy <=> method
376
+ end
377
+ it "should create the document" do
378
+ @db.save({'_id' => 'my-doc', 'will-exist' => 'here'})
379
+ lambda{@db.save({'_id' => 'my-doc'})}.should raise_error(RestClient::Request::RequestFailed)
380
+ end
381
+ end
382
+
383
+ describe "PUT (existing document with rev)" do
384
+ before(:each) do
385
+ @db.save({'_id' => 'my-doc', 'will-exist' => 'here'})
386
+ @doc = @db.get('my-doc')
387
+ @docid = "http://example.com/stuff.cgi?things=and%20stuff"
388
+ @db.save({'_id' => @docid, 'now' => 'save'})
389
+ end
390
+ it "should start with the document" do
391
+ @doc['will-exist'].should == 'here'
392
+ @db.get(@docid)['now'].should == 'save'
393
+ end
394
+ it "should save with url id" do
395
+ doc = @db.get(@docid)
396
+ doc['yaml'] = ['json', 'word.']
397
+ @db.save doc
398
+ @db.get(@docid)['yaml'].should == ['json', 'word.']
399
+ end
400
+ it "should fail to resave without the rev" do
401
+ @doc['them-keys'] = 'huge'
402
+ @doc['_rev'] = 'wrong'
403
+ # @db.save(@doc)
404
+ lambda {@db.save(@doc)}.should raise_error
405
+ end
406
+ it "should update the document" do
407
+ @doc['them-keys'] = 'huge'
408
+ @db.save(@doc)
409
+ now = @db.get('my-doc')
410
+ now['them-keys'].should == 'huge'
411
+ end
412
+ end
413
+
414
+ describe "cached bulk save" do
415
+ it "stores documents in a database-specific cache" do
416
+ td = {"_id" => "btd1", "val" => "test"}
417
+ @db.save(td, true)
418
+ @db.instance_variable_get("@bulk_save_cache").should == [td]
419
+
420
+ end
421
+
422
+ it "doesn't save to the database until the configured cache size is exceded" do
423
+ @db.bulk_save_cache_limit = 3
424
+ td1 = {"_id" => "td1", "val" => true}
425
+ td2 = {"_id" => "td2", "val" => 4}
426
+ @db.save(td1, true)
427
+ @db.save(td2, true)
428
+ lambda do
429
+ @db.get(td1["_id"])
430
+ end.should raise_error(RestClient::ResourceNotFound)
431
+ lambda do
432
+ @db.get(td2["_id"])
433
+ end.should raise_error(RestClient::ResourceNotFound)
434
+ td3 = {"_id" => "td3", "val" => "foo"}
435
+ @db.save(td3, true)
436
+ @db.get(td1["_id"])["val"].should == td1["val"]
437
+ @db.get(td2["_id"])["val"].should == td2["val"]
438
+ @db.get(td3["_id"])["val"].should == td3["val"]
439
+ end
440
+
441
+ it "clears the bulk save cache the first time a non bulk save is requested" do
442
+ td1 = {"_id" => "blah", "val" => true}
443
+ td2 = {"_id" => "steve", "val" => 3}
444
+ @db.bulk_save_cache_limit = 50
445
+ @db.save(td1, true)
446
+ lambda do
447
+ @db.get(td1["_id"])
448
+ end.should raise_error(RestClient::ResourceNotFound)
449
+ @db.save(td2)
450
+ @db.get(td1["_id"])["val"].should == td1["val"]
451
+ @db.get(td2["_id"])["val"].should == td2["val"]
452
+ end
453
+ end
454
+
455
+ describe "DELETE existing document" do
456
+ before(:each) do
457
+ @r = @db.save({'lemons' => 'from texas', 'and' => 'spain'})
458
+ @docid = "http://example.com/stuff.cgi?things=and%20stuff"
459
+ @db.save({'_id' => @docid, 'will-exist' => 'here'})
460
+ end
461
+ it "should work" do
462
+ doc = @db.get(@r['id'])
463
+ doc['and'].should == 'spain'
464
+ @db.delete doc
465
+ lambda{@db.get @r['id']}.should raise_error
466
+ end
467
+ it "should work with uri id" do
468
+ doc = @db.get(@docid)
469
+ @db.delete doc
470
+ lambda{@db.get @docid}.should raise_error
471
+ end
472
+ it "should fail without an _id" do
473
+ lambda{@db.delete({"not"=>"a real doc"})}.should raise_error(ArgumentError)
474
+ end
475
+ it "should defer actual deletion when using bulk save" do
476
+ doc = @db.get(@docid)
477
+ @db.delete doc, true
478
+ lambda{@db.get @docid}.should_not raise_error
479
+ @db.bulk_save
480
+ lambda{@db.get @docid}.should raise_error
481
+ end
482
+
483
+ end
484
+
485
+ describe "COPY existing document" do
486
+ before :each do
487
+ @r = @db.save({'artist' => 'Zappa', 'title' => 'Muffin Man'})
488
+ @docid = 'tracks/zappa/muffin-man'
489
+ @doc = @db.get(@r['id'])
490
+ end
491
+ describe "to a new location" do
492
+ it "should work" do
493
+ @db.copy @doc, @docid
494
+ newdoc = @db.get(@docid)
495
+ newdoc['artist'].should == 'Zappa'
496
+ end
497
+ it "should fail without an _id" do
498
+ lambda{@db.copy({"not"=>"a real doc"})}.should raise_error(ArgumentError)
499
+ end
500
+ end
501
+ describe "to an existing location" do
502
+ before :each do
503
+ @db.save({'_id' => @docid, 'will-exist' => 'here'})
504
+ end
505
+ it "should fail without a rev" do
506
+ lambda{@db.copy @doc, @docid}.should raise_error(RestClient::RequestFailed)
507
+ end
508
+ it "should succeed with a rev" do
509
+ @to_be_overwritten = @db.get(@docid)
510
+ @db.copy @doc, "#{@docid}?rev=#{@to_be_overwritten['_rev']}"
511
+ newdoc = @db.get(@docid)
512
+ newdoc['artist'].should == 'Zappa'
513
+ end
514
+ it "should succeed given the doc to overwrite" do
515
+ @to_be_overwritten = @db.get(@docid)
516
+ @db.copy @doc, @to_be_overwritten
517
+ newdoc = @db.get(@docid)
518
+ newdoc['artist'].should == 'Zappa'
519
+ end
520
+ end
521
+ end
522
+
523
+ describe "MOVE existing document" do
524
+ before :each do
525
+ @r = @db.save({'artist' => 'Zappa', 'title' => 'Muffin Man'})
526
+ @docid = 'tracks/zappa/muffin-man'
527
+ @doc = @db.get(@r['id'])
528
+ end
529
+ describe "to a new location" do
530
+ it "should work" do
531
+ @db.move @doc, @docid
532
+ newdoc = @db.get(@docid)
533
+ newdoc['artist'].should == 'Zappa'
534
+ lambda {@db.get(@r['id'])}.should raise_error(RestClient::ResourceNotFound)
535
+ end
536
+ it "should fail without an _id or _rev" do
537
+ lambda{@db.move({"not"=>"a real doc"})}.should raise_error(ArgumentError)
538
+ lambda{@db.move({"_id"=>"not a real doc"})}.should raise_error(ArgumentError)
539
+ end
540
+ end
541
+ describe "to an existing location" do
542
+ before :each do
543
+ @db.save({'_id' => @docid, 'will-exist' => 'here'})
544
+ end
545
+ it "should fail without a rev" do
546
+ lambda{@db.move @doc, @docid}.should raise_error(RestClient::RequestFailed)
547
+ lambda{@db.get(@r['id'])}.should_not raise_error
548
+ end
549
+ it "should succeed with a rev" do
550
+ @to_be_overwritten = @db.get(@docid)
551
+ @db.move @doc, "#{@docid}?rev=#{@to_be_overwritten['_rev']}"
552
+ newdoc = @db.get(@docid)
553
+ newdoc['artist'].should == 'Zappa'
554
+ lambda {@db.get(@r['id'])}.should raise_error(RestClient::ResourceNotFound)
555
+ end
556
+ it "should succeed given the doc to overwrite" do
557
+ @to_be_overwritten = @db.get(@docid)
558
+ @db.move @doc, @to_be_overwritten
559
+ newdoc = @db.get(@docid)
560
+ newdoc['artist'].should == 'Zappa'
561
+ lambda {@db.get(@r['id'])}.should raise_error(RestClient::ResourceNotFound)
562
+ end
563
+ end
564
+ end
565
+
566
+
567
+ it "should list documents" do
568
+ 5.times do
569
+ @db.save({'another' => 'doc', 'will-exist' => 'anywhere'})
570
+ end
571
+ ds = @db.documents
572
+ ds['rows'].should be_an_instance_of(Array)
573
+ ds['rows'][0]['id'].should_not be_nil
574
+ ds['total_rows'].should == 5
575
+ end
576
+
577
+ describe "documents / _all_docs" do
578
+ before(:each) do
579
+ 9.times do |i|
580
+ @db.save({'_id' => "doc#{i}",'another' => 'doc', 'will-exist' => 'here'})
581
+ end
582
+ end
583
+ it "should list documents with keys and such" do
584
+ ds = @db.documents
585
+ ds['rows'].should be_an_instance_of(Array)
586
+ ds['rows'][0]['id'].should == "doc0"
587
+ ds['total_rows'].should == 9
588
+ end
589
+ it "should take query params" do
590
+ ds = @db.documents(:startkey => 'doc0', :endkey => 'doc3')
591
+ ds['rows'].length.should == 4
592
+ ds = @db.documents(:key => 'doc0')
593
+ ds['rows'].length.should == 1
594
+ end
595
+ it "should work with multi-key" do
596
+ rs = @db.documents :keys => ["doc0", "doc7"]
597
+ rs['rows'].length.should == 2
598
+ end
599
+ it "should work with include_docs" do
600
+ ds = @db.documents(:startkey => 'doc0', :endkey => 'doc3', :include_docs => true)
601
+ ds['rows'][0]['doc']['another'].should == "doc"
602
+ end
603
+ end
604
+
605
+
606
+ describe "compacting a database" do
607
+ it "should compact the database" do
608
+ db = @cr.database('couchrest-test')
609
+ # r =
610
+ db.compact!
611
+ # r['ok'].should == true
612
+ end
613
+ end
614
+
615
+ describe "deleting a database" do
616
+ it "should start with the test database" do
617
+ @cr.databases.should include('couchrest-test')
618
+ end
619
+ it "should delete the database" do
620
+ db = @cr.database('couchrest-test')
621
+ # r =
622
+ db.delete!
623
+ # r['ok'].should == true
624
+ @cr.databases.should_not include('couchrest-test')
625
+ end
626
+ end
627
+
628
+
629
+ end