couchchanges 0.2
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/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
|