couchrest 0.12.2

Sign up to get free protection for your applications and to get access to all the features.
Files changed (47) hide show
  1. data/LICENSE +176 -0
  2. data/README.md +69 -0
  3. data/Rakefile +103 -0
  4. data/THANKS.md +18 -0
  5. data/bin/couchdir +20 -0
  6. data/examples/model/example.rb +138 -0
  7. data/examples/word_count/markov +38 -0
  8. data/examples/word_count/views/books/chunked-map.js +3 -0
  9. data/examples/word_count/views/books/united-map.js +1 -0
  10. data/examples/word_count/views/markov/chain-map.js +6 -0
  11. data/examples/word_count/views/markov/chain-reduce.js +7 -0
  12. data/examples/word_count/views/word_count/count-map.js +6 -0
  13. data/examples/word_count/views/word_count/count-reduce.js +3 -0
  14. data/examples/word_count/word_count.rb +67 -0
  15. data/examples/word_count/word_count_query.rb +39 -0
  16. data/lib/couchrest/commands/generate.rb +71 -0
  17. data/lib/couchrest/commands/push.rb +103 -0
  18. data/lib/couchrest/core/database.rb +239 -0
  19. data/lib/couchrest/core/design.rb +89 -0
  20. data/lib/couchrest/core/document.rb +63 -0
  21. data/lib/couchrest/core/model.rb +607 -0
  22. data/lib/couchrest/core/server.rb +51 -0
  23. data/lib/couchrest/core/view.rb +4 -0
  24. data/lib/couchrest/helper/pager.rb +103 -0
  25. data/lib/couchrest/helper/streamer.rb +44 -0
  26. data/lib/couchrest/monkeypatches.rb +24 -0
  27. data/lib/couchrest.rb +140 -0
  28. data/spec/couchrest/core/couchrest_spec.rb +201 -0
  29. data/spec/couchrest/core/database_spec.rb +630 -0
  30. data/spec/couchrest/core/design_spec.rb +131 -0
  31. data/spec/couchrest/core/document_spec.rb +130 -0
  32. data/spec/couchrest/core/model_spec.rb +855 -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 +156 -0
@@ -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,24 @@
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.#{u.usec} +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
data/lib/couchrest.rb ADDED
@@ -0,0 +1,140 @@
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
+ # require 'extlib'
19
+
20
+ $:.unshift File.dirname(__FILE__) unless
21
+ $:.include?(File.dirname(__FILE__)) ||
22
+ $:.include?(File.expand_path(File.dirname(__FILE__)))
23
+
24
+
25
+ require 'couchrest/monkeypatches'
26
+
27
+ # = CouchDB, close to the metal
28
+ module CouchRest
29
+ VERSION = '0.12.2'
30
+
31
+ autoload :Server, 'couchrest/core/server'
32
+ autoload :Database, 'couchrest/core/database'
33
+ autoload :Document, 'couchrest/core/document'
34
+ autoload :Design, 'couchrest/core/design'
35
+ autoload :View, 'couchrest/core/view'
36
+ autoload :Model, 'couchrest/core/model'
37
+ autoload :Pager, 'couchrest/helper/pager'
38
+ autoload :FileManager, 'couchrest/helper/file_manager'
39
+ autoload :Streamer, 'couchrest/helper/streamer'
40
+
41
+ # The CouchRest module methods handle the basic JSON serialization
42
+ # and deserialization, as well as query parameters. The module also includes
43
+ # some helpers for tasks like instantiating a new Database or Server instance.
44
+ class << self
45
+
46
+ # todo, make this parse the url and instantiate a Server or Database instance
47
+ # depending on the specificity.
48
+ def new(*opts)
49
+ Server.new(*opts)
50
+ end
51
+
52
+ def parse url
53
+ case url
54
+ when /^http:\/\/(.*)\/(.*)\/(.*)/
55
+ host = $1
56
+ db = $2
57
+ docid = $3
58
+ when /^http:\/\/(.*)\/(.*)/
59
+ host = $1
60
+ db = $2
61
+ when /^http:\/\/(.*)/
62
+ host = $1
63
+ when /(.*)\/(.*)\/(.*)/
64
+ host = $1
65
+ db = $2
66
+ docid = $3
67
+ when /(.*)\/(.*)/
68
+ host = $1
69
+ db = $2
70
+ else
71
+ db = url
72
+ end
73
+
74
+ db = nil if db && db.empty?
75
+
76
+ {
77
+ :host => host || "127.0.0.1:5984",
78
+ :database => db,
79
+ :doc => docid
80
+ }
81
+ end
82
+
83
+ # set proxy for RestClient to use
84
+ def proxy url
85
+ RestClient.proxy = url
86
+ end
87
+
88
+ # ensure that a database exists
89
+ # creates it if it isn't already there
90
+ # returns it after it's been created
91
+ def database! url
92
+ parsed = parse url
93
+ cr = CouchRest.new(parsed[:host])
94
+ cr.database!(parsed[:database])
95
+ end
96
+
97
+ def database url
98
+ parsed = parse url
99
+ cr = CouchRest.new(parsed[:host])
100
+ cr.database(parsed[:database])
101
+ end
102
+
103
+ def put uri, doc = nil
104
+ payload = doc.to_json if doc
105
+ JSON.parse(RestClient.put(uri, payload))
106
+ end
107
+
108
+ def get uri
109
+ JSON.parse(RestClient.get(uri), :max_nesting => false)
110
+ end
111
+
112
+ def post uri, doc = nil
113
+ payload = doc.to_json if doc
114
+ JSON.parse(RestClient.post(uri, payload))
115
+ end
116
+
117
+ def delete uri
118
+ JSON.parse(RestClient.delete(uri))
119
+ end
120
+
121
+ def copy uri, destination
122
+ JSON.parse(RestClient.copy(uri, {'Destination' => destination}))
123
+ end
124
+
125
+ def move uri, destination
126
+ JSON.parse(RestClient.move(uri, {'Destination' => destination}))
127
+ end
128
+
129
+ def paramify_url url, params = {}
130
+ if params && !params.empty?
131
+ query = params.collect do |k,v|
132
+ v = v.to_json if %w{key startkey endkey}.include?(k.to_s)
133
+ "#{k}=#{CGI.escape(v.to_s)}"
134
+ end.join("&")
135
+ url = "#{url}?#{query}"
136
+ end
137
+ url
138
+ end
139
+ end # class << self
140
+ 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