flipper 0.2.1 → 0.3.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.travis.yml +8 -0
- data/Gemfile +2 -1
- data/lib/flipper.rb +8 -8
- data/lib/flipper/adapter.rb +31 -19
- data/lib/flipper/adapters/memory.rb +15 -6
- data/lib/flipper/adapters/operation_logger.rb +57 -0
- data/lib/flipper/dsl.rb +12 -6
- data/lib/flipper/feature.rb +18 -7
- data/lib/flipper/gate.rb +2 -7
- data/lib/flipper/gates/percentage_of_actors.rb +3 -1
- data/lib/flipper/key.rb +19 -0
- data/lib/flipper/registry.rb +2 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +24 -29
- data/lib/flipper/toggle.rb +8 -2
- data/lib/flipper/toggles/boolean.rb +6 -6
- data/lib/flipper/toggles/set.rb +4 -2
- data/lib/flipper/toggles/value.rb +4 -2
- data/lib/flipper/type.rb +1 -5
- data/lib/flipper/types/actor.rb +1 -6
- data/lib/flipper/types/boolean.rb +1 -5
- data/lib/flipper/types/group.rb +1 -3
- data/lib/flipper/types/percentage.rb +3 -6
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +46 -2
- data/spec/flipper/adapters/memoized_spec.rb +2 -2
- data/spec/flipper/adapters/memory_spec.rb +2 -2
- data/spec/flipper/dsl_spec.rb +32 -18
- data/spec/flipper/feature_spec.rb +6 -392
- data/spec/flipper/key_spec.rb +17 -0
- data/spec/flipper/middleware/local_cache_spec.rb +18 -30
- data/spec/flipper/registry_spec.rb +4 -4
- data/spec/flipper/types/actor_spec.rb +3 -14
- data/spec/flipper/types/percentage_spec.rb +21 -0
- data/spec/integration_spec.rb +455 -0
- metadata +12 -5
data/.travis.yml
ADDED
data/Gemfile
CHANGED
data/lib/flipper.rb
CHANGED
@@ -1,11 +1,3 @@
|
|
1
|
-
require 'flipper/dsl'
|
2
|
-
require 'flipper/errors'
|
3
|
-
require 'flipper/feature'
|
4
|
-
require 'flipper/gate'
|
5
|
-
require 'flipper/registry'
|
6
|
-
require 'flipper/toggle'
|
7
|
-
require 'flipper/type'
|
8
|
-
|
9
1
|
module Flipper
|
10
2
|
def self.new(*args)
|
11
3
|
DSL.new(*args)
|
@@ -31,3 +23,11 @@ module Flipper
|
|
31
23
|
groups.get(name)
|
32
24
|
end
|
33
25
|
end
|
26
|
+
|
27
|
+
require 'flipper/dsl'
|
28
|
+
require 'flipper/errors'
|
29
|
+
require 'flipper/feature'
|
30
|
+
require 'flipper/gate'
|
31
|
+
require 'flipper/registry'
|
32
|
+
require 'flipper/toggle'
|
33
|
+
require 'flipper/type'
|
data/lib/flipper/adapter.rb
CHANGED
@@ -1,5 +1,7 @@
|
|
1
1
|
module Flipper
|
2
|
-
# Internal: Adapter wrapper that wraps vanilla adapter instances
|
2
|
+
# Internal: Adapter wrapper that wraps vanilla adapter instances. Adds things
|
3
|
+
# like local caching and convenience methods for adding/reading features from
|
4
|
+
# the adapter.
|
3
5
|
#
|
4
6
|
# So what is this local cache crap?
|
5
7
|
#
|
@@ -17,6 +19,8 @@ module Flipper
|
|
17
19
|
# To see an example adapter that this would wrap, checkout the [memory
|
18
20
|
# adapter included with flipper](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb).
|
19
21
|
class Adapter
|
22
|
+
FeaturesKey = 'features'
|
23
|
+
|
20
24
|
# Internal: Wraps vanilla adapter instance for use internally in flipper.
|
21
25
|
#
|
22
26
|
# object - Either an instance of Flipper::Adapter or a vanilla adapter instance
|
@@ -65,35 +69,27 @@ module Flipper
|
|
65
69
|
end
|
66
70
|
|
67
71
|
def read(key)
|
68
|
-
|
69
|
-
cache(key) { @adapter.read(key) }
|
70
|
-
else
|
71
|
-
@adapter.read(key)
|
72
|
-
end
|
72
|
+
perform_read(key) { @adapter.read(key) }
|
73
73
|
end
|
74
74
|
|
75
75
|
def write(key, value)
|
76
|
-
@adapter.write(key, value)
|
76
|
+
perform_update(key) { @adapter.write(key, value) }
|
77
77
|
end
|
78
78
|
|
79
79
|
def delete(key)
|
80
|
-
|
80
|
+
perform_update(key) { @adapter.delete(key) }
|
81
81
|
end
|
82
82
|
|
83
83
|
def set_members(key)
|
84
|
-
|
85
|
-
cache(key) { @adapter.set_members(key) }
|
86
|
-
else
|
87
|
-
@adapter.set_members(key)
|
88
|
-
end
|
84
|
+
perform_read(key) { @adapter.set_members(key) }
|
89
85
|
end
|
90
86
|
|
91
87
|
def set_add(key, value)
|
92
|
-
@adapter.set_add(key, value)
|
88
|
+
perform_update(key) { @adapter.set_add(key, value) }
|
93
89
|
end
|
94
90
|
|
95
91
|
def set_delete(key, value)
|
96
|
-
@adapter.set_delete(key, value)
|
92
|
+
perform_update(key) { @adapter.set_delete(key, value) }
|
97
93
|
end
|
98
94
|
|
99
95
|
def eql?(other)
|
@@ -101,14 +97,30 @@ module Flipper
|
|
101
97
|
end
|
102
98
|
alias :== :eql?
|
103
99
|
|
100
|
+
def features
|
101
|
+
set_members(FeaturesKey)
|
102
|
+
end
|
103
|
+
|
104
|
+
def feature_add(name)
|
105
|
+
set_add(FeaturesKey, name.to_s)
|
106
|
+
end
|
107
|
+
|
104
108
|
private
|
105
109
|
|
106
|
-
def
|
107
|
-
|
110
|
+
def perform_read(key)
|
111
|
+
if using_local_cache?
|
112
|
+
local_cache.fetch(key.to_s) { local_cache[key.to_s] = yield }
|
113
|
+
else
|
114
|
+
yield
|
115
|
+
end
|
108
116
|
end
|
109
117
|
|
110
|
-
def
|
111
|
-
|
118
|
+
def perform_update(key)
|
119
|
+
result = yield
|
120
|
+
if using_local_cache?
|
121
|
+
local_cache.delete(key.to_s)
|
122
|
+
end
|
123
|
+
result
|
112
124
|
end
|
113
125
|
end
|
114
126
|
end
|
@@ -8,27 +8,36 @@ module Flipper
|
|
8
8
|
end
|
9
9
|
|
10
10
|
def read(key)
|
11
|
-
@source[key]
|
11
|
+
@source[key.to_s]
|
12
12
|
end
|
13
13
|
|
14
14
|
def write(key, value)
|
15
|
-
@source[key] = value
|
15
|
+
@source[key.to_s] = value
|
16
16
|
end
|
17
17
|
|
18
18
|
def delete(key)
|
19
|
-
@source.delete(key)
|
19
|
+
@source.delete(key.to_s)
|
20
20
|
end
|
21
21
|
|
22
22
|
def set_add(key, value)
|
23
|
-
|
23
|
+
ensure_set_initialized(key)
|
24
|
+
@source[key.to_s].add(value)
|
24
25
|
end
|
25
26
|
|
26
27
|
def set_delete(key, value)
|
27
|
-
|
28
|
+
ensure_set_initialized(key)
|
29
|
+
@source[key.to_s].delete(value)
|
28
30
|
end
|
29
31
|
|
30
32
|
def set_members(key)
|
31
|
-
|
33
|
+
ensure_set_initialized(key)
|
34
|
+
@source[key.to_s]
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def ensure_set_initialized(key)
|
40
|
+
@source[key.to_s] ||= Set.new
|
32
41
|
end
|
33
42
|
end
|
34
43
|
end
|
@@ -0,0 +1,57 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Adapters
|
3
|
+
# Public: Adapter that wraps another adapter and stores the operations.
|
4
|
+
#
|
5
|
+
# Useful in tests to verify calls and such.
|
6
|
+
class OperationLogger
|
7
|
+
attr_reader :operations
|
8
|
+
|
9
|
+
Read = Struct.new(:key)
|
10
|
+
Write = Struct.new(:key, :value)
|
11
|
+
Delete = Struct.new(:key)
|
12
|
+
SetAdd = Struct.new(:key, :value)
|
13
|
+
SetDelete = Struct.new(:key, :value)
|
14
|
+
SetMember = Struct.new(:key)
|
15
|
+
|
16
|
+
def initialize(adapter)
|
17
|
+
@operations = []
|
18
|
+
@adapter = adapter
|
19
|
+
end
|
20
|
+
|
21
|
+
def read(key)
|
22
|
+
@operations << Read.new(key.to_s)
|
23
|
+
@adapter.read key
|
24
|
+
end
|
25
|
+
|
26
|
+
def write(key, value)
|
27
|
+
@operations << Write.new(key.to_s, value)
|
28
|
+
@adapter.write key, value
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete(key)
|
32
|
+
@operations << Delete.new(key.to_s, nil)
|
33
|
+
@adapter.delete key
|
34
|
+
end
|
35
|
+
|
36
|
+
def set_add(key, value)
|
37
|
+
@operations << SetAdd.new(key.to_s, value)
|
38
|
+
@adapter.set_add key, value
|
39
|
+
end
|
40
|
+
|
41
|
+
def set_delete(key, value)
|
42
|
+
@operations << SetDelete.new(key.to_s, value)
|
43
|
+
@adapter.set_delete key, value
|
44
|
+
end
|
45
|
+
|
46
|
+
def set_members(key)
|
47
|
+
@operations << SetMembers.new(key.to_s)
|
48
|
+
@adapter.set_members key
|
49
|
+
end
|
50
|
+
|
51
|
+
# Public: Clears operation log
|
52
|
+
def reset
|
53
|
+
@operations.clear
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
57
|
+
end
|
data/lib/flipper/dsl.rb
CHANGED
@@ -25,7 +25,7 @@ module Flipper
|
|
25
25
|
end
|
26
26
|
|
27
27
|
def feature(name)
|
28
|
-
|
28
|
+
memoized_features[name.to_sym] ||= Feature.new(name, @adapter)
|
29
29
|
end
|
30
30
|
|
31
31
|
alias :[] :feature
|
@@ -35,21 +35,27 @@ module Flipper
|
|
35
35
|
end
|
36
36
|
|
37
37
|
def actor(thing)
|
38
|
-
|
38
|
+
Types::Actor.new(thing)
|
39
39
|
end
|
40
40
|
|
41
41
|
def random(number)
|
42
|
-
|
42
|
+
Types::PercentageOfRandom.new(number)
|
43
43
|
end
|
44
|
+
alias :percentage_of_random :random
|
44
45
|
|
45
46
|
def actors(number)
|
46
|
-
|
47
|
+
Types::PercentageOfActors.new(number)
|
48
|
+
end
|
49
|
+
alias :percentage_of_actors :actors
|
50
|
+
|
51
|
+
def features
|
52
|
+
adapter.features.map { |name| feature(name) }.to_set
|
47
53
|
end
|
48
54
|
|
49
55
|
private
|
50
56
|
|
51
|
-
def
|
52
|
-
@
|
57
|
+
def memoized_features
|
58
|
+
@memoized_features ||= {}
|
53
59
|
end
|
54
60
|
end
|
55
61
|
end
|
data/lib/flipper/feature.rb
CHANGED
@@ -30,13 +30,9 @@ module Flipper
|
|
30
30
|
!enabled?(actor)
|
31
31
|
end
|
32
32
|
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
gates.detect { |gate| gate.protects?(thing) } ||
|
37
|
-
raise(GateNotFound.new(thing))
|
38
|
-
end
|
39
|
-
|
33
|
+
# Internal: Gates to check to see if feature is enabled/disabled
|
34
|
+
#
|
35
|
+
# Returns an array of gates
|
40
36
|
def gates
|
41
37
|
@gates ||= [
|
42
38
|
Gates::Boolean.new(self),
|
@@ -46,5 +42,20 @@ module Flipper
|
|
46
42
|
Gates::PercentageOfRandom.new(self),
|
47
43
|
]
|
48
44
|
end
|
45
|
+
|
46
|
+
# Internal: Returns gate that protects thing
|
47
|
+
#
|
48
|
+
# thing - The object for which you would like to find a gate
|
49
|
+
#
|
50
|
+
# Raises Flipper::GateNotFound if no gate found for thing
|
51
|
+
def gate_for(thing)
|
52
|
+
find_gate(thing) || raise(GateNotFound.new(thing))
|
53
|
+
end
|
54
|
+
|
55
|
+
private
|
56
|
+
|
57
|
+
def find_gate(thing)
|
58
|
+
gates.detect { |gate| gate.protects?(thing) }
|
59
|
+
end
|
49
60
|
end
|
50
61
|
end
|
data/lib/flipper/gate.rb
CHANGED
@@ -1,11 +1,10 @@
|
|
1
1
|
require 'forwardable'
|
2
|
+
require 'flipper/key'
|
2
3
|
|
3
4
|
module Flipper
|
4
5
|
class Gate
|
5
6
|
extend Forwardable
|
6
7
|
|
7
|
-
Separator = '/'
|
8
|
-
|
9
8
|
attr_reader :feature
|
10
9
|
|
11
10
|
def_delegator :@feature, :adapter
|
@@ -14,12 +13,8 @@ module Flipper
|
|
14
13
|
@feature = feature
|
15
14
|
end
|
16
15
|
|
17
|
-
def key_prefix
|
18
|
-
@feature.name
|
19
|
-
end
|
20
|
-
|
21
16
|
def key
|
22
|
-
|
17
|
+
@key ||= Key.new(@feature.name, type_key)
|
23
18
|
end
|
24
19
|
|
25
20
|
def toggle_class
|
@@ -1,3 +1,5 @@
|
|
1
|
+
require 'zlib'
|
2
|
+
|
1
3
|
module Flipper
|
2
4
|
module Gates
|
3
5
|
class PercentageOfActors < Gate
|
@@ -13,7 +15,7 @@ module Flipper
|
|
13
15
|
if percentage.nil?
|
14
16
|
false
|
15
17
|
else
|
16
|
-
actor.identifier % 100 < percentage
|
18
|
+
Zlib.crc32(actor.identifier.to_s) % 100 < percentage
|
17
19
|
end
|
18
20
|
end
|
19
21
|
|
data/lib/flipper/key.rb
ADDED
@@ -0,0 +1,19 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Key
|
3
|
+
Separator = '/'
|
4
|
+
|
5
|
+
attr_reader :prefix, :suffix
|
6
|
+
|
7
|
+
def initialize(prefix, suffix)
|
8
|
+
@prefix, @suffix = prefix, suffix
|
9
|
+
end
|
10
|
+
|
11
|
+
def separator
|
12
|
+
Separator.dup
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
"#{prefix}#{separator}#{suffix}"
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
data/lib/flipper/registry.rb
CHANGED
@@ -5,74 +5,69 @@ require 'set'
|
|
5
5
|
# read_key(key)
|
6
6
|
# write_key(key, value)
|
7
7
|
shared_examples_for 'a flipper adapter' do
|
8
|
-
|
9
|
-
let(:separator) { Flipper::Gate::Separator }
|
8
|
+
let(:key) { Flipper::Key.new(:foo, :bar) }
|
10
9
|
|
10
|
+
describe "#write" do
|
11
11
|
it "sets key to value in store" do
|
12
|
-
subject.write(
|
13
|
-
read_key(
|
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
|
12
|
+
subject.write(key, true)
|
13
|
+
read_key(key).should be_true
|
19
14
|
end
|
20
15
|
end
|
21
16
|
|
22
17
|
describe "#read" do
|
23
18
|
it "returns nil if key not in store" do
|
24
|
-
subject.read(
|
19
|
+
subject.read(key).should be_nil
|
25
20
|
end
|
26
21
|
|
27
22
|
it "returns value if key in store" do
|
28
|
-
write_key
|
29
|
-
subject.read(
|
23
|
+
write_key key, 'bar'
|
24
|
+
subject.read(key).should eq('bar')
|
30
25
|
end
|
31
26
|
end
|
32
27
|
|
33
28
|
describe "#delete" do
|
34
29
|
it "deletes key" do
|
35
|
-
write_key
|
36
|
-
subject.delete(
|
37
|
-
read_key(
|
30
|
+
write_key key, 'bar'
|
31
|
+
subject.delete(key)
|
32
|
+
read_key(key).should be_nil
|
38
33
|
end
|
39
34
|
end
|
40
35
|
|
41
36
|
describe "#set_add" do
|
42
37
|
it "adds value to store" do
|
43
|
-
subject.set_add(
|
44
|
-
read_key(
|
38
|
+
subject.set_add(key, 1)
|
39
|
+
read_key(key).should eq(Set[1])
|
45
40
|
end
|
46
41
|
|
47
42
|
it "does not add same value more than once" do
|
48
|
-
subject.set_add(
|
49
|
-
subject.set_add(
|
50
|
-
subject.set_add(
|
51
|
-
subject.set_add(
|
52
|
-
read_key(
|
43
|
+
subject.set_add(key, 1)
|
44
|
+
subject.set_add(key, 1)
|
45
|
+
subject.set_add(key, 1)
|
46
|
+
subject.set_add(key, 2)
|
47
|
+
read_key(key).should eq(Set[1, 2])
|
53
48
|
end
|
54
49
|
end
|
55
50
|
|
56
51
|
describe "#set_delete" do
|
57
52
|
it "removes value from set if key in store" do
|
58
|
-
write_key
|
59
|
-
subject.set_delete(
|
60
|
-
read_key(
|
53
|
+
write_key key, Set[1, 2]
|
54
|
+
subject.set_delete(key, 1)
|
55
|
+
read_key(key).should eq(Set[2])
|
61
56
|
end
|
62
57
|
|
63
58
|
it "works fine if key not in store" do
|
64
|
-
subject.set_delete(
|
59
|
+
subject.set_delete(key, 'bar')
|
65
60
|
end
|
66
61
|
end
|
67
62
|
|
68
63
|
describe "#set_members" do
|
69
64
|
it "defaults to empty set" do
|
70
|
-
subject.set_members(
|
65
|
+
subject.set_members(key).should eq(Set.new)
|
71
66
|
end
|
72
67
|
|
73
68
|
it "returns set if in store" do
|
74
|
-
write_key
|
75
|
-
subject.set_members(
|
69
|
+
write_key key, Set[1, 2]
|
70
|
+
subject.set_members(key).should eq(Set[1, 2])
|
76
71
|
end
|
77
72
|
end
|
78
73
|
|