flipper 0.3.0 → 0.4.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (67) hide show
  1. data/.rspec +1 -0
  2. data/Changelog.md +12 -0
  3. data/Gemfile +4 -7
  4. data/Guardfile +16 -4
  5. data/README.md +63 -34
  6. data/examples/basic.rb +1 -1
  7. data/examples/dsl.rb +10 -12
  8. data/examples/group.rb +10 -4
  9. data/examples/individual_actor.rb +9 -6
  10. data/examples/instrumentation.rb +39 -0
  11. data/examples/percentage_of_actors.rb +12 -9
  12. data/examples/percentage_of_random.rb +4 -2
  13. data/lib/flipper.rb +43 -10
  14. data/lib/flipper/adapter.rb +106 -21
  15. data/lib/flipper/adapters/memoized.rb +7 -0
  16. data/lib/flipper/adapters/memory.rb +10 -3
  17. data/lib/flipper/adapters/operation_logger.rb +7 -0
  18. data/lib/flipper/dsl.rb +73 -16
  19. data/lib/flipper/errors.rb +6 -0
  20. data/lib/flipper/feature.rb +117 -19
  21. data/lib/flipper/gate.rb +72 -4
  22. data/lib/flipper/gates/actor.rb +41 -12
  23. data/lib/flipper/gates/boolean.rb +21 -11
  24. data/lib/flipper/gates/group.rb +45 -12
  25. data/lib/flipper/gates/percentage_of_actors.rb +29 -10
  26. data/lib/flipper/gates/percentage_of_random.rb +22 -9
  27. data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
  28. data/lib/flipper/instrumentation/metriks.rb +6 -0
  29. data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
  30. data/lib/flipper/instrumenters/memory.rb +25 -0
  31. data/lib/flipper/instrumenters/noop.rb +9 -0
  32. data/lib/flipper/key.rb +23 -4
  33. data/lib/flipper/registry.rb +22 -6
  34. data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
  35. data/lib/flipper/toggle.rb +19 -2
  36. data/lib/flipper/toggles/boolean.rb +36 -3
  37. data/lib/flipper/toggles/set.rb +9 -3
  38. data/lib/flipper/toggles/value.rb +9 -3
  39. data/lib/flipper/type.rb +1 -0
  40. data/lib/flipper/types/actor.rb +12 -14
  41. data/lib/flipper/types/percentage.rb +8 -2
  42. data/lib/flipper/version.rb +1 -1
  43. data/spec/flipper/adapter_spec.rb +163 -27
  44. data/spec/flipper/adapters/memoized_spec.rb +6 -6
  45. data/spec/flipper/dsl_spec.rb +51 -54
  46. data/spec/flipper/feature_spec.rb +179 -17
  47. data/spec/flipper/gate_spec.rb +47 -0
  48. data/spec/flipper/gates/actor_spec.rb +52 -0
  49. data/spec/flipper/gates/boolean_spec.rb +52 -0
  50. data/spec/flipper/gates/group_spec.rb +79 -0
  51. data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
  52. data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
  53. data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
  54. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
  55. data/spec/flipper/instrumenters/memory_spec.rb +26 -0
  56. data/spec/flipper/instrumenters/noop_spec.rb +22 -0
  57. data/spec/flipper/key_spec.rb +8 -2
  58. data/spec/flipper/registry_spec.rb +20 -2
  59. data/spec/flipper/toggle_spec.rb +22 -0
  60. data/spec/flipper/toggles/boolean_spec.rb +40 -0
  61. data/spec/flipper/toggles/set_spec.rb +35 -0
  62. data/spec/flipper/toggles/value_spec.rb +55 -0
  63. data/spec/flipper/types/actor_spec.rb +28 -33
  64. data/spec/flipper_spec.rb +16 -3
  65. data/spec/helper.rb +37 -3
  66. data/spec/integration_spec.rb +90 -83
  67. metadata +40 -4
@@ -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
@@ -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
- def initialize(name, adapter)
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
- @adapter = Adapter.wrap(adapter)
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
- gate_for(thing).enable(thing)
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
- gate_for(thing).disable(thing)
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
- def enabled?(actor = nil)
26
- !! catch(:short_circuit) { gates.detect { |gate| gate.open?(actor) } }
27
- end
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
- def disabled?(actor = nil)
30
- !enabled?(actor)
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: Returns gate that protects thing
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
- find_gate(thing) || raise(GateNotFound.new(thing))
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
- private
146
+ # Private
147
+ def instrument(operation, thing)
148
+ payload = {
149
+ :feature_name => name,
150
+ :operation => operation,
151
+ :thing => thing,
152
+ }
56
153
 
57
- def find_gate(thing)
58
- gates.detect { |gate| gate.protects?(thing) }
154
+ @instrumenter.instrument(InstrumentationName, payload) {
155
+ payload[:result] = yield(payload) if block_given?
156
+ }
59
157
  end
60
158
  end
61
159
  end
@@ -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
- def initialize(feature)
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
- @key ||= Key.new(@feature.name, type_key)
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
- def protects?(thing)
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
- def match?(actor)
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
 
@@ -1,29 +1,58 @@
1
1
  module Flipper
2
2
  module Gates
3
3
  class Actor < Gate
4
- Key = :actors
4
+ # Internal: The name of the gate. Used for instrumentation, etc.
5
+ def name
6
+ :actor
7
+ end
5
8
 
6
- def type_key
7
- Key
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
- def open?(actor)
15
- return if actor.nil?
16
- return unless Types::Actor.wrappable?(actor)
17
- actor = Types::Actor.wrap(actor)
18
- identifiers.include?(actor.identifier)
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 identifiers
22
- toggle.value
38
+ def protects?(thing)
39
+ Types::Actor.wrappable?(thing)
23
40
  end
24
41
 
25
- def protects?(thing)
26
- thing.is_a?(Flipper::Types::Actor)
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
- Key = :boolean
4
+ # Internal: The name of the gate. Used for instrumentation, etc.
5
+ def name
6
+ :boolean
7
+ end
5
8
 
6
- def type_key
7
- Key
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
- def open?(actor)
15
- value = toggle.value
16
-
17
- if value.nil?
18
- false
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
@@ -1,31 +1,64 @@
1
1
  module Flipper
2
2
  module Gates
3
3
  class Group < Gate
4
- Key = :groups
4
+ # Internal: The name of the gate. Used for instrumentation, etc.
5
+ def name
6
+ :group
7
+ end
5
8
 
6
- def type_key
7
- Key
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
- def open?(actor)
15
- return if actor.nil?
16
- groups.any? { |group| group.match?(actor) }
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 group_names
20
- toggle.value
32
+ def protects?(thing)
33
+ thing.is_a?(Flipper::Types::Group)
21
34
  end
22
35
 
23
- def groups
24
- group_names.map { |name| Flipper.group(name) }.compact
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
- def protects?(thing)
28
- thing.is_a?(Flipper::Types::Group)
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