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.
- data/.rspec +1 -0
- data/Changelog.md +12 -0
- data/Gemfile +4 -7
- data/Guardfile +16 -4
- data/README.md +63 -34
- data/examples/basic.rb +1 -1
- data/examples/dsl.rb +10 -12
- data/examples/group.rb +10 -4
- data/examples/individual_actor.rb +9 -6
- data/examples/instrumentation.rb +39 -0
- data/examples/percentage_of_actors.rb +12 -9
- data/examples/percentage_of_random.rb +4 -2
- data/lib/flipper.rb +43 -10
- data/lib/flipper/adapter.rb +106 -21
- data/lib/flipper/adapters/memoized.rb +7 -0
- data/lib/flipper/adapters/memory.rb +10 -3
- data/lib/flipper/adapters/operation_logger.rb +7 -0
- data/lib/flipper/dsl.rb +73 -16
- data/lib/flipper/errors.rb +6 -0
- data/lib/flipper/feature.rb +117 -19
- data/lib/flipper/gate.rb +72 -4
- data/lib/flipper/gates/actor.rb +41 -12
- data/lib/flipper/gates/boolean.rb +21 -11
- data/lib/flipper/gates/group.rb +45 -12
- data/lib/flipper/gates/percentage_of_actors.rb +29 -10
- data/lib/flipper/gates/percentage_of_random.rb +22 -9
- data/lib/flipper/instrumentation/log_subscriber.rb +107 -0
- data/lib/flipper/instrumentation/metriks.rb +6 -0
- data/lib/flipper/instrumentation/metriks_subscriber.rb +92 -0
- data/lib/flipper/instrumenters/memory.rb +25 -0
- data/lib/flipper/instrumenters/noop.rb +9 -0
- data/lib/flipper/key.rb +23 -4
- data/lib/flipper/registry.rb +22 -6
- data/lib/flipper/spec/shared_adapter_specs.rb +59 -12
- data/lib/flipper/toggle.rb +19 -2
- data/lib/flipper/toggles/boolean.rb +36 -3
- data/lib/flipper/toggles/set.rb +9 -3
- data/lib/flipper/toggles/value.rb +9 -3
- data/lib/flipper/type.rb +1 -0
- data/lib/flipper/types/actor.rb +12 -14
- data/lib/flipper/types/percentage.rb +8 -2
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +163 -27
- data/spec/flipper/adapters/memoized_spec.rb +6 -6
- data/spec/flipper/dsl_spec.rb +51 -54
- data/spec/flipper/feature_spec.rb +179 -17
- data/spec/flipper/gate_spec.rb +47 -0
- data/spec/flipper/gates/actor_spec.rb +52 -0
- data/spec/flipper/gates/boolean_spec.rb +52 -0
- data/spec/flipper/gates/group_spec.rb +79 -0
- data/spec/flipper/gates/percentage_of_actors_spec.rb +98 -0
- data/spec/flipper/gates/percentage_of_random_spec.rb +54 -0
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +104 -0
- data/spec/flipper/instrumentation/metriks_subscriber_spec.rb +69 -0
- data/spec/flipper/instrumenters/memory_spec.rb +26 -0
- data/spec/flipper/instrumenters/noop_spec.rb +22 -0
- data/spec/flipper/key_spec.rb +8 -2
- data/spec/flipper/registry_spec.rb +20 -2
- data/spec/flipper/toggle_spec.rb +22 -0
- data/spec/flipper/toggles/boolean_spec.rb +40 -0
- data/spec/flipper/toggles/set_spec.rb +35 -0
- data/spec/flipper/toggles/value_spec.rb +55 -0
- data/spec/flipper/types/actor_spec.rb +28 -33
- data/spec/flipper_spec.rb +16 -3
- data/spec/helper.rb +37 -3
- data/spec/integration_spec.rb +90 -83
- metadata +40 -4
data/lib/flipper/adapter.rb
CHANGED
@@ -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
|
-
|
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
|
-
#
|
56
|
-
#
|
57
|
-
|
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
|
-
@
|
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(
|
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(
|
104
|
+
perform_update(:write, key, value.to_s)
|
77
105
|
end
|
78
106
|
|
107
|
+
# Public: Deletes a key.
|
79
108
|
def delete(key)
|
80
|
-
|
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(
|
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(
|
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(
|
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
|
-
|
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
|
-
|
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
|
-
|
153
|
+
# Private
|
154
|
+
def perform_read(operation, key)
|
111
155
|
if using_local_cache?
|
112
|
-
local_cache.fetch(key.to_s) {
|
156
|
+
local_cache.fetch(key.to_s) {
|
157
|
+
local_cache[key.to_s] = @adapter.send(operation, key)
|
158
|
+
}
|
113
159
|
else
|
114
|
-
|
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
|
-
|
119
|
-
|
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
|
data/lib/flipper/dsl.rb
CHANGED
@@ -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
|
-
|
8
|
-
|
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
|
-
|
16
|
-
|
17
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|