flipper 1.0.0 → 1.1.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/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +7 -3
- data/.github/workflows/examples.yml +27 -5
- data/Changelog.md +42 -0
- data/Gemfile +4 -4
- data/README.md +13 -11
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/flipper.gemspec +1 -2
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +26 -7
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +18 -13
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +0 -4
- data/lib/flipper/adapters/poll.rb +1 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +4 -4
- data/lib/flipper/adapters/strict.rb +47 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/cloud/configuration.rb +121 -52
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
- data/lib/flipper/cloud/telemetry.rb +183 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +28 -3
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +11 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +55 -0
- data/lib/flipper/gate.rb +1 -0
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/poller.rb +1 -1
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
- data/lib/flipper/test/shared_adapter_test.rb +24 -5
- data/lib/flipper/typecast.rb +34 -6
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +38 -1
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/http_spec.rb +39 -5
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +62 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -23
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +156 -0
- data/spec/flipper/cloud_spec.rb +12 -12
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -0
- data/spec/flipper/engine_spec.rb +108 -7
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_spec.rb +360 -1
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +89 -1
- data/spec/spec_helper.rb +5 -0
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +104 -18
- data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -80,13 +80,13 @@ RSpec.describe Flipper::GateValues do
|
|
80
80
|
it 'raises argument error for percentage of time value that cannot be converted to an integer' do
|
81
81
|
expect do
|
82
82
|
described_class.new(percentage_of_time: ['asdf'])
|
83
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a
|
83
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
|
84
84
|
end
|
85
85
|
|
86
86
|
it 'raises argument error for percentage of actors value that cannot be converted to an int' do
|
87
87
|
expect do
|
88
88
|
described_class.new(percentage_of_actors: ['asdf'])
|
89
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a
|
89
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
|
90
90
|
end
|
91
91
|
|
92
92
|
it 'raises argument error for actors value that cannot be converted to a set' do
|
@@ -0,0 +1,108 @@
|
|
1
|
+
RSpec.describe Flipper::Gates::Expression do
|
2
|
+
let(:feature_name) { :search }
|
3
|
+
|
4
|
+
subject do
|
5
|
+
described_class.new
|
6
|
+
end
|
7
|
+
|
8
|
+
def context(expression, properties: {})
|
9
|
+
Flipper::FeatureCheckContext.new(
|
10
|
+
feature_name: feature_name,
|
11
|
+
values: Flipper::GateValues.new(expression: expression),
|
12
|
+
actors: [Flipper::Types::Actor.new(Flipper::Actor.new(1, properties))]
|
13
|
+
)
|
14
|
+
end
|
15
|
+
|
16
|
+
describe '#enabled?' do
|
17
|
+
context 'for nil value' do
|
18
|
+
it 'returns false' do
|
19
|
+
expect(subject.enabled?(nil)).to eq(false)
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
context 'for empty value' do
|
24
|
+
it 'returns false' do
|
25
|
+
expect(subject.enabled?({})).to eq(false)
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
context "for not empty value" do
|
30
|
+
it 'returns true' do
|
31
|
+
expect(subject.enabled?({"Boolean" => [true]})).to eq(true)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
describe '#open?' do
|
37
|
+
context 'for expression that evaluates to true' do
|
38
|
+
it 'returns true' do
|
39
|
+
expression = Flipper.boolean(true).eq(true)
|
40
|
+
expect(subject.open?(context(expression.value))).to be(true)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
context 'for expression that evaluates to false' do
|
45
|
+
it 'returns false' do
|
46
|
+
expression = Flipper.boolean(true).eq(false)
|
47
|
+
expect(subject.open?(context(expression.value))).to be(false)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
context 'for properties that have string keys' do
|
52
|
+
it 'returns true when expression evalutes to true' do
|
53
|
+
expression = Flipper.property(:type).eq("User")
|
54
|
+
context = context(expression.value, properties: {"type" => "User"})
|
55
|
+
expect(subject.open?(context)).to be(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it 'returns false when expression evaluates to false' do
|
59
|
+
expression = Flipper.property(:type).eq("User")
|
60
|
+
context = context(expression.value, properties: {"type" => "Org"})
|
61
|
+
expect(subject.open?(context)).to be(false)
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
context 'for properties that have symbol keys' do
|
66
|
+
it 'returns true when expression evalutes to true' do
|
67
|
+
expression = Flipper.property(:type).eq("User")
|
68
|
+
context = context(expression.value, properties: {type: "User"})
|
69
|
+
expect(subject.open?(context)).to be(true)
|
70
|
+
end
|
71
|
+
|
72
|
+
it 'returns false when expression evaluates to false' do
|
73
|
+
expression = Flipper.property(:type).eq("User")
|
74
|
+
context = context(expression.value, properties: {type: "Org"})
|
75
|
+
expect(subject.open?(context)).to be(false)
|
76
|
+
end
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
describe '#protects?' do
|
81
|
+
it 'returns true for Flipper::Expression' do
|
82
|
+
expression = Flipper.number(20).eq(20)
|
83
|
+
expect(subject.protects?(expression)).to be(true)
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'returns true for Hash' do
|
87
|
+
expression = Flipper.number(20).eq(20)
|
88
|
+
expect(subject.protects?(expression.value)).to be(true)
|
89
|
+
end
|
90
|
+
|
91
|
+
it 'returns false for other things' do
|
92
|
+
expect(subject.protects?(false)).to be(false)
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
describe '#wrap' do
|
97
|
+
it 'returns self for Flipper::Expression' do
|
98
|
+
expression = Flipper.number(20).eq(20)
|
99
|
+
expect(subject.wrap(expression)).to be(expression)
|
100
|
+
end
|
101
|
+
|
102
|
+
it 'returns Flipper::Expression for Hash' do
|
103
|
+
expression = Flipper.number(20).eq(20)
|
104
|
+
expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expression)
|
105
|
+
expect(subject.wrap(expression.value)).to eq(expression)
|
106
|
+
end
|
107
|
+
end
|
108
|
+
end
|
@@ -2,12 +2,11 @@ require 'flipper/identifier'
|
|
2
2
|
|
3
3
|
RSpec.describe Flipper::Identifier do
|
4
4
|
describe '#flipper_id' do
|
5
|
-
class User < Struct.new(:id)
|
6
|
-
include Flipper::Identifier
|
7
|
-
end
|
8
|
-
|
9
5
|
it 'uses class name and id' do
|
10
|
-
|
6
|
+
class BlahBlah < Struct.new(:id)
|
7
|
+
include Flipper::Identifier
|
8
|
+
end
|
9
|
+
expect(BlahBlah.new(5).flipper_id).to eq('BlahBlah;5')
|
11
10
|
end
|
12
11
|
end
|
13
12
|
end
|
@@ -1,6 +1,5 @@
|
|
1
1
|
require 'flipper/adapters/instrumented'
|
2
2
|
require 'flipper/instrumentation/statsd'
|
3
|
-
require 'statsd'
|
4
3
|
|
5
4
|
begin
|
6
5
|
require 'active_support/isolated_execution_state'
|
@@ -78,4 +77,19 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
|
|
78
77
|
flipper[:stats].disable(user)
|
79
78
|
assert_timer 'flipper.adapter.memory.disable'
|
80
79
|
end
|
80
|
+
|
81
|
+
context 'when client is nil' do
|
82
|
+
before do
|
83
|
+
described_class.client = nil
|
84
|
+
end
|
85
|
+
|
86
|
+
it 'does not raise error' do
|
87
|
+
expect { flipper[:stats].enable(user) }.not_to raise_error
|
88
|
+
end
|
89
|
+
|
90
|
+
it 'does not update metrics' do
|
91
|
+
flipper[:stats].enable(user)
|
92
|
+
expect(socket.buffer).to be_empty
|
93
|
+
end
|
94
|
+
end
|
81
95
|
end
|
@@ -196,6 +196,73 @@ RSpec.describe Flipper::Middleware::Memoizer do
|
|
196
196
|
end
|
197
197
|
end
|
198
198
|
|
199
|
+
context 'with preload block' do
|
200
|
+
let(:app) do
|
201
|
+
app = lambda do |_env|
|
202
|
+
flipper[:stats].enabled?
|
203
|
+
flipper[:stats].enabled?
|
204
|
+
flipper[:shiny].enabled?
|
205
|
+
flipper[:shiny].enabled?
|
206
|
+
[200, {}, []]
|
207
|
+
end
|
208
|
+
|
209
|
+
described_class.new(app, preload: ->(request) {
|
210
|
+
case request.path
|
211
|
+
when "/true"
|
212
|
+
true
|
213
|
+
when "/specific"
|
214
|
+
[:stats]
|
215
|
+
else
|
216
|
+
false
|
217
|
+
end
|
218
|
+
})
|
219
|
+
end
|
220
|
+
|
221
|
+
include_examples 'flipper middleware'
|
222
|
+
|
223
|
+
it 'eagerly caches known features for duration of request if block returns true' do
|
224
|
+
flipper[:stats].enable
|
225
|
+
flipper[:shiny].enable
|
226
|
+
|
227
|
+
# clear the log of operations
|
228
|
+
adapter.reset
|
229
|
+
|
230
|
+
get '/true', {}, 'flipper' => flipper
|
231
|
+
|
232
|
+
expect(adapter.operations.size).to be(1)
|
233
|
+
expect(adapter.count(:get_all)).to be(1)
|
234
|
+
expect(adapter.count(:get)).to be(0)
|
235
|
+
end
|
236
|
+
|
237
|
+
it 'does not eagerly cache known features if block returns false' do
|
238
|
+
flipper[:stats].enable
|
239
|
+
flipper[:shiny].enable
|
240
|
+
|
241
|
+
# clear the log of operations
|
242
|
+
adapter.reset
|
243
|
+
|
244
|
+
get '/false', {}, 'flipper' => flipper
|
245
|
+
|
246
|
+
expect(adapter.operations.size).to be(2)
|
247
|
+
expect(adapter.count(:get_all)).to be(0)
|
248
|
+
expect(adapter.count(:get)).to be(2)
|
249
|
+
end
|
250
|
+
|
251
|
+
it 'eagerly caches specified features for duration of request if block returns array of specified features' do
|
252
|
+
flipper[:stats].enable
|
253
|
+
flipper[:shiny].enable
|
254
|
+
|
255
|
+
# clear the log of operations
|
256
|
+
adapter.reset
|
257
|
+
|
258
|
+
get '/specific', {}, 'flipper' => flipper
|
259
|
+
|
260
|
+
expect(adapter.operations.size).to be(2)
|
261
|
+
expect(adapter.count(:get_multi)).to be(1)
|
262
|
+
expect(adapter.count(:get)).to be(1)
|
263
|
+
end
|
264
|
+
end
|
265
|
+
|
199
266
|
context 'with multiple instances' do
|
200
267
|
let(:app) do
|
201
268
|
# ensure scoped for builder block, annoying...
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'flipper/serializers/gzip'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Serializers::Gzip do
|
4
|
+
it "serializes and deserializes" do
|
5
|
+
serialized = described_class.serialize("my data")
|
6
|
+
expect(described_class.deserialize(serialized)).to eq("my data")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "doesn't fail with nil" do
|
10
|
+
expect(described_class.serialize(nil)).to be(nil)
|
11
|
+
expect(described_class.deserialize(nil)).to be(nil)
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
require 'flipper/serializers/json'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Serializers::Json do
|
4
|
+
it "serializes and deserializes" do
|
5
|
+
serialized = described_class.serialize("my data")
|
6
|
+
expect(described_class.deserialize(serialized)).to eq("my data")
|
7
|
+
end
|
8
|
+
|
9
|
+
it "doesn't fail with nil" do
|
10
|
+
expect(described_class.serialize(nil)).to be(nil)
|
11
|
+
expect(described_class.deserialize(nil)).to be(nil)
|
12
|
+
end
|
13
|
+
end
|
@@ -56,7 +56,7 @@ RSpec.describe Flipper::Typecast do
|
|
56
56
|
nil => 0,
|
57
57
|
'' => 0,
|
58
58
|
0 => 0,
|
59
|
-
0.0 => 0,
|
59
|
+
0.0 => 0.0,
|
60
60
|
1 => 1,
|
61
61
|
1.1 => 1.1,
|
62
62
|
'0.01' => 0.01,
|
@@ -65,9 +65,9 @@ RSpec.describe Flipper::Typecast do
|
|
65
65
|
'99' => 99,
|
66
66
|
'99.9' => 99.9,
|
67
67
|
}.each do |value, expected|
|
68
|
-
context "#
|
68
|
+
context "#to_number for #{value.inspect}" do
|
69
69
|
it "returns #{expected}" do
|
70
|
-
expect(described_class.
|
70
|
+
expect(described_class.to_number(value)).to be(expected)
|
71
71
|
end
|
72
72
|
end
|
73
73
|
end
|
@@ -99,14 +99,14 @@ RSpec.describe Flipper::Typecast do
|
|
99
99
|
|
100
100
|
it 'raises argument error for bad integer percentage' do
|
101
101
|
expect do
|
102
|
-
described_class.
|
103
|
-
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a
|
102
|
+
described_class.to_number(['asdf'])
|
103
|
+
end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
|
104
104
|
end
|
105
105
|
|
106
106
|
it 'raises argument error for bad float percentage' do
|
107
107
|
expect do
|
108
|
-
described_class.
|
109
|
-
end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a
|
108
|
+
described_class.to_number(['asdf.0'])
|
109
|
+
end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a number))
|
110
110
|
end
|
111
111
|
|
112
112
|
it 'raises argument error for set value that cannot be converted to a set' do
|
@@ -127,6 +127,30 @@ RSpec.describe Flipper::Typecast do
|
|
127
127
|
expect(result["search"]).not_to be(hash["search"])
|
128
128
|
end
|
129
129
|
|
130
|
+
it "converts does not convert expressions" do
|
131
|
+
hash = {
|
132
|
+
"search" => {
|
133
|
+
boolean: nil,
|
134
|
+
expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
|
135
|
+
groups: ['a', 'b'],
|
136
|
+
actors: ['User;1'],
|
137
|
+
percentage_of_actors: nil,
|
138
|
+
percentage_of_time: nil,
|
139
|
+
},
|
140
|
+
}
|
141
|
+
result = described_class.features_hash(hash)
|
142
|
+
expect(result).to eq({
|
143
|
+
"search" => {
|
144
|
+
boolean: nil,
|
145
|
+
expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
|
146
|
+
groups: Set['a', 'b'],
|
147
|
+
actors: Set['User;1'],
|
148
|
+
percentage_of_actors: nil,
|
149
|
+
percentage_of_time: nil,
|
150
|
+
},
|
151
|
+
})
|
152
|
+
end
|
153
|
+
|
130
154
|
it "converts gate value arrays to sets" do
|
131
155
|
hash = {
|
132
156
|
"search" => {
|
@@ -193,4 +217,16 @@ RSpec.describe Flipper::Typecast do
|
|
193
217
|
})
|
194
218
|
end
|
195
219
|
end
|
220
|
+
|
221
|
+
it "converts to and from json" do
|
222
|
+
source = {"foo" => "bar"}
|
223
|
+
output = described_class.to_json(source)
|
224
|
+
expect(described_class.from_json(output)).to eq(source)
|
225
|
+
end
|
226
|
+
|
227
|
+
it "converts to and from gzip" do
|
228
|
+
source = "foo bar"
|
229
|
+
output = described_class.to_gzip(source)
|
230
|
+
expect(described_class.from_gzip(output)).to eq(source)
|
231
|
+
end
|
196
232
|
end
|
@@ -11,12 +11,19 @@ RSpec.describe Flipper::Types::Actor do
|
|
11
11
|
attr_reader :flipper_id
|
12
12
|
|
13
13
|
def initialize(flipper_id)
|
14
|
-
@flipper_id = flipper_id
|
14
|
+
@flipper_id = flipper_id.to_s
|
15
15
|
end
|
16
16
|
|
17
17
|
def admin?
|
18
18
|
true
|
19
19
|
end
|
20
|
+
|
21
|
+
def flipper_properties
|
22
|
+
{
|
23
|
+
"flipper_id" => flipper_id,
|
24
|
+
"admin" => admin?,
|
25
|
+
}
|
26
|
+
end
|
20
27
|
end
|
21
28
|
end
|
22
29
|
|
@@ -87,6 +94,15 @@ RSpec.describe Flipper::Types::Actor do
|
|
87
94
|
expect(actor.admin?).to eq(true)
|
88
95
|
end
|
89
96
|
|
97
|
+
it 'proxies flipper_properties to actor' do
|
98
|
+
actor = actor_class.new(10)
|
99
|
+
actor = described_class.new(actor)
|
100
|
+
expect(actor.flipper_properties).to eq({
|
101
|
+
"flipper_id" => "10",
|
102
|
+
"admin" => true,
|
103
|
+
})
|
104
|
+
end
|
105
|
+
|
90
106
|
it 'exposes actor' do
|
91
107
|
actor = actor_class.new(10)
|
92
108
|
actor_type_instance = described_class.new(actor)
|
@@ -104,6 +120,7 @@ RSpec.describe Flipper::Types::Actor do
|
|
104
120
|
actor = actor_class.new(10)
|
105
121
|
actor_type_instance = described_class.new(actor)
|
106
122
|
expect(actor_type_instance.respond_to?(:admin?)).to eq(true)
|
123
|
+
expect(actor_type_instance.respond_to?(:flipper_properties)).to eq(true)
|
107
124
|
end
|
108
125
|
|
109
126
|
it 'returns false if does not respond to method and actor does not respond to method' do
|
@@ -8,17 +8,24 @@ RSpec.describe Flipper do
|
|
8
8
|
let(:dev_group) { flipper.group(:devs) }
|
9
9
|
|
10
10
|
let(:admin_actor) do
|
11
|
-
double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false
|
11
|
+
double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false, flipper_properties: {"admin" => true, "dev" => false}
|
12
12
|
end
|
13
13
|
let(:dev_actor) do
|
14
|
-
double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true
|
14
|
+
double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true, flipper_properties: {"admin" => false, "dev" => true}
|
15
15
|
end
|
16
16
|
|
17
17
|
let(:admin_truthy_actor) do
|
18
|
-
double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false
|
18
|
+
double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false, flipper_properties: {"admin" => "true-ish", "dev" => false}
|
19
19
|
end
|
20
20
|
let(:admin_falsey_actor) do
|
21
|
-
double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false
|
21
|
+
double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false, flipper_properties: {"admin" => nil, "dev" => false}
|
22
|
+
end
|
23
|
+
|
24
|
+
let(:basic_plan_actor) do
|
25
|
+
double 'Non Flipper Thing', flipper_id: 1, flipper_properties: {"plan" => "basic"}
|
26
|
+
end
|
27
|
+
let(:premium_plan_actor) do
|
28
|
+
double 'Non Flipper Thing', flipper_id: 10, flipper_properties: {"plan" => "premium"}
|
22
29
|
end
|
23
30
|
|
24
31
|
let(:pitt) { Flipper::Actor.new(1) }
|
@@ -70,10 +77,12 @@ RSpec.describe Flipper do
|
|
70
77
|
|
71
78
|
it 'enables feature for flipper actor in group' do
|
72
79
|
expect(feature.enabled?(Flipper::Types::Actor.new(admin_actor))).to eq(true)
|
80
|
+
expect(feature.enabled?(admin_actor)).to eq(true)
|
73
81
|
end
|
74
82
|
|
75
83
|
it 'does not enable for flipper actor not in group' do
|
76
84
|
expect(feature.enabled?(Flipper::Types::Actor.new(dev_actor))).to eq(false)
|
85
|
+
expect(feature.enabled?(dev_actor)).to eq(false)
|
77
86
|
end
|
78
87
|
|
79
88
|
it 'does not enable feature for all' do
|
@@ -257,10 +266,12 @@ RSpec.describe Flipper do
|
|
257
266
|
|
258
267
|
it 'disables feature for flipper actor in group' do
|
259
268
|
expect(feature.enabled?(Flipper::Types::Actor.new(admin_actor))).to eq(false)
|
269
|
+
expect(feature.enabled?(admin_actor)).to eq(false)
|
260
270
|
end
|
261
271
|
|
262
272
|
it 'does not disable feature for flipper actor in other groups' do
|
263
273
|
expect(feature.enabled?(Flipper::Types::Actor.new(dev_actor))).to eq(true)
|
274
|
+
expect(feature.enabled?(dev_actor)).to eq(true)
|
264
275
|
end
|
265
276
|
|
266
277
|
it 'adds feature to set of features' do
|
@@ -379,6 +390,7 @@ RSpec.describe Flipper do
|
|
379
390
|
|
380
391
|
it 'returns true for truthy block values' do
|
381
392
|
expect(feature.enabled?(Flipper::Types::Actor.new(admin_truthy_actor))).to eq(true)
|
393
|
+
expect(feature.enabled?(admin_truthy_actor)).to eq(true)
|
382
394
|
end
|
383
395
|
|
384
396
|
it 'returns true if any actor is in enabled group' do
|
@@ -394,6 +406,7 @@ RSpec.describe Flipper do
|
|
394
406
|
|
395
407
|
it 'returns false for falsey block values' do
|
396
408
|
expect(feature.enabled?(Flipper::Types::Actor.new(admin_falsey_actor))).to eq(false)
|
409
|
+
expect(feature.enabled?(admin_falsey_actor)).to eq(false)
|
397
410
|
end
|
398
411
|
end
|
399
412
|
|
@@ -549,4 +562,89 @@ RSpec.describe Flipper do
|
|
549
562
|
expect(feature.enabled?(dev_actor)).to eq(false)
|
550
563
|
end
|
551
564
|
end
|
565
|
+
|
566
|
+
context "for expression" do
|
567
|
+
it "works" do
|
568
|
+
feature.enable Flipper.property(:plan).eq("basic")
|
569
|
+
|
570
|
+
expect(feature.enabled?).to be(false)
|
571
|
+
expect(feature.enabled?(basic_plan_actor)).to be(true)
|
572
|
+
expect(feature.enabled?(premium_plan_actor)).to be(false)
|
573
|
+
expect(feature.enabled?(admin_actor)).to be(false)
|
574
|
+
end
|
575
|
+
|
576
|
+
it "works for true expression with no actor" do
|
577
|
+
feature.enable Flipper.boolean(true)
|
578
|
+
expect(feature.enabled?).to be(true)
|
579
|
+
end
|
580
|
+
|
581
|
+
it "works for multiple actors" do
|
582
|
+
feature.enable Flipper.property(:plan).eq("basic")
|
583
|
+
|
584
|
+
expect(feature.enabled?(basic_plan_actor, premium_plan_actor)).to be(true)
|
585
|
+
expect(feature.enabled?(premium_plan_actor, basic_plan_actor)).to be(true)
|
586
|
+
expect(feature.enabled?(premium_plan_actor, admin_actor)).to be(false)
|
587
|
+
end
|
588
|
+
end
|
589
|
+
|
590
|
+
context "for Any" do
|
591
|
+
it "works" do
|
592
|
+
expression = Flipper.any(
|
593
|
+
Flipper.property(:plan).eq("basic"),
|
594
|
+
Flipper.property(:plan).eq("plus"),
|
595
|
+
)
|
596
|
+
feature.enable expression
|
597
|
+
|
598
|
+
expect(feature.enabled?(basic_plan_actor)).to be(true)
|
599
|
+
expect(feature.enabled?(premium_plan_actor)).to be(false)
|
600
|
+
end
|
601
|
+
end
|
602
|
+
|
603
|
+
context "for All" do
|
604
|
+
it "works" do
|
605
|
+
true_actor = Flipper::Actor.new("User;1", {
|
606
|
+
"plan" => "basic",
|
607
|
+
"age" => 21,
|
608
|
+
})
|
609
|
+
false_actor = Flipper::Actor.new("User;1", {
|
610
|
+
"plan" => "basic",
|
611
|
+
"age" => 20,
|
612
|
+
})
|
613
|
+
expression = Flipper.all(
|
614
|
+
Flipper.property(:plan).eq("basic"),
|
615
|
+
Flipper.property(:age).eq(21)
|
616
|
+
)
|
617
|
+
feature.enable expression
|
618
|
+
|
619
|
+
expect(feature.enabled?(true_actor)).to be(true)
|
620
|
+
expect(feature.enabled?(false_actor)).to be(false)
|
621
|
+
end
|
622
|
+
|
623
|
+
it "works when nested" do
|
624
|
+
admin_actor = Flipper::Actor.new("User;1", {
|
625
|
+
"admin" => true,
|
626
|
+
})
|
627
|
+
true_actor = Flipper::Actor.new("User;1", {
|
628
|
+
"plan" => "basic",
|
629
|
+
"age" => 21,
|
630
|
+
})
|
631
|
+
false_actor = Flipper::Actor.new("User;1", {
|
632
|
+
"plan" => "basic",
|
633
|
+
"age" => 20,
|
634
|
+
})
|
635
|
+
expression = Flipper.any(
|
636
|
+
Flipper.property(:admin).eq(true),
|
637
|
+
Flipper.all(
|
638
|
+
Flipper.property(:plan).eq("basic"),
|
639
|
+
Flipper.property(:age).eq(21)
|
640
|
+
)
|
641
|
+
)
|
642
|
+
|
643
|
+
feature.enable expression
|
644
|
+
|
645
|
+
expect(feature.enabled?(admin_actor)).to be(true)
|
646
|
+
expect(feature.enabled?(true_actor)).to be(true)
|
647
|
+
expect(feature.enabled?(false_actor)).to be(false)
|
648
|
+
end
|
649
|
+
end
|
552
650
|
end
|
data/spec/flipper_spec.rb
CHANGED
@@ -64,7 +64,12 @@ RSpec.describe Flipper do
|
|
64
64
|
|
65
65
|
describe "delegation to instance" do
|
66
66
|
let(:group) { Flipper::Types::Group.new(:admins) }
|
67
|
-
let(:actor) {
|
67
|
+
let(:actor) {
|
68
|
+
Flipper::Actor.new("1", {
|
69
|
+
"plan" => "basic",
|
70
|
+
})
|
71
|
+
}
|
72
|
+
let(:expression) { Flipper.property(:plan).eq("basic") }
|
68
73
|
|
69
74
|
before do
|
70
75
|
described_class.configure do |config|
|
@@ -88,6 +93,37 @@ RSpec.describe Flipper do
|
|
88
93
|
expect(described_class.instance.enabled?(:search)).to be(false)
|
89
94
|
end
|
90
95
|
|
96
|
+
it 'delegates expression to instance' do
|
97
|
+
expect(described_class.expression(:search)).to be(nil)
|
98
|
+
|
99
|
+
expression = Flipper.property(:plan).eq("basic")
|
100
|
+
Flipper.instance.enable_expression :search, expression
|
101
|
+
|
102
|
+
expect(described_class.expression(:search)).to eq(expression)
|
103
|
+
end
|
104
|
+
|
105
|
+
it 'delegates enable_expression to instance' do
|
106
|
+
described_class.enable_expression(:search, expression)
|
107
|
+
expect(described_class.instance.enabled?(:search, actor)).to be(true)
|
108
|
+
end
|
109
|
+
|
110
|
+
it 'delegates disable_expression to instance' do
|
111
|
+
described_class.disable_expression(:search)
|
112
|
+
expect(described_class.instance.enabled?(:search, actor)).to be(false)
|
113
|
+
end
|
114
|
+
|
115
|
+
it 'delegates add_expression to instance' do
|
116
|
+
described_class.add_expression(:search, expression)
|
117
|
+
expect(described_class.instance.enabled?(:search, actor)).to be(true)
|
118
|
+
end
|
119
|
+
|
120
|
+
it 'delegates remove_expression to instance' do
|
121
|
+
described_class.enable_expression(:search, Flipper.any(expression))
|
122
|
+
expect(described_class.instance.enabled?(:search, actor)).to be(true)
|
123
|
+
described_class.remove_expression(:search, expression)
|
124
|
+
expect(described_class.instance.enabled?(:search, actor)).to be(false)
|
125
|
+
end
|
126
|
+
|
91
127
|
it 'delegates enable_actor to instance' do
|
92
128
|
described_class.enable_actor(:search, actor)
|
93
129
|
expect(described_class.instance.enabled?(:search, actor)).to be(true)
|
@@ -192,6 +228,10 @@ RSpec.describe Flipper do
|
|
192
228
|
expect(described_class.memoizing?).to eq(described_class.adapter.memoizing?)
|
193
229
|
end
|
194
230
|
|
231
|
+
it 'delegates read_only? to instance' do
|
232
|
+
expect(described_class.read_only?).to eq(described_class.adapter.read_only?)
|
233
|
+
end
|
234
|
+
|
195
235
|
it 'delegates sync stuff to instance and does nothing' do
|
196
236
|
expect(described_class.sync).to be(nil)
|
197
237
|
expect(described_class.sync_secret).to be(nil)
|
@@ -326,4 +366,52 @@ RSpec.describe Flipper do
|
|
326
366
|
expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
|
327
367
|
end
|
328
368
|
end
|
369
|
+
|
370
|
+
describe ".constant" do
|
371
|
+
it "returns Flipper::Expression::Constant instance" do
|
372
|
+
expect(described_class.constant(false)).to eq(Flipper::Expression::Constant.new(false))
|
373
|
+
expect(described_class.constant("string")).to eq(Flipper::Expression::Constant.new("string"))
|
374
|
+
end
|
375
|
+
end
|
376
|
+
|
377
|
+
describe ".property" do
|
378
|
+
it "returns Flipper::Expressions::Property expression" do
|
379
|
+
expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name"))
|
380
|
+
end
|
381
|
+
end
|
382
|
+
|
383
|
+
describe ".boolean" do
|
384
|
+
it "returns Flipper::Expressions::Boolean expression" do
|
385
|
+
expect(described_class.boolean(true)).to eq(Flipper::Expression.build(Boolean: true))
|
386
|
+
expect(described_class.boolean(false)).to eq(Flipper::Expression.build(Boolean: false))
|
387
|
+
end
|
388
|
+
end
|
389
|
+
|
390
|
+
describe ".random" do
|
391
|
+
it "returns Flipper::Expressions::Random expression" do
|
392
|
+
expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100))
|
393
|
+
end
|
394
|
+
end
|
395
|
+
|
396
|
+
describe ".any" do
|
397
|
+
let(:age_expression) { Flipper.property(:age).gte(21) }
|
398
|
+
let(:plan_expression) { Flipper.property(:plan).eq("basic") }
|
399
|
+
|
400
|
+
it "returns Flipper::Expressions::Any instance" do
|
401
|
+
expect(Flipper.any(age_expression, plan_expression)).to eq(
|
402
|
+
Flipper::Expression.build({Any: [age_expression, plan_expression]})
|
403
|
+
)
|
404
|
+
end
|
405
|
+
end
|
406
|
+
|
407
|
+
describe ".all" do
|
408
|
+
let(:age_expression) { Flipper.property(:age).gte(21) }
|
409
|
+
let(:plan_expression) { Flipper.property(:plan).eq("basic") }
|
410
|
+
|
411
|
+
it "returns Flipper::Expressions::All instance" do
|
412
|
+
expect(Flipper.all(age_expression, plan_expression)).to eq(
|
413
|
+
Flipper::Expression.build({All: [age_expression, plan_expression]})
|
414
|
+
)
|
415
|
+
end
|
416
|
+
end
|
329
417
|
end
|