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,3 +1,5 @@
1
+ require 'flipper/instrumenters/noop'
2
+
1
3
  module Flipper
2
4
  # Internal: Adapter wrapper that wraps vanilla adapter instances. Adds things
3
5
  # like local caching and convenience methods for adding/reading features from
@@ -19,6 +21,10 @@ module Flipper
19
21
  # To see an example adapter that this would wrap, checkout the [memory
20
22
  # adapter included with flipper](https://github.com/jnunemaker/flipper/blob/master/lib/flipper/adapters/memory.rb).
21
23
  class Adapter
24
+ # Private: The name of instrumentation events.
25
+ InstrumentationName = "adapter_operation.#{InstrumentationNamespace}"
26
+
27
+ # Private: The name of the key that stores the set of known features.
22
28
  FeaturesKey = 'features'
23
29
 
24
30
  # Internal: Wraps vanilla adapter instance for use internally in flipper.
@@ -37,89 +43,168 @@ module Flipper
37
43
  # # => Flipper::Adapter instance
38
44
  #
39
45
  # Returns Flipper::Adapter instance
40
- def self.wrap(object)
46
+ def self.wrap(object, options = {})
41
47
  if object.is_a?(Flipper::Adapter)
42
48
  object
43
49
  else
44
- new(object)
50
+ new(object, options)
45
51
  end
46
52
  end
47
53
 
48
- attr_reader :adapter, :local_cache
54
+ # Private: What adapter is being wrapped and will ultimately be used.
55
+ attr_reader :adapter
56
+
57
+ # Private: The name of the adapter. Based on the class name.
58
+ attr_reader :name
59
+
60
+ # Private: What is used to store the local cache.
61
+ attr_reader :local_cache
62
+
63
+ # Private: What is used to instrument all the things.
64
+ attr_reader :instrumenter
49
65
 
50
- # Internal: Initializes a new instance
66
+ # Internal: Initializes a new adapter instance.
51
67
  #
52
68
  # adapter - Vanilla adapter instance to wrap. Just needs to respond to
53
69
  # read, write, delete, set_members, set_add, and set_delete.
54
70
  #
55
- # local_cache - Where to store the local cache data (default: {}).
56
- # Must respond to fetch(key, block), delete(key) and clear.
57
- def initialize(adapter, local_cache = {})
71
+ # options - The Hash of options.
72
+ # :local_cache - Where to store the local cache data (default: {}).
73
+ # Must respond to fetch(key, block), delete(key)
74
+ # and clear.
75
+ # :instrumenter - What to use to instrument all the things.
76
+ #
77
+ def initialize(adapter, options = {})
58
78
  @adapter = adapter
59
- @local_cache = local_cache
79
+ @name = adapter.class.name.split('::').last.downcase.to_sym
80
+ @local_cache = options[:local_cache] || {}
81
+ @instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
60
82
  end
61
83
 
84
+ # Public: Turns local caching on/off.
85
+ #
86
+ # value - The Boolean that decides if local caching is on.
62
87
  def use_local_cache=(value)
63
88
  local_cache.clear
64
89
  @use_local_cache = value
65
90
  end
66
91
 
92
+ # Public: Returns true for using local cache, false for not.
67
93
  def using_local_cache?
68
94
  @use_local_cache == true
69
95
  end
70
96
 
97
+ # Public: Reads a key.
71
98
  def read(key)
72
- perform_read(key) { @adapter.read(key) }
99
+ perform_read(:read, key)
73
100
  end
74
101
 
102
+ # Public: Set a key to a value.
75
103
  def write(key, value)
76
- perform_update(key) { @adapter.write(key, value) }
104
+ perform_update(:write, key, value.to_s)
77
105
  end
78
106
 
107
+ # Public: Deletes a key.
79
108
  def delete(key)
80
- perform_update(key) { @adapter.delete(key) }
109
+ perform_delete(:delete, key)
81
110
  end
82
111
 
112
+ # Public: Returns the members of a set.
83
113
  def set_members(key)
84
- perform_read(key) { @adapter.set_members(key) }
114
+ perform_read(:set_members, key)
85
115
  end
