flipper 0.4.0 → 0.5.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 (59) hide show
  1. data/Guardfile +3 -8
  2. data/README.md +26 -38
  3. data/examples/percentage_of_actors.rb +17 -12
  4. data/examples/percentage_of_random.rb +3 -7
  5. data/lib/flipper.rb +8 -1
  6. data/lib/flipper/adapter.rb +2 -208
  7. data/lib/flipper/adapters/decorator.rb +9 -0
  8. data/lib/flipper/adapters/instrumented.rb +92 -0
  9. data/lib/flipper/adapters/memoizable.rb +88 -0
  10. data/lib/flipper/adapters/memory.rb +89 -7
  11. data/lib/flipper/adapters/operation_logger.rb +31 -45
  12. data/lib/flipper/decorator.rb +6 -0
  13. data/lib/flipper/dsl.rb +29 -2
  14. data/lib/flipper/feature.rb +83 -49
  15. data/lib/flipper/gate.rb +24 -41
  16. data/lib/flipper/gates/actor.rb +24 -24
  17. data/lib/flipper/gates/boolean.rb +28 -15
  18. data/lib/flipper/gates/group.rb +25 -34
  19. data/lib/flipper/gates/percentage_of_actors.rb +21 -13
  20. data/lib/flipper/gates/percentage_of_random.rb +20 -12
  21. data/lib/flipper/instrumentation/log_subscriber.rb +14 -22
  22. data/lib/flipper/middleware/memoizer.rb +23 -0
  23. data/lib/flipper/spec/shared_adapter_specs.rb +141 -92
  24. data/lib/flipper/types/boolean.rb +5 -1
  25. data/lib/flipper/version.rb +1 -1
  26. data/spec/flipper/adapters/instrumented_spec.rb +92 -0
  27. data/spec/flipper/adapters/memoizable_spec.rb +184 -0
  28. data/spec/flipper/adapters/memory_spec.rb +1 -11
  29. data/spec/flipper/adapters/operation_logger_spec.rb +93 -0
  30. data/spec/flipper/dsl_spec.rb +18 -43
  31. data/spec/flipper/feature_spec.rb +25 -9
  32. data/spec/flipper/gate_spec.rb +8 -20
  33. data/spec/flipper/gates/actor_spec.rb +6 -14
  34. data/spec/flipper/gates/boolean_spec.rb +80 -13
  35. data/spec/flipper/gates/group_spec.rb +8 -18
  36. data/spec/flipper/gates/percentage_of_actors_spec.rb +12 -28
  37. data/spec/flipper/gates/percentage_of_random_spec.rb +6 -14
  38. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -8
  39. data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +3 -6
  40. data/spec/flipper/middleware/{local_cache_spec.rb → memoizer_spec.rb} +25 -55
  41. data/spec/flipper/types/boolean_spec.rb +13 -3
  42. data/spec/flipper_spec.rb +7 -0
  43. data/spec/helper.rb +21 -3
  44. data/spec/integration_spec.rb +115 -116
  45. metadata +17 -27
  46. data/lib/flipper/adapters/memoized.rb +0 -55
  47. data/lib/flipper/key.rb +0 -38
  48. data/lib/flipper/middleware/local_cache.rb +0 -36
  49. data/lib/flipper/toggle.rb +0 -54
  50. data/lib/flipper/toggles/boolean.rb +0 -54
  51. data/lib/flipper/toggles/set.rb +0 -25
  52. data/lib/flipper/toggles/value.rb +0 -25
  53. data/spec/flipper/adapter_spec.rb +0 -463
  54. data/spec/flipper/adapters/memoized_spec.rb +0 -93
  55. data/spec/flipper/key_spec.rb +0 -23
  56. data/spec/flipper/toggle_spec.rb +0 -22
  57. data/spec/flipper/toggles/boolean_spec.rb +0 -40
  58. data/spec/flipper/toggles/set_spec.rb +0 -35
  59. data/spec/flipper/toggles/value_spec.rb +0 -55
