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
data/spec/flipper/engine_spec.rb
CHANGED
@@ -6,7 +6,7 @@ RSpec.describe Flipper::Engine do
|
|
6
6
|
Class.new(Rails::Application) do
|
7
7
|
config.eager_load = false
|
8
8
|
config.logger = ActiveSupport::Logger.new($stdout)
|
9
|
-
end
|
9
|
+
end.instance
|
10
10
|
end
|
11
11
|
|
12
12
|
before do
|
@@ -15,11 +15,70 @@ RSpec.describe Flipper::Engine do
|
|
15
15
|
ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
|
16
16
|
end
|
17
17
|
|
18
|
+
# Reset Rails.env around each example
|
19
|
+
around do |example|
|
20
|
+
begin
|
21
|
+
env = Rails.env.to_s
|
22
|
+
example.run
|
23
|
+
ensure
|
24
|
+
Rails.env = env
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
18
28
|
let(:config) { application.config.flipper }
|
19
29
|
|
20
30
|
subject { application.initialize! }
|
21
31
|
|
32
|
+
shared_examples 'config.strict' do
|
33
|
+
let(:adapter) { Flipper.adapter.adapter }
|
34
|
+
|
35
|
+
it 'can set strict=true from ENV' do
|
36
|
+
with_env 'FLIPPER_STRICT' => 'true' do
|
37
|
+
subject
|
38
|
+
expect(config.strict).to eq(:raise)
|
39
|
+
expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
it 'can set strict=warn from ENV' do
|
44
|
+
with_env 'FLIPPER_STRICT' => 'warn' do
|
45
|
+
subject
|
46
|
+
expect(config.strict).to eq(:warn)
|
47
|
+
expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
|
48
|
+
expect(adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
it 'can set strict=false from ENV' do
|
53
|
+
with_env 'FLIPPER_STRICT' => 'false' do
|
54
|
+
subject
|
55
|
+
expect(config.strict).to eq(false)
|
56
|
+
expect(adapter).to be_instance_of(Flipper::Adapters::Memory)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
it "defaults to strict=false in RAILS_ENV=production" do
|
61
|
+
Rails.env = "production"
|
62
|
+
subject
|
63
|
+
expect(config.strict).to eq(false)
|
64
|
+
expect(adapter).to be_instance_of(Flipper::Adapters::Memory)
|
65
|
+
end
|
66
|
+
|
67
|
+
%w(development test).each do |env|
|
68
|
+
it "defaults to strict=warn in RAILS_ENV=#{env}" do
|
69
|
+
Rails.env = env
|
70
|
+
expect(Rails.env).to eq(env)
|
71
|
+
subject
|
72
|
+
expect(config.strict).to eq(:warn)
|
73
|
+
expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
|
74
|
+
expect(adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
22
79
|
context 'cloudless' do
|
80
|
+
it_behaves_like 'config.strict'
|
81
|
+
|
23
82
|
it 'can set env_key from ENV' do
|
24
83
|
with_env 'FLIPPER_ENV_KEY' => 'flopper' do
|
25
84
|
subject
|
@@ -94,12 +153,6 @@ RSpec.describe Flipper::Engine do
|
|
94
153
|
if: nil
|
95
154
|
})
|
96
155
|
end
|
97
|
-
|
98
|
-
it "defines #flipper_id on AR::Base" do
|
99
|
-
subject
|
100
|
-
require 'active_record'
|
101
|
-
expect(ActiveRecord::Base.ancestors).to include(Flipper::Identifier)
|
102
|
-
end
|
103
156
|
end
|
104
157
|
|
105
158
|
context 'with cloud' do
|
@@ -112,6 +165,14 @@ RSpec.describe Flipper::Engine do
|
|
112
165
|
# App for Rack::Test
|
113
166
|
let(:app) { application.routes }
|
114
167
|
|
168
|
+
it_behaves_like 'config.strict' do
|
169
|
+
let(:adapter) do
|
170
|
+
dual_write = Flipper.adapter.adapter
|
171
|
+
poll = dual_write.local
|
172
|
+
poll.adapter
|
173
|
+
end
|
174
|
+
end
|
175
|
+
|
115
176
|
it "initializes cloud configuration" do
|
116
177
|
stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
|
117
178
|
|
@@ -181,6 +242,46 @@ RSpec.describe Flipper::Engine do
|
|
181
242
|
end
|
182
243
|
end
|
183
244
|
|
245
|
+
context 'with cloud secrets in Rails.credentials' do
|
246
|
+
around do |example|
|
247
|
+
# Create temporary directory for Rails.root to write credentials to
|
248
|
+
# Once Rails 5.2 support is dropped, this can all be replaced with
|
249
|
+
# `config.credentials.content_path = Tempfile.new.path`
|
250
|
+
Dir.mktmpdir do |dir|
|
251
|
+
Dir.chdir(dir) do
|
252
|
+
Dir.mkdir("#{dir}/config")
|
253
|
+
|
254
|
+
example.run
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
before do
|
260
|
+
# Set master key which is needed to write credentials
|
261
|
+
ENV["RAILS_MASTER_KEY"] = "a" * 32
|
262
|
+
|
263
|
+
application.credentials.write(YAML.dump({
|
264
|
+
flipper: {
|
265
|
+
cloud_token: "credentials-token",
|
266
|
+
cloud_sync_secret: "credentials-secret",
|
267
|
+
}
|
268
|
+
}))
|
269
|
+
end
|
270
|
+
|
271
|
+
it "enables cloud" do
|
272
|
+
application.initialize!
|
273
|
+
expect(ENV["FLIPPER_CLOUD_TOKEN"]).to eq("credentials-token")
|
274
|
+
expect(ENV["FLIPPER_CLOUD_SYNC_SECRET"]).to eq("credentials-secret")
|
275
|
+
expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
it "includes model methods" do
|
280
|
+
subject
|
281
|
+
require 'active_record'
|
282
|
+
expect(ActiveRecord::Base.ancestors).to include(Flipper::Model::ActiveRecord)
|
283
|
+
end
|
284
|
+
|
184
285
|
# Add app initializer in the same order as config/initializers/*
|
185
286
|
def initializer(&block)
|
186
287
|
application.initializer 'spec', before: :load_config_initializers do
|
@@ -25,9 +25,9 @@ RSpec.describe Flipper::Exporters::Json::V1 do
|
|
25
25
|
export = subject.call(adapter)
|
26
26
|
|
27
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"},
|
28
|
+
"google_analytics" => {actors: Set.new, boolean: nil, expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
29
|
+
"plausible" => {actors: Set.new, boolean: "true", expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
|
30
|
+
"search" => {actors: Set["User;1", "User;100"], boolean: nil, expression: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
|
31
31
|
})
|
32
32
|
end
|
33
33
|
end
|
@@ -0,0 +1,248 @@
|
|
1
|
+
RSpec.describe Flipper::Expression::Builder do
|
2
|
+
def build(object)
|
3
|
+
Flipper::Expression.build(object)
|
4
|
+
end
|
5
|
+
|
6
|
+
describe "#add" do
|
7
|
+
it "converts to Any and adds new expressions" do
|
8
|
+
expression = build("something")
|
9
|
+
first = Flipper.boolean(true).eq(true)
|
10
|
+
second = Flipper.boolean(false).eq(false)
|
11
|
+
new_expression = expression.add(first, second)
|
12
|
+
expect(new_expression).to eq(build({ Any: ["something", first, second] }))
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
describe "#remove" do
|
17
|
+
it "converts to Any and removes any expressions that match" do
|
18
|
+
expression = build("something")
|
19
|
+
first = Flipper.boolean(true).eq(true)
|
20
|
+
second = Flipper.boolean(false).eq(false)
|
21
|
+
new_expression = expression.remove(build("something"), first, second)
|
22
|
+
expect(new_expression).to eq(build(Any: []))
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
it "can convert to Any" do
|
27
|
+
expression = build("something")
|
28
|
+
converted = expression.any
|
29
|
+
expect(converted).to be_instance_of(Flipper::Expression)
|
30
|
+
expect(converted.function).to be(Flipper::Expressions::Any)
|
31
|
+
expect(converted.args).to eq([expression])
|
32
|
+
end
|
33
|
+
|
34
|
+
it "can convert to All" do
|
35
|
+
expression = build("something")
|
36
|
+
converted = expression.all
|
37
|
+
expect(converted).to eq(build(All: ["something"]))
|
38
|
+
end
|
39
|
+
|
40
|
+
context "Any" do
|
41
|
+
describe "#any" do
|
42
|
+
it "returns self" do
|
43
|
+
expression = build(Any: [
|
44
|
+
Flipper.boolean(true),
|
45
|
+
Flipper.string("yep").eq("yep"),
|
46
|
+
])
|
47
|
+
expect(expression.any).to be(expression)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
describe "#add" do
|
52
|
+
it "returns new instance with expression added" do
|
53
|
+
expression = Flipper.boolean(true)
|
54
|
+
other = Flipper.string("yep").eq("yep")
|
55
|
+
|
56
|
+
result = expression.add(other)
|
57
|
+
expect(result.args).to eq([
|
58
|
+
Flipper.boolean(true),
|
59
|
+
Flipper.string("yep").eq("yep"),
|
60
|
+
])
|
61
|
+
end
|
62
|
+
|
63
|
+
it "returns new instance with many expressions added" do
|
64
|
+
expression = Flipper.boolean(true)
|
65
|
+
second = Flipper.string("yep").eq("yep")
|
66
|
+
third = Flipper.number(1).lte(20)
|
67
|
+
|
68
|
+
result = expression.add(second, third)
|
69
|
+
expect(result.args).to eq([
|
70
|
+
Flipper.boolean(true),
|
71
|
+
Flipper.string("yep").eq("yep"),
|
72
|
+
Flipper.number(1).lte(20),
|
73
|
+
])
|
74
|
+
end
|
75
|
+
|
76
|
+
it "returns new instance with array of expressions added" do
|
77
|
+
expression = Flipper.boolean(true)
|
78
|
+
second = Flipper.string("yep").eq("yep")
|
79
|
+
third = Flipper.number(1).lte(20)
|
80
|
+
|
81
|
+
result = expression.add([second, third])
|
82
|
+
expect(result.args).to eq([
|
83
|
+
Flipper.boolean(true),
|
84
|
+
Flipper.string("yep").eq("yep"),
|
85
|
+
Flipper.number(1).lte(20),
|
86
|
+
])
|
87
|
+
end
|
88
|
+
end
|
89
|
+
|
90
|
+
describe "#remove" do
|
91
|
+
it "returns new instance with expression removed" do
|
92
|
+
first = Flipper.boolean(true)
|
93
|
+
second = Flipper.string("yep").eq("yep")
|
94
|
+
third = Flipper.number(1).lte(20)
|
95
|
+
expression = Flipper.any([first, second, third])
|
96
|
+
|
97
|
+
result = expression.remove(second)
|
98
|
+
expect(expression.args).to eq([first, second, third])
|
99
|
+
expect(result.args).to eq([first, third])
|
100
|
+
end
|
101
|
+
|
102
|
+
it "returns new instance with many expressions removed" do
|
103
|
+
first = Flipper.boolean(true)
|
104
|
+
second = Flipper.string("yep").eq("yep")
|
105
|
+
third = Flipper.number(1).lte(20)
|
106
|
+
expression = Flipper.any([first, second, third])
|
107
|
+
|
108
|
+
result = expression.remove(second, third)
|
109
|
+
expect(expression.args).to eq([first, second, third])
|
110
|
+
expect(result.args).to eq([first])
|
111
|
+
end
|
112
|
+
|
113
|
+
it "returns new instance with array of expressions removed" do
|
114
|
+
first = Flipper.boolean(true)
|
115
|
+
second = Flipper.string("yep").eq("yep")
|
116
|
+
third = Flipper.number(1).lte(20)
|
117
|
+
expression = Flipper.any([first, second, third])
|
118
|
+
|
119
|
+
result = expression.remove([second, third])
|
120
|
+
expect(expression.args).to eq([first, second, third])
|
121
|
+
expect(result.args).to eq([first])
|
122
|
+
end
|
123
|
+
end
|
124
|
+
end
|
125
|
+
|
126
|
+
[
|
127
|
+
[2, 3, "equal", "eq", :Equal],
|
128
|
+
[2, 3, "not_equal", "neq", :NotEqual],
|
129
|
+
[2, 3, "greater_than", "gt", :GreaterThan],
|
130
|
+
[2, 3, "greater_than_or_equal_to", "gte", :GreaterThanOrEqualTo],
|
131
|
+
[2, 3, "greater_than_or_equal_to", "greater_than_or_equal", :GreaterThanOrEqualTo],
|
132
|
+
[2, 3, "less_than", "lt", :LessThan],
|
133
|
+
[2, 3, "less_than_or_equal_to", "lte", :LessThanOrEqualTo],
|
134
|
+
[2, 3, "less_than_or_equal_to", "less_than_or_equal", :LessThanOrEqualTo],
|
135
|
+
].each do |(left, right, method_name, shortcut_name, function)|
|
136
|
+
it "can convert to #{function}" do
|
137
|
+
expression = build(left)
|
138
|
+
other = build(right)
|
139
|
+
converted = expression.send(method_name, other)
|
140
|
+
expect(converted).to eq(build({ function => [ left, right] }))
|
141
|
+
end
|
142
|
+
|
143
|
+
it "can convert to #{function} using #{shortcut_name}" do
|
144
|
+
expression = build(left)
|
145
|
+
other = build(right)
|
146
|
+
converted = expression.send(shortcut_name, other)
|
147
|
+
expect(converted).to eq(build({ function => [ left, right] }))
|
148
|
+
end
|
149
|
+
|
150
|
+
it "builds args into expressions when converting to #{function}" do
|
151
|
+
expression = build(left)
|
152
|
+
other = Flipper.property(:age)
|
153
|
+
converted = expression.send(method_name, other.value)
|
154
|
+
expect(converted).to eq(build({ function => [ left, other.value] }))
|
155
|
+
end
|
156
|
+
end
|
157
|
+
|
158
|
+
it "can convert to PercentageOfActors" do
|
159
|
+
expression = Flipper.constant("User;1").percentage_of_actors(40)
|
160
|
+
expect(expression).to eq(build({ PercentageOfActors: [ "User;1", 40 ] }))
|
161
|
+
end
|
162
|
+
|
163
|
+
context "All" do
|
164
|
+
describe "#all" do
|
165
|
+
it "returns self" do
|
166
|
+
expression = Flipper.all([
|
167
|
+
Flipper.boolean(true),
|
168
|
+
Flipper.string("yep").eq("yep"),
|
169
|
+
])
|
170
|
+
expect(expression.all).to be(expression)
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
describe "#add" do
|
175
|
+
it "returns new instance with expression added" do
|
176
|
+
expression = Flipper.all([Flipper.boolean(true)])
|
177
|
+
other = Flipper.string("yep").eq("yep")
|
178
|
+
|
179
|
+
result = expression.add(other)
|
180
|
+
expect(result.args).to eq([
|
181
|
+
Flipper.boolean(true),
|
182
|
+
Flipper.string("yep").eq("yep"),
|
183
|
+
])
|
184
|
+
end
|
185
|
+
|
186
|
+
it "returns new instance with many expressions added" do
|
187
|
+
expression = Flipper.all([Flipper.boolean(true)])
|
188
|
+
second = Flipper.string("yep").eq("yep")
|
189
|
+
third = Flipper.number(1).lte(20)
|
190
|
+
|
191
|
+
result = expression.add(second, third)
|
192
|
+
expect(result.args).to eq([
|
193
|
+
Flipper.boolean(true),
|
194
|
+
Flipper.string("yep").eq("yep"),
|
195
|
+
Flipper.number(1).lte(20),
|
196
|
+
])
|
197
|
+
end
|
198
|
+
|
199
|
+
it "returns new instance with array of expressions added" do
|
200
|
+
expression = Flipper.all([Flipper.boolean(true)])
|
201
|
+
second = Flipper.string("yep").eq("yep")
|
202
|
+
third = Flipper.number(1).lte(20)
|
203
|
+
|
204
|
+
result = expression.add([second, third])
|
205
|
+
expect(result.args).to eq([
|
206
|
+
Flipper.boolean(true),
|
207
|
+
Flipper.string("yep").eq("yep"),
|
208
|
+
Flipper.number(1).lte(20),
|
209
|
+
])
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
describe "#remove" do
|
214
|
+
it "returns new instance with expression removed" do
|
215
|
+
first = Flipper.boolean(true)
|
216
|
+
second = Flipper.string("yep").eq("yep")
|
217
|
+
third = Flipper.number(1).lte(20)
|
218
|
+
expression = Flipper.all([first, second, third])
|
219
|
+
|
220
|
+
result = expression.remove(second)
|
221
|
+
expect(expression.args).to eq([first, second, third])
|
222
|
+
expect(result.args).to eq([first, third])
|
223
|
+
end
|
224
|
+
|
225
|
+
it "returns new instance with many expressions removed" do
|
226
|
+
first = Flipper.boolean(true)
|
227
|
+
second = Flipper.string("yep").eq("yep")
|
228
|
+
third = Flipper.number(1).lte(20)
|
229
|
+
expression = Flipper.all([first, second, third])
|
230
|
+
|
231
|
+
result = expression.remove(second, third)
|
232
|
+
expect(expression.args).to eq([first, second, third])
|
233
|
+
expect(result.args).to eq([first])
|
234
|
+
end
|
235
|
+
|
236
|
+
it "returns new instance with array of expressions removed" do
|
237
|
+
first = Flipper.boolean(true)
|
238
|
+
second = Flipper.string("yep").eq("yep")
|
239
|
+
third = Flipper.number(1).lte(20)
|
240
|
+
expression = Flipper.all([first, second, third])
|
241
|
+
|
242
|
+
result = expression.remove([second, third])
|
243
|
+
expect(expression.args).to eq([first, second, third])
|
244
|
+
expect(result.args).to eq([first])
|
245
|
+
end
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
@@ -0,0 +1,188 @@
|
|
1
|
+
require 'flipper/expression'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::Expression do
|
4
|
+
describe "#build" do
|
5
|
+
it "can build Equal" do
|
6
|
+
expression = described_class.build({
|
7
|
+
"Equal" => [
|
8
|
+
"basic",
|
9
|
+
"basic",
|
10
|
+
]
|
11
|
+
})
|
12
|
+
|
13
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
14
|
+
expect(expression.function).to be(Flipper::Expressions::Equal)
|
15
|
+
expect(expression.args).to eq([
|
16
|
+
Flipper.constant("basic"),
|
17
|
+
Flipper.constant("basic"),
|
18
|
+
])
|
19
|
+
end
|
20
|
+
|
21
|
+
it "can build GreaterThanOrEqualTo" do
|
22
|
+
expression = described_class.build({
|
23
|
+
"GreaterThanOrEqualTo" => [
|
24
|
+
2,
|
25
|
+
1,
|
26
|
+
]
|
27
|
+
})
|
28
|
+
|
29
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
30
|
+
expect(expression.function).to be(Flipper::Expressions::GreaterThanOrEqualTo)
|
31
|
+
expect(expression.args).to eq([
|
32
|
+
Flipper.constant(2),
|
33
|
+
Flipper.constant(1),
|
34
|
+
])
|
35
|
+
end
|
36
|
+
|
37
|
+
it "can build GreaterThan" do
|
38
|
+
expression = described_class.build({
|
39
|
+
"GreaterThan" => [
|
40
|
+
2,
|
41
|
+
1,
|
42
|
+
]
|
43
|
+
})
|
44
|
+
|
45
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
46
|
+
expect(expression.function).to be(Flipper::Expressions::GreaterThan)
|
47
|
+
expect(expression.args).to eq([
|
48
|
+
Flipper.constant(2),
|
49
|
+
Flipper.constant(1),
|
50
|
+
])
|
51
|
+
end
|
52
|
+
|
53
|
+
it "can build LessThanOrEqualTo" do
|
54
|
+
expression = described_class.build({
|
55
|
+
"LessThanOrEqualTo" => [
|
56
|
+
2,
|
57
|
+
1,
|
58
|
+
]
|
59
|
+
})
|
60
|
+
|
61
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
62
|
+
expect(expression.function).to be(Flipper::Expressions::LessThanOrEqualTo)
|
63
|
+
expect(expression.args).to eq([
|
64
|
+
Flipper.constant(2),
|
65
|
+
Flipper.constant(1),
|
66
|
+
])
|
67
|
+
end
|
68
|
+
|
69
|
+
it "can build LessThan" do
|
70
|
+
expression = described_class.build({
|
71
|
+
"LessThan" => [2, 1]
|
72
|
+
})
|
73
|
+
|
74
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
75
|
+
expect(expression.function).to be(Flipper::Expressions::LessThan)
|
76
|
+
expect(expression.args).to eq([
|
77
|
+
Flipper.constant(2),
|
78
|
+
Flipper.constant(1),
|
79
|
+
])
|
80
|
+
end
|
81
|
+
|
82
|
+
it "can build NotEqual" do
|
83
|
+
expression = described_class.build({
|
84
|
+
"NotEqual" => [
|
85
|
+
"basic",
|
86
|
+
"plus",
|
87
|
+
]
|
88
|
+
})
|
89
|
+
|
90
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
91
|
+
expect(expression.function).to be(Flipper::Expressions::NotEqual)
|
92
|
+
expect(expression.args).to eq([
|
93
|
+
Flipper.constant("basic"),
|
94
|
+
Flipper.constant("plus"),
|
95
|
+
])
|
96
|
+
end
|
97
|
+
|
98
|
+
it "can build Number" do
|
99
|
+
expression = described_class.build(1)
|
100
|
+
|
101
|
+
expect(expression).to be_instance_of(Flipper::Expression::Constant)
|
102
|
+
expect(expression.value).to eq(1)
|
103
|
+
end
|
104
|
+
|
105
|
+
it "can build Percentage" do
|
106
|
+
expression = described_class.build({
|
107
|
+
"Percentage" => [1]
|
108
|
+
})
|
109
|
+
|
110
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
111
|
+
expect(expression.function).to be(Flipper::Expressions::Percentage)
|
112
|
+
expect(expression.args).to eq([Flipper.constant(1)])
|
113
|
+
end
|
114
|
+
|
115
|
+
it "can build PercentageOfActors" do
|
116
|
+
expression = described_class.build({
|
117
|
+
"PercentageOfActors" => [
|
118
|
+
"User;1",
|
119
|
+
40,
|
120
|
+
]
|
121
|
+
})
|
122
|
+
|
123
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
124
|
+
expect(expression.function).to be(Flipper::Expressions::PercentageOfActors)
|
125
|
+
expect(expression.args).to eq([
|
126
|
+
Flipper.constant("User;1"),
|
127
|
+
Flipper.constant(40),
|
128
|
+
])
|
129
|
+
end
|
130
|
+
|
131
|
+
it "can build String" do
|
132
|
+
expression = described_class.build("basic")
|
133
|
+
|
134
|
+
expect(expression).to be_instance_of(Flipper::Expression::Constant)
|
135
|
+
expect(expression.value).to eq("basic")
|
136
|
+
end
|
137
|
+
|
138
|
+
it "can build Property" do
|
139
|
+
expression = described_class.build({
|
140
|
+
"Property" => ["flipper_id"]
|
141
|
+
})
|
142
|
+
|
143
|
+
expect(expression).to be_instance_of(Flipper::Expression)
|
144
|
+
expect(expression.function).to be(Flipper::Expressions::Property)
|
145
|
+
expect(expression.args).to eq([Flipper.constant("flipper_id")])
|
146
|
+
end
|
147
|
+
end
|
148
|
+
|
149
|
+
describe "#eql?" do
|
150
|
+
it "returns true for same class and args" do
|
151
|
+
expression = described_class.build("foo")
|
152
|
+
other = described_class.build("foo")
|
153
|
+
expect(expression.eql?(other)).to be(true)
|
154
|
+
end
|
155
|
+
|
156
|
+
it "returns false for different class" do
|
157
|
+
expression = described_class.build("foo")
|
158
|
+
other = Object.new
|
159
|
+
expect(expression.eql?(other)).to be(false)
|
160
|
+
end
|
161
|
+
|
162
|
+
it "returns false for different args" do
|
163
|
+
expression = described_class.build("foo")
|
164
|
+
other = described_class.build("bar")
|
165
|
+
expect(expression.eql?(other)).to be(false)
|
166
|
+
end
|
167
|
+
end
|
168
|
+
|
169
|
+
describe "#==" do
|
170
|
+
it "returns true for same class and args" do
|
171
|
+
expression = described_class.build("foo")
|
172
|
+
other = described_class.build("foo")
|
173
|
+
expect(expression == other).to be(true)
|
174
|
+
end
|
175
|
+
|
176
|
+
it "returns false for different class" do
|
177
|
+
expression = described_class.build("foo")
|
178
|
+
other = Object.new
|
179
|
+
expect(expression == other).to be(false)
|
180
|
+
end
|
181
|
+
|
182
|
+
it "returns false for different args" do
|
183
|
+
expression = described_class.build("foo")
|
184
|
+
other = described_class.build("bar")
|
185
|
+
expect(expression == other).to be(false)
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
RSpec.describe Flipper::Expressions::All do
|
2
|
+
describe "#call" do
|
3
|
+
it "returns true if all args evaluate as true" do
|
4
|
+
expect(described_class.call(true, true)).to be(true)
|
5
|
+
end
|
6
|
+
|
7
|
+
it "returns false if any args evaluate as false" do
|
8
|
+
expect(described_class.call(false, true)).to be(false)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns true with empty args" do
|
12
|
+
expect(described_class.call).to be(true)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
RSpec.describe Flipper::Expressions::Any do
|
2
|
+
describe "#call" do
|
3
|
+
it "returns true if any args evaluate as true" do
|
4
|
+
expect(described_class.call(true, false)).to be(true)
|
5
|
+
end
|
6
|
+
|
7
|
+
it "returns false if all args evaluate as false" do
|
8
|
+
expect(described_class.call(false, false)).to be(false)
|
9
|
+
end
|
10
|
+
|
11
|
+
it "returns false with empty args" do
|
12
|
+
expect(described_class.call).to be(false)
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
RSpec.describe Flipper::Expressions::Boolean do
|
2
|
+
describe "#call" do
|
3
|
+
[true, 'true', 1, '1'].each do |value|
|
4
|
+
it "returns a true for #{value.inspect}" do
|
5
|
+
expect(described_class.call(value)).to be(true)
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
[false, 'false', 0, '0', nil].each do |value|
|
10
|
+
it "returns a true for #{value.inspect}" do
|
11
|
+
expect(described_class.call(value)).to be(false)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
RSpec.describe Flipper::Expressions::Duration do
|
2
|
+
describe "#call" do
|
3
|
+
it "raises error with invalid value" do
|
4
|
+
expect { described_class.call(false, 'minute') }.to raise_error(ArgumentError)
|
5
|
+
end
|
6
|
+
|
7
|
+
it "raises error with invalid unit" do
|
8
|
+
expect { described_class.call(4, 'score') }.to raise_error(ArgumentError)
|
9
|
+
end
|
10
|
+
|
11
|
+
it 'defaults unit to seconds' do
|
12
|
+
expect(described_class.call(10)).to eq(10)
|
13
|
+
end
|
14
|
+
|
15
|
+
it "evaluates seconds" do
|
16
|
+
expect(described_class.call(10, 'seconds')).to eq(10)
|
17
|
+
end
|
18
|
+
|
19
|
+
it "evaluates minutes" do
|
20
|
+
expect(described_class.call(2, 'minutes')).to eq(120)
|
21
|
+
end
|
22
|
+
|
23
|
+
it "evaluates hours" do
|
24
|
+
expect(described_class.call(2, 'hours')).to eq(7200)
|
25
|
+
end
|
26
|
+
|
27
|
+
it "evaluates days" do
|
28
|
+
expect(described_class.call(2, 'days')).to eq(172_800)
|
29
|
+
end
|
30
|
+
|
31
|
+
it "evaluates weeks" do
|
32
|
+
expect(described_class.call(2, 'weeks')).to eq(1_209_600)
|
33
|
+
end
|
34
|
+
|
35
|
+
it "evaluates months" do
|
36
|
+
expect(described_class.call(2, 'months')).to eq(5_259_492)
|
37
|
+
end
|
38
|
+
|
39
|
+
it "evaluates years" do
|
40
|
+
expect(described_class.call(2, 'years')).to eq(63_113_904)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|