flipper 0.1.0
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.
- data/.gitignore +18 -0
- data/Gemfile +20 -0
- data/Guardfile +18 -0
- data/LICENSE +22 -0
- data/README.md +182 -0
- data/Rakefile +7 -0
- data/examples/basic.rb +27 -0
- data/examples/dsl.rb +85 -0
- data/examples/example_setup.rb +8 -0
- data/examples/group.rb +36 -0
- data/examples/individual_actor.rb +29 -0
- data/examples/percentage_of_actors.rb +35 -0
- data/examples/percentage_of_random.rb +33 -0
- data/flipper.gemspec +17 -0
- data/lib/flipper.rb +33 -0
- data/lib/flipper/adapters/memory.rb +35 -0
- data/lib/flipper/dsl.rb +61 -0
- data/lib/flipper/errors.rb +11 -0
- data/lib/flipper/feature.rb +49 -0
- data/lib/flipper/gate.rb +55 -0
- data/lib/flipper/gates/actor.rb +29 -0
- data/lib/flipper/gates/boolean.rb +29 -0
- data/lib/flipper/gates/group.rb +32 -0
- data/lib/flipper/gates/percentage_of_actors.rb +25 -0
- data/lib/flipper/gates/percentage_of_random.rb +25 -0
- data/lib/flipper/registry.rb +36 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +78 -0
- data/lib/flipper/toggle.rb +31 -0
- data/lib/flipper/toggles/boolean.rb +22 -0
- data/lib/flipper/toggles/set.rb +17 -0
- data/lib/flipper/toggles/value.rb +17 -0
- data/lib/flipper/type.rb +18 -0
- data/lib/flipper/types/actor.rb +17 -0
- data/lib/flipper/types/boolean.rb +13 -0
- data/lib/flipper/types/group.rb +22 -0
- data/lib/flipper/types/percentage.rb +19 -0
- data/lib/flipper/types/percentage_of_actors.rb +6 -0
- data/lib/flipper/types/percentage_of_random.rb +6 -0
- data/lib/flipper/version.rb +3 -0
- data/spec/flipper/adapters/memory_spec.rb +19 -0
- data/spec/flipper/dsl_spec.rb +185 -0
- data/spec/flipper/feature_spec.rb +401 -0
- data/spec/flipper/registry_spec.rb +71 -0
- data/spec/flipper/types/actor_spec.rb +23 -0
- data/spec/flipper/types/boolean_spec.rb +9 -0
- data/spec/flipper/types/group_spec.rb +32 -0
- data/spec/flipper/types/percentage_of_actors_spec.rb +6 -0
- data/spec/flipper/types/percentage_of_random_spec.rb +6 -0
- data/spec/flipper/types/percentage_spec.rb +6 -0
- data/spec/flipper_spec.rb +59 -0
- data/spec/helper.rb +47 -0
- metadata +114 -0
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Gates
|
3
|
+
class PercentageOfRandom < Gate
|
4
|
+
Key = :perc_time
|
5
|
+
|
6
|
+
def type_key
|
7
|
+
Key
|
8
|
+
end
|
9
|
+
|
10
|
+
def open?(actor)
|
11
|
+
percentage = toggle.value
|
12
|
+
|
13
|
+
if percentage.nil?
|
14
|
+
false
|
15
|
+
else
|
16
|
+
rand < (percentage / 100.0)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def protects?(thing)
|
21
|
+
thing.is_a?(Flipper::Types::PercentageOfRandom)
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Registry
|
3
|
+
class Error < StandardError; end
|
4
|
+
class DuplicateKey < Error; end
|
5
|
+
class MissingKey < Error; end
|
6
|
+
|
7
|
+
def initialize(source = {})
|
8
|
+
@mutex = Mutex.new
|
9
|
+
@source = source
|
10
|
+
end
|
11
|
+
|
12
|
+
def add(key, value)
|
13
|
+
@mutex.synchronize do
|
14
|
+
if @source[key]
|
15
|
+
raise DuplicateKey, "#{key} is already registered"
|
16
|
+
else
|
17
|
+
@source[key] = value
|
18
|
+
end
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def get(key)
|
23
|
+
@mutex.synchronize do
|
24
|
+
@source[key]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def each(&block)
|
29
|
+
@mutex.synchronize { @source.dup }.each(&block)
|
30
|
+
end
|
31
|
+
|
32
|
+
def clear
|
33
|
+
@mutex.synchronize { @source.clear }
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
@@ -0,0 +1,78 @@
|
|
1
|
+
require 'set'
|
2
|
+
|
3
|
+
# Requires the following methods
|
4
|
+
# subject
|
5
|
+
# read_key(key)
|
6
|
+
# write_key(key, value)
|
7
|
+
shared_examples_for 'a flipper adapter' do
|
8
|
+
describe "#write" do
|
9
|
+
let(:separator) { Flipper::Gate::Separator }
|
10
|
+
|
11
|
+
it "sets key to value in store" do
|
12
|
+
subject.write('foo', true)
|
13
|
+
read_key('foo').should be_true
|
14
|
+
end
|
15
|
+
|
16
|
+
it "works with separator" do
|
17
|
+
subject.write("foo#{separator}bar", true)
|
18
|
+
read_key("foo#{separator}bar").should be_true
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
describe "#read" do
|
23
|
+
it "returns nil if key not in store" do
|
24
|
+
subject.read('foo').should be_nil
|
25
|
+
end
|
26
|
+
|
27
|
+
it "returns value if key in store" do
|
28
|
+
write_key 'foo', 'bar'
|
29
|
+
subject.read('foo').should eq('bar')
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
describe "#delete" do
|
34
|
+
it "deletes key" do
|
35
|
+
write_key 'foo', 'bar'
|
36
|
+
subject.delete('foo')
|
37
|
+
read_key('foo').should be_nil
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
describe "#set_add" do
|
42
|
+
it "adds value to store" do
|
43
|
+
subject.set_add('foo', 1)
|
44
|
+
read_key('foo').should eq(Set[1])
|
45
|
+
end
|
46
|
+
|
47
|
+
it "does not add same value more than once" do
|
48
|
+
subject.set_add('foo', 1)
|
49
|
+
subject.set_add('foo', 1)
|
50
|
+
subject.set_add('foo', 1)
|
51
|
+
subject.set_add('foo', 2)
|
52
|
+
read_key('foo').should eq(Set[1, 2])
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
describe "#set_delete" do
|
57
|
+
it "removes value from set if key in store" do
|
58
|
+
write_key 'foo', Set[1, 2]
|
59
|
+
subject.set_delete('foo', 1)
|
60
|
+
read_key('foo').should eq(Set[2])
|
61
|
+
end
|
62
|
+
|
63
|
+
it "works fine if key not in store" do
|
64
|
+
subject.set_delete('foo', 'bar')
|
65
|
+
end
|
66
|
+
end
|
67
|
+
|
68
|
+
describe "#set_members" do
|
69
|
+
it "defaults to empty set" do
|
70
|
+
subject.set_members('foo').should eq(Set.new)
|
71
|
+
end
|
72
|
+
|
73
|
+
it "returns set if in store" do
|
74
|
+
write_key 'foo', Set[1, 2]
|
75
|
+
subject.set_members('foo').should eq(Set[1, 2])
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
require 'forwardable'
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class Toggle
|
5
|
+
extend Forwardable
|
6
|
+
|
7
|
+
attr_reader :gate
|
8
|
+
|
9
|
+
def_delegators :@gate, :key, :feature, :adapter
|
10
|
+
|
11
|
+
def initialize(gate)
|
12
|
+
@gate = gate
|
13
|
+
end
|
14
|
+
|
15
|
+
def enable(thing)
|
16
|
+
raise 'Not implemented'
|
17
|
+
end
|
18
|
+
|
19
|
+
def disable(thing)
|
20
|
+
raise 'Not implemented'
|
21
|
+
end
|
22
|
+
|
23
|
+
def value
|
24
|
+
raise 'Not implemented'
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
require 'flipper/toggles/boolean'
|
30
|
+
require 'flipper/toggles/set'
|
31
|
+
require 'flipper/toggles/value'
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Toggles
|
3
|
+
class Boolean < Toggle
|
4
|
+
def enable(thing)
|
5
|
+
adapter.write key, thing.enabled_value
|
6
|
+
end
|
7
|
+
|
8
|
+
def disable(thing)
|
9
|
+
adapter.delete key
|
10
|
+
|
11
|
+
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::Actor::Key}"
|
12
|
+
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::Group::Key}"
|
13
|
+
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::PercentageOfActors::Key}"
|
14
|
+
adapter.delete "#{gate.key_prefix}#{Gate::Separator}#{Gates::PercentageOfRandom::Key}"
|
15
|
+
end
|
16
|
+
|
17
|
+
def value
|
18
|
+
adapter.read key
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Toggles
|
3
|
+
class Set < Toggle
|
4
|
+
def enable(thing)
|
5
|
+
adapter.set_add key, thing.enabled_value
|
6
|
+
end
|
7
|
+
|
8
|
+
def disable(thing)
|
9
|
+
adapter.set_delete key, thing.disabled_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def value
|
13
|
+
adapter.set_members key
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Toggles
|
3
|
+
class Value < Toggle
|
4
|
+
def enable(thing)
|
5
|
+
adapter.write key, thing.enabled_value
|
6
|
+
end
|
7
|
+
|
8
|
+
def disable(thing)
|
9
|
+
adapter.write key, thing.disabled_value
|
10
|
+
end
|
11
|
+
|
12
|
+
def value
|
13
|
+
adapter.read key
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
data/lib/flipper/type.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Type
|
3
|
+
def enabled_value
|
4
|
+
raise 'Not implemented'
|
5
|
+
end
|
6
|
+
|
7
|
+
def disabled_value
|
8
|
+
raise 'Not implemented'
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
require 'flipper/types/actor'
|
14
|
+
require 'flipper/types/boolean'
|
15
|
+
require 'flipper/types/group'
|
16
|
+
require 'flipper/types/percentage'
|
17
|
+
require 'flipper/types/percentage_of_actors'
|
18
|
+
require 'flipper/types/percentage_of_random'
|
@@ -0,0 +1,17 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Types
|
3
|
+
class Actor < Type
|
4
|
+
attr_reader :identifier
|
5
|
+
|
6
|
+
def initialize(identifier)
|
7
|
+
@identifier = identifier.to_i
|
8
|
+
end
|
9
|
+
|
10
|
+
def enabled_value
|
11
|
+
@identifier
|
12
|
+
end
|
13
|
+
|
14
|
+
alias_method :disabled_value, :enabled_value
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Types
|
3
|
+
class Group < Type
|
4
|
+
attr_reader :name
|
5
|
+
|
6
|
+
def initialize(name, &block)
|
7
|
+
@name = name.to_sym
|
8
|
+
@block = block
|
9
|
+
end
|
10
|
+
|
11
|
+
def match?(*args)
|
12
|
+
@block.call(*args) == true
|
13
|
+
end
|
14
|
+
|
15
|
+
def enabled_value
|
16
|
+
@name
|
17
|
+
end
|
18
|
+
|
19
|
+
alias_method :disabled_value, :enabled_value
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/adapters/memory'
|
3
|
+
require 'flipper/spec/shared_adapter_specs'
|
4
|
+
|
5
|
+
describe Flipper::Adapters::Memory do
|
6
|
+
let(:source) { {} }
|
7
|
+
|
8
|
+
subject { Flipper::Adapters::Memory.new(source) }
|
9
|
+
|
10
|
+
def read_key(key)
|
11
|
+
source[key]
|
12
|
+
end
|
13
|
+
|
14
|
+
def write_key(key, value)
|
15
|
+
source[key] = value
|
16
|
+
end
|
17
|
+
|
18
|
+
it_should_behave_like 'a flipper adapter'
|
19
|
+
end
|
@@ -0,0 +1,185 @@
|
|
1
|
+
require 'helper'
|
2
|
+
require 'flipper/dsl'
|
3
|
+
|
4
|
+
describe Flipper::DSL do
|
5
|
+
subject { Flipper::DSL.new(adapter) }
|
6
|
+
|
7
|
+
let(:source) { {} }
|
8
|
+
let(:adapter) { Flipper::Adapters::Memory.new(source) }
|
9
|
+
|
10
|
+
let(:admins_feature) { feature(:admins) }
|
11
|
+
|
12
|
+
def feature(name)
|
13
|
+
Flipper::Feature.new(name, adapter)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#enabled?" do
|
17
|
+
before do
|
18
|
+
subject.stub(:feature => admins_feature)
|
19
|
+
end
|
20
|
+
|
21
|
+
it "passes arguments to feature enabled check and returns result" do
|
22
|
+
admins_feature.should_receive(:enabled?).with(:foo).and_return(true)
|
23
|
+
subject.should_receive(:feature).with(:stats).and_return(admins_feature)
|
24
|
+
subject.enabled?(:stats, :foo).should be_true
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
describe "#disabled?" do
|
29
|
+
it "passes all args to enabled? and returns the opposite" do
|
30
|
+
subject.should_receive(:enabled?).with(:stats, :foo).and_return(true)
|
31
|
+
subject.disabled?(:stats, :foo).should be_false
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
describe "#enable" do
|
36
|
+
before do
|
37
|
+
subject.stub(:feature => admins_feature)
|
38
|
+
end
|
39
|
+
|
40
|
+
it "calls enable for feature with arguments" do
|
41
|
+
admins_feature.should_receive(:enable).with(:foo)
|
42
|
+
subject.should_receive(:feature).with(:stats).and_return(admins_feature)
|
43
|
+
subject.enable :stats, :foo
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
describe "#disable" do
|
48
|
+
before do
|
49
|
+
subject.stub(:feature => admins_feature)
|
50
|
+
end
|
51
|
+
|
52
|
+
it "calls disable for feature with arguments" do
|
53
|
+
admins_feature.should_receive(:disable).with(:foo)
|
54
|
+
subject.should_receive(:feature).with(:stats).and_return(admins_feature)
|
55
|
+
subject.disable :stats, :foo
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
describe "#feature" do
|
60
|
+
before do
|
61
|
+
@result = subject.feature(:stats)
|
62
|
+
end
|
63
|
+
|
64
|
+
it "returns instance of feature with correct name and adapter" do
|
65
|
+
@result.should be_instance_of(Flipper::Feature)
|
66
|
+
@result.name.should eq(:stats)
|
67
|
+
@result.adapter.should eq(adapter)
|
68
|
+
end
|
69
|
+
|
70
|
+
it "memoizes the feature" do
|
71
|
+
subject.feature(:stats).should equal(@result)
|
72
|
+
end
|
73
|
+
end
|
74
|
+
|
75
|
+
describe "#[]" do
|
76
|
+
before do
|
77
|
+
@result = subject[:stats]
|
78
|
+
end
|
79
|
+
|
80
|
+
it "returns instance of feature with correct name and adapter" do
|
81
|
+
@result.should be_instance_of(Flipper::Feature)
|
82
|
+
@result.name.should eq(:stats)
|
83
|
+
@result.adapter.should eq(adapter)
|
84
|
+
end
|
85
|
+
|
86
|
+
it "memoizes the feature" do
|
87
|
+
subject[:stats].should equal(@result)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
describe "#group" do
|
92
|
+
context "for registered group" do
|
93
|
+
before do
|
94
|
+
@group = Flipper.register(:admins) { }
|
95
|
+
end
|
96
|
+
|
97
|
+
it "returns group" do
|
98
|
+
subject.group(:admins).should eq(@group)
|
99
|
+
end
|
100
|
+
|
101
|
+
it "always returns same instance for same name" do
|
102
|
+
subject.group(:admins).should equal(subject.group(:admins))
|
103
|
+
end
|
104
|
+
end
|
105
|
+
|
106
|
+
context "for unregistered group" do
|
107
|
+
it "returns nil" do
|
108
|
+
subject.group(:admins).should be_nil
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
describe "#actor" do
|
114
|
+
context "for something that responds to id" do
|
115
|
+
it "returns actor instance with identifier set to id" do
|
116
|
+
user = Struct.new(:id).new(23)
|
117
|
+
actor = subject.actor(user)
|
118
|
+
actor.should be_instance_of(Flipper::Types::Actor)
|
119
|
+
actor.identifier.should eq(23)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
|
123
|
+
context "for something that responds to identifier" do
|
124
|
+
it "returns actor instance with identifier set to id" do
|
125
|
+
user = Struct.new(:identifier).new(45)
|
126
|
+
actor = subject.actor(user)
|
127
|
+
actor.should be_instance_of(Flipper::Types::Actor)
|
128
|
+
actor.identifier.should eq(45)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
context "for something that responds to identifier and id" do
|
133
|
+
it "returns actor instance with identifier set to identifier" do
|
134
|
+
user = Struct.new(:id, :identifier).new(1, 50)
|
135
|
+
actor = subject.actor(user)
|
136
|
+
actor.should be_instance_of(Flipper::Types::Actor)
|
137
|
+
actor.identifier.should eq(50)
|
138
|
+
end
|
139
|
+
end
|
140
|
+
|
141
|
+
context "for a number" do
|
142
|
+
it "returns actor instance with identifer set to number" do
|
143
|
+
actor = subject.actor(33)
|
144
|
+
actor.should be_instance_of(Flipper::Types::Actor)
|
145
|
+
actor.identifier.should eq(33)
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
context "for nil" do
|
150
|
+
it "raises error" do
|
151
|
+
expect {
|
152
|
+
subject.actor(nil)
|
153
|
+
}.to raise_error(ArgumentError)
|
154
|
+
end
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
describe "#random" do
|
159
|
+
before do
|
160
|
+
@result = subject.random(5)
|
161
|
+
end
|
162
|
+
|
163
|
+
it "returns percentage of random" do
|
164
|
+
@result.should be_instance_of(Flipper::Types::PercentageOfRandom)
|
165
|
+
end
|
166
|
+
|
167
|
+
it "sets value" do
|
168
|
+
@result.value.should eq(5)
|
169
|
+
end
|
170
|
+
end
|
171
|
+
|
172
|
+
describe "#actors" do
|
173
|
+
before do
|
174
|
+
@result = subject.actors(17)
|
175
|
+
end
|
176
|
+
|
177
|
+
it "returns percentage of actors" do
|
178
|
+
@result.should be_instance_of(Flipper::Types::PercentageOfActors)
|
179
|
+
end
|
180
|
+
|
181
|
+
it "sets value" do
|
182
|
+
@result.value.should eq(17)
|
183
|
+
end
|
184
|
+
end
|
185
|
+
end
|