vines-redis 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,49 @@
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 [Redis](http://redis.io/).
5
+
6
+ Additional documentation can be found at [getvines.org](http://www.getvines.org/).
7
+
8
+ ## Usage
9
+
10
+ ```
11
+ $ gem install vines vines-redis
12
+ $ vines init wonderland.lit
13
+ $ cd wonderland.lit && vines start
14
+ ```
15
+
16
+ ## Dependencies
17
+
18
+ Vines requires Ruby 1.9.3 or better. Instructions for installing the
19
+ needed OS packages, as well as Ruby itself, are available at
20
+ [getvines.org/ruby](http://www.getvines.org/ruby).
21
+
22
+ ## Configuration
23
+
24
+ Add the following configuration block to a virtual host definition in
25
+ the server's `conf/config.rb` file.
26
+
27
+ ```ruby
28
+ storage 'redis' do
29
+ host 'localhost'
30
+ port 6379
31
+ database 0
32
+ password ''
33
+ end
34
+ ```
35
+
36
+ ## Development
37
+
38
+ ```
39
+ $ script/bootstrap
40
+ $ script/tests
41
+ ```
42
+
43
+ ## Contact
44
+
45
+ * David Graham <david@negativecode.com>
46
+
47
+ ## License
48
+
49
+ 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-redis.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,127 @@
1
+ require 'em-hiredis'
2
+
3
+ module Vines
4
+ class Storage
5
+ class Redis < Storage
6
+ register :redis
7
+
8
+ %w[host port database password].each do |name|
9
+ define_method(name) do |*args|
10
+ if args.first
11
+ @config[name.to_sym] = args.first
12
+ else
13
+ @config[name.to_sym]
14
+ end
15
+ end
16
+ end
17
+
18
+ def initialize(&block)
19
+ @config = {}
20
+ instance_eval(&block)
21
+ end
22
+
23
+ def find_user(jid)
24
+ jid = JID.new(jid).bare.to_s
25
+ return if jid.empty?
26
+ response = query(:get, "user:#{jid}")
27
+ return unless response
28
+ doc = JSON.parse(response) rescue nil
29
+ return unless doc
30
+ User.new(jid: jid).tap do |user|
31
+ user.name, user.password = doc.values_at('name', 'password')
32
+ user.roster = find_roster(jid)
33
+ end
34
+ end
35
+
36
+ def save_user(user)
37
+ doc = {name: user.name, password: user.password}
38
+ contacts = user.roster.map {|c| [c.jid.to_s, c.to_h.to_json] }.flatten
39
+ roster = "roster:#{user.jid.bare}"
40
+
41
+ redis.multi
42
+ redis.set("user:#{user.jid.bare}", doc.to_json)
43
+ redis.del(roster)
44
+ redis.hmset(roster, *contacts) unless contacts.empty?
45
+
46
+ req = redis.exec
47
+ req.callback { yield }
48
+ req.errback { yield }
49
+ end
50
+ fiber :save_user
51
+
52
+ def find_vcard(jid)
53
+ jid = JID.new(jid).bare.to_s
54
+ return if jid.empty?
55
+ response = query(:get, "vcard:#{jid}")
56
+ return unless response
57
+ doc = JSON.parse(response) rescue nil
58
+ Nokogiri::XML(doc['card']).root rescue nil
59
+ end
60
+
61
+ def save_vcard(jid, card)
62
+ jid = JID.new(jid).bare.to_s
63
+ doc = {card: card.to_xml}
64
+ query(:set, "vcard:#{jid}", doc.to_json)
65
+ end
66
+
67
+ def find_fragment(jid, node)
68
+ jid = JID.new(jid).bare.to_s
69
+ return if jid.empty?
70
+ response = query(:hget, "fragments:#{jid}", fragment_id(node))
71
+ return unless response
72
+ doc = JSON.parse(response) rescue nil
73
+ Nokogiri::XML(doc['xml']).root rescue nil
74
+ end
75
+
76
+ def save_fragment(jid, node)
77
+ jid = JID.new(jid).bare.to_s
78
+ doc = {xml: node.to_xml}
79
+ query(:hset, "fragments:#{jid}", fragment_id(node), doc.to_json)
80
+ end
81
+
82
+ private
83
+
84
+ def fragment_id(node)
85
+ Digest::SHA1.hexdigest("#{node.name}:#{node.namespace.href}")
86
+ end
87
+
88
+ # Retrieve the hash stored at roster:jid and return an array of
89
+ # +Vines::Contact+ objects.
90
+ def find_roster(jid)
91
+ jid = JID.new(jid).bare
92
+ roster = query(:hgetall, "roster:#{jid}")
93
+ Hash[*roster].map do |jid, json|
94
+ contact = JSON.parse(json) rescue nil
95
+ Contact.new(
96
+ jid: jid,
97
+ name: contact['name'],
98
+ subscription: contact['subscription'],
99
+ ask: contact['ask'],
100
+ groups: contact['groups'] || []) if contact
101
+ end.compact
102
+ end
103
+
104
+ def query(name, *args)
105
+ req = redis.send(name, *args)
106
+ req.callback {|response| yield response }
107
+ req.errback { yield }
108
+ end
109
+ fiber :query
110
+
111
+ # Cache and return a redis connection object. The em-hiredis gem reconnects
112
+ # in unbind so only create one connection.
113
+ def redis
114
+ @redis ||= connect
115
+ end
116
+
117
+ # Return a new redis connection using the configuration attributes from the
118
+ # conf/config.rb file.
119
+ def connect
120
+ args = @config.values_at(:host, :port, :password, :database)
121
+ conn = EM::Hiredis::Client.new(*args)
122
+ conn.connect
123
+ conn
124
+ end
125
+ end
126
+ end
127
+ end
@@ -0,0 +1,95 @@
1
+ # A mock redis storage implementation that saves data to an in-memory Hash.
2
+ class MockRedis
3
+ attr_reader :db
4
+
5
+ # Mimic em-hiredis behavior.
6
+ def self.defer(method)
7
+ old = instance_method(method)
8
+ define_method method do |*args, &block|
9
+ result = old.bind(self).call(*args)
10
+ deferred = EM::DefaultDeferrable.new
11
+ deferred.callback(&block) if block
12
+ EM.next_tick { deferred.succeed(result) }
13
+ deferred
14
+ end
15
+ end
16
+
17
+ def initialize
18
+ @db = {}
19
+ end
20
+
21
+ def del(key)
22
+ @db.delete(key)
23
+ end
24
+ defer :del
25
+
26
+ def get(key)
27
+ @db[key]
28
+ end
29
+ defer :get
30
+
31
+ def set(key, value)
32
+ @db[key] = value
33
+ end
34
+ defer :set
35
+
36
+ def hget(key, field)
37
+ @db[key][field] rescue nil
38
+ end
39
+ defer :hget
40
+
41
+ def hdel(key, field)
42
+ @db[key].delete(field) rescue nil
43
+ end
44
+ defer :hdel
45
+
46
+ def hgetall(key)
47
+ (@db[key] || {}).map do |k, v|
48
+ [k, v]
49
+ end.flatten
50
+ end
51
+ defer :hgetall
52
+
53
+ def hset(key, field, value)
54
+ @db[key] ||= {}
55
+ @db[key][field] = value
56
+ end
57
+ defer :hset
58
+
59
+ def hmset(key, *args)
60
+ @db[key] = Hash[*args]
61
+ end
62
+ defer :hmset
63
+
64
+ def sadd(key, obj)
65
+ @db[key] ||= Set.new
66
+ @db[key] << obj
67
+ end
68
+ defer :sadd
69
+
70
+ def srem(key, obj)
71
+ @db[key].delete(obj) rescue nil
72
+ end
73
+ defer :srem
74
+
75
+ def smembers
76
+ @db[key].to_a rescue []
77
+ end
78
+ defer :smembers
79
+
80
+ def flushdb
81
+ @db.clear
82
+ end
83
+ defer :flushdb
84
+
85
+ def multi
86
+ @transaction = true
87
+ end
88
+ defer :multi
89
+
90
+ def exec
91
+ raise 'transaction must start with multi' unless @transaction
92
+ @transaction = false
93
+ end
94
+ defer :exec
95
+ end
@@ -0,0 +1,4 @@
1
+ require 'vines'
2
+ require 'mock_redis'
3
+ require 'storage_specs'
4
+ require 'minitest/autorun'
@@ -0,0 +1,61 @@
1
+ require 'spec_helper'
2
+
3
+ describe Vines::Storage::Redis do
4
+ include StorageSpecs
5
+
6
+ MOCK_REDIS = MockRedis.new
7
+
8
+ before do
9
+ fibered do
10
+ db = MOCK_REDIS
11
+ db.set('user:empty@wonderland.lit', {}.to_json)
12
+ db.set('user:no_password@wonderland.lit', {'foo' => 'bar'}.to_json)
13
+ db.set('user:clear_password@wonderland.lit', {'password' => 'secret'}.to_json)
14
+ db.set('user:bcrypt_password@wonderland.lit', {'password' => BCrypt::Password.create('secret')}.to_json)
15
+ db.set('user:full@wonderland.lit', {
16
+ 'password' => BCrypt::Password.create('secret'),
17
+ 'name' => 'Tester'
18
+ }.to_json)
19
+ db.hmset('roster:full@wonderland.lit',
20
+ 'contact1@wonderland.lit',
21
+ {'name' => 'Contact1', 'groups' => %w[Group1 Group2]}.to_json,
22
+ 'contact2@wonderland.lit',
23
+ {'name' => 'Contact2', 'groups' => %w[Group3 Group4]}.to_json)
24
+ db.set('vcard:full@wonderland.lit', {'card' => vcard.to_xml}.to_json)
25
+ db.hset('fragments:full@wonderland.lit', fragment_id, {'xml' => fragment.to_xml}.to_json)
26
+ end
27
+ end
28
+
29
+ after do
30
+ MOCK_REDIS.flushdb
31
+ end
32
+
33
+ def storage
34
+ storage = Vines::Storage::Redis.new { host 'localhost'; port 6397 }
35
+ def storage.redis; MOCK_REDIS; end
36
+ storage
37
+ end
38
+
39
+ describe 'creating a new instance' do
40
+ it 'does not raise with no arguments' do
41
+ fibered do
42
+ obj = Vines::Storage::Redis.new {}
43
+ obj.wont_be_nil
44
+ end
45
+ end
46
+
47
+ it 'does not raise when port is missing' do
48
+ fibered do
49
+ obj = Vines::Storage::Redis.new { host 'localhost' }
50
+ obj.wont_be_nil
51
+ end
52
+ end
53
+
54
+ it 'does not raise with host and port' do
55
+ fibered do
56
+ obj = Vines::Storage::Redis.new { host'localhost'; port '6379' }
57
+ obj.wont_be_nil
58
+ end
59
+ end
60
+ end
61
+ 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-redis'
3
+ s.version = '0.1.0'
4
+ s.summary = %q[Provides a Redis storage adapter for the Vines XMPP chat server.]
5
+ s.description = %q[Stores Vines user data in Redis.]
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-redis.gemspec', 'lib/**/*'] - ['Gemfile.lock']
13
+ s.test_files = Dir['spec/**/*']
14
+ s.require_path = 'lib'
15
+
16
+ s.add_dependency 'em-hiredis', '~> 0.1.1'
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,127 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: vines-redis
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-hiredis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ~>
20
+ - !ruby/object:Gem::Version
21
+ version: 0.1.1
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: 0.1.1
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 Redis.
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-redis.gemspec
90
+ - lib/vines/storage/redis.rb
91
+ - spec/mock_redis.rb
92
+ - spec/spec_helper.rb
93
+ - spec/storage/redis_spec.rb
94
+ - spec/storage_specs.rb
95
+ homepage: http://www.getvines.org
96
+ licenses:
97
+ - MIT
98
+ post_install_message:
99
+ rdoc_options: []
100
+ require_paths:
101
+ - lib
102
+ required_ruby_version: !ruby/object:Gem::Requirement
103
+ none: false
104
+ requirements:
105
+ - - ! '>='
106
+ - !ruby/object:Gem::Version
107
+ version: 1.9.3
108
+ required_rubygems_version: !ruby/object:Gem::Requirement
109
+ none: false
110
+ requirements:
111
+ - - ! '>='
112
+ - !ruby/object:Gem::Version
113
+ version: '0'
114
+ segments:
115
+ - 0
116
+ hash: 8918539191823912
117
+ requirements: []
118
+ rubyforge_project:
119
+ rubygems_version: 1.8.23
120
+ signing_key:
121
+ specification_version: 3
122
+ summary: Provides a Redis storage adapter for the Vines XMPP chat server.
123
+ test_files:
124
+ - spec/mock_redis.rb
125
+ - spec/spec_helper.rb
126
+ - spec/storage/redis_spec.rb
127
+ - spec/storage_specs.rb