opt_out 1.0.0

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,14 @@
1
+ *.gem
2
+ *.rbc
3
+ *.db
4
+ .bundle
5
+ .config
6
+ .yardoc
7
+ .env
8
+ Gemfile.lock
9
+ _yardoc
10
+ doc/
11
+ pkg
12
+ rdoc
13
+ tmp
14
+ vendor/gems
data/Gemfile ADDED
@@ -0,0 +1,10 @@
1
+ source 'https://rubygems.org'
2
+
3
+ # Specify your gem's dependencies in html-pipeline.gemspec
4
+ gemspec
5
+
6
+ group :development do
7
+ gem 'bundler'
8
+ gem 'rake'
9
+ gem 'dotenv'
10
+ end
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2013 Jerry Cheung
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,71 @@
1
+ # OptOut
2
+
3
+ OptOut is a rubygem for tracking unsubscriptions to newsletters.
4
+
5
+ ## Usage
6
+
7
+ ```ruby
8
+ OptOut.unsubscribe('newsletters', '5') # unsubscribe user id '5' from 'newsletters'
9
+ OptOut.subscribed?('newsletters', '5')
10
+ => false
11
+
12
+ OptOut.subscribe('newsletters', '5') # re-subscribe a user to 'newsletters'
13
+ OptOut.subscribed?('newsletters', '5')
14
+ => true
15
+
16
+ OptOut.unsubscribed?('newsletters', '5') # another way to query
17
+ => false
18
+
19
+ OptOut.subscribed('newsletters', '8') # users are subscribed by default unless explicitly unsubscribed
20
+ => true
21
+
22
+ ['1', '2', '3'].each {|user_id| OptOut.unsubscribe('newsletters', user_id)}
23
+ OptOut.unsubscribers # returns a list of unsubscribed user ids
24
+ => ['1', '2', '3']
25
+ ```
26
+
27
+ ## Configuration
28
+
29
+ The persistence backend can be configured to be one of:
30
+
31
+ * [MemoryAdapter](lib/opt_out/adapters/memory_adapter.rb)
32
+ * [RedisAdapter](lib/opt_out/adapters/redis_adapter.rb)
33
+ * [ActiveRecordAdapter](lib/opt_out/adapters/active_record_adapter.rb)
34
+
35
+ For example, to configure OptOut to store unsubscriptions in Redis:
36
+
37
+ ```ruby
38
+ OptOut.configure do |c|
39
+ c.adapter = OptOut::Adapters::RedisAdapter
40
+ c.options = {
41
+ :url => 'redis://localhost:6379'
42
+ }
43
+ end
44
+ ```
45
+
46
+ See individual adapter classes for setup and configuration options. To write a
47
+ custom adapter, take a look at [AbstractAdapter](lib/opt_out/adapters/abstract_adapter.rb)
48
+
49
+
50
+ ## Development
51
+
52
+ To run tests, you will need a running redis instance. Add a `.env` file to the
53
+ project root to configure where redis lives:
54
+
55
+ ```
56
+ REDIS_URL=redis://localhost:6379
57
+ ```
58
+
59
+ To run tests:
60
+
61
+ ```sh
62
+ $ rake
63
+ ```
64
+
65
+ ## Contributing
66
+
67
+ 1. [Fork it](https://help.github.com/articles/fork-a-repo)
68
+ 2. Create your feature branch (`git checkout -b my-new-feature`)
69
+ 3. Commit your changes (`git commit -am 'Added some feature'`)
70
+ 4. Push to the branch (`git push origin my-new-feature`)
71
+ 5. Create new [Pull Request](https://help.github.com/articles/using-pull-requests)
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ #!/usr/bin/env rake
2
+ require "bundler/gem_tasks"
3
+ require 'rake/testtask'
4
+
5
+ Rake::TestTask.new do |t|
6
+ t.libs << "test"
7
+ t.test_files = FileList['test/**/*_test.rb']
8
+ t.verbose = true
9
+ end
10
+
11
+ task :default => :test
data/lib/opt_out.rb ADDED
@@ -0,0 +1,58 @@
1
+ # Track user unsubscriptions by list.
2
+ #
3
+ # OptOut.unsubscribe('newsletters', '5') # unsubscribe user id '5' from 'newsletters'
4
+ # OptOut.subscribed?('newsletters', '5')
5
+ # => false
6
+ #
7
+ # OptOut.subscribe('newsletters', '5') # re-subscribe a user to 'newsletters'
8
+ # OptOut.subscribed?('newsletters', '5')
9
+ # => true
10
+ #
11
+ # OptOut.unsubscribed?('newsletters', '5') # another way to query
12
+ # => false
13
+ #
14
+ # OptOut.subscribed('newsletters', '8') # users are subscribed by default unless explicitly unsubscribed
15
+ # => true
16
+ #
17
+ # ['1', '2', '3'].each {|user_id| OptOut.unsubscribe('newsletters', user_id)}
18
+ # OptOut.unsubscribers # returns a list of unsubscribed user ids
19
+ # => ['1', '2', '3']
20
+ require 'forwardable'
21
+ require 'opt_out/adapters'
22
+
23
+ module OptOut
24
+ # Options:
25
+ # :adapter - subclass of OptOut::Adapters::AbstractAdapter
26
+ # :options - instantiation options to pass to `adapter`
27
+ class Configuration < Struct.new(:adapter, :options)
28
+ end
29
+
30
+ class << self
31
+ extend Forwardable
32
+ delegate [:subscribe, :subscribed?, :unsubscribe, :unsubscribed?, :unsubscribers, :reset] => :adapter
33
+
34
+ # Private: returns a memoized instance of adapter to use
35
+ def adapter
36
+ @adapter ||= config.adapter.new(config.options)
37
+ end
38
+
39
+ # Public: Configure OptOut. Returns Configuration.
40
+ #
41
+ # Example:
42
+ #
43
+ # OptOut.configure do |c|
44
+ # c.adapter = OptOut::Adapters::RedisAdapter
45
+ # c.options = {:host => 'localhost', :port => '6379', :password => ''}
46
+ # end
47
+ def configure(&blk)
48
+ blk.call(config)
49
+ @adapter = nil # invalidate adapter on reconfiguration
50
+ config
51
+ end
52
+
53
+ # Public: Returns Configuration
54
+ def config
55
+ @config ||= Configuration.new
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,8 @@
1
+ module OptOut
2
+ module Adapters
3
+ autoload :AbstractAdapter, 'opt_out/adapters/abstract_adapter'
4
+ autoload :MemoryAdapter, 'opt_out/adapters/memory_adapter'
5
+ autoload :RedisAdapter, 'opt_out/adapters/redis_adapter'
6
+ autoload :ActiveRecordAdapter, 'opt_out/adapters/active_record_adapter'
7
+ end
8
+ end
@@ -0,0 +1,52 @@
1
+ module OptOut
2
+ module Adapters
3
+ # An adapter is responsible for tracking (un/re)subscriptions, and
4
+ # unsubscribers.
5
+ class AbstractAdapter
6
+ def initialize(options = nil)
7
+ @options = options || {}
8
+ end
9
+
10
+ # Public: `user_id` is subscribed? to `list_id` iff it's unsubscribed.
11
+ #
12
+ # Returns boolean.
13
+ def subscribed?(list_id, user_id)
14
+ !unsubscribed?(list_id, user_id)
15
+ end
16
+
17
+ # Public: Resubscribe `user_id` to `list_id`. Note that adapters should
18
+ # only keep track of unsubscriptions. Even if subscribe has never been
19
+ # called before, a user is unsubscribed only if `#unsubscribe` is
20
+ # called.
21
+ #
22
+ # Returns nothing.
23
+ def subscribe(list_id, user_id)
24
+ raise NotImplementedError.new
25
+ end
26
+
27
+ # Public: unsubscribe `user_id` from `list_id`
28
+ #
29
+ # Returns nothing.
30
+ def unsubscribe(list_id, user_id)
31
+ raise NotImplementedError.new
32
+ end
33
+
34
+ # Public: is `user_id` unsubscribed from `list_id`?
35
+ #
36
+ # Returns boolean.
37
+ def unsubscribed?(list_id, user_id)
38
+ raise NotImplementedError.new
39
+ end
40
+
41
+ # Public: returns an array of unsubscribers for `list_id`
42
+ def unsubscribers(list_id)
43
+ raise NotImplementedError.new
44
+ end
45
+
46
+ # Private: reset internal data store for testing
47
+ def reset
48
+ raise NotImplementedError.new
49
+ end
50
+ end
51
+ end
52
+ end
@@ -0,0 +1,56 @@
1
+ require 'active_record'
2
+
3
+ module OptOut
4
+ module Adapters
5
+ # Adapter that stores persists data through ActiveRecord.
6
+ # It requires the following table:
7
+ #
8
+ # :list_id string
9
+ # :user_id string
10
+ # composite index on (list_id, user_id)
11
+ #
12
+ # Options
13
+ # :table_name - name of storage table. Defaults to 'opt_outs'
14
+ class ActiveRecordAdapter < AbstractAdapter
15
+
16
+ def subscribe(list_id, user_id)
17
+ return if [list_id, user_id].any? {|s| s.nil? || s == ''}
18
+ store.where(:list_id => list_id.to_s, :user_id => user_id.to_s).delete_all
19
+ nil
20
+ end
21
+
22
+ # TODO: would prefer opt_outs table to not have a primary key `id`, but
23
+ # that's not working right now
24
+ def unsubscribe(list_id, user_id)
25
+ store.create(:list_id => list_id.to_s, :user_id => user_id.to_s)
26
+ rescue ActiveRecord::RecordNotUnique
27
+ # already unsubscribed
28
+ ensure
29
+ return nil
30
+ end
31
+
32
+ def unsubscribed?(list_id, user_id)
33
+ store.exists?(:list_id => list_id.to_s, :user_id => user_id.to_s)
34
+ end
35
+
36
+ def unsubscribers(list_id)
37
+ store.where(:list_id => list_id.to_s).map(&:user_id).to_a
38
+ end
39
+
40
+ def reset
41
+ store.delete_all
42
+ end
43
+
44
+ private
45
+
46
+ def store
47
+ return @store if @store
48
+
49
+ table_name = @options[:table_name]
50
+ @store = Class.new(ActiveRecord::Base) do
51
+ self.table_name = table_name
52
+ end
53
+ end
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ require 'set'
2
+
3
+ module OptOut
4
+ module Adapters
5
+ # Adapter that stores persists data in memory in a hash.
6
+ #
7
+ # Options
8
+ # :store - optional Hash instance to store unsubscriptions
9
+ class MemoryAdapter < AbstractAdapter
10
+ # Subscribe `user_id` to `list_id`. Returns nothing.
11
+ def subscribe(list_id, user_id)
12
+ store[list_id].delete(user_id) and return
13
+ end
14
+
15
+ def unsubscribe(list_id, user_id)
16
+ store[list_id] ||= Set.new
17
+ store[list_id] << user_id
18
+ nil
19
+ end
20
+
21
+ def unsubscribed?(list_id, user_id)
22
+ (store[list_id] || []).include?(user_id)
23
+ end
24
+
25
+ def unsubscribers(list_id)
26
+ store[list_id].to_a
27
+ end
28
+
29
+ def reset
30
+ store.clear
31
+ end
32
+
33
+ private
34
+
35
+ def store
36
+ @store ||= @options[:store] || Hash.new
37
+ end
38
+ end
39
+ end
40
+ end
@@ -0,0 +1,64 @@
1
+ require 'redis'
2
+ require 'uri'
3
+
4
+ module OptOut
5
+ module Adapters
6
+ # Adapter that persists data in a Redis set.
7
+ #
8
+ # Options
9
+ # :redis - redis connection object OR
10
+ # :url - redis connection url OR
11
+ # :host
12
+ # :port
13
+ # :password
14
+ # :key_format - format string for redis key. list_id is interpolated into this option.
15
+ # Default is "opt_out:%s"
16
+ class RedisAdapter < AbstractAdapter
17
+ def subscribe(list_id, user_id)
18
+ redis.srem(key(list_id), user_id) and return
19
+ end
20
+
21
+ def unsubscribe(list_id, user_id)
22
+ redis.sadd(key(list_id), user_id) and return
23
+ end
24
+
25
+ def unsubscribed?(list_id, user_id)
26
+ redis.sismember(key(list_id), user_id)
27
+ end
28
+
29
+ def unsubscribers(list_id)
30
+ redis.smembers(key(list_id))
31
+ end
32
+
33
+ def reset
34
+ redis.flushdb
35
+ end
36
+
37
+ def key_format
38
+ @key_format || @options[:key_format] || "opt_out:%s"
39
+ end
40
+ attr_writer :key_format
41
+
42
+ # Private: returns redis client for this adapter
43
+ def redis
44
+ return @redis if @redis
45
+
46
+ @redis = if @options[:redis]
47
+ @options[:redis]
48
+ elsif @options[:url]
49
+ uri = URI.parse(@options[:url])
50
+ Redis.new(:host => uri.host, :port => uri.port, :password => uri.password)
51
+ else
52
+ Redis.new(:host => @options[:host], :port => @options[:port], :password => @options[:password])
53
+ end
54
+ end
55
+
56
+ private
57
+
58
+ # Returns key to use for redis set add from `:key_format` option
59
+ def key(list_id)
60
+ key_format % list_id
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,3 @@
1
+ module OptOut
2
+ VERSION = '1.0.0'
3
+ end
data/opt_out.gemspec ADDED
@@ -0,0 +1,21 @@
1
+ # -*- encoding: utf-8 -*-
2
+ require File.expand_path("../lib/opt_out/version", __FILE__)
3
+
4
+ Gem::Specification.new do |gem|
5
+ gem.name = "opt_out"
6
+ gem.version = OptOut::VERSION
7
+ gem.license = "MIT"
8
+ gem.authors = ["Jerry Cheung"]
9
+ gem.email = ["jch@whatcodecraves.com"]
10
+ gem.description = %q{Track newsletter unsubscriptions}
11
+ gem.summary = %q{Utilities for managing user unsubscribes from lists}
12
+ gem.homepage = "https://github.com/jch/opt_out"
13
+
14
+ gem.files = `git ls-files`.split $/
15
+ gem.test_files = gem.files.grep(%r{^test})
16
+ gem.require_paths = ["lib"]
17
+
18
+ gem.add_development_dependency "redis"
19
+ gem.add_development_dependency "activerecord"
20
+ gem.add_development_dependency "sqlite3"
21
+ end
@@ -0,0 +1,20 @@
1
+ require 'adapter_tests'
2
+ require 'sqlite3'
3
+
4
+ class OptOut::ActiveRecordAdapterTest < Test::Unit::TestCase
5
+ include AdapterTests
6
+ test_adapter OptOut::Adapters::ActiveRecordAdapter, :table_name => 'opt_outs' do
7
+ ActiveRecord::Base.establish_connection({
8
+ :adapter => 'sqlite3',
9
+ :database => './test.db'
10
+ })
11
+ conn = ActiveRecord::Base.connection
12
+
13
+ unless conn.table_exists?(:opt_outs)
14
+ conn.create_table(:opt_outs) do |t|
15
+ t.string "list_id"
16
+ t.string "user_id"
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,79 @@
1
+ require 'opt_out'
2
+ require 'test/unit'
3
+ require 'dotenv'
4
+
5
+ Dotenv.load
6
+
7
+ # All adapters must pass these tests. To setup a new adapter test:
8
+ #
9
+ # class MyAdapterTest < Test::Unit::TestCase
10
+ # include AdapterTests
11
+ # test_adapter(MyAdapter, {:some => 'options'}) do
12
+ # # optional test setup block
13
+ # end
14
+ # end
15
+ module AdapterTests
16
+ def self.included(base)
17
+ base.extend Macros
18
+ end
19
+
20
+ module Macros
21
+ attr_accessor :original_config, :test_config
22
+
23
+ def test_adapter(adapter, options = {}, &blk)
24
+ self.original_config = OptOut.config.dup
25
+ self.test_config = {
26
+ :adapter => adapter,
27
+ :options => options,
28
+ :setup => blk
29
+ }
30
+ end
31
+ end
32
+
33
+ def setup
34
+ if custom_setup = self.class.test_config[:setup]
35
+ custom_setup.call
36
+ end
37
+ OptOut.configure do |c|
38
+ c.adapter = self.class.test_config[:adapter]
39
+ c.options = self.class.test_config[:options]
40
+ end
41
+ OptOut.reset
42
+ end
43
+
44
+ def teardown
45
+ OptOut.config.adapter = self.class.original_config[:adapter]
46
+ OptOut.config.options = self.class.original_config[:options]
47
+ OptOut.reset
48
+ end
49
+
50
+ def test_auto_subscribed
51
+ assert OptOut.subscribed?('newsletters', '5')
52
+ end
53
+
54
+ def test_resubscribe
55
+ OptOut.unsubscribe('newsletters', '5')
56
+ OptOut.subscribe('newsletters', '5')
57
+ assert OptOut.subscribed?('newsletters', '5')
58
+ assert !OptOut.unsubscribed?('newsletters', '5')
59
+ end
60
+
61
+ def test_unsubscribe
62
+ OptOut.unsubscribe('newsletters', '5')
63
+ assert !OptOut.subscribed?('newsletters', '5')
64
+ assert OptOut.unsubscribed?('newsletters', '5')
65
+ end
66
+
67
+ def test_multi_unsubscribe
68
+ OptOut.unsubscribe('newsletters', '5')
69
+ OptOut.unsubscribe('newsletters', '5')
70
+ assert !OptOut.subscribed?('newsletters', '5')
71
+ assert OptOut.unsubscribed?('newsletters', '5')
72
+ end
73
+
74
+ def test_unsubscribers
75
+ OptOut.unsubscribe('newsletters', '5')
76
+ OptOut.unsubscribe('newsletters', '6')
77
+ assert_equal ['5', '6'], OptOut.unsubscribers('newsletters').sort
78
+ end
79
+ end
@@ -0,0 +1,6 @@
1
+ require 'adapter_tests'
2
+
3
+ class OptOut::MemoryAdapterTest < Test::Unit::TestCase
4
+ include AdapterTests
5
+ test_adapter OptOut::Adapters::MemoryAdapter, :store => {}
6
+ end
@@ -0,0 +1,17 @@
1
+ require 'adapter_tests'
2
+
3
+ class OptOut::RedisAdapterTest < Test::Unit::TestCase
4
+ include AdapterTests
5
+ test_adapter OptOut::Adapters::RedisAdapter, :url => ENV['REDIS_URL']
6
+
7
+ def test_default_key_format
8
+ OptOut.unsubscribe('releases', '9')
9
+ assert_equal ['9'], OptOut.adapter.redis.smembers("opt_out:releases")
10
+ end
11
+
12
+ def test_custom_key_format
13
+ OptOut.adapter.key_format = "notifications:%s:subscribe"
14
+ OptOut.unsubscribe('releases', '9')
15
+ assert_equal ['9'], OptOut.adapter.redis.smembers("notifications:releases:subscribe")
16
+ end
17
+ end
metadata ADDED
@@ -0,0 +1,115 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: opt_out
3
+ version: !ruby/object:Gem::Version
4
+ version: 1.0.0
5
+ prerelease:
6
+ platform: ruby
7
+ authors:
8
+ - Jerry Cheung
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2013-08-09 00:00:00.000000000 Z
13
+ dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: redis
16
+ requirement: !ruby/object:Gem::Requirement
17
+ none: false
18
+ requirements:
19
+ - - ! '>='
20
+ - !ruby/object:Gem::Version
21
+ version: '0'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ none: false
26
+ requirements:
27
+ - - ! '>='
28
+ - !ruby/object:Gem::Version
29
+ version: '0'
30
+ - !ruby/object:Gem::Dependency
31
+ name: activerecord
32
+ requirement: !ruby/object:Gem::Requirement
33
+ none: false
34
+ requirements:
35
+ - - ! '>='
36
+ - !ruby/object:Gem::Version
37
+ version: '0'
38
+ type: :development
39
+ prerelease: false
40
+ version_requirements: !ruby/object:Gem::Requirement
41
+ none: false
42
+ requirements:
43
+ - - ! '>='
44
+ - !ruby/object:Gem::Version
45
+ version: '0'
46
+ - !ruby/object:Gem::Dependency
47
+ name: sqlite3
48
+ requirement: !ruby/object:Gem::Requirement
49
+ none: false
50
+ requirements:
51
+ - - ! '>='
52
+ - !ruby/object:Gem::Version
53
+ version: '0'
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: '0'
62
+ description: Track newsletter unsubscriptions
63
+ email:
64
+ - jch@whatcodecraves.com
65
+ executables: []
66
+ extensions: []
67
+ extra_rdoc_files: []
68
+ files:
69
+ - .gitignore
70
+ - Gemfile
71
+ - LICENSE
72
+ - README.md
73
+ - Rakefile
74
+ - lib/opt_out.rb
75
+ - lib/opt_out/adapters.rb
76
+ - lib/opt_out/adapters/abstract_adapter.rb
77
+ - lib/opt_out/adapters/active_record_adapter.rb
78
+ - lib/opt_out/adapters/memory_adapter.rb
79
+ - lib/opt_out/adapters/redis_adapter.rb
80
+ - lib/opt_out/version.rb
81
+ - opt_out.gemspec
82
+ - test/active_record_adapter_test.rb
83
+ - test/adapter_tests.rb
84
+ - test/memory_adapter_test.rb
85
+ - test/redis_adapter_test.rb
86
+ homepage: https://github.com/jch/opt_out
87
+ licenses:
88
+ - MIT
89
+ post_install_message:
90
+ rdoc_options: []
91
+ require_paths:
92
+ - lib
93
+ required_ruby_version: !ruby/object:Gem::Requirement
94
+ none: false
95
+ requirements:
96
+ - - ! '>='
97
+ - !ruby/object:Gem::Version
98
+ version: '0'
99
+ required_rubygems_version: !ruby/object:Gem::Requirement
100
+ none: false
101
+ requirements:
102
+ - - ! '>='
103
+ - !ruby/object:Gem::Version
104
+ version: '0'
105
+ requirements: []
106
+ rubyforge_project:
107
+ rubygems_version: 1.8.23
108
+ signing_key:
109
+ specification_version: 3
110
+ summary: Utilities for managing user unsubscribes from lists
111
+ test_files:
112
+ - test/active_record_adapter_test.rb
113
+ - test/adapter_tests.rb
114
+ - test/memory_adapter_test.rb
115
+ - test/redis_adapter_test.rb