@@ -0,0 +1,88 @@
1
+ require 'flipper/adapters/decorator'
2
+
3
+ module Flipper
4
+ module Adapters
5
+ class Memoizable < Decorator
6
+ FeaturesKey = :flipper_features
7
+
8
+ # Internal
9
+ def self.cache
10
+ Thread.current[:flipper_memoize_cache] ||= {}
11
+ end
12
+
13
+ # Internal
14
+ def self.memoizing?
15
+ !!Thread.current[:flipper_memoize]
16
+ end
17
+
18
+ # Internal
19
+ def self.memoize=(value)
20
+ cache.clear
21
+ Thread.current[:flipper_memoize] = value
22
+ end
23
+
24
+ # Public
25
+ def initialize(adapter)
26
+ super(adapter)
27
+ end
28
+
29
+ # Public
30
+ def get(feature)
31
+ if memoizing?
32
+ cache.fetch(feature) { cache[feature] = super }
33
+ else
34
+ super
35
+ end
36
+ end
37
+
38
+ # Public
39
+ def enable(feature, gate, thing)
40
+ result = super
41
+ cache.delete(feature) if memoizing?
42
+ result
43
+ end
44
+
45
+ # Public
46
+ def disable(feature, gate, thing)
47
+ result = super
48
+ cache.delete(feature) if memoizing?
49
+ result
50
+ end
51
+
52
+ # Public
53
+ def features
54
+ if memoizing?
55
+ cache.fetch(FeaturesKey) {
56
+ cache[FeaturesKey] = super
57
+ }
58
+ else
59
+ super
60
+ end
61
+ end
62
+
63
+ # Public
64
+ def add(feature)
65
+ result = super
66
+ cache.delete(FeaturesKey) if memoizing?
67
+ result
68
+ end
69
+
70
+ # Internal
71
+ def cache
72
+ self.class.cache
73
+ end
74
+
75
+ # Internal: Turns local caching on/off.
76
+ #
77
+ # value - The Boolean that decides if local caching is on.
78
+ def memoize=(value)
79
+ self.class.memoize = value
80
+ end
81
+
82
+ # Internal: Returns true for using local cache, false for not.
83
+ def memoizing?
84
+ self.class.memoizing?
85
+ end
86
+ end
87
+ end
88
+ end
@@ -3,46 +3,128 @@ require 'set'
3
3
  module Flipper
4
4
  module Adapters
5
5
  class Memory
6
+ include Flipper::Adapter
7
+
8
+ FeaturesKey = :flipper_features
9
+
10
+ # Public: The name of the adapter.
11
+ attr_reader :name
12
+
6
13
  # Public
7
14
  def initialize(source = nil)
8
15
  @source = source || {}
16
+ @name = :memory
17
+ end
18
+
19
+ # Public
20
+ def get(feature)
21
+ result = {}
22
+
23
+ feature.gates.each do |gate|
24
+ result[gate.key] = case gate.data_type
25
+ when :boolean, :integer
26
+ read key(feature, gate)
27
+ when :set
28
+ set_members key(feature, gate)
29
+ else
30
+ raise "#{gate} is not supported by this adapter yet"
31
+ end
32
+ end
33
+
34
+ result
9
35
  end
10
36
 
11
37
  # Public
38
+ def enable(feature, gate, thing)
39
+ case gate.data_type
40
+ when :boolean, :integer
41
+ write key(feature, gate), thing.value.to_s
42
+ when :set
43
+ set_add key(feature, gate), thing.value.to_s
44
+ else
45
+ raise "#{gate} is not supported by this adapter yet"
46
+ end
47
+
48
+ true
49
+ end
50
+
51
+ # Public
52
+ def disable(feature, gate, thing)
53
+ case gate.data_type
54
+ when :boolean
55
+ feature.gates.each do |gate|
56
+ delete key(feature, gate)
57
+ end
58
+ when :integer
59
+ write key(feature, gate), thing.value.to_s
60
+ when :set
61
+ set_delete key(feature, gate), thing.value.to_s
62
+ else
63
+ raise "#{gate} is not supported by this adapter yet"
64
+ end
65
+
66
+ true
67
+ end
68
+
69
+ # Public: Adds a feature to the set of known features.
70
+ def add(feature)
71
+ features.add(feature.name.to_s)
72
+
73
+ true
74
+ end
75
+
76
+ # Public: The set of known features.
77
+ def features
78
+ set_members(FeaturesKey)
79
+ end
80
+
81
+ def inspect
82
+ attributes = [
83
+ "name=:memory",
84
+ "source=#{@source.inspect}",
85
+ ]
86
+ "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
87
+ end
88
+
89
+ # private
90
+ def key(feature, gate)
91
+ "#{feature.key}/#{gate.key}"
92
+ end
93
+
94
+ # Private
12
95
  def read(key)
