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.
@@ -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