86
116
 
117
+ # Public: Adds a value to a set.
87
118
  def set_add(key, value)
88
- perform_update(key) { @adapter.set_add(key, value) }
119
+ perform_update(:set_add, key, value.to_s)
89
120
  end
90
121
 
122
+ # Public: Deletes a value from a set.
91
123
  def set_delete(key, value)
92
- perform_update(key) { @adapter.set_delete(key, value) }
124
+ perform_update(:set_delete, key, value.to_s)
93
125
  end
94
126
 
127
+ # Public: Determines equality for an adapter instance when compared to
128
+ # another object.
95
129
  def eql?(other)
96
130
  self.class.eql?(other.class) && adapter == other.adapter
97
131
  end
98
- alias :== :eql?
132
+ alias_method :==, :eql?
99
133
 
134
+ # Public: Returns all the features that the adapter knows of.
100
135
  def features
101
136
  set_members(FeaturesKey)
102
137
  end
103
138
 
139
+ # Internal: Adds a known feature to the set of features.
104
140
  def feature_add(name)
105
141
  set_add(FeaturesKey, name.to_s)
106
142
  end
107
143
 
108
- private
144
+ # Public: Pretty string version for debugging.
145
+ def inspect
146
+ attributes = [
147
+ "name=#{name.inspect}",
148
+ "use_local_cache=#{@use_local_cache.inspect}"
149
+ ]
150
+ "#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
151
+ end
109
152
 
110
- def perform_read(key)
153
+ # Private
154
+ def perform_read(operation, key)
111
155
  if using_local_cache?
112
- local_cache.fetch(key.to_s) { local_cache[key.to_s] = yield }
156
+ local_cache.fetch(key.to_s) {
157
+ local_cache[key.to_s] = @adapter.send(operation, key)
158
+ }
113
159
  else
114
- yield
160
+ payload = {
161
+ :key => key,
162
+ :operation => operation,
163
+ :adapter_name => @name,
164
+ }
165
+
166
+ @instrumenter.instrument(InstrumentationName, payload) { |payload|
167
+ payload[:result] = @adapter.send(operation, key)
168
+ }
169
+ end
170
+ end
171
+
172
+ # Private
173
+ def perform_update(operation, key, value)
174
+ payload = {
175
+ :key => key,
176
+ :value => value,
177
+ :operation => operation,
178
+ :adapter_name => @name,
179
+ }
180
+
181
+ result = @instrumenter.instrument(InstrumentationName, payload) { |payload|
182
+ payload[:result] = @adapter.send(operation, key, value)
183
+ }
184
+
185
+ if using_local_cache?
186
+ local_cache.delete(key.to_s)
115
187
  end
188
+
189
+ result
116
190
  end
117
191
 
118
- def perform_update(key)
119
- result = yield
192
+ # Private
193
+ def perform_delete(operation, key)
194
+ payload = {
195
+ :key => key,
196
+ :operation => operation,
197
+ :adapter_name => @name,
198
+ }
199
+
200
+ result = @instrumenter.instrument(InstrumentationName, payload) { |payload|
201
+ payload[:result] = @adapter.send(operation, key)
202
+ }
203
+
120
204
  if using_local_cache?
121
205
  local_cache.delete(key.to_s)
122
206
  end
207
+
123
208
  result
124
209
  end
125
210
  end
@@ -3,41 +3,48 @@ require 'set'
3
3
  module Flipper
4
4
  module Adapters
5
5
  class Memoized
6
+ # Public
6
7
  def initialize(adapter, cache = {})
7
8
  @adapter = adapter
8
9
  @cache = cache
9
10
  end
10
11
 
12
+ # Public
11
13
  def read(key)
12
14
  @cache.fetch(key) {
13
15
  @cache[key] = @adapter.read(key)
14
16
  }
15
17
  end
16
18
 
19
+ # Public
17
20
  def write(key, value)
18
21
  result = @adapter.write(key, value)
19
22
  @cache.delete(key)
20
23
  result
21
24
  end
22
25
 
26
+ # Public
23
27
  def delete(key)
24
28
  result = @adapter.delete(key)
25
29
  @cache.delete(key)