13
96
  @source[key.to_s]
14
97
  end
15
98
 
16
- # Public
99
+ # Private
17
100
  def write(key, value)
18
101
  @source[key.to_s] = value.to_s
19
102
  end
20
103
 
21
- # Public
104
+ # Private
22
105
  def delete(key)
23
106
  @source.delete(key.to_s)
24
107
  end
25
108
 
26
- # Public
109
+ # Private
27
110
  def set_add(key, value)
28
111
  ensure_set_initialized(key)
29
112
  @source[key.to_s].add(value.to_s)
30
113
  end
31
114
 
32
- # Public
115
+ # Private
33
116
  def set_delete(key, value)
34
117
  ensure_set_initialized(key)
35
118
  @source[key.to_s].delete(value.to_s)
36
119
  end
37
120
 
38
- # Public
121
+ # Private
39
122
  def set_members(key)
40
123
  ensure_set_initialized(key)
41
124
  @source[key.to_s]
42
125
  end
43
126
 
44
- private
45
-
127
+ # Private
46
128
  def ensure_set_initialized(key)
47
129
  @source[key.to_s] ||= Set.new
48
130
  end
@@ -1,61 +1,47 @@
1
+ require 'flipper/adapters/decorator'
2
+
1
3
  module Flipper
2
4
  module Adapters
3
5
  # Public: Adapter that wraps another adapter and stores the operations.
4
6
  #
5
- # Useful in tests to verify calls and such.
6
- class OperationLogger
7
+ # Useful in tests to verify calls and such. Never use outside of testing.
8
+ class OperationLogger < Decorator
9
+ Operation = Struct.new(:type, :args)
10
+
11
+ OperationTypes = [
12
+ :get,
13
+ :add,
14
+ :enable,
15
+ :disable,
16
+ :features
17
+ ]
18
+
19
+ # Internal: An array of the operations that have happened.
7
20
  attr_reader :operations
8
21
 
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
- # Public
17
- def initialize(adapter)
18
- @operations = []
19
- @adapter = adapter
20
- end
21
-
22
- # Public
23
- def read(key)
24
- @operations << Read.new(key.to_s)
25
- @adapter.read key
26
- end
27
-
28
22
  # Public
29
- def write(key, value)
30
- @operations << Write.new(key.to_s, value)
31
- @adapter.write key, value
23
+ def initialize(adapter, operations = nil)
24
+ super(adapter)
25
+ @operations = operations || []
32
26
  end
33
27
 
