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.
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