flipper 0.2.1 → 0.3.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/.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
|
|