34
- # Public
35
- def delete(key)
36
- @operations << Delete.new(key.to_s, nil)
37
- @adapter.delete key
38
- end
39
-
40
- # Public
41
- def set_add(key, value)
42
- @operations << SetAdd.new(key.to_s, value)
43
- @adapter.set_add key, value
28
+ OperationTypes.each do |type|
29
+ class_eval <<-EOE
30
+ def #{type}(*args)
31
+ @operations << Operation.new(:#{type}, args)
32
+ super
33
+ end
34
+ EOE
44
35
  end
45
36
 
46
- # Public
47
- def set_delete(key, value)
48
- @operations << SetDelete.new(key.to_s, value)
49
- @adapter.set_delete key, value
50
- end
51
-
52
- # Public
53
- def set_members(key)
54
- @operations << SetMembers.new(key.to_s)
55
- @adapter.set_members key
37
+ # Public: Count the number of times a certain operation happened.
38
+ def count(type)
39
+ @operations.select { |operation|
40
+ operation.type == type
41
+ }.size
56
42
  end
57
43
 
58
- # Public: Clears operation log
44
+ # Public: Resets the operation log to empty
59
45
  def reset
60
46
  @operations.clear
61
47
  end
@@ -0,0 +1,6 @@
1
+ require 'delegate'
2
+
3
+ module Flipper
4
+ class Decorator < SimpleDelegator
5
+ end
6
+ end
data/lib/flipper/dsl.rb CHANGED
@@ -1,4 +1,5 @@
1
- require 'flipper/adapter'
1
+ require 'flipper/adapters/instrumented'
2
+ require 'flipper/adapters/memoizable'
2
3
  require 'flipper/instrumenters/noop'
3
4
 
4
5
  module Flipper
@@ -16,7 +17,13 @@ module Flipper
16
17
  # :instrumenter - What should be used to instrument all the things.
17
18
  def initialize(adapter, options = {})
18
19
  @instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
19
- @adapter = Adapter.wrap(adapter, :instrumenter => @instrumenter)
20
+
21
+ instrumented = Flipper::Adapters::Instrumented.new(adapter, {
22
+ :instrumenter => @instrumenter,
23
+ })
24
+ memoized = Flipper::Adapters::Memoizable.new(instrumented)
25
+ @adapter = memoized
26
+
20
27
  @memoized_features = {}
21
28
  end
22
29
 
@@ -56,6 +63,10 @@ module Flipper
56
63
  #
57
64
  # Returns an instance of Flipper::Feature.
58
65
  def feature(name)
66
+ if !name.is_a?(String) && !name.is_a?(Symbol)
67
+ raise ArgumentError, "#{name} must be a String or Symbol"
68
+ end
69
+
59
70
  @memoized_features[name.to_sym] ||= Feature.new(name, @adapter, {
60
71
  :instrumenter => instrumenter,
61
72
  })
@@ -68,6 +79,22 @@ module Flipper
68
79
  # Returns an instance of Flipper::Feature.
69
80
  alias_method :[], :feature
70
81
 
82
+ # Public: Shortcut for getting a boolean type instance.
83
+ #
84
+ # value - The true or false value for the boolean.
85
+ #
86
+ # Returns a Flipper::Types::Boolean instance.
87
+ def boolean(value = true)
88
+ Types::Boolean.new(value)
89
+ end
90
+
91
+ # Public: Event shorter shortcut for getting a boolean type instance.
92
+ #
93
+ # value - The true or false value for the boolean.
94
+ #
95
+ # Returns a Flipper::Types::Boolean instance.
96
+ alias_method :bool, :boolean
97
+
71
98
  # Public: Access a flipper group by name.
72
99
  #
73
100
  # name - The String or Symbol name of the feature.
@@ -1,7 +1,5 @@
1
- require 'flipper/adapter'
2
1
  require 'flipper/errors'
3
2
  require 'flipper/type'
4
- require 'flipper/toggle'
5
3
  require 'flipper/gate'
6
4
  require 'flipper/instrumenters/noop'
7
5
 
@@ -13,6 +11,9 @@ module Flipper
13
11
  # Internal: The name of the feature.
14
12
  attr_reader :name
15
13
 
14
+ # Internal: Name converted to value safe for adapter.
15
+ attr_reader :key
16
+
16
17
  # Private: The adapter this feature should use.
17
18
  attr_reader :adapter
18
19
 
@@ -29,29 +30,36 @@ module Flipper
29
30
  #
30
31
  def initialize(name, adapter, options = {})
31
32
  @name = name
33
+ @key = name.to_s
32
34
  @instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
33
- @adapter = Adapter.wrap(adapter, :instrumenter => @instrumenter)
35
+ @adapter = adapter
34
36
  end
35
37
 
36
38
  # Public: Enable this feature for something.
37
39
  #
38
40
  # Returns the result of Flipper::Gate#enable.
39
- def enable(thing = Types::Boolean.new)
41
+ def enable(thing = Types::Boolean.new(true))
40
42
  instrument(:enable, thing) { |payload|
43
+ adapter.add self
44
+
41
45
  gate = gate_for(thing)
42
46
  payload[:gate_name] = gate.name
43
- gate.enable(thing)
47
+
48
+ adapter.enable self, gate, gate.wrap(thing)
44
49
  }
45
50
  end
46
51
 
47
52
  # Public: Disable this feature for something.
48
53
  #
49
54
  # Returns the result of Flipper::Gate#disable.
50
- def disable(thing = Types::Boolean.new)
55
+ def disable(thing = Types::Boolean.new(false))
51
56
  instrument(:disable, thing) { |payload|
57
+ adapter.add self
58
+
52
59
  gate = gate_for(thing)
53
60
  payload[:gate_name] = gate.name
54
- gate.disable(thing)
61
+
62
+ adapter.disable self, gate, gate.wrap(thing)
55
63
  }
56
64
  end
57
65
 
@@ -60,7 +68,11 @@ module Flipper
60
68
  # Returns true if enabled, false if not.
61
69
  def enabled?(thing = nil)
62
70
  instrument(:enabled?, thing) { |payload|
63
- gate = gates.detect { |gate| gate.open?(thing) }
71
+ gate_values = adapter.get(self)
72
+
73
+ gate = gates.detect { |gate|
74
+ gate.open?(thing, gate_values[gate.key])
75
+ }
64
76
 
65
77
  if gate.nil?
66
78
  false
@@ -71,19 +83,71 @@ module Flipper
71
83
  }
72
84
  end
73
85
 
86
+ # Public
87
+ def state
88
+ gate_values = adapter.get(self)
89
+ boolean_value = gate_values[:boolean]
90
+
91
+ if boolean_gate.enabled?(boolean_value)
92
+ :on
93
+ elsif conditional_gates(gate_values).any?
94
+ :conditional
95
+ else
96
+ :off
97
+ end
98
+ end
99
+
100
+ # Public
101
+ def description
102
+ gate_values = adapter.get(self)
103
+ boolean_value = gate_values[:boolean]
104
+ conditional_gates = conditional_gates(gate_values)
105
+
106
+ if boolean_gate.enabled?(boolean_value)
107
+ boolean_gate.description(boolean_value).capitalize
108
+ elsif conditional_gates.any?
109
+ fragments = conditional_gates.map { |gate|
110
+ value = gate_values[gate.key]
111
+ gate.description(value)
112
+ }
113
+
114
+ "Enabled for #{fragments.join(', ')}"
115
+ else
116
+ boolean_gate.description(boolean_value).capitalize
117
+ end
118
+ end
119
+
120
+ # Public: Pretty string version for debugging.
121
+ def inspect
122
+ attributes = [
123
+ "name=#{name.inspect}",
124
+ "state=#{state.inspect}",
125
+ "description=#{description.inspect}",
126
+ "adapter=#{adapter.name.inspect}",
127
+ ]
128
+ "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
129
+ end
130
+
74
131
  # Internal: Gates to check to see if feature is enabled/disabled
75
132
  #
76
133
  # Returns an array of gates
77
134
  def gates
78
135
  @gates ||= [
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),
136
+ Gates::Boolean.new(@name, :instrumenter => @instrumenter),
137
+ Gates::Group.new(@name, :instrumenter => @instrumenter),
138
+ Gates::Actor.new(@name, :instrumenter => @instrumenter),
139
+ Gates::PercentageOfActors.new(@name, :instrumenter => @instrumenter),
140
+ Gates::PercentageOfRandom.new(@name, :instrumenter => @instrumenter),
84
141
  ]
