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