26
30
  result
27
31
  end
28
32
 
33
+ # Public
29
34
  def set_add(key, value)
30
35
  result = @adapter.set_add(key, value)
31
36
  @cache.delete(key)
32
37
  result
33
38
  end
34
39
 
40
+ # Public
35
41
  def set_delete(key, value)
36
42
  result = @adapter.set_delete(key, value)
37
43
  @cache.delete(key)
38
44
  result
39
45
  end
40
46
 
47
+ # Public
41
48
  def set_members(key)
42
49
  @cache.fetch(key) {
43
50
  @cache[key] = @adapter.set_members(key)
@@ -3,32 +3,39 @@ require 'set'
3
3
  module Flipper
4
4
  module Adapters
5
5
  class Memory
6
+ # Public
6
7
  def initialize(source = nil)
7
8
  @source = source || {}
8
9
  end
9
10
 
11
+ # Public
10
12
  def read(key)
11
13
  @source[key.to_s]
12
14
  end
13
15
 
16
+ # Public
14
17
  def write(key, value)
15
- @source[key.to_s] = value
18
+ @source[key.to_s] = value.to_s
16
19
  end
17
20
 
21
+ # Public
18
22
  def delete(key)
19
23
  @source.delete(key.to_s)
20
24
  end
21
25
 
26
+ # Public
22
27
  def set_add(key, value)
23
28
  ensure_set_initialized(key)
24
- @source[key.to_s].add(value)
29
+ @source[key.to_s].add(value.to_s)
25
30
  end
26
31
 
32
+ # Public
27
33
  def set_delete(key, value)
28
34
  ensure_set_initialized(key)
29
- @source[key.to_s].delete(value)
35
+ @source[key.to_s].delete(value.to_s)
30
36
  end
31
37
 
38
+ # Public
32
39
  def set_members(key)
33
40
  ensure_set_initialized(key)
34
41
  @source[key.to_s]
@@ -13,36 +13,43 @@ module Flipper
13
13
  SetDelete = Struct.new(:key, :value)
14
14
  SetMember = Struct.new(:key)
15
15
 
16
+ # Public
16
17
  def initialize(adapter)
17
18
  @operations = []
18
19
  @adapter = adapter
19
20
  end
20
21
 
22
+ # Public
21
23
  def read(key)
22
24
  @operations << Read.new(key.to_s)
23
25
  @adapter.read key
24
26
  end
25
27
 
28
+ # Public
26
29
  def write(key, value)
27
30
  @operations << Write.new(key.to_s, value)
28
31
  @adapter.write key, value
29
32
  end
30
33
 
34
+ # Public
31
35
  def delete(key)
32
36
  @operations << Delete.new(key.to_s, nil)
33
37
  @adapter.delete key
34
38
  end
35
39
 
40
+ # Public
36
41
  def set_add(key, value)
37
42
  @operations << SetAdd.new(key.to_s, value)
38
43
  @adapter.set_add key, value
39
44
  end
40
45
 
46
+ # Public
41
47
  def set_delete(key, value)
42
48
  @operations << SetDelete.new(key.to_s, value)
43
49
  @adapter.set_delete key, value
44
50
  end
45
51
 
52
+ # Public
46
53
  def set_members(key)
47
54
  @operations << SetMembers.new(key.to_s)
48
55
  @adapter.set_members key
@@ -1,61 +1,118 @@
1
1
  require 'flipper/adapter'
2
+ require 'flipper/instrumenters/noop'
2
3
 
3
4
  module Flipper
4
5
  class DSL
6
+ # Private
5
7
  attr_reader :adapter
6
8
 
7
- def initialize(adapter)
8
- @adapter = Adapter.wrap(adapter)
9
+ # Private: What is being used to instrument all the things.
10
+ attr_reader :instrumenter
11
+
12
+ # Public: Returns a new instance of the DSL.
13
+ #
14
+ # adapter - The adapter that this DSL instance should use.
15
+ # options - The Hash of options.
16
+ # :instrumenter - What should be used to instrument all the things.
17
+ def initialize(adapter, options = {})
18
+ @instrumenter = options.fetch(:instrumenter, Flipper::Instrumenters::Noop)
19
+ @adapter = Adapter.wrap(adapter, :instrumenter => @instrumenter)
20
+ @memoized_features = {}
9
21
  end
10
22
 
23
+ # Public: Check if a feature is enabled.
24
+ #
25
+ # name - The String or Symbol name of the feature.
26
+ # args - The args passed through to the enabled check.
27
+ #
28
+ # Returns true if feature is enabled, false if not.
11
29
  def enabled?(name, *args)
12
30
  feature(name).enabled?(*args)
13
31
  end
14
32
 
15
- def disabled?(name, *args)
16
- !enabled?(name, *args)
17
- end
18
-
33
+ # Public: Enable a feature.
34
+ #
35
+ # name - The String or Symbol name of the feature.
36
+ # args - The args passed through to the feature instance enable call.
37
+ #
38
+ # Returns the result of the feature instance enable call.
19
39
  def enable(name, *args)
20
40
  feature(name).enable(*args)
21
41
  end
22
42
 
43
+ # Public: Disable a feature.
44
+ #
45
+ # name - The String or Symbol name of the feature.
46
+ # args - The args passed through to the feature instance enable call.
47
+ #
48
+ # Returns the result of the feature instance disable call.
23
49
  def disable(name, *args)
24
50
  feature(name).disable(*args)
25
51
  end
26
52
 
53
+ # Public: Access a feature instance by name.
54
+ #
55
+ # name - The String or Symbol name of the feature.
56
+ #
57
+ # Returns an instance of Flipper::Feature.
27
58
  def feature(name)
28
- memoized_features[name.to_sym] ||= Feature.new(name, @adapter)
59
+ @memoized_features[name.to_sym] ||= Feature.new(name, @adapter, {
60
+ :instrumenter => instrumenter,
61
+ })
29
62
  end
30
63
 
31
- alias :[] :feature
64
+ # Public: Shortcut access to a feature instance by name.
65
+ #
66
+ # name - The String or Symbol name of the feature.
67
+ #
68
+ # Returns an instance of Flipper::Feature.
69
+ alias_method :[], :feature
32
70
 
71
+ # Public: Access a flipper group by name.
72
+ #
73
+ # name - The String or Symbol name of the feature.
74
+ #
75
+ # Returns an instance of Flipper::Group.
76
+ # Raises Flipper::GroupNotRegistered if group has not been registered.
33
77
  def group(name)
34
78
  Flipper.group(name)
35
79
  end
36
80
 
81
+ # Public: Wraps an object as a flipper actor.
82
+ #
83
+ # thing - The object that you would like to wrap.
84
+ #
85
+ # Returns an instance of Flipper::Types::Actor.
86
+ # Raises ArgumentError if thing not wrappable.
37
87
  def actor(thing)
38
88
  Types::Actor.new(thing)
39
89
  end
40
90
 
91
+ # Public: Shortcut for getting a percentage of random instance.
92
+ #
93
+ # number - The percentage of random that should be enabled.
94
+ #
95
+ # Returns Flipper::Types::PercentageOfRandom.
41
96
  def random(number)
42
97
  Types::PercentageOfRandom.new(number)
43
98
  end
44
- alias :percentage_of_random :random
99
+ alias_method :percentage_of_random, :random
45
100
 
101
+ # Public: Shortcut for getting a percentage of actors instance.
102
+ #
103
+ # number - The percentage of actors that should be enabled.
104
+ #
105
+ # Returns Flipper::Types::PercentageOfActors.
46
106
  def actors(number)
47
107
  Types::PercentageOfActors.new(number)
48
108
  end
49
- alias :percentage_of_actors :actors
109
+ alias_method :percentage_of_actors, :actors
50
110
 
111
+ # Internal: Returns a Set of the known features for this adapter.
112
+ #
113
+ # Returns Set of Flipper::Feature instances.
51
114
  def features
52
115
  adapter.features.map { |name| feature(name) }.to_set
53
116
  end
54
-
55
- private
56
-
57
- def memoized_features
58
- @memoized_features ||= {}
59
- end
60
117
  end
61
118
  end