flipper 0.4.0 → 0.5.0

Sign up to get free protection for your applications and to get access to all the features.
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