85
142
  end
86
143
 
144
+ # Internal: Finds a gate by name.
145
+ #
146
+ # Returns a Flipper::Gate if found, nil if not.
147
+ def gate(name)
148
+ gates.detect { |gate| gate.name == name.to_sym }
149
+ end
150
+
87
151
  # Internal: Find the gate that protects a thing.
88
152
  #
89
153
  # thing - The object for which you would like to find a gate
@@ -95,42 +159,9 @@ module Flipper
95
159
  raise(GateNotFound.new(thing))
96
160
  end
97
161
 
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
162
  # Private
132
163
  def boolean_gate
133
- @boolean_gate ||= gates.detect { |gate| gate.name == :boolean }
164
+ @boolean_gate ||= gate(:boolean)
134
165
  end
135
166
 
136
167
  # Private
@@ -139,8 +170,11 @@ module Flipper
139
170
  end
140
171
 
141
172
  # Private
142
- def conditional_gates
143
- @conditional_gates ||= non_boolean_gates.select { |gate| gate.enabled? }
173
+ def conditional_gates(gate_values)
174
+ @conditional_gates ||= non_boolean_gates.select { |gate|
175
+ value = gate_values[gate.key]
176
+ gate.enabled?(value)
177
+ }
144
178
  end
145
179
 
146
180
  # Private