flipper 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.rspec +1 -0
- data/Changelog.md +12 -0
- data/Gemfile +4 -7
- data/Guardfile +16 -4
- data/README.md +63 -34
- data/examples/basic.rb +1 -1
- data/examples/dsl.rb +10 -12
- data/examples/group.rb +10 -4
- data/examples/individual_actor.rb +9 -6
- data/examples/instrumentation.rb +39 -0
- data/examples/percentage_of_actors.rb +12 -9
- data/examples/percentage_of_random.rb +4 -2
- data/lib/flipper.rb +43 -10
- data/lib/flipper/adapter.rb +106 -21
- data/lib/flipper/adapters/memoized.rb +7 -0
- data/lib/flipper/adapters/memory.rb +10 -3
- data/lib/flipper/adapters/operation_logger.rb +7 -0
- data/lib/flipper/dsl.rb +73 -16
- data/lib/flipper/errors.rb +6 -0
- data/lib/flipper/feature.rb +117 -19
- data/lib/flipper/gate.rb +72 -4
- data/lib/flipper/gates/actor.rb +41 -12
- data/lib/flipper/gates/boolean.rb +21 -11
- data/lib/flipper/gates/group.rb +45 -12
- data/lib/flipper/gates/percentage_of_actors.rb +29 -10
- data/lib/flipper/gates/percentage_of_random.rb +22 -9
- data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
- data/lib/flipper/instrumentation/metriks.rb +6 -0
- data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
- data/lib/flipper/instrumenters/memory.rb +25 -0
- data/lib/flipper/instrumenters/noop.rb +9 -0
- data/lib/flipper/key.rb +23 -4
- data/lib/flipper/registry.rb +22 -6
- data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
- data/lib/flipper/toggle.rb +19 -2
- data/lib/flipper/toggles/boolean.rb +36 -3
- data/lib/flipper/toggles/set.rb +9 -3
- data/lib/flipper/toggles/value.rb +9 -3
- data/lib/flipper/type.rb +1 -0
- data/lib/flipper/types/actor.rb +12 -14
- data/lib/flipper/types/percentage.rb +8 -2
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +163 -27
- data/spec/flipper/adapters/memoized_spec.rb +6 -6
- data/spec/flipper/dsl_spec.rb +51 -54
- data/spec/flipper/feature_spec.rb +179 -17
- data/spec/flipper/gate_spec.rb +47 -0
- data/spec/flipper/gates/actor_spec.rb +52 -0
- data/spec/flipper/gates/boolean_spec.rb +52 -0
- data/spec/flipper/gates/group_spec.rb +79 -0
- data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
- data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
- data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
- data/spec/flipper/instrumenters/memory_spec.rb +26 -0
- data/spec/flipper/instrumenters/noop_spec.rb +22 -0
- data/spec/flipper/key_spec.rb +8 -2
- data/spec/flipper/registry_spec.rb +20 -2
- data/spec/flipper/toggle_spec.rb +22 -0
- data/spec/flipper/toggles/boolean_spec.rb +40 -0
- data/spec/flipper/toggles/set_spec.rb +35 -0
- data/spec/flipper/toggles/value_spec.rb +55 -0
- data/spec/flipper/types/actor_spec.rb +28 -33
- data/spec/flipper_spec.rb +16 -3
- data/spec/helper.rb +37 -3
- data/spec/integration_spec.rb +90 -83
- metadata +40 -4
data/lib/flipper/errors.rb
CHANGED
@@ -1,11 +1,17 @@
|
|
1
1
|
module Flipper
|
2
|
+
# Top level error that all other errors inherit from.
|
2
3
|
class Error < StandardError; end
|
3
4
|
|
5
|
+
# Raised when gate can not be found for a thing.
|
4
6
|
class GateNotFound < Error
|
5
7
|
def initialize(thing)
|
6
8
|
super "Could not find gate for #{thing.inspect}"
|
7
9
|
end
|
8
10
|
end
|
9
11
|
|
12
|
+
# Raised when attempting to declare a group name that has already been used.
|
10
13
|
class DuplicateGroup < Error; end
|
14
|
+
|
15
|
+
# Raised when attempting to access a group that is not registered.
|
16
|
+
class GroupNotRegistered < Error; end
|
11
17
|
end
|
data/lib/flipper/feature.rb
CHANGED
@@ -3,31 +3,72 @@ require 'flipper/errors'
|
|
3
3
|
require 'flipper/type'
|
4
4
|
require 'flipper/toggle'
|
5
5
|
require 'flipper/gate'
|
6
|
+
require 'flipper/instrumenters/noop'
|
6
7
|
|
7
8
|
module Flipper
|
8
9
|
class Feature
|
10
|
+
# Private: The name of instrumentation events.
|
11
|
+
InstrumentationName = "feature_operation.#{InstrumentationNamespace}"
|
12
|
+
|
13
|
+
# Internal: The name of the feature.
|
9
14
|
attr_reader :name
|
15
|
+
|
16
|
+
# Private: The adapter this feature should use.
|
10
17
|
attr_reader :adapter
|
11
18
|
|
12
|
-
|
19
|
+
# Private: What is being used to instrument all the things.
|
20
|
+
attr_reader :instrumenter
|
21
|
+
|
22
|
+
# Internal: Initializes a new feature instance.
|
23
|
+
#
|
24
|
+
# name - The Symbol or String name of the feature.
|
25
|
+
# adapter - The adapter that will be used to store details about this feature.
|
26
|
+
#
|
27
|
+
# options - The Hash of options.
|
28
|
+
# :instrumenter - What to use to instrument all the things.
|
29
|
+
#
|
30
|
+
def initialize(name, adapter, options = {})
|
13
31
|
@name = name
|
14
|
-
@
|
32
|
+
@instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
|
33
|
+
@adapter = Adapter.wrap(adapter, :instrumenter => @instrumenter)
|
15
34
|
end
|
16
35
|
|
36
|
+
# Public: Enable this feature for something.
|
37
|
+
#
|
38
|
+
# Returns the result of Flipper::Gate#enable.
|
17
39
|
def enable(thing = Types::Boolean.new)
|
18
|
-
|
40
|
+
instrument(:enable, thing) { |payload|
|
41
|
+
gate = gate_for(thing)
|
42
|
+
payload[:gate_name] = gate.name
|
43
|
+
gate.enable(thing)
|
44
|
+
}
|
19
45
|
end
|
20
46
|
|
47
|
+
# Public: Disable this feature for something.
|
48
|
+
#
|
49
|
+
# Returns the result of Flipper::Gate#disable.
|
21
50
|
def disable(thing = Types::Boolean.new)
|
22
|
-
|
51
|
+
instrument(:disable, thing) { |payload|
|
52
|
+
gate = gate_for(thing)
|
53
|
+
payload[:gate_name] = gate.name
|
54
|
+
gate.disable(thing)
|
55
|
+
}
|
23
56
|
end
|
24
57
|
|
25
|
-
|
26
|
-
|
27
|
-
|
58
|
+
# Public: Check if a feature is enabled for a thing.
|
59
|
+
#
|
60
|
+
# Returns true if enabled, false if not.
|
61
|
+
def enabled?(thing = nil)
|
62
|
+
instrument(:enabled?, thing) { |payload|
|
63
|
+
gate = gates.detect { |gate| gate.open?(thing) }
|
28
64
|
|
29
|
-
|
30
|
-
|
65
|
+
if gate.nil?
|
66
|
+
false
|
67
|
+
else
|
68
|
+
payload[:gate_name] = gate.name
|
69
|
+
true
|
70
|
+
end
|
71
|
+
}
|
31
72
|
end
|
32
73
|
|
33
74
|
# Internal: Gates to check to see if feature is enabled/disabled
|
@@ -35,27 +76,84 @@ module Flipper
|
|
35
76
|
# Returns an array of gates
|
36
77
|
def gates
|
37
78
|
@gates ||= [
|
38
|
-
Gates::Boolean.new(self),
|
39
|
-
Gates::Group.new(self),
|
40
|
-
Gates::Actor.new(self),
|
41
|
-
Gates::PercentageOfActors.new(self),
|
42
|
-
Gates::PercentageOfRandom.new(self),
|
79
|
+
Gates::Boolean.new(self, :instrumenter => @instrumenter),
|
80
|
+
Gates::Group.new(self, :instrumenter => @instrumenter),
|
81
|
+
Gates::Actor.new(self, :instrumenter => @instrumenter),
|
82
|
+
Gates::PercentageOfActors.new(self, :instrumenter => @instrumenter),
|
83
|
+
Gates::PercentageOfRandom.new(self, :instrumenter => @instrumenter),
|
43
84
|
]
|
44
85
|
end
|
45
86
|
|
46
|
-
# Internal:
|
87
|
+
# Internal: Find the gate that protects a thing.
|
47
88
|
#
|
48
89
|
# thing - The object for which you would like to find a gate
|
49
90
|
#
|
91
|
+
# Returns a Flipper::Gate.
|
50
92
|
# Raises Flipper::GateNotFound if no gate found for thing
|
51
93
|
def gate_for(thing)
|
52
|
-
|
94
|
+
gates.detect { |gate| gate.protects?(thing) } ||
|
95
|
+
raise(GateNotFound.new(thing))
|
96
|
+
end
|
97
|
+
|
98
|
+
# Public: Pretty string version for debugging.
|
99
|
+
def inspect
|
100
|
+
attributes = [
|
101
|
+
"name=#{name.inspect}",
|
102
|
+
"state=#{state.inspect}",
|
103
|
+
"adapter=#{adapter.name.inspect}",
|
104
|
+
]
|
105
|
+
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
106
|
+
end
|
107
|
+
|
108
|
+
# Public
|
109
|
+
def state
|
110
|
+
if boolean_gate.enabled?
|
111
|
+
:on
|
112
|
+
elsif conditional_gates.any?
|
113
|
+
:conditional
|
114
|
+
else
|
115
|
+
:off
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
# Public
|
120
|
+
def description
|
121
|
+
if boolean_gate.enabled?
|
122
|
+
boolean_gate.description.capitalize
|
123
|
+
elsif conditional_gates.any?
|
124
|
+
fragments = conditional_gates.map(&:description)
|
125
|
+
"Enabled for #{fragments.join(', ')}"
|
126
|
+
else
|
127
|
+
boolean_gate.description.capitalize
|
128
|
+
end
|
129
|
+
end
|
130
|
+
|
131
|
+
# Private
|
132
|
+
def boolean_gate
|
133
|
+
@boolean_gate ||= gates.detect { |gate| gate.name == :boolean }
|
134
|
+
end
|
135
|
+
|
136
|
+
# Private
|
137
|
+
def non_boolean_gates
|
138
|
+
@non_boolean_gates ||= gates - [boolean_gate]
|
139
|
+
end
|
140
|
+
|
141
|
+
# Private
|
142
|
+
def conditional_gates
|
143
|
+
@conditional_gates ||= non_boolean_gates.select { |gate| gate.enabled? }
|
53
144
|
end
|
54
145
|
|
55
|
-
|
146
|
+
# Private
|
147
|
+
def instrument(operation, thing)
|
148
|
+
payload = {
|
149
|
+
:feature_name => name,
|
150
|
+
:operation => operation,
|
151
|
+
:thing => thing,
|
152
|
+
}
|
56
153
|
|
57
|
-
|
58
|
-
|
154
|
+
@instrumenter.instrument(InstrumentationName, payload) {
|
155
|
+
payload[:result] = yield(payload) if block_given?
|
156
|
+
}
|
59
157
|
end
|
60
158
|
end
|
61
159
|
end
|
data/lib/flipper/gate.rb
CHANGED
@@ -1,45 +1,113 @@
|
|
1
1
|
require 'forwardable'
|
2
2
|
require 'flipper/key'
|
3
|
+
require 'flipper/instrumenters/noop'
|
3
4
|
|
4
5
|
module Flipper
|
5
6
|
class Gate
|
6
7
|
extend Forwardable
|
7
8
|
|
9
|
+
# Private: The name of instrumentation events.
|
10
|
+
InstrumentationName = "gate_operation.#{InstrumentationNamespace}"
|
11
|
+
|
12
|
+
# Private
|
8
13
|
attr_reader :feature
|
9
14
|
|
15
|
+
# Private: What is used to instrument all the things.
|
16
|
+
attr_reader :instrumenter
|
17
|
+
|
10
18
|
def_delegator :@feature, :adapter
|
11
19
|
|
12
|
-
|
20
|
+
# Public
|
21
|
+
def initialize(feature, options = {})
|
13
22
|
@feature = feature
|
23
|
+
@instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
|
24
|
+
end
|
25
|
+
|
26
|
+
# Public: The name of the gate. Implemented in subclass.
|
27
|
+
def name
|
28
|
+
raise 'Not implemented'
|
14
29
|
end
|
15
30
|
|
31
|
+
# Private: The piece of the adapter key that is unique to the gate class.
|
32
|
+
# Implemented in subclass.
|
16
33
|
def key
|
17
|
-
|
34
|
+
raise 'Not implemented'
|
18
35
|
end
|
19
36
|
|
37
|
+
# Internal: The key where details about this gate can be retrieved from the
|
38
|
+
# adapter.
|
39
|
+
def adapter_key
|
40
|
+
@key ||= Key.new(@feature.name, key)
|
41
|
+
end
|
42
|
+
|
43
|
+
# Internal: The toggle class to use for this gate.
|
20
44
|
def toggle_class
|
21
45
|
Toggles::Value
|
22
46
|
end
|
23
47
|
|
48
|
+
# Internal: The toggle to use to enable/disable this gate.
|
24
49
|
def toggle
|
25
50
|
@toggle ||= toggle_class.new(self)
|
26
51
|
end
|
27
52
|
|
28
|
-
|
53
|
+
# Internal: Check if a gate is open for a thing. Implemented in subclass.
|
54
|
+
#
|
55
|
+
# Returns true if gate open for thing, false if not.
|
56
|
+
def open?(thing)
|
29
57
|
false
|
30
58
|
end
|
31
59
|
|
32
|
-
|
60
|
+
# Internal: Check if a gate is protects a thing. Implemented in subclass.
|
61
|
+
#
|
62
|
+
# Returns true if gate protects thing, false if not.
|
63
|
+
def protects?(thing)
|
33
64
|
false
|
34
65
|
end
|
35
66
|
|
67
|
+
# Internal: Enable this gate for a thing.
|
68
|
+
#
|
69
|
+
# Returns the result of Flipper::Toggle#enable.
|
36
70
|
def enable(thing)
|
37
71
|
toggle.enable(thing)
|
38
72
|
end
|
39
73
|
|
74
|
+
# Internal: Disable this gate for a thing.
|
75
|
+
#
|
76
|
+
# Returns the result of Flipper::Toggle#disable.
|
40
77
|
def disable(thing)
|
41
78
|
toggle.disable(thing)
|
42
79
|
end
|
80
|
+
|
81
|
+
def enabled?
|
82
|
+
toggle.enabled?
|
83
|
+
end
|
84
|
+
|
85
|
+
# Public: Pretty string version for debugging.
|
86
|
+
def inspect
|
87
|
+
attributes = [
|
88
|
+
"feature=#{feature.name.inspect}",
|
89
|
+
"description=#{description.inspect}",
|
90
|
+
"adapter=#{adapter.name.inspect}",
|
91
|
+
"adapter_key=#{adapter_key.inspect}",
|
92
|
+
"toggle_class=#{toggle_class.inspect}",
|
93
|
+
"toggle_value=#{toggle.value.inspect}",
|
94
|
+
]
|
95
|
+
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
96
|
+
end
|
97
|
+
|
98
|
+
# Private
|
99
|
+
def instrument(operation, thing)
|
100
|
+
payload = {
|
101
|
+
:thing => thing,
|
102
|
+
:operation => operation,
|
103
|
+
:gate_name => name,
|
104
|
+
:feature_name => @feature.name,
|
105
|
+
}
|
106
|
+
|
107
|
+
@instrumenter.instrument(InstrumentationName, payload) {
|
108
|
+
payload[:result] = yield(payload) if block_given?
|
109
|
+
}
|
110
|
+
end
|
43
111
|
end
|
44
112
|
end
|
45
113
|
|
data/lib/flipper/gates/actor.rb
CHANGED
@@ -1,29 +1,58 @@
|
|
1
1
|
module Flipper
|
2
2
|
module Gates
|
3
3
|
class Actor < Gate
|
4
|
-
|
4
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
5
|
+
def name
|
6
|
+
:actor
|
7
|
+
end
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
# Internal: The piece of the adapter key that is unique to the gate class.
|
10
|
+
def key
|
11
|
+
:actors
|
8
12
|
end
|
9
13
|
|
14
|
+
# Internal: The toggle class used to enable/disable the gate for a thing.
|
10
15
|
def toggle_class
|
11
16
|
Toggles::Set
|
12
17
|
end
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
+
# Internal: Checks if the gate is open for a thing.
|
20
|
+
#
|
21
|
+
# Returns true if gate open for thing, false if not.
|
22
|
+
def open?(thing)
|
23
|
+
instrument(:open?, thing) { |payload|
|
24
|
+
if thing.nil?
|
25
|
+
false
|
26
|
+
else
|
27
|
+
if Types::Actor.wrappable?(thing)
|
28
|
+
actor = Types::Actor.wrap(thing)
|
29
|
+
enabled_actor_ids = toggle.value
|
30
|
+
enabled_actor_ids.include?(actor.value)
|
31
|
+
else
|
32
|
+
false
|
33
|
+
end
|
34
|
+
end
|
35
|
+
}
|
19
36
|
end
|
20
37
|
|
21
|
-
def
|
22
|
-
|
38
|
+
def protects?(thing)
|
39
|
+
Types::Actor.wrappable?(thing)
|
23
40
|
end
|
24
41
|
|
25
|
-
def
|
26
|
-
|
42
|
+
def enable(thing)
|
43
|
+
super Types::Actor.wrap(thing)
|
44
|
+
end
|
45
|
+
|
46
|
+
def disable(thing)
|
47
|
+
super Types::Actor.wrap(thing)
|
48
|
+
end
|
49
|
+
|
50
|
+
def description
|
51
|
+
if enabled?
|
52
|
+
"actors (#{toggle.value.to_a.sort.join(', ')})"
|
53
|
+
else
|
54
|
+
'disabled'
|
55
|
+
end
|
27
56
|
end
|
28
57
|
end
|
29
58
|
end
|
@@ -1,29 +1,39 @@
|
|
1
1
|
module Flipper
|
2
2
|
module Gates
|
3
3
|
class Boolean < Gate
|
4
|
-
|
4
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
5
|
+
def name
|
6
|
+
:boolean
|
7
|
+
end
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
# Internal: The piece of the adapter key that is unique to the gate class.
|
10
|
+
def key
|
11
|
+
:boolean
|
8
12
|
end
|
9
13
|
|
14
|
+
# Internal: The toggle class used to enable/disable the gate for a thing.
|
10
15
|
def toggle_class
|
11
16
|
Toggles::Boolean
|
12
17
|
end
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
else
|
20
|
-
throw :short_circuit, !!value
|
21
|
-
end
|
19
|
+
# Internal: Checks if the gate is open for a thing.
|
20
|
+
#
|
21
|
+
# Returns true if gate open for thing, false if not.
|
22
|
+
def open?(thing)
|
23
|
+
instrument(:open?, thing) { |payload| toggle.value }
|
22
24
|
end
|
23
25
|
|
24
26
|
def protects?(thing)
|
25
27
|
thing.is_a?(Flipper::Types::Boolean)
|
26
28
|
end
|
29
|
+
|
30
|
+
def description
|
31
|
+
if enabled?
|
32
|
+
'Enabled'
|
33
|
+
else
|
34
|
+
'Disabled'
|
35
|
+
end
|
36
|
+
end
|
27
37
|
end
|
28
38
|
end
|
29
39
|
end
|
data/lib/flipper/gates/group.rb
CHANGED
@@ -1,31 +1,64 @@
|
|
1
1
|
module Flipper
|
2
2
|
module Gates
|
3
3
|
class Group < Gate
|
4
|
-
|
4
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
5
|
+
def name
|
6
|
+
:group
|
7
|
+
end
|
5
8
|
|
6
|
-
|
7
|
-
|
9
|
+
# Internal: The piece of the adapter key that is unique to the gate class.
|
10
|
+
def key
|
11
|
+
:groups
|
8
12
|
end
|
9
13
|
|
14
|
+
# Internal: The toggle class used to enable/disable the gate for a thing.
|
10
15
|
def toggle_class
|
11
16
|
Toggles::Set
|
12
17
|
end
|
13
18
|
|
14
|
-
|
15
|
-
|
16
|
-
|
19
|
+
# Internal: Checks if the gate is open for a thing.
|
20
|
+
#
|
21
|
+
# Returns true if gate open for thing, false if not.
|
22
|
+
def open?(thing)
|
23
|
+
instrument(:open?, thing) { |payload|
|
24
|
+
if thing.nil?
|
25
|
+
false
|
26
|
+
else
|
27
|
+
enabled_groups.any? { |group| group.match?(thing) }
|
28
|
+
end
|
29
|
+
}
|
17
30
|
end
|
18
31
|
|
19
|
-
def
|
20
|
-
|
32
|
+
def protects?(thing)
|
33
|
+
thing.is_a?(Flipper::Types::Group)
|
21
34
|
end
|
22
35
|
|
23
|
-
def
|
24
|
-
|
36
|
+
def description
|
37
|
+
if enabled?
|
38
|
+
"groups (#{toggle.value.to_a.sort.join(', ')})"
|
39
|
+
else
|
40
|
+
'disabled'
|
41
|
+
end
|
25
42
|
end
|
26
43
|
|
27
|
-
|
28
|
-
|
44
|
+
# Private: Get all the enabled groups for this gate.
|
45
|
+
#
|
46
|
+
# Returns an Array of Flipper::Types::Group instances.
|
47
|
+
def enabled_groups
|
48
|
+
enabled_group_names.map { |name|
|
49
|
+
begin
|
50
|
+
Flipper.group(name)
|
51
|
+
rescue GroupNotRegistered
|
52
|
+
nil
|
53
|
+
end
|
54
|
+
}.compact
|
55
|
+
end
|
56
|
+
|
57
|
+
# Private: Get all the names of enabled groups.
|
58
|
+
#
|
59
|
+
# Returns a Set of the enabled group names.
|
60
|
+
def enabled_group_names
|
61
|
+
toggle.value
|
29
62
|
end
|
30
63
|
end
|
31
64
|
end
|