flipper 0.26.0 → 0.27.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +13 -11
  3. data/.github/workflows/examples.yml +5 -10
  4. data/Changelog.md +16 -0
  5. data/Gemfile +6 -3
  6. data/benchmark/enabled_ips.rb +10 -0
  7. data/benchmark/enabled_profile.rb +20 -0
  8. data/benchmark/instrumentation_ips.rb +21 -0
  9. data/benchmark/typecast_ips.rb +19 -0
  10. data/examples/api/basic.ru +3 -4
  11. data/examples/api/custom_memoized.ru +3 -4
  12. data/examples/api/memoized.ru +3 -4
  13. data/flipper.gemspec +0 -2
  14. data/lib/flipper/adapter.rb +23 -7
  15. data/lib/flipper/adapters/http.rb +11 -3
  16. data/lib/flipper/adapters/instrumented.rb +25 -2
  17. data/lib/flipper/adapters/memoizable.rb +19 -2
  18. data/lib/flipper/adapters/memory.rb +56 -39
  19. data/lib/flipper/adapters/operation_logger.rb +16 -3
  20. data/lib/flipper/adapters/poll/poller.rb +2 -125
  21. data/lib/flipper/adapters/poll.rb +4 -0
  22. data/lib/flipper/dsl.rb +1 -5
  23. data/lib/flipper/export.rb +26 -0
  24. data/lib/flipper/exporter.rb +17 -0
  25. data/lib/flipper/exporters/json/export.rb +32 -0
  26. data/lib/flipper/exporters/json/v1.rb +33 -0
  27. data/lib/flipper/feature.rb +22 -18
  28. data/lib/flipper/feature_check_context.rb +4 -4
  29. data/lib/flipper/gate_values.rb +0 -16
  30. data/lib/flipper/gates/actor.rb +2 -12
  31. data/lib/flipper/gates/boolean.rb +1 -1
  32. data/lib/flipper/gates/group.rb +4 -8
  33. data/lib/flipper/gates/percentage_of_actors.rb +9 -11
  34. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  35. data/lib/flipper/instrumentation/subscriber.rb +8 -0
  36. data/lib/flipper/poller.rb +117 -0
  37. data/lib/flipper/spec/shared_adapter_specs.rb +23 -0
  38. data/lib/flipper/test/shared_adapter_test.rb +24 -0
  39. data/lib/flipper/typecast.rb +28 -15
  40. data/lib/flipper/version.rb +1 -1
  41. data/lib/flipper.rb +2 -1
  42. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  43. data/spec/flipper/adapter_spec.rb +29 -2
  44. data/spec/flipper/adapters/http_spec.rb +25 -3
  45. data/spec/flipper/adapters/instrumented_spec.rb +28 -10
  46. data/spec/flipper/adapters/memoizable_spec.rb +30 -10
  47. data/spec/flipper/adapters/memory_spec.rb +3 -1
  48. data/spec/flipper/adapters/operation_logger_spec.rb +29 -10
  49. data/spec/flipper/dsl_spec.rb +20 -3
  50. data/spec/flipper/export_spec.rb +13 -0
  51. data/spec/flipper/exporter_spec.rb +16 -0
  52. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  53. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  54. data/spec/flipper/feature_check_context_spec.rb +12 -12
  55. data/spec/flipper/gate_values_spec.rb +2 -33
  56. data/spec/flipper/gates/percentage_of_actors_spec.rb +1 -1
  57. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -0
  58. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +10 -0
  59. data/spec/flipper/poller_spec.rb +47 -0
  60. data/spec/flipper/typecast_spec.rb +82 -3
  61. data/spec/flipper_spec.rb +7 -1
  62. data/spec/spec_helper.rb +1 -1
  63. data/spec/support/skippable.rb +18 -0
  64. metadata +25 -3
  65. 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
@@ -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
- if value.respond_to?(:to_i)
25
- value.to_i
26
- else
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
- if value.respond_to?(:to_f)
37
- value.to_f
38
- else
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
- if value.to_s.include?('.'.freeze)
49
- to_float(value)
50
- else
51
- to_integer(value)
52
- end
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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.26.0'.freeze
2
+ VERSION = '0.27.0'.freeze
3
3
  end
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 nothing' do
33
+ it 'returns true' do
34
34
  result = destination_flipper.import(source_flipper)
35
- expect(result).to be(nil)
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
- expect(source).to eq({
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
@@ -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
- destination_flipper = build_flipper
347
- expect(subject.adapter).to receive(:import).with(destination_flipper.adapter)
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