flipper 0.17.1 → 0.21.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.
- checksums.yaml +4 -4
- data/.github/workflows/ci.yml +57 -0
- data/Changelog.md +114 -1
- data/Dockerfile +1 -1
- data/Gemfile +3 -6
- data/README.md +103 -47
- data/Rakefile +1 -4
- data/docs/Adapters.md +9 -9
- data/docs/Caveats.md +2 -2
- data/docs/DockerCompose.md +0 -1
- data/docs/Gates.md +74 -74
- data/docs/Optimization.md +70 -47
- data/docs/http/README.md +12 -11
- data/docs/images/banner.jpg +0 -0
- data/docs/read-only/README.md +8 -5
- data/examples/basic.rb +1 -12
- data/examples/configuring_default.rb +2 -5
- data/examples/dsl.rb +13 -24
- data/examples/enabled_for_actor.rb +8 -15
- data/examples/group.rb +3 -6
- data/examples/group_dynamic_lookup.rb +5 -19
- data/examples/group_with_members.rb +4 -14
- data/examples/importing.rb +1 -1
- data/examples/individual_actor.rb +2 -5
- data/examples/instrumentation.rb +1 -2
- data/examples/memoizing.rb +35 -0
- data/examples/percentage_of_actors.rb +6 -16
- data/examples/percentage_of_actors_enabled_check.rb +7 -10
- data/examples/percentage_of_actors_group.rb +5 -18
- data/examples/percentage_of_time.rb +3 -6
- data/flipper.gemspec +3 -4
- data/lib/flipper.rb +7 -3
- data/lib/flipper/adapters/dual_write.rb +67 -0
- data/lib/flipper/adapters/http.rb +32 -28
- data/lib/flipper/adapters/memory.rb +23 -94
- data/lib/flipper/adapters/operation_logger.rb +5 -0
- data/lib/flipper/adapters/pstore.rb +8 -1
- data/lib/flipper/adapters/sync.rb +7 -7
- data/lib/flipper/adapters/sync/interval_synchronizer.rb +1 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +1 -0
- data/lib/flipper/configuration.rb +33 -7
- data/lib/flipper/dsl.rb +8 -0
- data/lib/flipper/errors.rb +2 -3
- data/lib/flipper/feature.rb +2 -2
- data/lib/flipper/identifier.rb +17 -0
- data/lib/flipper/middleware/memoizer.rb +30 -15
- data/lib/flipper/middleware/setup_env.rb +13 -3
- data/lib/flipper/railtie.rb +38 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +15 -0
- data/lib/flipper/test/shared_adapter_test.rb +16 -1
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/adapter_spec.rb +2 -2
- data/spec/flipper/adapters/dual_write_spec.rb +71 -0
- data/spec/flipper/adapters/http_spec.rb +74 -8
- data/spec/flipper/adapters/memory_spec.rb +21 -1
- data/spec/flipper/adapters/operation_logger_spec.rb +9 -0
- data/spec/flipper/adapters/sync_spec.rb +4 -4
- data/spec/flipper/configuration_spec.rb +20 -2
- data/spec/flipper/feature_spec.rb +5 -5
- data/spec/flipper/identifier_spec.rb +14 -0
- data/spec/flipper/middleware/memoizer_spec.rb +95 -35
- data/spec/flipper/middleware/setup_env_spec.rb +23 -3
- data/spec/flipper/railtie_spec.rb +69 -0
- data/spec/{integration_spec.rb → flipper_integration_spec.rb} +0 -0
- data/spec/flipper_spec.rb +26 -0
- data/spec/helper.rb +3 -3
- data/spec/support/descriptions.yml +1 -0
- data/spec/support/spec_helpers.rb +25 -0
- data/test/test_helper.rb +2 -1
- metadata +19 -10
- data/.rubocop.yml +0 -52
- data/.rubocop_todo.yml +0 -562
- data/examples/example_setup.rb +0 -8
data/flipper.gemspec
CHANGED
@@ -25,13 +25,12 @@ Gem::Specification.new do |gem|
|
|
25
25
|
gem.authors = ['John Nunemaker']
|
26
26
|
gem.email = ['nunemaker@gmail.com']
|
27
27
|
gem.summary = 'Feature flipper for ANYTHING'
|
28
|
-
gem.description = 'Feature flipper is the act of enabling/disabling features in your application, ideally without re-deploying or changing anything in your code base. Flipper makes this extremely easy to do with any backend you would like to use.' # rubocop:disable Metrics/LineLength
|
29
28
|
gem.homepage = 'https://github.com/jnunemaker/flipper'
|
30
29
|
gem.license = 'MIT'
|
31
30
|
|
32
|
-
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
33
|
-
gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
|
34
|
-
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
|
31
|
+
gem.executables = `git ls-files -- bin/*`.split("\n").map { |f| File.basename(f) }
|
32
|
+
gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
|
33
|
+
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
|
35
34
|
gem.name = 'flipper'
|
36
35
|
gem.require_paths = ['lib']
|
37
36
|
gem.version = Flipper::VERSION
|
data/lib/flipper.rb
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
require "forwardable"
|
2
2
|
|
3
3
|
module Flipper
|
4
|
-
extend self
|
4
|
+
extend self
|
5
5
|
extend Forwardable
|
6
6
|
|
7
7
|
# Private: The namespace for all instrumented events.
|
@@ -16,7 +16,7 @@ module Flipper
|
|
16
16
|
# Public: Configure flipper.
|
17
17
|
#
|
18
18
|
# Flipper.configure do |config|
|
19
|
-
# config.
|
19
|
+
# config.adapter { ... }
|
20
20
|
# end
|
21
21
|
#
|
22
22
|
# Yields Flipper::Configuration instance.
|
@@ -65,7 +65,8 @@ module Flipper
|
|
65
65
|
:time, :percentage_of_time,
|
66
66
|
:features, :feature, :[], :preload, :preload_all,
|
67
67
|
:adapter, :add, :exist?, :remove, :import,
|
68
|
-
:memoize=, :memoizing
|
68
|
+
:memoize=, :memoizing?,
|
69
|
+
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
69
70
|
|
70
71
|
# Public: Use this to register a group by name.
|
71
72
|
#
|
@@ -151,6 +152,7 @@ require 'flipper/feature'
|
|
151
152
|
require 'flipper/gate'
|
152
153
|
require 'flipper/instrumenters/memory'
|
153
154
|
require 'flipper/instrumenters/noop'
|
155
|
+
require 'flipper/identifier'
|
154
156
|
require 'flipper/middleware/memoizer'
|
155
157
|
require 'flipper/middleware/setup_env'
|
156
158
|
require 'flipper/registry'
|
@@ -162,3 +164,5 @@ require 'flipper/types/percentage'
|
|
162
164
|
require 'flipper/types/percentage_of_actors'
|
163
165
|
require 'flipper/types/percentage_of_time'
|
164
166
|
require 'flipper/typecast'
|
167
|
+
|
168
|
+
require "flipper/railtie" if defined?(Rails::Railtie)
|
@@ -0,0 +1,67 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Adapters
|
3
|
+
class DualWrite
|
4
|
+
include ::Flipper::Adapter
|
5
|
+
|
6
|
+
# Public: The name of the adapter.
|
7
|
+
attr_reader :name
|
8
|
+
|
9
|
+
# Public: Build a new sync instance.
|
10
|
+
#
|
11
|
+
# local - The local flipper adapter that should serve reads.
|
12
|
+
# remote - The remote flipper adapter that writes should go to first (in
|
13
|
+
# addition to the local adapter).
|
14
|
+
def initialize(local, remote, options = {})
|
15
|
+
@name = :dual_write
|
16
|
+
@local = local
|
17
|
+
@remote = remote
|
18
|
+
end
|
19
|
+
|
20
|
+
def features
|
21
|
+
@local.features
|
22
|
+
end
|
23
|
+
|
24
|
+
def get(feature)
|
25
|
+
@local.get(feature)
|
26
|
+
end
|
27
|
+
|
28
|
+
def get_multi(features)
|
29
|
+
@local.get_multi(features)
|
30
|
+
end
|
31
|
+
|
32
|
+
def get_all
|
33
|
+
@local.get_all
|
34
|
+
end
|
35
|
+
|
36
|
+
def add(feature)
|
37
|
+
result = @remote.add(feature)
|
38
|
+
@local.add(feature)
|
39
|
+
result
|
40
|
+
end
|
41
|
+
|
42
|
+
def remove(feature)
|
43
|
+
result = @remote.remove(feature)
|
44
|
+
@local.remove(feature)
|
45
|
+
result
|
46
|
+
end
|
47
|
+
|
48
|
+
def clear(feature)
|
49
|
+
result = @remote.clear(feature)
|
50
|
+
@local.clear(feature)
|
51
|
+
result
|
52
|
+
end
|
53
|
+
|
54
|
+
def enable(feature, gate, thing)
|
55
|
+
result = @remote.enable(feature, gate, thing)
|
56
|
+
@local.enable(feature, gate, thing)
|
57
|
+
result
|
58
|
+
end
|
59
|
+
|
60
|
+
def disable(feature, gate, thing)
|
61
|
+
result = @remote.disable(feature, gate, thing)
|
62
|
+
@local.disable(feature, gate, thing)
|
63
|
+
result
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -35,12 +35,6 @@ module Flipper
|
|
35
35
|
end
|
36
36
|
end
|
37
37
|
|
38
|
-
def add(feature)
|
39
|
-
body = JSON.generate(name: feature.key)
|
40
|
-
response = @client.post('/features', body)
|
41
|
-
response.is_a?(Net::HTTPOK)
|
42
|
-
end
|
43
|
-
|
44
38
|
def get_multi(features)
|
45
39
|
csv_keys = features.map(&:key).join(',')
|
46
40
|
response = @client.get("/features?keys=#{csv_keys}")
|
@@ -87,51 +81,61 @@ module Flipper
|
|
87
81
|
parsed_response['features'].map { |feature| feature['key'] }.to_set
|
88
82
|
end
|
89
83
|
|
84
|
+
def add(feature)
|
85
|
+
body = JSON.generate(name: feature.key)
|
86
|
+
response = @client.post('/features', body)
|
87
|
+
raise Error, response unless response.is_a?(Net::HTTPOK)
|
88
|
+
true
|
89
|
+
end
|
90
|
+
|
90
91
|
def remove(feature)
|
91
92
|
response = @client.delete("/features/#{feature.key}")
|
92
|
-
response.is_a?(Net::HTTPNoContent)
|
93
|
+
raise Error, response unless response.is_a?(Net::HTTPNoContent)
|
94
|
+
true
|
93
95
|
end
|
94
96
|
|
95
97
|
def enable(feature, gate, thing)
|
96
98
|
body = request_body_for_gate(gate, thing.value.to_s)
|
97
99
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
98
100
|
response = @client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
99
|
-
response.is_a?(Net::HTTPOK)
|
101
|
+
raise Error, response unless response.is_a?(Net::HTTPOK)
|
102
|
+
true
|
100
103
|
end
|
101
104
|
|
102
105
|
def disable(feature, gate, thing)
|
103
106
|
body = request_body_for_gate(gate, thing.value.to_s)
|
104
107
|
query_string = gate.key == :groups ? "?allow_unregistered_groups=true" : ""
|
105
|
-
response =
|
106
|
-
|
107
|
-
|
108
|
-
|
109
|
-
|
110
|
-
|
111
|
-
|
112
|
-
|
108
|
+
response = case gate.key
|
109
|
+
when :percentage_of_actors, :percentage_of_time
|
110
|
+
@client.post("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
111
|
+
else
|
112
|
+
@client.delete("/features/#{feature.key}/#{gate.key}#{query_string}", body)
|
113
|
+
end
|
114
|
+
raise Error, response unless response.is_a?(Net::HTTPOK)
|
115
|
+
true
|
113
116
|
end
|
114
117
|
|
115
118
|
def clear(feature)
|
116
119
|
response = @client.delete("/features/#{feature.key}/clear")
|
117
|
-
response.is_a?(Net::HTTPNoContent)
|
120
|
+
raise Error, response unless response.is_a?(Net::HTTPNoContent)
|
121
|
+
true
|
118
122
|
end
|
119
123
|
|
120
124
|
private
|
121
125
|
|
122
126
|
def request_body_for_gate(gate, value)
|
123
127
|
data = case gate.key
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
128
|
+
when :boolean
|
129
|
+
{}
|
130
|
+
when :groups
|
131
|
+
{ name: value }
|
132
|
+
when :actors
|
133
|
+
{ flipper_id: value }
|
134
|
+
when :percentage_of_actors, :percentage_of_time
|
135
|
+
{ percentage: value }
|
136
|
+
else
|
137
|
+
raise "#{gate.key} is not a valid flipper gate key"
|
138
|
+
end
|
135
139
|
JSON.generate(data)
|
136
140
|
end
|
137
141
|
|
@@ -20,55 +20,57 @@ module Flipper
|
|
20
20
|
|
21
21
|
# Public: The set of known features.
|
22
22
|
def features
|
23
|
-
|
23
|
+
@source.keys.to_set
|
24
24
|
end
|
25
25
|
|
26
26
|
# Public: Adds a feature to the set of known features.
|
27
27
|
def add(feature)
|
28
|
-
|
28
|
+
@source[feature.key] ||= default_config
|
29
29
|
true
|
30
30
|
end
|
31
31
|
|
32
32
|
# Public: Removes a feature from the set of known features and clears
|
33
33
|
# all the values for the feature.
|
34
34
|
def remove(feature)
|
35
|
-
|
36
|
-
clear(feature)
|
35
|
+
@source.delete(feature.key)
|
37
36
|
true
|
38
37
|
end
|
39
38
|
|
40
39
|
# Public: Clears all the gate values for a feature.
|
41
40
|
def clear(feature)
|
42
|
-
feature.
|
43
|
-
delete key(feature, gate)
|
44
|
-
end
|
41
|
+
@source[feature.key] = default_config
|
45
42
|
true
|
46
43
|
end
|
47
44
|
|
48
45
|
# Public
|
49
46
|
def get(feature)
|
50
|
-
|
47
|
+
@source[feature.key] || default_config
|
51
48
|
end
|
52
49
|
|
53
50
|
def get_multi(features)
|
54
|
-
|
51
|
+
result = {}
|
52
|
+
features.each do |feature|
|
53
|
+
result[feature.key] = @source[feature.key] || default_config
|
54
|
+
end
|
55
|
+
result
|
55
56
|
end
|
56
57
|
|
57
58
|
def get_all
|
58
|
-
|
59
|
-
Flipper::Feature.new(key, self)
|
60
|
-
end
|
61
|
-
|
62
|
-
read_many_features(features)
|
59
|
+
@source
|
63
60
|
end
|
64
61
|
|
65
62
|
# Public
|
66
63
|
def enable(feature, gate, thing)
|
64
|
+
@source[feature.key] ||= default_config
|
65
|
+
|
67
66
|
case gate.data_type
|
68
|
-
when :boolean
|
69
|
-
|
67
|
+
when :boolean
|
68
|
+
clear(feature)
|
69
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
70
|
+
when :integer
|
71
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
70
72
|
when :set
|
71
|
-
|
73
|
+
@source[feature.key][gate.key] << thing.value.to_s
|
72
74
|
else
|
73
75
|
raise "#{gate} is not supported by this adapter yet"
|
74
76
|
end
|
@@ -78,13 +80,15 @@ module Flipper
|
|
78
80
|
|
79
81
|
# Public
|
80
82
|
def disable(feature, gate, thing)
|
83
|
+
@source[feature.key] ||= default_config
|
84
|
+
|
81
85
|
case gate.data_type
|
82
86
|
when :boolean
|
83
87
|
clear(feature)
|
84
88
|
when :integer
|
85
|
-
|
89
|
+
@source[feature.key][gate.key] = thing.value.to_s
|
86
90
|
when :set
|
87
|
-
|
91
|
+
@source[feature.key][gate.key].delete thing.value.to_s
|
88
92
|
else
|
89
93
|
raise "#{gate} is not supported by this adapter yet"
|
90
94
|
end
|
@@ -100,81 +104,6 @@ module Flipper
|
|
100
104
|
]
|
101
105
|
"#<#{self.class.name}:#{object_id} #{attributes.join(', ')}>"
|
102
106
|
end
|
103
|
-
|
104
|
-
private
|
105
|
-
|
106
|
-
def read_feature_keys
|
107
|
-
set_members(FeaturesKey)
|
108
|
-
end
|
109
|
-
|
110
|
-
# Private
|
111
|
-
def key(feature, gate)
|
112
|
-
"feature/#{feature.key}/#{gate.key}"
|
113
|
-
end
|
114
|
-
|
115
|
-
def read_many_features(features)
|
116
|
-
result = {}
|
117
|
-
features.each do |feature|
|
118
|
-
result[feature.key] = read_feature(feature)
|
119
|
-
end
|
120
|
-
result
|
121
|
-
end
|
122
|
-
|
123
|
-
def read_feature(feature)
|
124
|
-
result = {}
|
125
|
-
|
126
|
-
feature.gates.each do |gate|
|
127
|
-
result[gate.key] =
|
128
|
-
case gate.data_type
|
129
|
-
when :boolean, :integer
|
130
|
-
read key(feature, gate)
|
131
|
-
when :set
|
132
|
-
set_members key(feature, gate)
|
133
|
-
else
|
134
|
-
raise "#{gate} is not supported by this adapter yet"
|
135
|
-
end
|
136
|
-
end
|
137
|
-
|
138
|
-
result
|
139
|
-
end
|
140
|
-
|
141
|
-
# Private
|
142
|
-
def read(key)
|
143
|
-
@source[key.to_s]
|
144
|
-
end
|
145
|
-
|
146
|
-
# Private
|
147
|
-
def write(key, value)
|
148
|
-
@source[key.to_s] = value.to_s
|
149
|
-
end
|
150
|
-
|
151
|
-
# Private
|
152
|
-
def delete(key)
|
153
|
-
@source.delete(key.to_s)
|
154
|
-
end
|
155
|
-
|
156
|
-
# Private
|
157
|
-
def set_add(key, value)
|
158
|
-
ensure_set_initialized(key)
|
159
|
-
@source[key.to_s].add(value.to_s)
|
160
|
-
end
|
161
|
-
|
162
|
-
# Private
|
163
|
-
def set_delete(key, value)
|
164
|
-
ensure_set_initialized(key)
|
165
|
-
@source[key.to_s].delete(value.to_s)
|
166
|
-
end
|
167
|
-
|
168
|
-
# Private
|
169
|
-
def set_members(key)
|
170
|
-
ensure_set_initialized(key)
|
171
|
-
@source[key.to_s]
|
172
|
-
end
|
173
|
-
|
174
|
-
# Private
|
175
|
-
def ensure_set_initialized(key)
|
176
|
-
@source[key.to_s] ||= Set.new
|
177
|
-
end
|
178
107
|
end
|
179
108
|
end
|
180
109
|
end
|
@@ -117,6 +117,11 @@ module Flipper
|
|
117
117
|
def reset
|
118
118
|
@operations.clear
|
119
119
|
end
|
120
|
+
|
121
|
+
def inspect
|
122
|
+
inspect_id = ::Kernel::format "%x", (object_id * 2)
|
123
|
+
%(#<#{self.class}:0x#{inspect_id} @name=#{name.inspect}, @operations=#{@operations.inspect}, @adapter=#{@adapter.inspect}>)
|
124
|
+
end
|
120
125
|
end
|
121
126
|
end
|
122
127
|
end
|
@@ -84,7 +84,10 @@ module Flipper
|
|
84
84
|
def enable(feature, gate, thing)
|
85
85
|
@store.transaction do
|
86
86
|
case gate.data_type
|
87
|
-
when :boolean
|
87
|
+
when :boolean
|
88
|
+
clear_gates(feature)
|
89
|
+
write key(feature, gate), thing.value.to_s
|
90
|
+
when :integer
|
88
91
|
write key(feature, gate), thing.value.to_s
|
89
92
|
when :set
|
90
93
|
set_add key(feature, gate), thing.value.to_s
|
@@ -213,3 +216,7 @@ module Flipper
|
|
213
216
|
end
|
214
217
|
end
|
215
218
|
end
|
219
|
+
|
220
|
+
Flipper.configure do |config|
|
221
|
+
config.adapter { Flipper::Adapters::PStore.new }
|
222
|
+
end
|
@@ -17,7 +17,7 @@ module Flipper
|
|
17
17
|
# Public: Build a new sync instance.
|
18
18
|
#
|
19
19
|
# local - The local flipper adapter that should serve reads.
|
20
|
-
# remote - The remote flipper
|
20
|
+
# remote - The remote flipper adapter that should serve writes and update
|
21
21
|
# the local on an interval.
|
22
22
|
# interval - The Float or Integer number of seconds between syncs from
|
23
23
|
# remote to local. Default value is set in IntervalSynchronizer.
|
@@ -34,26 +34,26 @@ module Flipper
|
|
34
34
|
synchronizer = Synchronizer.new(@local, @remote, sync_options)
|
35
35
|
IntervalSynchronizer.new(synchronizer, interval: options[:interval])
|
36
36
|
end
|
37
|
-
|
37
|
+
synchronize
|
38
38
|
end
|
39
39
|
|
40
40
|
def features
|
41
|
-
|
41
|
+
synchronize
|
42
42
|
@local.features
|
43
43
|
end
|
44
44
|
|
45
45
|
def get(feature)
|
46
|
-
|
46
|
+
synchronize
|
47
47
|
@local.get(feature)
|
48
48
|
end
|
49
49
|
|
50
50
|
def get_multi(features)
|
51
|
-
|
51
|
+
synchronize
|
52
52
|
@local.get_multi(features)
|
53
53
|
end
|
54
54
|
|
55
55
|
def get_all
|
56
|
-
|
56
|
+
synchronize
|
57
57
|
@local.get_all
|
58
58
|
end
|
59
59
|
|
@@ -89,7 +89,7 @@ module Flipper
|
|
89
89
|
|
90
90
|
private
|
91
91
|
|
92
|
-
def
|
92
|
+
def synchronize
|
93
93
|
@synchronizer.call
|
94
94
|
end
|
95
95
|
end
|