couchchanges 0.2

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
+ }
@@ -0,0 +1,7 @@
1
+ task :default => :test
2
+
3
+ desc "run tests"
4
+ task :test do
5
+ require "rake/runtest"
6
+ Rake.run_tests "test/test_*.rb"
7
+ end
@@ -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
+
@@ -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