vines-couchdb 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
data/Gemfile ADDED
@@ -0,0 +1,3 @@
1
+ source "https://rubygems.org"
2
+
3
+ gemspec
data/LICENSE ADDED
@@ -0,0 +1,19 @@
1
+ Copyright (c) 2010-2013 Negative Code
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining a copy
4
+ of this software and associated documentation files (the "Software"), to deal
5
+ in the Software without restriction, including without limitation the rights
6
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7
+ copies of the Software, and to permit persons to whom the Software is
8
+ furnished to do so, subject to the following conditions:
9
+
10
+ The above copyright notice and this permission notice shall be included in
11
+ all copies or substantial portions of the Software.
12
+
13
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
19
+ THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,52 @@
1
+ # Welcome to Vines
2
+
3
+ Vines is a scalable XMPP chat server, using EventMachine for asynchronous IO.
4
+ This gem provides support for storing user data in
5
+ [Apache CouchDB](https://couchdb.apache.org/).
6
+
7
+ Additional documentation can be found at [getvines.org](http://www.getvines.org/).
8
+
9
+ ## Usage
10
+
11
+ ```
12
+ $ gem install vines vines-couchdb
13
+ $ vines init wonderland.lit
14
+ $ cd wonderland.lit && vines start
15
+ ```
16
+
17
+ ## Configuration
18
+
19
+ Add the following configuration block to a virtual host definition in
20
+ the server's `conf/config.rb` file.
21
+
22
+ ```ruby
23
+ storage 'couchdb' do
24
+ host 'localhost'
25
+ port 6984
26
+ database 'xmpp'
27
+ tls true
28
+ username ''
29
+ password ''
30
+ end
31
+ ```
32
+
33
+ ## Dependencies
34
+
35
+ Vines requires Ruby 1.9.3 or better. Instructions for installing the
36
+ needed OS packages, as well as Ruby itself, are available at
37
+ [getvines.org/ruby](http://www.getvines.org/ruby).
38
+
39
+ ## Development
40
+
41
+ ```
42
+ $ script/bootstrap
43
+ $ script/tests
44
+ ```
45
+
46
+ ## Contact
47
+
48
+ * David Graham <david@negativecode.com>
49
+
50
+ ## License
51
+
52
+ Vines is released under the MIT license. Check the LICENSE file for details.
data/Rakefile ADDED
@@ -0,0 +1,20 @@
1
+ require 'rake'
2
+ require 'rake/clean'
3
+ require 'rake/testtask'
4
+
5
+ CLOBBER.include('pkg')
6
+
7
+ directory 'pkg'
8
+
9
+ desc 'Build distributable packages'
10
+ task :build => [:pkg] do
11
+ system 'gem build vines-couchdb.gemspec && mv vines-*.gem pkg/'
12
+ end
13
+
14
+ Rake::TestTask.new(:test) do |test|
15
+ test.libs << 'spec'
16
+ test.pattern = 'spec/**/*_spec.rb'
17
+ test.warning = false
18
+ end
19
+
20
+ task :default => [:clobber, :test, :build]
@@ -0,0 +1,131 @@
1
+ require 'digest/sha1'
2
+ require 'em-http'
3
+ require 'uri'
4
+
5
+ module Vines
6
+ class Storage
7
+ class CouchDB < Storage
8
+ register :couchdb
9
+
10
+ %w[host port database tls username password].each do |name|
11
+ define_method(name) do |*args|
12
+ if args.first
13
+ @config[name.to_sym] = args.first
14
+ else
15
+ @config[name.to_sym]
16
+ end
17
+ end
18
+ end
19
+
20
+ def initialize(&block)
21
+ @config = {}
22
+ instance_eval(&block)
23
+ [:host, :port, :database].each {|key| raise "Must provide #{key}" unless @config[key] }
24
+ @url = url(@config)
25
+ end
26
+
27
+ def find_user(jid)
28
+ jid = JID.new(jid).bare.to_s
29
+ return if jid.empty?
30
+ doc = get("user:#{jid}")
31
+ return unless doc && doc['type'] == 'User'
32
+ User.new(jid: jid).tap do |user|
33
+ user.name, user.password = doc.values_at('name', 'password')
34
+ (doc['roster'] || {}).each_pair do |jid, props|
35
+ user.roster << Contact.new(
36
+ jid: jid,
37
+ name: props['name'],
38
+ subscription: props['subscription'],
39
+ ask: props['ask'],
40
+ groups: props['groups'] || [])
41
+ end
42
+ end
43
+ end
44
+
45
+ def save_user(user)
46
+ id = "user:#{user.jid.bare}"
47
+ doc = get(id) || {'_id' => id}
48
+ doc['type'] = 'User'
49
+ doc['name'] = user.name
50
+ doc['password'] = user.password
51
+ doc['roster'] = {}
52
+ user.roster.each do |contact|
53
+ doc['roster'][contact.jid.bare.to_s] = contact.to_h
54
+ end
55
+ save_doc(doc)
56
+ end
57
+
58
+ def find_vcard(jid)
59
+ jid = JID.new(jid).bare.to_s
60
+ return if jid.empty?
61
+ doc = get("vcard:#{jid}")
62
+ return unless doc && doc['type'] == 'Vcard'
63
+ Nokogiri::XML(doc['card']).root rescue nil
64
+ end
65
+
66
+ def save_vcard(jid, card)
67
+ jid = JID.new(jid).bare.to_s
68
+ id = "vcard:#{jid}"
69
+ doc = get(id) || {'_id' => id}
70
+ doc['type'] = 'Vcard'
71
+ doc['card'] = card.to_xml
72
+ save_doc(doc)
73
+ end
74
+
75
+ def find_fragment(jid, node)
76
+ jid = JID.new(jid).bare.to_s
77
+ return if jid.empty?
78
+ doc = get(fragment_id(jid, node))
79
+ return unless doc && doc['type'] == 'Fragment'
80
+ Nokogiri::XML(doc['xml']).root rescue nil
81
+ end
82
+
83
+ def save_fragment(jid, node)
84
+ jid = JID.new(jid).bare.to_s
85
+ id = fragment_id(jid, node)
86
+ doc = get(id) || {'_id' => id}
87
+ doc['type'] = 'Fragment'
88
+ doc['xml'] = node.to_xml
89
+ save_doc(doc)
90
+ end
91
+
92
+ private
93
+
94
+ def fragment_id(jid, node)
95
+ id = Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
96
+ "fragment:#{jid}:#{id}"
97
+ end
98
+
99
+ def url(config)
100
+ scheme = config[:tls] ? 'https' : 'http'
101
+ user, password = config.values_at(:username, :password)
102
+ credentials = empty?(user, password) ? '' : "%s:%s@" % [user, password]
103
+ "%s://%s%s:%s/%s" % [scheme, credentials, *config.values_at(:host, :port, :database)]
104
+ end
105
+
106
+ def get(jid)
107
+ http = EM::HttpRequest.new("#{@url}/#{escape(jid)}").get
108
+ http.errback { yield }
109
+ http.callback do
110
+ doc = if http.response_header.status == 200
111
+ JSON.parse(http.response) rescue nil
112
+ end
113
+ yield doc
114
+ end
115
+ end
116
+ fiber :get
117
+
118
+ def save_doc(doc)
119
+ head = {'Content-Type' => 'application/json'}
120
+ http = EM::HttpRequest.new(@url).post(head: head, body: doc.to_json)
121
+ http.callback { yield }
122
+ http.errback { yield }
123
+ end
124
+ fiber :save_doc
125
+
126
+ def escape(jid)
127
+ URI.escape(jid, Regexp.new("[^#{URI::PATTERN::UNRESERVED}]"))
128
+ end
129
+ end
130
+ end
131
+ end
@@ -0,0 +1,3 @@
1
+ require 'vines'
2
+ require 'storage_specs'
3
+ require 'minitest/autorun'
@@ -0,0 +1,116 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vines::Storage::CouchDB do
4
+ include StorageSpecs
5
+
6
+ URL = 'http://localhost:5984/xmpp_testcase'.freeze
7
+
8
+ before do
9
+ fibered do
10
+ database(:put)
11
+ save_doc({'_id' => 'user:empty@wonderland.lit'})
12
+
13
+ save_doc({
14
+ '_id' => 'user:no_password@wonderland.lit',
15
+ 'type' => 'User',
16
+ 'foo' => 'bar'})
17
+
18
+ save_doc({
19
+ '_id' => 'user:clear_password@wonderland.lit',
20
+ 'type' => 'User',
21
+ 'password' => 'secret'})
22
+
23
+ save_doc({
24
+ '_id' => 'user:bcrypt_password@wonderland.lit',
25
+ 'type' => 'User',
26
+ 'password' => BCrypt::Password.create('secret')})
27
+
28
+ save_doc({
29
+ '_id' => 'user:full@wonderland.lit',
30
+ 'type' => 'User',
31
+ 'password' => BCrypt::Password.create('secret'),
32
+ 'name' => 'Tester',
33
+ 'roster' => {
34
+ 'contact1@wonderland.lit' => {
35
+ 'name' => 'Contact1',
36
+ 'groups' => %w[Group1 Group2]
37
+ },
38
+ 'contact2@wonderland.lit' => {
39
+ 'name' => 'Contact2',
40
+ 'groups' => %w[Group3 Group4]
41
+ }
42
+ }
43
+ })
44
+
45
+ save_doc({
46
+ '_id' => 'vcard:full@wonderland.lit',
47
+ 'type' => 'Vcard',
48
+ 'card' => vcard.to_xml
49
+ })
50
+
51
+ save_doc({
52
+ '_id' => "fragment:full@wonderland.lit:#{fragment_id}",
53
+ 'type' => 'Fragment',
54
+ 'xml' => fragment.to_xml
55
+ })
56
+ end
57
+ end
58
+
59
+ after do
60
+ fibered do
61
+ database(:delete)
62
+ end
63
+ end
64
+
65
+ def save_doc(doc)
66
+ fiber = Fiber.current
67
+ http = EM::HttpRequest.new(URL).post(
68
+ head: {'Content-Type' => 'application/json'},
69
+ body: doc.to_json)
70
+ http.callback { fiber.resume }
71
+ http.errback { raise 'save_doc failed' }
72
+ Fiber.yield
73
+ end
74
+
75
+ def database(method=:put)
76
+ fiber = Fiber.current
77
+ http = EM::HttpRequest.new(URL).send(method)
78
+ http.callback { fiber.resume }
79
+ http.errback { raise "#{method} database failed" }
80
+ Fiber.yield
81
+ end
82
+
83
+ def storage
84
+ Vines::Storage::CouchDB.new do
85
+ host 'localhost'
86
+ port 5984
87
+ database 'xmpp_testcase'
88
+ end
89
+ end
90
+
91
+ describe 'creating a new instance' do
92
+ it 'raises with no host' do
93
+ fibered do
94
+ -> { Vines::Storage::CouchDB.new {} }.must_raise RuntimeError
95
+ end
96
+ end
97
+
98
+ it 'raises with no port' do
99
+ fibered do
100
+ -> { Vines::Storage::CouchDB.new { host 'localhost' } }.must_raise RuntimeError
101
+ end
102
+ end
103
+
104
+ it 'does not raise when given all args' do
105
+ fibered do
106
+ obj =
107
+ Vines::Storage::CouchDB.new do
108
+ host 'localhost'
109
+ port '5984'
110
+ database 'test'
111
+ end
112
+ obj.wont_be_nil
113
+ end
114
+ end
115
+ end
116
+ end
@@ -0,0 +1,182 @@
1
+ # Mixin methods for storage implementation test classes. The behavioral
2
+ # tests are the same regardless of implementation so share those methods
3
+ # here.
4
+ module StorageSpecs
5
+ def fragment_id
6
+ Digest::SHA1.hexdigest("characters:urn:wonderland")
7
+ end
8
+
9
+ def fragment
10
+ Nokogiri::XML(%q{
11
+ <characters xmlns="urn:wonderland">
12
+ <character>Alice</character>
13
+ </characters>
14
+ }.strip).root
15
+ end
16
+
17
+ def vcard
18
+ Nokogiri::XML(%q{
19
+ <vCard xmlns="vcard-temp">
20
+ <FN>Alice in Wonderland</FN>
21
+ </vCard>
22
+ }.strip).root
23
+ end
24
+
25
+ def fibered
26
+ EM.run do
27
+ Fiber.new do
28
+ yield
29
+ EM.stop
30
+ end.resume
31
+ end
32
+ end
33
+
34
+ def test_authenticate
35
+ fibered do
36
+ db = storage
37
+ db.authenticate(nil, nil).must_be_nil
38
+ db.authenticate(nil, 'secret').must_be_nil
39
+ db.authenticate('bogus', nil).must_be_nil
40
+ db.authenticate('bogus', 'secret').must_be_nil
41
+ db.authenticate('empty@wonderland.lit', 'secret').must_be_nil
42
+ db.authenticate('no_password@wonderland.lit', 'secret').must_be_nil
43
+ db.authenticate('clear_password@wonderland.lit', 'secret').must_be_nil
44
+
45
+ user = db.authenticate('bcrypt_password@wonderland.lit', 'secret')
46
+ user.wont_be_nil
47
+ user.jid.to_s.must_equal 'bcrypt_password@wonderland.lit'
48
+
49
+ user = db.authenticate('full@wonderland.lit', 'secret')
50
+ user.wont_be_nil
51
+ user.name.must_equal 'Tester'
52
+ user.jid.to_s.must_equal 'full@wonderland.lit'
53
+
54
+ user.roster.length.must_equal 2
55
+ user.roster[0].jid.to_s.must_equal 'contact1@wonderland.lit'
56
+ user.roster[0].name.must_equal 'Contact1'
57
+ user.roster[0].groups.length.must_equal 2
58
+ user.roster[0].groups[0].must_equal 'Group1'
59
+ user.roster[0].groups[1].must_equal 'Group2'
60
+
61
+ user.roster[1].jid.to_s.must_equal 'contact2@wonderland.lit'
62
+ user.roster[1].name.must_equal 'Contact2'
63
+ user.roster[1].groups.length.must_equal 2
64
+ user.roster[1].groups[0].must_equal 'Group3'
65
+ user.roster[1].groups[1].must_equal 'Group4'
66
+ end
67
+ end
68
+
69
+ def test_find_user
70
+ fibered do
71
+ db = storage
72
+ user = db.find_user(nil)
73
+ user.must_be_nil
74
+
75
+ user = db.find_user('full@wonderland.lit')
76
+ user.wont_be_nil
77
+ user.jid.to_s.must_equal 'full@wonderland.lit'
78
+
79
+ user = db.find_user(Vines::JID.new('full@wonderland.lit'))
80
+ user.wont_be_nil
81
+ user.jid.to_s.must_equal 'full@wonderland.lit'
82
+
83
+ user = db.find_user(Vines::JID.new('full@wonderland.lit/resource'))
84
+ user.wont_be_nil
85
+ user.jid.to_s.must_equal 'full@wonderland.lit'
86
+ end
87
+ end
88
+
89
+ def test_save_user
90
+ fibered do
91
+ db = storage
92
+ user = Vines::User.new(
93
+ jid: 'save_user@domain.tld/resource1',
94
+ name: 'Save User',
95
+ password: 'secret')
96
+ user.roster << Vines::Contact.new(
97
+ jid: 'contact1@domain.tld/resource2',
98
+ name: 'Contact 1')
99
+ db.save_user(user)
100
+ user = db.find_user('save_user@domain.tld')
101
+ user.wont_be_nil
102
+ user.jid.to_s.must_equal 'save_user@domain.tld'
103
+ user.name.must_equal 'Save User'
104
+ user.roster.length.must_equal 1
105
+ user.roster[0].jid.to_s.must_equal 'contact1@domain.tld'
106
+ user.roster[0].name.must_equal 'Contact 1'
107
+ end
108
+ end
109
+
110
+ def test_find_vcard
111
+ fibered do
112
+ db = storage
113
+ card = db.find_vcard(nil)
114
+ card.must_be_nil
115
+
116
+ card = db.find_vcard('full@wonderland.lit')
117
+ card.wont_be_nil
118
+ card.to_s.must_equal vcard.to_s
119
+
120
+ card = db.find_vcard(Vines::JID.new('full@wonderland.lit'))
121
+ card.wont_be_nil
122
+ card.to_s.must_equal vcard.to_s
123
+
124
+ card = db.find_vcard(Vines::JID.new('full@wonderland.lit/resource'))
125
+ card.wont_be_nil
126
+ card.to_s.must_equal vcard.to_s
127
+ end
128
+ end
129
+
130
+ def test_save_vcard
131
+ fibered do
132
+ db = storage
133
+ db.save_user(Vines::User.new(jid: 'save_user@domain.tld'))
134
+ db.save_vcard('save_user@domain.tld/resource1', vcard)
135
+ card = db.find_vcard('save_user@domain.tld')
136
+ card.wont_be_nil
137
+ card.to_s.must_equal vcard.to_s
138
+ end
139
+ end
140
+
141
+ def test_find_fragment
142
+ fibered do
143
+ db = storage
144
+ root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
145
+ bad_name = Nokogiri::XML(%q{<not_characters xmlns="urn:wonderland"/>}).root
146
+ bad_ns = Nokogiri::XML(%q{<characters xmlns="not:wonderland"/>}).root
147
+
148
+ node = db.find_fragment(nil, nil)
149
+ node.must_be_nil
150
+
151
+ node = db.find_fragment('full@wonderland.lit', bad_name)
152
+ node.must_be_nil
153
+
154
+ node = db.find_fragment('full@wonderland.lit', bad_ns)
155
+ node.must_be_nil
156
+
157
+ node = db.find_fragment('full@wonderland.lit', root)
158
+ node.wont_be_nil
159
+ node.to_s.must_equal fragment.to_s
160
+
161
+ node = db.find_fragment(Vines::JID.new('full@wonderland.lit'), root)
162
+ node.wont_be_nil
163
+ node.to_s.must_equal fragment.to_s
164
+
165
+ node = db.find_fragment(Vines::JID.new('full@wonderland.lit/resource'), root)
166
+ node.wont_be_nil
167
+ node.to_s.must_equal fragment.to_s
168
+ end
169
+ end
170
+
171
+ def test_save_fragment
172
+ fibered do
173
+ db = storage
174
+ root = Nokogiri::XML(%q{<characters xmlns="urn:wonderland"/>}).root
175
+ db.save_user(Vines::User.new(jid: 'save_user@domain.tld'))
176
+ db.save_fragment('save_user@domain.tld/resource1', fragment)
177
+ node = db.find_fragment('save_user@domain.tld', root)
178
+ node.wont_be_nil
179
+ node.to_s.must_equal fragment.to_s
180
+ end
181
+ end
182
+ end
@@ -0,0 +1,23 @@
1
+ Gem::Specification.new do |s|
2
+ s.name = 'vines-couchdb'
3
+ s.version = '0.1.0'
4
+ s.summary = %q[Provides an Apache CouchDB storage adapter for the Vines XMPP chat server.]
5
+ s.description = %q[Stores Vines user data in Apache CouchDB.]
6
+
7
+ s.authors = ['David Graham']
8
+ s.email = %w[david@negativecode.com]
9
+ s.homepage = 'http://www.getvines.org'
10
+ s.license = 'MIT'
11
+
12
+ s.files = Dir['[A-Z]*', 'vines-couchdb.gemspec', 'lib/**/*'] - ['Gemfile.lock']
13
+ s.test_files = Dir['spec/**/*']
14
+ s.require_path = 'lib'
15
+
16
+ s.add_dependency 'em-http-request', '~> 1.0.3'
17
+ s.add_dependency 'vines', '>= 0.4.5'
18
+
19
+ s.add_development_dependency 'minitest', '~> 4.7.4'
20
+ s.add_development_dependency 'rake', '~> 10.1.0'
21
+
22
+ s.required_ruby_version = '>= 1.9.3'
23
+ end
metadata ADDED
@@ -0,0 +1,125 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vines-couchdb
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - David Graham
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-06-21 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: em-http-request
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 1.0.3
22
+ type: :runtime
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ~>
28
+ - !ruby/object:Gem::Version
29
+ version: 1.0.3
30
+ - !ruby/object:Gem::Dependency
31
+ name: vines
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: 0.4.5
38
+ type: :runtime
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: 0.4.5
46
+ - !ruby/object:Gem::Dependency
47
+ name: minitest
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ~>
52
+ - !ruby/object:Gem::Version
53
+ version: 4.7.4
54
+ type: :development
55
+ prerelease: false
56
+ version_requirements: !ruby/object:Gem::Requirement
57
+ none: false
58
+ requirements:
59
+ - - ~>
60
+ - !ruby/object:Gem::Version
61
+ version: 4.7.4
62
+ - !ruby/object:Gem::Dependency
63
+ name: rake
64
+ requirement: !ruby/object:Gem::Requirement
65
+ none: false
66
+ requirements:
67
+ - - ~>
68
+ - !ruby/object:Gem::Version
69
+ version: 10.1.0
70
+ type: :development
71
+ prerelease: false
72
+ version_requirements: !ruby/object:Gem::Requirement
73
+ none: false
74
+ requirements:
75
+ - - ~>
76
+ - !ruby/object:Gem::Version
77
+ version: 10.1.0
78
+ description: Stores Vines user data in Apache CouchDB.
79
+ email:
80
+ - david@negativecode.com
81
+ executables: []
82
+ extensions: []
83
+ extra_rdoc_files: []
84
+ files:
85
+ - Gemfile
86
+ - LICENSE
87
+ - Rakefile
88
+ - README.md
89
+ - vines-couchdb.gemspec
90
+ - lib/vines/storage/couchdb.rb
91
+ - spec/spec_helper.rb
92
+ - spec/storage/couchdb_spec.rb
93
+ - spec/storage_specs.rb
94
+ homepage: http://www.getvines.org
95
+ licenses:
96
+ - MIT
97
+ post_install_message:
98
+ rdoc_options: []
99
+ require_paths:
100
+ - lib
101
+ required_ruby_version: !ruby/object:Gem::Requirement
102
+ none: false
103
+ requirements:
104
+ - - ! '>='
105
+ - !ruby/object:Gem::Version
106
+ version: 1.9.3
107
+ required_rubygems_version: !ruby/object:Gem::Requirement
108
+ none: false
109
+ requirements:
110
+ - - ! '>='
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ segments:
114
+ - 0
115
+ hash: 2430317977277905770
116
+ requirements: []
117
+ rubyforge_project:
118
+ rubygems_version: 1.8.23
119
+ signing_key:
120
+ specification_version: 3
121
+ summary: Provides an Apache CouchDB storage adapter for the Vines XMPP chat server.
122
+ test_files:
123
+ - spec/spec_helper.rb
124
+ - spec/storage/couchdb_spec.rb
125
+ - spec/storage_specs.rb