couchchanges 0.2
Sign up to get free protection for your applications and to get access to all the features.
- data/README.md +30 -0
- data/Rakefile +7 -0
- data/couchchanges.gemspec +25 -0
- data/lib/couchchanges.rb +87 -0
- data/test/test_changes.rb +211 -0
- metadata +133 -0
data/README.md
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# couchchanges
|
2
|
+
|
3
|
+
a ruby consumer for couchdb's _changes feed. eventmachine based.
|
4
|
+
|
5
|
+
## example.rb
|
6
|
+
|
7
|
+
require "eventmachine"
|
8
|
+
require "couchchanges"
|
9
|
+
|
10
|
+
EventMachine.run {
|
11
|
+
couch = CouchChanges.new :url => "http://127.0.0.1:5984/my_db"
|
12
|
+
|
13
|
+
couch.change {|change|
|
14
|
+
puts "doc created, updated or deleted"
|
15
|
+
}
|
16
|
+
couch.update {|change|
|
17
|
+
puts "doc created or updated"
|
18
|
+
}
|
19
|
+
couch.delete {|change|
|
20
|
+
puts "doc deleted"
|
21
|
+
}
|
22
|
+
|
23
|
+
# if you don't specify a disconnect block, couchchanges will
|
24
|
+
# automatically reconnect to couchdb. normally you shouldn't
|
25
|
+
# care about the disconnect callback, but it can come in
|
26
|
+
# handy in tests etc.
|
27
|
+
couch.disconnect {|last_seq|
|
28
|
+
puts "disconnected from couch. last_seq: #{last_seq}"
|
29
|
+
}
|
30
|
+
}
|
data/Rakefile
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
Gem::Specification.new do |s|
|
2
|
+
s.name = "couchchanges"
|
3
|
+
s.version = "0.2"
|
4
|
+
s.date = "2011-02-25"
|
5
|
+
s.summary = "ruby consumer for couchdb's _changes api"
|
6
|
+
s.email = "harry@vangberg.name"
|
7
|
+
s.homepage = "http://github.com/ichverstehe/couchchanges"
|
8
|
+
s.description = "ruby consumer for couchdb's _changes api"
|
9
|
+
s.authors = ["Harry Vangberg"]
|
10
|
+
s.files = [
|
11
|
+
"README.md",
|
12
|
+
"couchchanges.gemspec",
|
13
|
+
"Rakefile",
|
14
|
+
"lib/couchchanges.rb",
|
15
|
+
]
|
16
|
+
s.test_files = Dir.glob("test/test_*.rb")
|
17
|
+
|
18
|
+
s.add_dependency "em-http-request", ">= 0.2.7"
|
19
|
+
s.add_dependency "json"
|
20
|
+
|
21
|
+
s.add_development_dependency "contest"
|
22
|
+
s.add_development_dependency "eventmachine"
|
23
|
+
s.add_development_dependency "couchrest"
|
24
|
+
end
|
25
|
+
|
data/lib/couchchanges.rb
ADDED
@@ -0,0 +1,87 @@
|
|
1
|
+
require "em-http"
|
2
|
+
require "uri"
|
3
|
+
require "json"
|
4
|
+
|
5
|
+
class CouchChanges
|
6
|
+
def initialize options={}
|
7
|
+
@options = options.dup
|
8
|
+
@uri = URI.parse(@options.delete(:url) + "/_changes")
|
9
|
+
@last_seq = 0
|
10
|
+
end
|
11
|
+
|
12
|
+
def change &block
|
13
|
+
block ? @change = block : @change
|
14
|
+
end
|
15
|
+
|
16
|
+
def update &block
|
17
|
+
block ? @update = block : @update
|
18
|
+
end
|
19
|
+
|
20
|
+
def delete &block
|
21
|
+
block ? @delete = block : @delete
|
22
|
+
end
|
23
|
+
|
24
|
+
def disconnect &block
|
25
|
+
block ? @disconnect = block : @disconnect
|
26
|
+
end
|
27
|
+
|
28
|
+
def listen
|
29
|
+
@http = http!
|
30
|
+
buffer = ""
|
31
|
+
@http.stream {|chunk|
|
32
|
+
buffer += chunk
|
33
|
+
while line = buffer.slice!(/.+\r?\n/)
|
34
|
+
handle line
|
35
|
+
end
|
36
|
+
}
|
37
|
+
@http.errback { disconnected }
|
38
|
+
@http
|
39
|
+
end
|
40
|
+
|
41
|
+
# REFACTOR!
|
42
|
+
def http!
|
43
|
+
options = {
|
44
|
+
:timeout => 0,
|
45
|
+
:query => @options.merge({:feed => "continuous"})
|
46
|
+
}
|
47
|
+
if @uri.user
|
48
|
+
options[:head] = {'authorization' => [@uri.user, @uri.password]}
|
49
|
+
end
|
50
|
+
|
51
|
+
EM::HttpRequest.new(@uri.to_s).get(options)
|
52
|
+
end
|
53
|
+
|
54
|
+
def handle line
|
55
|
+
return if line.chomp.empty?
|
56
|
+
|
57
|
+
hash = JSON.parse(line)
|
58
|
+
if hash["last_seq"]
|
59
|
+
disconnected
|
60
|
+
else
|
61
|
+
hash["rev"] = hash.delete("changes")[0]["rev"]
|
62
|
+
@last_seq = hash["seq"]
|
63
|
+
|
64
|
+
callbacks hash
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
def disconnected
|
69
|
+
if @disconnect
|
70
|
+
@disconnect.call @last_seq
|
71
|
+
else
|
72
|
+
EM.add_timer(@options[:reconnect]) {
|
73
|
+
@options[:since] = @last_seq
|
74
|
+
listen
|
75
|
+
}
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
def callbacks hash
|
80
|
+
@change.call hash if @change
|
81
|
+
if hash["deleted"]
|
82
|
+
@delete.call hash if @delete
|
83
|
+
else
|
84
|
+
@update.call hash if @update
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
@@ -0,0 +1,211 @@
|
|
1
|
+
$:.unshift "lib"
|
2
|
+
|
3
|
+
require "test/unit"
|
4
|
+
require "contest"
|
5
|
+
require "couchrest"
|
6
|
+
require "eventmachine"
|
7
|
+
|
8
|
+
require "couchchanges"
|
9
|
+
|
10
|
+
COUCH = "http://127.0.0.1:5984"
|
11
|
+
DATABASE = "couchchanges-test"
|
12
|
+
URL = "#{COUCH}/#{DATABASE}"
|
13
|
+
|
14
|
+
class TestCouchChanges < Test::Unit::TestCase
|
15
|
+
def changes options={}
|
16
|
+
EM.run {
|
17
|
+
c = CouchChanges.new({:url => URL, :reconnect => 0}.merge(options))
|
18
|
+
yield c
|
19
|
+
c.listen
|
20
|
+
}
|
21
|
+
end
|
22
|
+
|
23
|
+
def couch
|
24
|
+
@couch ||= CouchRest.new(COUCH)
|
25
|
+
end
|
26
|
+
|
27
|
+
def db
|
28
|
+
@db ||= couch.database(DATABASE)
|
29
|
+
end
|
30
|
+
|
31
|
+
setup do
|
32
|
+
db.recreate!
|
33
|
+
@doc = {"foo" => "bar"}
|
34
|
+
db.save_doc @doc
|
35
|
+
end
|
36
|
+
|
37
|
+
test "new document" do
|
38
|
+
changes do |c|
|
39
|
+
c.change {|c|
|
40
|
+
assert_equal @doc["_id"], c["id"]
|
41
|
+
assert_equal @doc["_rev"], c["rev"]
|
42
|
+
EM.stop
|
43
|
+
}
|
44
|
+
|
45
|
+
c.update {|c|
|
46
|
+
assert_equal @doc["_id"], c["id"]
|
47
|
+
assert_equal @doc["_rev"], c["rev"]
|
48
|
+
EM.stop
|
49
|
+
}
|
50
|
+
|
51
|
+
c.delete {|c| flunk "delete triggered"; EM.stop}
|
52
|
+
end
|
53
|
+
end
|
54
|
+
|
55
|
+
test "update document" do
|
56
|
+
@doc["new_key"] = "new value"
|
57
|
+
db.save_doc @doc
|
58
|
+
|
59
|
+
changes do |c|
|
60
|
+
c.update {|c|
|
61
|
+
assert_equal @doc["_id"], c["id"]
|
62
|
+
assert_equal @doc["_rev"], c["rev"]
|
63
|
+
EM.stop
|
64
|
+
}
|
65
|
+
|
66
|
+
c.delete {|c| flunk "delete triggered"; EM.stop}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
test "delete document" do
|
71
|
+
db.delete_doc @doc
|
72
|
+
|
73
|
+
changes do |c|
|
74
|
+
c.delete {|c|
|
75
|
+
assert c["deleted"]
|
76
|
+
assert_equal @doc["_id"], c["id"]
|
77
|
+
EM.stop
|
78
|
+
}
|
79
|
+
|
80
|
+
c.update {|c| flunk "update triggered"; EM.stop}
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
test "line spanning multiple chunks (\\n)" do
|
85
|
+
EM.run {
|
86
|
+
c = CouchChanges.new(:url => URL)
|
87
|
+
|
88
|
+
c.delete {|c|
|
89
|
+
assert_equal "doc1", c["id"]
|
90
|
+
EM.stop
|
91
|
+
}
|
92
|
+
|
93
|
+
listener = c.listen
|
94
|
+
|
95
|
+
listener.on_decoded_body_data "{\"seq\":129,\"i"
|
96
|
+
listener.on_decoded_body_data "d\":\"doc1\",\"changes\":[{\"rev\":\"6-9ed852183290a143552caf4df76dea87\"}],\"deleted\":true}\n"
|
97
|
+
}
|
98
|
+
end
|
99
|
+
|
100
|
+
test "multiple changes" do
|
101
|
+
doc2, doc3 = {}, {"doc" => 3}
|
102
|
+
db.save_doc doc2
|
103
|
+
db.save_doc doc3
|
104
|
+
db.delete_doc doc3
|
105
|
+
|
106
|
+
count = 0
|
107
|
+
|
108
|
+
changes do |c|
|
109
|
+
c.change {|c| count += 1 }
|
110
|
+
c.delete {|c|
|
111
|
+
assert_equal 3, count
|
112
|
+
EM.stop
|
113
|
+
}
|
114
|
+
end
|
115
|
+
end
|
116
|
+
|
117
|
+
test "since" do
|
118
|
+
seq = db.info["update_seq"]
|
119
|
+
doc2 = {}
|
120
|
+
db.save_doc doc2
|
121
|
+
|
122
|
+
changes :since => seq do |c|
|
123
|
+
c.change {|c|
|
124
|
+
assert c["rev"] > @doc["_rev"], "earlier revision"
|
125
|
+
assert_equal doc2["_id"], c["id"]
|
126
|
+
EM.stop
|
127
|
+
}
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
test "include_docs" do
|
132
|
+
changes :include_docs => true do |c|
|
133
|
+
c.update {|c|
|
134
|
+
assert_not_nil c["doc"]
|
135
|
+
assert_equal @doc["_id"], c["doc"]["_id"]
|
136
|
+
assert_equal @doc["_rev"], c["doc"]["_rev"]
|
137
|
+
assert_equal @doc["foo"], c["doc"]["foo"]
|
138
|
+
EM.stop
|
139
|
+
}
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
test "filter" do
|
144
|
+
db.save_doc({
|
145
|
+
"_id" => "_design/app",
|
146
|
+
"filters" => {
|
147
|
+
"filtered" => "function(doc, req) { return doc.type === 'filtered' }"
|
148
|
+
}
|
149
|
+
})
|
150
|
+
|
151
|
+
filtered = {"type" => "filtered"}
|
152
|
+
other = {"type" => "other"}
|
153
|
+
db.save_doc filtered
|
154
|
+
db.save_doc other
|
155
|
+
|
156
|
+
changes :filter => "app/filtered" do |c|
|
157
|
+
|
158
|
+
c.update {|c|
|
159
|
+
assert_equal filtered["_id"], c["id"]
|
160
|
+
EM.stop
|
161
|
+
}
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
test "heartbeat" do
|
166
|
+
changes :heartbeat => 100 do |c|
|
167
|
+
c.change {|c|
|
168
|
+
assert_equal @doc["_id"], c["id"]
|
169
|
+
assert_equal @doc["_rev"], c["rev"]
|
170
|
+
}
|
171
|
+
|
172
|
+
EM.add_timer(0.2) { EM.stop }
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
176
|
+
test "with disconnect: invoke with last_seq" do
|
177
|
+
changes :timeout => 1 do |c|
|
178
|
+
c.disconnect {|last_seq|
|
179
|
+
assert_equal 1, last_seq
|
180
|
+
EM.stop
|
181
|
+
}
|
182
|
+
EM.add_timer(0.2) { flunk "should invoke disconnect handler" }
|
183
|
+
end
|
184
|
+
end
|
185
|
+
|
186
|
+
test "without disconnect: default to reconnect" do
|
187
|
+
counter = 0
|
188
|
+
|
189
|
+
changes :timeout => 100 do |c|
|
190
|
+
c.update {|c|
|
191
|
+
counter += 1
|
192
|
+
flunk "don't rerun all changes" if counter > 1
|
193
|
+
}
|
194
|
+
c.delete {|c| EM.stop}
|
195
|
+
|
196
|
+
EM.add_timer(0.2) {
|
197
|
+
db.delete_doc @doc
|
198
|
+
}
|
199
|
+
EM.add_timer(0.5) {
|
200
|
+
flunk "didn't reconnect"
|
201
|
+
}
|
202
|
+
end
|
203
|
+
end
|
204
|
+
|
205
|
+
test "don't modify passed in options hash" do
|
206
|
+
hash = {:url => "http://127.0.0.1:5984/foo"}
|
207
|
+
CouchChanges.new(hash)
|
208
|
+
|
209
|
+
assert_equal({:url => "http://127.0.0.1:5984/foo"}, hash)
|
210
|
+
end
|
211
|
+
end
|
metadata
ADDED
@@ -0,0 +1,133 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: couchchanges
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
prerelease: false
|
5
|
+
segments:
|
6
|
+
- 0
|
7
|
+
- 2
|
8
|
+
version: "0.2"
|
9
|
+
platform: ruby
|
10
|
+
authors:
|
11
|
+
- Harry Vangberg
|
12
|
+
autorequire:
|
13
|
+
bindir: bin
|
14
|
+
cert_chain: []
|
15
|
+
|
16
|
+
date: 2011-02-25 00:00:00 -03:00
|
17
|
+
default_executable:
|
18
|
+
dependencies:
|
19
|
+
- !ruby/object:Gem::Dependency
|
20
|
+
name: em-http-request
|
21
|
+
prerelease: false
|
22
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
23
|
+
none: false
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
segments:
|
28
|
+
- 0
|
29
|
+
- 2
|
30
|
+
- 7
|
31
|
+
version: 0.2.7
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
- !ruby/object:Gem::Dependency
|
35
|
+
name: json
|
36
|
+
prerelease: false
|
37
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
38
|
+
none: false
|
39
|
+
requirements:
|
40
|
+
- - ">="
|
41
|
+
- !ruby/object:Gem::Version
|
42
|
+
segments:
|
43
|
+
- 0
|
44
|
+
version: "0"
|
45
|
+
type: :runtime
|
46
|
+
version_requirements: *id002
|
47
|
+
- !ruby/object:Gem::Dependency
|
48
|
+
name: contest
|
49
|
+
prerelease: false
|
50
|
+
requirement: &id003 !ruby/object:Gem::Requirement
|
51
|
+
none: false
|
52
|
+
requirements:
|
53
|
+
- - ">="
|
54
|
+
- !ruby/object:Gem::Version
|
55
|
+
segments:
|
56
|
+
- 0
|
57
|
+
version: "0"
|
58
|
+
type: :development
|
59
|
+
version_requirements: *id003
|
60
|
+
- !ruby/object:Gem::Dependency
|
61
|
+
name: eventmachine
|
62
|
+
prerelease: false
|
63
|
+
requirement: &id004 !ruby/object:Gem::Requirement
|
64
|
+
none: false
|
65
|
+
requirements:
|
66
|
+
- - ">="
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
segments:
|
69
|
+
- 0
|
70
|
+
version: "0"
|
71
|
+
type: :development
|
72
|
+
version_requirements: *id004
|
73
|
+
- !ruby/object:Gem::Dependency
|
74
|
+
name: couchrest
|
75
|
+
prerelease: false
|
76
|
+
requirement: &id005 !ruby/object:Gem::Requirement
|
77
|
+
none: false
|
78
|
+
requirements:
|
79
|
+
- - ">="
|
80
|
+
- !ruby/object:Gem::Version
|
81
|
+
segments:
|
82
|
+
- 0
|
83
|
+
version: "0"
|
84
|
+
type: :development
|
85
|
+
version_requirements: *id005
|
86
|
+
description: ruby consumer for couchdb's _changes api
|
87
|
+
email: harry@vangberg.name
|
88
|
+
executables: []
|
89
|
+
|
90
|
+
extensions: []
|
91
|
+
|
92
|
+
extra_rdoc_files: []
|
93
|
+
|
94
|
+
files:
|
95
|
+
- README.md
|
96
|
+
- couchchanges.gemspec
|
97
|
+
- Rakefile
|
98
|
+
- lib/couchchanges.rb
|
99
|
+
- test/test_changes.rb
|
100
|
+
has_rdoc: true
|
101
|
+
homepage: http://github.com/ichverstehe/couchchanges
|
102
|
+
licenses: []
|
103
|
+
|
104
|
+
post_install_message:
|
105
|
+
rdoc_options: []
|
106
|
+
|
107
|
+
require_paths:
|
108
|
+
- lib
|
109
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
110
|
+
none: false
|
111
|
+
requirements:
|
112
|
+
- - ">="
|
113
|
+
- !ruby/object:Gem::Version
|
114
|
+
segments:
|
115
|
+
- 0
|
116
|
+
version: "0"
|
117
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
118
|
+
none: false
|
119
|
+
requirements:
|
120
|
+
- - ">="
|
121
|
+
- !ruby/object:Gem::Version
|
122
|
+
segments:
|
123
|
+
- 0
|
124
|
+
version: "0"
|
125
|
+
requirements: []
|
126
|
+
|
127
|
+
rubyforge_project:
|
128
|
+
rubygems_version: 1.3.7
|
129
|
+
signing_key:
|
130
|
+
specification_version: 3
|
131
|
+
summary: ruby consumer for couchdb's _changes api
|
132
|
+
test_files:
|
133
|
+
- test/test_changes.rb
|