flipper 0.26.0 → 0.27.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 +13 -11
- data/.github/workflows/examples.yml +5 -10
- data/Changelog.md +16 -0
- data/Gemfile +6 -3
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +19 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- data/flipper.gemspec +0 -2
- data/lib/flipper/adapter.rb +23 -7
- data/lib/flipper/adapters/http.rb +11 -3
- data/lib/flipper/adapters/instrumented.rb +25 -2
- data/lib/flipper/adapters/memoizable.rb +19 -2
- data/lib/flipper/adapters/memory.rb +56 -39
- data/lib/flipper/adapters/operation_logger.rb +16 -3
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +4 -0
- data/lib/flipper/dsl.rb +1 -5
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- data/lib/flipper/feature.rb +22 -18
- data/lib/flipper/feature_check_context.rb +4 -4
- data/lib/flipper/gate_values.rb +0 -16
- data/lib/flipper/gates/actor.rb +2 -12
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/group.rb +4 -8
- data/lib/flipper/gates/percentage_of_actors.rb +9 -11
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/instrumentation/subscriber.rb +8 -0
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
- data/lib/flipper/test/shared_adapter_test.rb +24 -0
- data/lib/flipper/typecast.rb +28 -15
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +2 -1
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_spec.rb +29 -2
- data/spec/flipper/adapters/http_spec.rb +25 -3
- data/spec/flipper/adapters/instrumented_spec.rb +28 -10
- data/spec/flipper/adapters/memoizable_spec.rb +30 -10
- data/spec/flipper/adapters/memory_spec.rb +3 -1
- data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
- data/spec/flipper/dsl_spec.rb +20 -3
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- data/spec/flipper/feature_check_context_spec.rb +12 -12
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/percentage_of_actors_spec.rb +1 -1
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
- data/spec/flipper/poller_spec.rb +47 -0
- data/spec/flipper/typecast_spec.rb +82 -3
- data/spec/flipper_spec.rb +7 -1
- data/spec/spec_helper.rb +1 -1
- data/spec/support/skippable.rb +18 -0
- metadata +25 -3
- data/.github/workflows/release.yml +0 -44
@@ -33,7 +33,15 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
33
33
|
expect(subject.class.ancestors).to include(Flipper::Adapter)
|
34
34
|
end
|
35
35
|
|
36
|
+
it 'knows how to get adapter from source' do
|
37
|
+
adapter = Flipper::Adapters::Memory.new
|
38
|
+
flipper = Flipper.new(adapter)
|
39
|
+
expect(subject.class.from(adapter).class.ancestors).to include(Flipper::Adapter)
|
40
|
+
expect(subject.class.from(flipper).class.ancestors).to include(Flipper::Adapter)
|
41
|
+
end
|
42
|
+
|
36
43
|
it 'returns correct default values for the gates if none are enabled' do
|
44
|
+
expect(subject.get(feature)).to eq(subject.class.default_config)
|
37
45
|
expect(subject.get(feature)).to eq(subject.default_config)
|
38
46
|
end
|
39
47
|
|
@@ -304,4 +312,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
304
312
|
subject.enable(feature, boolean_gate, flipper.boolean(true))
|
305
313
|
expect(subject.get(feature)).to eq(subject.default_config.merge(boolean: "true"))
|
306
314
|
end
|
315
|
+
|
316
|
+
it 'can import and export' do
|
317
|
+
adapter = Flipper::Adapters::Memory.new
|
318
|
+
source_flipper = Flipper.new(adapter)
|
319
|
+
source_flipper.enable(:stats)
|
320
|
+
export = adapter.export
|
321
|
+
|
322
|
+
# some adapters cannot import so if they return false lets assert it
|
323
|
+
# didn't happen
|
324
|
+
if subject.import(export)
|
325
|
+
expect(flipper[:stats]).to be_enabled
|
326
|
+
else
|
327
|
+
expect(flipper[:stats]).not_to be_enabled
|
328
|
+
end
|
329
|
+
end
|
307
330
|
end
|
@@ -34,7 +34,16 @@ module Flipper
|
|
34
34
|
assert_includes @adapter.class.ancestors, Flipper::Adapter
|
35
35
|
end
|
36
36
|
|
37
|
+
def test_knows_how_to_get_adapter_from_source
|
38
|
+
adapter = Flipper::Adapters::Memory.new
|
39
|
+
flipper = Flipper.new(adapter)
|
40
|
+
|
41
|
+
assert_includes adapter.class.from(adapter).class.ancestors, Flipper::Adapter
|
42
|
+
assert_includes adapter.class.from(flipper).class.ancestors, Flipper::Adapter
|
43
|
+
end
|
44
|
+
|
37
45
|
def test_returns_correct_default_values_for_gates_if_none_are_enabled
|
46
|
+
assert_equal @adapter.class.default_config, @adapter.get(@feature)
|
38
47
|
assert_equal @adapter.default_config, @adapter.get(@feature)
|
39
48
|
end
|
40
49
|
|
@@ -300,6 +309,21 @@ module Flipper
|
|
300
309
|
assert_equal true, @adapter.enable(@feature, @boolean_gate, @flipper.boolean(true))
|
301
310
|
assert_equal @adapter.default_config.merge(boolean: "true"), @adapter.get(@feature)
|
302
311
|
end
|
312
|
+
|
313
|
+
def test_can_import_and_export
|
314
|
+
adapter = Flipper::Adapters::Memory.new
|
315
|
+
source_flipper = Flipper.new(adapter)
|
316
|
+
source_flipper.enable(:stats)
|
317
|
+
export = adapter.export
|
318
|
+
|
319
|
+
# some adapters cannot import so if they return false lets assert it
|
320
|
+
# didn't happen
|
321
|
+
if @adapter.import(export)
|
322
|
+
assert @flipper[:stats].enabled?
|
323
|
+
else
|
324
|
+
refute @flipper[:stats].enabled?
|
325
|
+
end
|
326
|
+
end
|
303
327
|
end
|
304
328
|
end
|
305
329
|
end
|
data/lib/flipper/typecast.rb
CHANGED
@@ -21,11 +21,9 @@ module Flipper
|
|
21
21
|
# Returns an Integer representation of the value.
|
22
22
|
# Raises ArgumentError if conversion is not possible.
|
23
23
|
def self.to_integer(value)
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
|
28
|
-
end
|
24
|
+
value.to_i
|
25
|
+
rescue NoMethodError
|
26
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to an integer"
|
29
27
|
end
|
30
28
|
|
31
29
|
# Internal: Convert value to a float.
|
@@ -33,11 +31,9 @@ module Flipper
|
|
33
31
|
# Returns a Float representation of the value.
|
34
32
|
# Raises ArgumentError if conversion is not possible.
|
35
33
|
def self.to_float(value)
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
40
|
-
end
|
34
|
+
value.to_f
|
35
|
+
rescue NoMethodError
|
36
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
41
37
|
end
|
42
38
|
|
43
39
|
# Internal: Convert value to a percentage.
|
@@ -45,11 +41,11 @@ module Flipper
|
|
45
41
|
# Returns a Integer or Float representation of the value.
|
46
42
|
# Raises ArgumentError if conversion is not possible.
|
47
43
|
def self.to_percentage(value)
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
44
|
+
result_to_f = value.to_f
|
45
|
+
result_to_i = result_to_f.to_i
|
46
|
+
result_to_f == result_to_i ? result_to_i : result_to_f
|
47
|
+
rescue NoMethodError
|
48
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a percentage"
|
53
49
|
end
|
54
50
|
|
55
51
|
# Internal: Convert value to a set.
|
@@ -66,5 +62,22 @@ module Flipper
|
|
66
62
|
raise ArgumentError, "#{value.inspect} cannot be converted to a set"
|
67
63
|
end
|
68
64
|
end
|
65
|
+
|
66
|
+
def self.features_hash(source)
|
67
|
+
normalized_source = {}
|
68
|
+
(source || {}).each do |feature_key, gates|
|
69
|
+
normalized_source[feature_key] ||= {}
|
70
|
+
gates.each do |gate_key, value|
|
71
|
+
normalized_value = case value
|
72
|
+
when Array, Set
|
73
|
+
value.to_set
|
74
|
+
else
|
75
|
+
value ? value.to_s : value
|
76
|
+
end
|
77
|
+
normalized_source[feature_key][gate_key.to_sym] = normalized_value
|
78
|
+
end
|
79
|
+
end
|
80
|
+
normalized_source
|
81
|
+
end
|
69
82
|
end
|
70
83
|
end
|
data/lib/flipper/version.rb
CHANGED
data/lib/flipper.rb
CHANGED
@@ -64,7 +64,7 @@ module Flipper
|
|
64
64
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
65
65
|
:time, :percentage_of_time,
|
66
66
|
:features, :feature, :[], :preload, :preload_all,
|
67
|
-
:adapter, :add, :exist?, :remove, :import,
|
67
|
+
:adapter, :add, :exist?, :remove, :import, :export,
|
68
68
|
:memoize=, :memoizing?,
|
69
69
|
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
70
70
|
|
@@ -155,6 +155,7 @@ require 'flipper/instrumenters/noop'
|
|
155
155
|
require 'flipper/identifier'
|
156
156
|
require 'flipper/middleware/memoizer'
|
157
157
|
require 'flipper/middleware/setup_env'
|
158
|
+
require 'flipper/poller'
|
158
159
|
require 'flipper/registry'
|
159
160
|
require 'flipper/type'
|
160
161
|
require 'flipper/types/actor'
|
@@ -0,0 +1,46 @@
|
|
1
|
+
{
|
2
|
+
"version": 1,
|
3
|
+
"features": {
|
4
|
+
"search": {
|
5
|
+
"boolean": null,
|
6
|
+
"actors": [
|
7
|
+
"john",
|
8
|
+
"another",
|
9
|
+
"testing"
|
10
|
+
],
|
11
|
+
"percentage_of_actors": null,
|
12
|
+
"percentage_of_time": null,
|
13
|
+
"groups": [
|
14
|
+
"admins"
|
15
|
+
]
|
16
|
+
},
|
17
|
+
"new_pricing": {
|
18
|
+
"boolean": "true",
|
19
|
+
"actors": [],
|
20
|
+
"percentage_of_actors": null,
|
21
|
+
"percentage_of_time": null,
|
22
|
+
"groups": []
|
23
|
+
},
|
24
|
+
"google_analytics_tag": {
|
25
|
+
"boolean": null,
|
26
|
+
"actors": [],
|
27
|
+
"percentage_of_actors": "100",
|
28
|
+
"percentage_of_time": null,
|
29
|
+
"groups": []
|
30
|
+
},
|
31
|
+
"help_scout_tag": {
|
32
|
+
"boolean": null,
|
33
|
+
"actors": [],
|
34
|
+
"percentage_of_actors": null,
|
35
|
+
"percentage_of_time": "50",
|
36
|
+
"groups": []
|
37
|
+
},
|
38
|
+
"nope": {
|
39
|
+
"boolean": null,
|
40
|
+
"actors": [],
|
41
|
+
"percentage_of_actors": null,
|
42
|
+
"percentage_of_time": null,
|
43
|
+
"groups": []
|
44
|
+
}
|
45
|
+
}
|
46
|
+
}
|
@@ -30,9 +30,9 @@ RSpec.describe Flipper::Adapter do
|
|
30
30
|
end
|
31
31
|
|
32
32
|
describe '#import' do
|
33
|
-
it 'returns
|
33
|
+
it 'returns true' do
|
34
34
|
result = destination_flipper.import(source_flipper)
|
35
|
-
expect(result).to be(
|
35
|
+
expect(result).to be(true)
|
36
36
|
end
|
37
37
|
|
38
38
|
it 'can import from one adapter to another' do
|
@@ -114,5 +114,32 @@ RSpec.describe Flipper::Adapter do
|
|
114
114
|
destination_flipper.import(source_flipper)
|
115
115
|
expect(destination_flipper.features.map(&:key)).to eq([])
|
116
116
|
end
|
117
|
+
|
118
|
+
it 'can import an export' do
|
119
|
+
source_flipper.enable(:search)
|
120
|
+
source_flipper.enable(:google_analytics, Flipper::Actor.new("User;1"))
|
121
|
+
|
122
|
+
destination_flipper.import(source_flipper.export)
|
123
|
+
|
124
|
+
feature = destination_flipper[:search]
|
125
|
+
expect(feature.boolean_value).to be(true)
|
126
|
+
|
127
|
+
feature = destination_flipper[:google_analytics]
|
128
|
+
expect(feature.actors_value).to eq(Set["User;1"])
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
describe "#export" do
|
133
|
+
it "exports features" do
|
134
|
+
source_flipper.enable(:search)
|
135
|
+
export = source_flipper.export
|
136
|
+
expect(export.features.dig("search", :boolean)).to eq("true")
|
137
|
+
end
|
138
|
+
|
139
|
+
it "exports with arguments" do
|
140
|
+
source_flipper.enable(:search)
|
141
|
+
export = source_flipper.export(format: :json, version: 1)
|
142
|
+
expect(export.features.dig("search", :boolean)).to eq("true")
|
143
|
+
end
|
117
144
|
end
|
118
145
|
end
|
@@ -52,6 +52,28 @@ RSpec.describe Flipper::Adapters::Http do
|
|
52
52
|
expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
|
53
53
|
expect(flipper[:search].groups_value).to eq(Set.new)
|
54
54
|
end
|
55
|
+
|
56
|
+
it "can import" do
|
57
|
+
adapter = Flipper::Adapters::Memory.new
|
58
|
+
source_flipper = Flipper.new(adapter)
|
59
|
+
source_flipper.enable_percentage_of_actors :search, 10
|
60
|
+
source_flipper.enable_percentage_of_time :search, 15
|
61
|
+
source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
|
62
|
+
source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
|
63
|
+
source_flipper.enable_group :search, :admins
|
64
|
+
source_flipper.enable_group :search, :employees
|
65
|
+
source_flipper.enable :plausible
|
66
|
+
source_flipper.disable :google_analytics
|
67
|
+
|
68
|
+
flipper = Flipper.new(subject)
|
69
|
+
flipper.import(source_flipper)
|
70
|
+
expect(flipper[:search].percentage_of_actors_value).to be(10)
|
71
|
+
expect(flipper[:search].percentage_of_time_value).to be(15)
|
72
|
+
expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
|
73
|
+
expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
|
74
|
+
expect(flipper[:plausible].boolean_value).to be(true)
|
75
|
+
expect(flipper[:google_analytics].boolean_value).to be(false)
|
76
|
+
end
|
55
77
|
end
|
56
78
|
|
57
79
|
it "sends default headers" do
|
@@ -82,7 +104,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
82
104
|
|
83
105
|
describe "#get_multi" do
|
84
106
|
it "raises error when not successful response" do
|
85
|
-
stub_request(:get, "http://app.com/flipper/features?keys=feature_panel")
|
107
|
+
stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
|
86
108
|
.to_return(status: 503, body: "", headers: {})
|
87
109
|
|
88
110
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
@@ -94,7 +116,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
94
116
|
|
95
117
|
describe "#get_all" do
|
96
118
|
it "raises error when not successful response" do
|
97
|
-
stub_request(:get, "http://app.com/flipper/features")
|
119
|
+
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
98
120
|
.to_return(status: 503, body: "", headers: {})
|
99
121
|
|
100
122
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
@@ -106,7 +128,7 @@ RSpec.describe Flipper::Adapters::Http do
|
|
106
128
|
|
107
129
|
describe "#features" do
|
108
130
|
it "raises error when not successful response" do
|
109
|
-
stub_request(:get, "http://app.com/flipper/features")
|
131
|
+
stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
|
110
132
|
.to_return(status: 503, body: "", headers: {})
|
111
133
|
|
112
134
|
adapter = described_class.new(url: 'http://app.com/flipper')
|
@@ -16,16 +16,6 @@ RSpec.describe Flipper::Adapters::Instrumented do
|
|
16
16
|
|
17
17
|
it_should_behave_like 'a flipper adapter'
|
18
18
|
|
19
|
-
it 'forwards missing methods to underlying adapter' do
|
20
|
-
adapter = Class.new do
|
21
|
-
def foo
|
22
|
-
:foo
|
23
|
-
end
|
24
|
-
end.new
|
25
|
-
instrumented = described_class.new(adapter)
|
26
|
-
expect(instrumented.foo).to eq(:foo)
|
27
|
-
end
|
28
|
-
|
29
19
|
describe '#name' do
|
30
20
|
it 'is instrumented' do
|
31
21
|
expect(subject.name).to be(:instrumented)
|
@@ -146,4 +136,32 @@ RSpec.describe Flipper::Adapters::Instrumented do
|
|
146
136
|
expect(event.payload[:result]).to be(result)
|
147
137
|
end
|
148
138
|
end
|
139
|
+
|
140
|
+
describe '#import' do
|
141
|
+
it 'records instrumentation' do
|
142
|
+
result = subject.import(Flipper::Adapters::Memory.new)
|
143
|
+
|
144
|
+
event = instrumenter.events.last
|
145
|
+
expect(event).not_to be_nil
|
146
|
+
expect(event.name).to eq('adapter_operation.flipper')
|
147
|
+
expect(event.payload[:operation]).to eq(:import)
|
148
|
+
expect(event.payload[:adapter_name]).to eq(:memory)
|
149
|
+
expect(event.payload[:result]).to be(result)
|
150
|
+
end
|
151
|
+
end
|
152
|
+
|
153
|
+
describe '#export' do
|
154
|
+
it 'records instrumentation' do
|
155
|
+
result = subject.export(format: :json, version: 1)
|
156
|
+
|
157
|
+
event = instrumenter.events.last
|
158
|
+
expect(event).not_to be_nil
|
159
|
+
expect(event.name).to eq('adapter_operation.flipper')
|
160
|
+
expect(event.payload[:operation]).to eq(:export)
|
161
|
+
expect(event.payload[:adapter_name]).to eq(:memory)
|
162
|
+
expect(event.payload[:format]).to be(:json)
|
163
|
+
expect(event.payload[:version]).to be(1)
|
164
|
+
expect(event.payload[:result]).to be(result)
|
165
|
+
end
|
166
|
+
end
|
149
167
|
end
|
@@ -11,16 +11,6 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
11
11
|
|
12
12
|
it_should_behave_like 'a flipper adapter'
|
13
13
|
|
14
|
-
it 'forwards missing methods to underlying adapter' do
|
15
|
-
adapter = Class.new do
|
16
|
-
def foo
|
17
|
-
:foo
|
18
|
-
end
|
19
|
-
end.new
|
20
|
-
memoizable = described_class.new(adapter)
|
21
|
-
expect(memoizable.foo).to eq(:foo)
|
22
|
-
end
|
23
|
-
|
24
14
|
describe '#name' do
|
25
15
|
it 'is instrumented' do
|
26
16
|
expect(subject.name).to be(:memoizable)
|
@@ -249,6 +239,36 @@ RSpec.describe Flipper::Adapters::Memoizable do
|
|
249
239
|
end
|
250
240
|
end
|
251
241
|
|
242
|
+
describe "#import" do
|
243
|
+
context "with memoization enabled" do
|
244
|
+
before do
|
245
|
+
subject.memoize = true
|
246
|
+
end
|
247
|
+
|
248
|
+
it "unmemoizes features" do
|
249
|
+
cache[:foo] = "bar"
|
250
|
+
flipper[:stats].enable
|
251
|
+
flipper[:search].disable
|
252
|
+
subject.import(Flipper::Adapters::Memory.new)
|
253
|
+
expect(cache).to be_empty
|
254
|
+
end
|
255
|
+
end
|
256
|
+
|
257
|
+
context "with memoization disabled" do
|
258
|
+
before do
|
259
|
+
subject.memoize = false
|
260
|
+
end
|
261
|
+
|
262
|
+
it "does not unmemoize features" do
|
263
|
+
cache[:foo] = "bar"
|
264
|
+
flipper[:stats].enable
|
265
|
+
flipper[:search].disable
|
266
|
+
subject.import(Flipper::Adapters::Memory.new)
|
267
|
+
expect(cache).not_to be_empty
|
268
|
+
end
|
269
|
+
end
|
270
|
+
end
|
271
|
+
|
252
272
|
describe '#features' do
|
253
273
|
context 'with memoization enabled' do
|
254
274
|
before do
|
@@ -14,7 +14,9 @@ RSpec.describe Flipper::Adapters::Memory do
|
|
14
14
|
flipper.enable_actor :following, Flipper::Actor.new('3')
|
15
15
|
flipper.enable_group :following, Flipper::Types::Group.new(:staff)
|
16
16
|
|
17
|
-
|
17
|
+
dup = described_class.new(subject.get_all)
|
18
|
+
|
19
|
+
expect(dup.get_all).to eq({
|
18
20
|
"subscriptions" => subject.default_config.merge(boolean: "true"),
|
19
21
|
"search" => subject.default_config,
|
20
22
|
"logging" => subject.default_config.merge(:percentage_of_time => "30"),
|
@@ -18,16 +18,6 @@ RSpec.describe Flipper::Adapters::OperationLogger do
|
|
18
18
|
expect(output).to match(/@adapter=#<Flipper::Adapters::Memory/)
|
19
19
|
end
|
20
20
|
|
21
|
-
it 'forwards missing methods to underlying adapter' do
|
22
|
-
adapter = Class.new do
|
23
|
-
def foo
|
24
|
-
:foo
|
25
|
-
end
|
26
|
-
end.new
|
27
|
-
operation_logger = described_class.new(adapter)
|
28
|
-
expect(operation_logger.foo).to eq(:foo)
|
29
|
-
end
|
30
|
-
|
31
21
|
describe '#get' do
|
32
22
|
before do
|
33
23
|
@feature = flipper[:stats]
|
@@ -106,4 +96,33 @@ RSpec.describe Flipper::Adapters::OperationLogger do
|
|
106
96
|
expect(@result).to eq(adapter.add(@feature))
|
107
97
|
end
|
108
98
|
end
|
99
|
+
|
100
|
+
describe '#import' do
|
101
|
+
before do
|
102
|
+
@source = Flipper::Adapters::Memory.new
|
103
|
+
@result = subject.import(@source)
|
104
|
+
end
|
105
|
+
|
106
|
+
it 'logs operation' do
|
107
|
+
expect(subject.count(:import)).to be(1)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'returns result' do
|
111
|
+
expect(@result).to eq(adapter.import(@source))
|
112
|
+
end
|
113
|
+
end
|
114
|
+
|
115
|
+
describe '#export' do
|
116
|
+
before do
|
117
|
+
@result = subject.export(format: :json, version: 1)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'logs operation' do
|
121
|
+
expect(subject.count(:export)).to be(1)
|
122
|
+
end
|
123
|
+
|
124
|
+
it 'returns result' do
|
125
|
+
expect(@result).to eq(adapter.export(format: :json, version: 1))
|
126
|
+
end
|
127
|
+
end
|
109
128
|
end
|
data/spec/flipper/dsl_spec.rb
CHANGED
@@ -342,10 +342,27 @@ RSpec.describe Flipper::DSL do
|
|
342
342
|
end
|
343
343
|
|
344
344
|
describe '#import' do
|
345
|
+
context "with flipper instance" do
|
346
|
+
it 'delegates to adapter' do
|
347
|
+
destination_flipper = build_flipper
|
348
|
+
expect(subject.adapter).to receive(:import).with(destination_flipper)
|
349
|
+
subject.import(destination_flipper)
|
350
|
+
end
|
351
|
+
end
|
352
|
+
|
353
|
+
context "with flipper adapter" do
|
354
|
+
it 'delegates to adapter' do
|
355
|
+
destination_flipper = build_flipper
|
356
|
+
expect(subject.adapter).to receive(:import).with(destination_flipper.adapter)
|
357
|
+
subject.import(destination_flipper.adapter)
|
358
|
+
end
|
359
|
+
end
|
360
|
+
end
|
361
|
+
|
362
|
+
describe "#export" do
|
345
363
|
it 'delegates to adapter' do
|
346
|
-
|
347
|
-
expect(subject.
|
348
|
-
subject.import(destination_flipper)
|
364
|
+
expect(subject.export).to eq(subject.adapter.export)
|
365
|
+
expect(subject.export(format: :json)).to eq(subject.adapter.export(format: :json))
|
349
366
|
end
|
350
367
|
end
|
351
368
|
|
@@ -0,0 +1,13 @@
|
|
1
|
+
RSpec.describe Flipper::Export do
|
2
|
+
it "can initialize" do
|
3
|
+
export = described_class.new(contents: "{}", format: :json, version: 1)
|
4
|
+
expect(export.contents).to eq("{}")
|
5
|
+
expect(export.format).to eq(:json)
|
6
|
+
expect(export.version).to eq(1)
|
7
|
+
end
|
8
|
+
|
9
|
+
it "raises not implemented for features" do
|
10
|
+
export = described_class.new(contents: "{}", format: :json, version: 1)
|
11
|
+
expect { export.features }.to raise_error(NotImplementedError)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
RSpec.describe Flipper::Exporter do
|
2
|
+
describe ".build" do
|
3
|
+
it "builds instance of exporter" do
|
4
|
+
exporter = described_class.build(format: :json, version: 1)
|
5
|
+
expect(exporter).to be_instance_of(Flipper::Exporters::Json::V1)
|
6
|
+
end
|
7
|
+
|
8
|
+
it "raises if format not found" do
|
9
|
+
expect { described_class.build(format: :nope, version: 1) }.to raise_error(KeyError)
|
10
|
+
end
|
11
|
+
|
12
|
+
it "raises if version not found" do
|
13
|
+
expect { described_class.build(format: :json, version: 0) }.to raise_error(KeyError)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
require 'flipper/exporters/json/v1'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Exporters::Json::Export do
|
4
|
+
let(:contents) {
|
5
|
+
<<~JSON
|
6
|
+
{
|
7
|
+
"version":1,
|
8
|
+
"features":{
|
9
|
+
"search":{"boolean":null,"groups":["admins","employees"],"actors":["User;1","User;100"],"percentage_of_actors":"10","percentage_of_time":"15"},
|
10
|
+
"plausible":{"boolean":"true","groups":[],"actors":[],"percentage_of_actors":null,"percentage_of_time":null},
|
11
|
+
"google_analytics":{"boolean":null,"groups":[],"actors":[],"percentage_of_actors":null,"percentage_of_time":null}
|
12
|
+
}
|
13
|
+
}
|
14
|
+
JSON
|
15
|
+
}
|
16
|
+
|
17
|
+
it "can initialize" do
|
18
|
+
export = described_class.new(contents: contents)
|
19
|
+
expect(export.format).to eq(:json)
|
20
|
+
expect(export.version).to be(1)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "can initialize with version" do
|
24
|
+
export = described_class.new(contents: contents, version: 1)
|
25
|
+
expect(export.version).to be(1)
|
26
|
+
end
|
27
|
+
|
28
|
+
it "can build features from contents" do
|
29
|
+
export = Flipper::Exporters::Json::Export.new(contents: contents)
|
30
|
+
expect(export.features).to eq({
|
31
|
+
"search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
|
32
|
+
"plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
33
|
+
"google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
34
|
+
})
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can build an adapter from features" do
|
38
|
+
export = Flipper::Exporters::Json::Export.new(contents: contents)
|
39
|
+
expect(export.adapter).to be_instance_of(Flipper::Adapters::Memory)
|
40
|
+
expect(export.adapter.get_all).to eq({
|
41
|
+
"plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
42
|
+
"search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
|
43
|
+
"google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
44
|
+
})
|
45
|
+
end
|
46
|
+
|
47
|
+
it "raises for invalid json" do
|
48
|
+
export = described_class.new(contents: "bad contents")
|
49
|
+
expect {
|
50
|
+
export.features
|
51
|
+
}.to raise_error(Flipper::Exporters::Json::JsonError)
|
52
|
+
end
|
53
|
+
|
54
|
+
it "raises for missing features key" do
|
55
|
+
export = described_class.new(contents: "{}")
|
56
|
+
expect {
|
57
|
+
export.features
|
58
|
+
}.to raise_error(Flipper::Exporters::Json::InvalidError)
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'flipper/exporters/json/v1'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Exporters::Json::V1 do
|
4
|
+
subject { described_class.new }
|
5
|
+
|
6
|
+
it "has a version number" do
|
7
|
+
adapter = Flipper::Adapters::Memory.new
|
8
|
+
export = subject.call(adapter)
|
9
|
+
data = JSON.parse(export.contents)
|
10
|
+
expect(data["version"]).to eq(1)
|
11
|
+
end
|
12
|
+
|
13
|
+
it "exports features and gates" do
|
14
|
+
adapter = Flipper::Adapters::Memory.new
|
15
|
+
flipper = Flipper.new(adapter)
|
16
|
+
flipper.enable_percentage_of_actors :search, 10
|
17
|
+
flipper.enable_percentage_of_time :search, 15
|
18
|
+
flipper.enable_actor :search, Flipper::Actor.new('User;1')
|
19
|
+
flipper.enable_actor :search, Flipper::Actor.new('User;100')
|
20
|
+
flipper.enable_group :search, :admins
|
21
|
+
flipper.enable_group :search, :employees
|
22
|
+
flipper.enable :plausible
|
23
|
+
flipper.disable :google_analytics
|
24
|
+
|
25
|
+
export = subject.call(adapter)
|
26
|
+
|
27
|
+
expect(export.features).to eq({
|
28
|
+
"google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
29
|
+
"plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
30
|
+
"search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
|
31
|
+
})
|
32
|
+
end
|
33
|
+
end
|