flipper 0.3.0 → 0.4.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/.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
|