ablab-core 0.2.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: f5f41b9cd3f4f0cfc04bfd00fcf5209cc800d932
4
+ data.tar.gz: 9e3afbbd5baf0a22bcb4a6719f111c8a38e6af6f
5
+ SHA512:
6
+ metadata.gz: cbe6dc86065bd3fa2e57e99292e008958c8d1829625a0774162bfa2d25a5d243fbc9bbaa31f1ed7b9a38fb5bb6bb647d101395556d3f7d19eb6705f798843b7f
7
+ data.tar.gz: 80b85998ece7e265af961bb25089d0e4d57ac0b2ecde848c0f002122e1e8aea35c5cf759cbdd7244a14a258f37ebc648fd3338e3dbe664b7ff362c08101f55e8
@@ -0,0 +1,20 @@
1
+ Copyright 2016 Luca Ongaro
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
@@ -0,0 +1,28 @@
1
+ begin
2
+ require 'bundler/setup'
3
+ rescue LoadError
4
+ puts 'You must `gem install bundler` and `bundle install` to run rake tasks'
5
+ end
6
+
7
+ require 'rdoc/task'
8
+
9
+ RDoc::Task.new(:rdoc) do |rdoc|
10
+ rdoc.rdoc_dir = 'rdoc'
11
+ rdoc.title = 'Ablab'
12
+ rdoc.options << '--line-numbers'
13
+ rdoc.rdoc_files.include('README.rdoc')
14
+ rdoc.rdoc_files.include('lib/**/*.rb')
15
+ end
16
+
17
+ load 'rails/tasks/statistics.rake'
18
+
19
+ require "rspec/core/rake_task"
20
+
21
+ Bundler::GemHelper.install_tasks(name: 'ablab')
22
+
23
+ namespace :core do
24
+ Bundler::GemHelper.install_tasks(name: 'ablab-core')
25
+ end
26
+
27
+ RSpec::Core::RakeTask.new(:spec)
28
+ task default: :spec
@@ -0,0 +1,158 @@
1
+ require "ablab/version"
2
+ require "ablab/helper"
3
+ require "ablab/store"
4
+ require "ablab/engine"
5
+ require "forwardable"
6
+
7
+ module Ablab
8
+ module ModuleMethods
9
+ attr_reader :experiments
10
+
11
+ def setup(&block)
12
+ instance_exec(&block)
13
+ end
14
+
15
+ def experiment(name, &block)
16
+ @experiments ||= {}
17
+ @experiments[name] = Experiment.new(name, &block)
18
+ end
19
+
20
+ def store(type, *args)
21
+ if type.is_a? Class
22
+ @tracker = Class.new(*args)
23
+ else
24
+ class_name = type.to_s.split('_').map(&:capitalize).join
25
+ @tracker = Ablab::Store.const_get(class_name).new(*args)
26
+ end
27
+ end
28
+
29
+ def tracker
30
+ @tracker ||= Ablab::Store::Memory.new
31
+ end
32
+
33
+ def dashboard_credentials(credentials = nil)
34
+ if credentials
35
+ unless credentials[:name] && credentials[:password]
36
+ raise InvalidCredentials, 'credentials should provide name and password'
37
+ end
38
+ @dashboard_credentials = credentials
39
+ end
40
+ @dashboard_credentials
41
+ end
42
+ end
43
+
44
+ class InvalidCredentials < ArgumentError; end
45
+
46
+ extend ModuleMethods
47
+
48
+ class Experiment
49
+ attr_reader :name, :groups, :control
50
+
51
+ def initialize(name, &block)
52
+ @name = name.to_sym
53
+ @control = Group.new(:control, 'control group')
54
+ @groups = [@control]
55
+ instance_exec(&block)
56
+ end
57
+
58
+ def description(desc = nil)
59
+ @description = desc if desc
60
+ @description
61
+ end
62
+
63
+ def goal(goal = nil)
64
+ @goal = goal if goal
65
+ @goal
66
+ end
67
+
68
+ def percentage_of_visitors(percentage = nil)
69
+ @percentage_of_visitors = percentage if percentage
70
+ @percentage_of_visitors || 100
71
+ end
72
+
73
+ def group(name, options = {})
74
+ group = Group.new(name, options[:description])
75
+ @groups << group
76
+ end
77
+
78
+ def results
79
+ @result ||= Result.new(self)
80
+ @result.data
81
+ end
82
+
83
+ def run(session_id)
84
+ Run.new(self, session_id)
85
+ end
86
+ end
87
+
88
+ class Run
89
+ attr_reader :experiment, :session_id
90
+
91
+ def initialize(experiment, session_id)
92
+ @experiment, @session_id = experiment, session_id
93
+ end
94
+
95
+ def in_group?(name)
96
+ group == name
97
+ end
98
+
99
+ def track_view!
100
+ Ablab.tracker.track_view!(experiment.name, group, session_id)
101
+ end
102
+
103
+ def track_success!
104
+ Ablab.tracker.track_success!(experiment.name, group, session_id)
105
+ end
106
+
107
+ def group
108
+ return @group unless @group.nil?
109
+ size = 1000.0 * (experiment.percentage_of_visitors) / 100.0
110
+ idx = (draw * experiment.groups.size / size).floor
111
+ @group = experiment.groups[idx].try(:name)
112
+ end
113
+
114
+ def draw
115
+ Random.new(session_id.hash ^ experiment.name.hash).rand(1000)
116
+ end
117
+ end
118
+
119
+ class Group
120
+ attr_reader :name, :description
121
+ def initialize(name, description = nil)
122
+ @name, @description = name.to_sym, description
123
+ end
124
+ end
125
+
126
+ class Result
127
+ extend Forwardable
128
+ def_delegators :@experiment, :name, :control, :groups
129
+
130
+ def initialize(experiment)
131
+ @experiment = experiment
132
+ end
133
+
134
+ def data
135
+ counts_c = counts(control)
136
+ groups.map do |group|
137
+ if group == control
138
+ next [group.name, counts_c.merge(control: true, description: group.description)]
139
+ end
140
+ counts = counts(group)
141
+ z = z_score(counts[:sessions], counts[:conversions],
142
+ counts_c[:sessions], counts_c[:conversions])
143
+ [group.name, counts.merge(z_score: z, control: false, description: group.description)]
144
+ end.to_h
145
+ end
146
+
147
+ private def counts(group)
148
+ Ablab.tracker.counts(name, group.name)
149
+ end
150
+
151
+ private def z_score(s, c, sc, cc)
152
+ return nil if s == 0 || sc == 0
153
+ p = c.to_f / s
154
+ pc = cc.to_f / sc
155
+ (p - pc) / Math.sqrt((p*(1 - p) / s) + (pc*(1 - pc) / sc))
156
+ end
157
+ end
158
+ end
@@ -0,0 +1,2 @@
1
+ require 'ablab/store/memory'
2
+ require 'ablab/store/redis'
@@ -0,0 +1,61 @@
1
+ require 'set'
2
+
3
+ module Ablab
4
+ module Store
5
+ class Memory
6
+ def initialize
7
+ @views = Hash.new do |hash, key|
8
+ hash[key] = Hash.new { |h, k| h[k] = 0 }
9
+ end
10
+ @sessions = Hash.new do |hash, key|
11
+ hash[key] = Hash.new { |h, k| h[k] = Set.new }
12
+ end
13
+ @successes = Hash.new do |hash, key|
14
+ hash[key] = Hash.new { |h, k| h[k] = 0 }
15
+ end
16
+ @conversions = Hash.new do |hash, key|
17
+ hash[key] = Hash.new { |h, k| h[k] = Set.new }
18
+ end
19
+ end
20
+
21
+ def track_view!(experiment, bucket, session_id)
22
+ track(experiment, bucket, session_id, @views, @sessions)
23
+ end
24
+
25
+ def track_success!(experiment, bucket, session_id)
26
+ track(experiment, bucket, session_id, @successes, @conversions)
27
+ end
28
+
29
+ def views(experiment, bucket)
30
+ @views[experiment][bucket]
31
+ end
32
+
33
+ def sessions(experiment, bucket)
34
+ @sessions[experiment][bucket].size
35
+ end
36
+
37
+ def successes(experiment, bucket)
38
+ @successes[experiment][bucket]
39
+ end
40
+
41
+ def conversions(experiment, bucket)
42
+ @conversions[experiment][bucket].size
43
+ end
44
+
45
+ def counts(experiment, bucket)
46
+ {
47
+ views: views(experiment, bucket),
48
+ sessions: sessions(experiment, bucket),
49
+ successes: successes(experiment, bucket),
50
+ conversions: conversions(experiment, bucket)
51
+ }
52
+ end
53
+
54
+ private def track(experiment, bucket, session_id, counter, set)
55
+ return false if bucket.nil?
56
+ counter[experiment][bucket] += 1
57
+ set[experiment][bucket].add(session_id)
58
+ end
59
+ end
60
+ end
61
+ end
@@ -0,0 +1,67 @@
1
+ require 'redis'
2
+
3
+ module Ablab
4
+ module Store
5
+ class Redis
6
+ attr_reader :redis
7
+
8
+ def initialize(opts = {})
9
+ @key_prefix = opts[:key_prefix] || 'ablab'
10
+ @redis = ::Redis.new(opts)
11
+ end
12
+
13
+ def track_view!(experiment, bucket, session_id)
14
+ track(experiment, bucket, session_id, :views, :sessions)
15
+ end
16
+
17
+ def track_success!(experiment, bucket, session_id)
18
+ track(experiment, bucket, session_id, :successes, :conversions)
19
+ end
20
+
21
+ def views(experiment, bucket)
22
+ (redis.get(key(:views, experiment, bucket)) || 0).to_i
23
+ end
24
+
25
+ def sessions(experiment, bucket)
26
+ (redis.pfcount(key(:sessions, experiment, bucket)) || 0).to_i
27
+ end
28
+
29
+ def successes(experiment, bucket)
30
+ (redis.get(key(:successes, experiment, bucket)) || 0).to_i
31
+ end
32
+
33
+ def conversions(experiment, bucket)
34
+ (redis.pfcount(key(:conversions, experiment, bucket)) || 0).to_i
35
+ end
36
+
37
+ def counts(experiment, bucket)
38
+ v, s, x, c = nil, nil, nil, nil
39
+ redis.multi do
40
+ v = redis.get(key(:views, experiment, bucket))
41
+ s = redis.pfcount(key(:sessions, experiment, bucket))
42
+ x = redis.get(key(:successes, experiment, bucket))
43
+ c = redis.pfcount(key(:conversions, experiment, bucket))
44
+ end
45
+ {
46
+ views: (v.value || 0).to_i,
47
+ sessions: (s.value || 0).to_i,
48
+ successes: (x.value || 0).to_i,
49
+ conversions: (c.value || 0).to_i
50
+ }
51
+ end
52
+
53
+ private def key(type, experiment, bucket)
54
+ "#{@key_prefix}:#{type}:#{experiment}:#{bucket}"
55
+ end
56
+
57
+ private def track(experiment, bucket, session_id, counter, set)
58
+ return false if bucket.nil?
59
+ redis.pipelined do
60
+ redis.incr(key(counter, experiment, bucket))
61
+ redis.pfadd(key(set, experiment, bucket), session_id)
62
+ end
63
+ end
64
+ end
65
+ end
66
+ end
67
+
@@ -0,0 +1,3 @@
1
+ module Ablab
2
+ VERSION = "0.2.1"
3
+ end
@@ -0,0 +1,4 @@
1
+ # desc "Explaining what the task does"
2
+ # task :ablab do
3
+ # # Task goes here
4
+ # end
@@ -0,0 +1,7 @@
1
+ require 'spec_helper'
2
+ require 'ablab/store/store_examples'
3
+
4
+ describe Ablab::Store::Memory do
5
+ let(:store) { Ablab::Store::Memory.new }
6
+ include_examples 'store', Ablab::Store::Memory.new
7
+ end
@@ -0,0 +1,25 @@
1
+ require 'spec_helper'
2
+ require 'ablab/store/store_examples'
3
+
4
+ describe Ablab::Store::Redis do
5
+ def cleanup
6
+ redis = ::Redis.new(db: 2)
7
+ keys = redis.keys('ablabtest:*')
8
+ redis.pipelined do
9
+ keys.each { |key| redis.del(key) }
10
+ end
11
+ end
12
+
13
+ around do |example|
14
+ begin
15
+ cleanup
16
+ example.run
17
+ ensure
18
+ cleanup
19
+ end
20
+ end
21
+
22
+ let(:store) { Ablab::Store::Redis.new(db: 2, key_prefix: 'ablabtest') }
23
+ include_examples 'store'
24
+ end
25
+
@@ -0,0 +1,82 @@
1
+ require 'spec_helper'
2
+
3
+ RSpec.shared_examples 'store' do
4
+ describe '#track_view!' do
5
+ it 'returns false if group is nil' do
6
+ expect(store.track_view!(:foo, nil, 'abc')).to be(false)
7
+ end
8
+
9
+ it 'tracks views' do
10
+ 5.times { store.track_view!(:foo, :a, 'abc') }
11
+ 7.times { store.track_view!(:foo, :b, 'abc') }
12
+ 3.times { store.track_view!(:bar, :b, 'abc') }
13
+ expect(store.views(:foo, :a)).to eq(5)
14
+ expect(store.views(:foo, :b)).to eq(7)
15
+ expect(store.views(:bar, :b)).to eq(3)
16
+ end
17
+
18
+ it 'tracks unique sessions' do
19
+ 2.times { store.track_view!(:foo, :a, 'foo') }
20
+ 3.times { store.track_view!(:foo, :a, 'bar') }
21
+ 3.times { store.track_view!(:foo, :b, 'foo') }
22
+ 3.times { store.track_view!(:bar, :b, 'bar') }
23
+ expect(store.sessions(:foo, :a)).to eq(2)
24
+ expect(store.sessions(:foo, :b)).to eq(1)
25
+ expect(store.sessions(:bar, :b)).to eq(1)
26
+ end
27
+ end
28
+
29
+ describe '#track_success!' do
30
+ it 'returns false if group is nil' do
31
+ expect(store.track_success!(:foo, nil, 'abc')).to be(false)
32
+ end
33
+
34
+ it 'tracks successes' do
35
+ 5.times { store.track_success!(:foo, :a, 'abc') }
36
+ 7.times { store.track_success!(:foo, :b, 'abc') }
37
+ 3.times { store.track_success!(:bar, :b, 'abc') }
38
+ expect(store.successes(:foo, :a)).to eq(5)
39
+ expect(store.successes(:foo, :b)).to eq(7)
40
+ expect(store.successes(:bar, :b)).to eq(3)
41
+ end
42
+
43
+ it 'tracks unique conversions' do
44
+ 5.times { |i| store.track_success!(:foo, :a, i) }
45
+ 2.times { store.track_success!(:foo, :b, 'foo') }
46
+ 3.times { store.track_success!(:foo, :b, 'bar') }
47
+ 3.times { store.track_success!(:bar, :b, 'foo') }
48
+ expect(store.conversions(:foo, :a)).to eq(5)
49
+ expect(store.conversions(:foo, :b)).to eq(2)
50
+ expect(store.conversions(:bar, :b)).to eq(1)
51
+ end
52
+ end
53
+
54
+ describe '#views' do
55
+ it 'returns 0 if nothing was tracked' do
56
+ expect(store.views(:xxx, :a)).to eq(0)
57
+ end
58
+ end
59
+
60
+ describe '#conversions' do
61
+ it 'returns 0 if nothing was tracked' do
62
+ expect(store.conversions(:xxx, :a)).to eq(0)
63
+ end
64
+ end
65
+
66
+ describe '#sessions' do
67
+ it 'returns 0 if nothing was tracked' do
68
+ expect(store.sessions(:xxx, :a)).to eq(0)
69
+ end
70
+ end
71
+
72
+ describe '#counts' do
73
+ it 'returns all counts in one call' do
74
+ 2.times { store.track_view!(:foo, :a, 'foo') }
75
+ 3.times { store.track_view!(:foo, :a, 'bar') }
76
+ 1.times { store.track_success!(:foo, :a, 'foo') }
77
+ 5.times { store.track_success!(:foo, :a, 'bar') }
78
+ 2.times { store.track_success!(:foo, :a, 'baz') }
79
+ expect(store.counts(:foo, :a)).to eq(views: 5, sessions: 2, successes: 8, conversions: 3)
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,187 @@
1
+ require 'spec_helper'
2
+
3
+ describe Ablab do
4
+ let(:ab) do
5
+ Module.new { extend Ablab::ModuleMethods }
6
+ end
7
+
8
+ it 'has a version number' do
9
+ expect(Ablab::VERSION).not_to be nil
10
+ end
11
+
12
+ describe '.experiment' do
13
+ it 'creates an experiment' do
14
+ ab.experiment :foo_bar do; end
15
+ expect(ab.experiments[:foo_bar]).to be_a(Ablab::Experiment)
16
+ end
17
+ end
18
+
19
+ describe '.tracker' do
20
+ around do |example|
21
+ begin
22
+ Ablab::Store::Dummy = Class.new
23
+ example.run
24
+ ensure
25
+ Ablab::Store.send(:remove_const, :Dummy)
26
+ end
27
+ end
28
+
29
+ it 'returns a Ablab::Store::Memory instance if store was never set' do
30
+ expect(ab.tracker).to be_a(Ablab::Store::Memory)
31
+ end
32
+
33
+ it 'returns the store if it was set' do
34
+ ab.store :dummy
35
+ expect(ab.tracker).to be_a(Ablab::Store::Dummy)
36
+ end
37
+ end
38
+
39
+ describe '.dashboard_credentials' do
40
+ it 'raises if called without name or password' do
41
+ expect {
42
+ ab.dashboard_credentials(name: 'foo')
43
+ }.to raise_error(Ablab::InvalidCredentials)
44
+
45
+ expect {
46
+ ab.dashboard_credentials(password: 'foo')
47
+ }.to raise_error(Ablab::InvalidCredentials)
48
+ end
49
+
50
+ it 'sets and gets the credentials' do
51
+ ab.dashboard_credentials(name: 'foo', password: 'bar')
52
+ expect(ab.dashboard_credentials).to eq(name: 'foo', password: 'bar')
53
+ end
54
+ end
55
+
56
+ describe Ablab::Experiment do
57
+ let(:experiment) do
58
+ Ablab::Experiment.new('foo') do; end
59
+ end
60
+
61
+ it 'automatically creates a control group' do
62
+ expect(experiment.control).to be_a(Ablab::Group)
63
+ expect(experiment.groups).to_not be_empty
64
+ end
65
+
66
+ it 'symbolizes its name' do
67
+ expect(experiment.name).to eq(:foo)
68
+ end
69
+
70
+ describe '#description' do
71
+ it 'sets the description' do
72
+ experiment.description 'foo bar'
73
+ expect(experiment.description).to eq('foo bar')
74
+ end
75
+ end
76
+
77
+ describe '#goal' do
78
+ it 'sets the experiment goal' do
79
+ experiment.goal 'foo bar'
80
+ expect(experiment.goal).to eq('foo bar')
81
+ end
82
+ end
83
+
84
+ describe '#percentage_of_visitors' do
85
+ it 'returns 100 if never set' do
86
+ expect(experiment.percentage_of_visitors).to eq(100)
87
+ end
88
+
89
+ it 'sets the percentage of visitors included in the experiment' do
90
+ experiment.percentage_of_visitors 25
91
+ expect(experiment.percentage_of_visitors).to eq(25)
92
+ end
93
+ end
94
+
95
+ describe '#group' do
96
+ it 'creates a group' do
97
+ experiment.group :a, description: 'foo bar baz'
98
+ expect(experiment.groups.last).to be_a(Ablab::Group)
99
+ expect(experiment.groups.last.description).to eq('foo bar baz')
100
+ end
101
+
102
+ it 'symbolizes the group name' do
103
+ experiment.group 'yeah'
104
+ expect(experiment.groups.last.name).to eq(:yeah)
105
+ end
106
+ end
107
+
108
+ describe '.results' do
109
+ it 'returns the results of the experiment' do
110
+ experiment.group :x, description: 'a test group'
111
+ allow(Ablab.tracker).to receive(:views) do |_, group|
112
+ { control: 201, x: 238 }[group]
113
+ end
114
+ allow(Ablab.tracker).to receive(:sessions) do |_, group|
115
+ { control: 182, x: 188 }[group]
116
+ end
117
+ allow(Ablab.tracker).to receive(:successes) do |_, group|
118
+ { control: 38, x: 70 }[group]
119
+ end
120
+ allow(Ablab.tracker).to receive(:conversions) do |_, group|
121
+ { control: 35, x: 61 }[group]
122
+ end
123
+ results = experiment.results
124
+ expect(results[:control]).to eq({
125
+ views: 201,
126
+ sessions: 182,
127
+ successes: 38,
128
+ conversions: 35,
129
+ control: true,
130
+ description: 'control group'
131
+ })
132
+ expect(results[:x]).to eq({
133
+ views: 238,
134
+ sessions: 188,
135
+ successes: 70,
136
+ conversions: 61,
137
+ control: false,
138
+ z_score: 2.9410157224928595,
139
+ description: 'a test group'
140
+ })
141
+ end
142
+ end
143
+ end
144
+
145
+ describe Ablab::Run do
146
+ let(:experiment) do
147
+ Ablab::Experiment.new(:foo) do
148
+ group :a
149
+ group :b
150
+ end
151
+ end
152
+
153
+ it 'gets assigned to the right group' do
154
+ run = Ablab::Run.new(experiment, 0)
155
+ allow(run).to receive(:draw).and_return 0
156
+ expect(run).to be_in_group(:control)
157
+ run = Ablab::Run.new(experiment, 0)
158
+ allow(run).to receive(:draw).and_return 334
159
+ expect(run).to be_in_group(:a)
160
+ run = Ablab::Run.new(experiment, 0)
161
+ allow(run).to receive(:draw).and_return 999
162
+ expect(run).to be_in_group(:b)
163
+ end
164
+
165
+ it 'assigns the same session ID to the same group' do
166
+ run1 = Ablab::Run.new(experiment, 'foobar')
167
+ run2 = Ablab::Run.new(experiment, 'foobar')
168
+ expect(run1.group).to eq(run2.group)
169
+ end
170
+
171
+ it 'selects only the given percentage of users' do
172
+ experiment.percentage_of_visitors 30
173
+ run = Ablab::Run.new(experiment, 0)
174
+ allow(run).to receive(:draw).and_return 0
175
+ expect(run).to be_in_group(:control)
176
+ run = Ablab::Run.new(experiment, 0)
177
+ allow(run).to receive(:draw).and_return 100
178
+ expect(run).to be_in_group(:a)
179
+ run = Ablab::Run.new(experiment, 0)
180
+ allow(run).to receive(:draw).and_return 200
181
+ expect(run).to be_in_group(:b)
182
+ run = Ablab::Run.new(experiment, 0)
183
+ allow(run).to receive(:draw).and_return 300
184
+ expect(run.group).to be_nil
185
+ end
186
+ end
187
+ end
@@ -0,0 +1,2 @@
1
+ $LOAD_PATH.unshift File.expand_path('../../lib', __FILE__)
2
+ require 'ablab'
metadata ADDED
@@ -0,0 +1,118 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: ablab-core
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.2.1
5
+ platform: ruby
6
+ authors:
7
+ - Luca Ongaro
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2016-01-21 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: redis
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - ">="
18
+ - !ruby/object:Gem::Version
19
+ version: '0'
20
+ type: :runtime
21
+ prerelease: false
22
+ version_requirements: !ruby/object:Gem::Requirement
23
+ requirements:
24
+ - - ">="
25
+ - !ruby/object:Gem::Version
26
+ version: '0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rake
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :development
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '0'
41
+ - !ruby/object:Gem::Dependency
42
+ name: bundler
43
+ requirement: !ruby/object:Gem::Requirement
44
+ requirements:
45
+ - - ">="
46
+ - !ruby/object:Gem::Version
47
+ version: '0'
48
+ type: :development
49
+ prerelease: false
50
+ version_requirements: !ruby/object:Gem::Requirement
51
+ requirements:
52
+ - - ">="
53
+ - !ruby/object:Gem::Version
54
+ version: '0'
55
+ - !ruby/object:Gem::Dependency
56
+ name: rspec
57
+ requirement: !ruby/object:Gem::Requirement
58
+ requirements:
59
+ - - ">="
60
+ - !ruby/object:Gem::Version
61
+ version: '0'
62
+ type: :development
63
+ prerelease: false
64
+ version_requirements: !ruby/object:Gem::Requirement
65
+ requirements:
66
+ - - ">="
67
+ - !ruby/object:Gem::Version
68
+ version: '0'
69
+ description: Ablab - A/B testing library
70
+ email:
71
+ - lukeongaro@gmail.com
72
+ executables: []
73
+ extensions: []
74
+ extra_rdoc_files: []
75
+ files:
76
+ - MIT-LICENSE
77
+ - Rakefile
78
+ - lib/ablab.rb
79
+ - lib/ablab/store.rb
80
+ - lib/ablab/store/memory.rb
81
+ - lib/ablab/store/redis.rb
82
+ - lib/ablab/version.rb
83
+ - lib/tasks/ablab_tasks.rake
84
+ - spec/ablab/store/memory_spec.rb
85
+ - spec/ablab/store/redis_spec.rb
86
+ - spec/ablab/store/store_examples.rb
87
+ - spec/ablab_spec.rb
88
+ - spec/spec_helper.rb
89
+ homepage: https://github.com/lucaong/ablab
90
+ licenses:
91
+ - MIT
92
+ metadata: {}
93
+ post_install_message:
94
+ rdoc_options: []
95
+ require_paths:
96
+ - lib
97
+ required_ruby_version: !ruby/object:Gem::Requirement
98
+ requirements:
99
+ - - ">="
100
+ - !ruby/object:Gem::Version
101
+ version: '0'
102
+ required_rubygems_version: !ruby/object:Gem::Requirement
103
+ requirements:
104
+ - - ">="
105
+ - !ruby/object:Gem::Version
106
+ version: '0'
107
+ requirements: []
108
+ rubyforge_project:
109
+ rubygems_version: 2.5.1
110
+ signing_key:
111
+ specification_version: 4
112
+ summary: Ablab core - A/B testing library
113
+ test_files:
114
+ - spec/ablab/store/memory_spec.rb
115
+ - spec/ablab/store/redis_spec.rb
116
+ - spec/ablab/store/store_examples.rb
117
+ - spec/ablab_spec.rb
118
+ - spec/spec_helper.rb