flipper 1.0.0 → 1.3.6
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 +50 -7
- data/.github/workflows/examples.yml +50 -8
- data/CLAUDE.md +74 -0
- data/Changelog.md +1 -584
- data/Gemfile +15 -8
- data/README.md +31 -27
- data/Rakefile +2 -2
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/banner.jpg +0 -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/exe/flipper +5 -0
- data/flipper.gemspec +6 -3
- 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/actor_limit.rb +28 -0
- data/lib/flipper/adapters/cache_base.rb +143 -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 +40 -12
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +19 -14
- 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 +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +45 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +263 -0
- data/lib/flipper/cloud/configuration.rb +131 -54
- data/lib/flipper/cloud/middleware.rb +5 -5
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -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 +100 -0
- data/lib/flipper/cloud/telemetry.rb +191 -0
- data/lib/flipper/cloud.rb +1 -1
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +42 -3
- data/lib/flipper/export.rb +0 -2
- 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 +9 -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 +63 -1
- data/lib/flipper/gate.rb +2 -1
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
- data/lib/flipper/instrumentation/statsd.rb +4 -2
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +0 -4
- data/lib/flipper/metadata.rb +4 -1
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +9 -8
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
- data/lib/flipper/test/shared_adapter_test.rb +41 -22
- data/lib/flipper/test_help.rb +43 -0
- data/lib/flipper/typecast.rb +37 -9
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +11 -1
- data/lib/flipper.rb +41 -2
- data/lib/generators/flipper/setup_generator.rb +68 -0
- data/lib/generators/flipper/templates/initializer.rb +45 -0
- data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
- data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
- data/lib/generators/flipper/update_generator.rb +35 -0
- data/package-lock.json +41 -0
- data/package.json +10 -0
- data/spec/fixtures/environment.rb +1 -0
- data/spec/flipper/adapter_builder_spec.rb +72 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +135 -74
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +64 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cli_spec.rb +166 -0
- data/spec/flipper/cloud/configuration_spec.rb +39 -57
- data/spec/flipper/cloud/dsl_spec.rb +6 -6
- data/spec/flipper/cloud/middleware_spec.rb +8 -8
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -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 +208 -0
- data/spec/flipper/cloud_spec.rb +31 -25
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -3
- data/spec/flipper/engine_spec.rb +226 -42
- 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 +380 -10
- 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/log_subscriber_spec.rb +10 -2
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
- data/spec/flipper/middleware/memoizer_spec.rb +79 -10
- data/spec/flipper/model/active_record_spec.rb +72 -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 +91 -3
- data/spec/spec_helper.rb +17 -5
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fail_on_output.rb +8 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +34 -8
- data/test/adapters/actor_limit_test.rb +20 -0
- data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
- data/test_rails/generators/flipper/update_generator_test.rb +96 -0
- data/test_rails/helper.rb +22 -2
- data/test_rails/system/test_help_test.rb +52 -0
- metadata +145 -29
- data/lib/flipper/cloud/instrumenter.rb +0 -48
- data/spec/support/climate_control.rb +0 -7
|
@@ -20,6 +20,14 @@ module Flipper
|
|
|
20
20
|
# # using with preload specific features
|
|
21
21
|
# use Flipper::Middleware::Memoizer, preload: [:stats, :search, :some_feature]
|
|
22
22
|
#
|
|
23
|
+
# # using with preload block that returns true/false
|
|
24
|
+
# use Flipper::Middleware::Memoizer, preload: ->(request) { !request.path.start_with?('/assets') }
|
|
25
|
+
#
|
|
26
|
+
# # using with preload block that returns specific features
|
|
27
|
+
# use Flipper::Middleware::Memoizer, preload: ->(request) {
|
|
28
|
+
# request.path.starts_with?('/admin') ? [:stats, :search] : false
|
|
29
|
+
# }
|
|
30
|
+
#
|
|
23
31
|
def initialize(app, opts = {})
|
|
24
32
|
if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
|
|
25
33
|
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
|
@@ -34,7 +42,7 @@ module Flipper
|
|
|
34
42
|
request = Rack::Request.new(env)
|
|
35
43
|
|
|
36
44
|
if memoize?(request)
|
|
37
|
-
memoized_call(
|
|
45
|
+
memoized_call(request)
|
|
38
46
|
else
|
|
39
47
|
@app.call(env)
|
|
40
48
|
end
|
|
@@ -52,26 +60,34 @@ module Flipper
|
|
|
52
60
|
end
|
|
53
61
|
end
|
|
54
62
|
|
|
55
|
-
def memoized_call(
|
|
56
|
-
|
|
57
|
-
flipper = env.fetch(@env_key) { Flipper }
|
|
63
|
+
def memoized_call(request)
|
|
64
|
+
flipper = request.env.fetch(@env_key) { Flipper }
|
|
58
65
|
|
|
59
66
|
# Already memoizing. This instance does not need to do anything.
|
|
60
67
|
if flipper.memoizing?
|
|
61
68
|
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/flippercloud/flipper/pull/523"
|
|
62
|
-
return @app.call(env)
|
|
69
|
+
return @app.call(request.env)
|
|
63
70
|
end
|
|
64
71
|
|
|
65
|
-
|
|
72
|
+
begin
|
|
73
|
+
flipper.memoize = true
|
|
66
74
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
75
|
+
# Preloading is pointless without memoizing.
|
|
76
|
+
preload = if @opts[:preload].respond_to?(:call)
|
|
77
|
+
@opts[:preload].call(request)
|
|
78
|
+
else
|
|
79
|
+
@opts[:preload]
|
|
80
|
+
end
|
|
71
81
|
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
82
|
+
case preload
|
|
83
|
+
when true then flipper.preload_all
|
|
84
|
+
when Array then flipper.preload(preload)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
@app.call(request.env)
|
|
88
|
+
ensure
|
|
89
|
+
flipper.memoize = false
|
|
90
|
+
end
|
|
75
91
|
end
|
|
76
92
|
end
|
|
77
93
|
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
module Model
|
|
3
|
+
module ActiveRecord
|
|
4
|
+
# The id of the record when used as an actor.
|
|
5
|
+
#
|
|
6
|
+
# class User < ActiveRecord::Base
|
|
7
|
+
# end
|
|
8
|
+
#
|
|
9
|
+
# user = User.first
|
|
10
|
+
# Flipper.enable :some_feature, user
|
|
11
|
+
# Flipper.enabled? :some_feature, user #=> true
|
|
12
|
+
#
|
|
13
|
+
def flipper_id
|
|
14
|
+
"#{self.class.base_class.name};#{id}"
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# Properties used to evaluate expressions
|
|
18
|
+
def flipper_properties
|
|
19
|
+
{"type" => self.class.name}.merge(attributes)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
data/lib/flipper/poller.rb
CHANGED
|
@@ -17,9 +17,11 @@ module Flipper
|
|
|
17
17
|
end
|
|
18
18
|
|
|
19
19
|
def self.reset
|
|
20
|
-
instances.each {|_,
|
|
20
|
+
instances.each {|_, instance| instance.stop }.clear
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
MINIMUM_POLL_INTERVAL = 10
|
|
24
|
+
|
|
23
25
|
def initialize(options = {})
|
|
24
26
|
@thread = nil
|
|
25
27
|
@pid = Process.pid
|
|
@@ -30,9 +32,9 @@ module Flipper
|
|
|
30
32
|
@last_synced_at = Concurrent::AtomicFixnum.new(0)
|
|
31
33
|
@adapter = Adapters::Memory.new(nil, threadsafe: true)
|
|
32
34
|
|
|
33
|
-
if @interval <
|
|
34
|
-
warn "Flipper::Cloud poll interval must be greater than or equal to
|
|
35
|
-
@interval =
|
|
35
|
+
if @interval < MINIMUM_POLL_INTERVAL
|
|
36
|
+
warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{@interval}. Setting @interval to #{MINIMUM_POLL_INTERVAL}."
|
|
37
|
+
@interval = MINIMUM_POLL_INTERVAL
|
|
36
38
|
end
|
|
37
39
|
|
|
38
40
|
@start_automatically = options.fetch(:start_automatically, true)
|
|
@@ -57,15 +59,14 @@ module Flipper
|
|
|
57
59
|
def run
|
|
58
60
|
loop do
|
|
59
61
|
sleep jitter
|
|
60
|
-
|
|
62
|
+
|
|
61
63
|
begin
|
|
62
64
|
sync
|
|
63
|
-
rescue
|
|
65
|
+
rescue
|
|
64
66
|
# you can instrument these using poller.flipper
|
|
65
67
|
end
|
|
66
68
|
|
|
67
|
-
|
|
68
|
-
sleep sleep_interval if sleep_interval.positive?
|
|
69
|
+
sleep interval
|
|
69
70
|
end
|
|
70
71
|
end
|
|
71
72
|
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
require "zlib"
|
|
2
|
+
require "stringio"
|
|
3
|
+
|
|
4
|
+
module Flipper
|
|
5
|
+
module Serializers
|
|
6
|
+
class Gzip
|
|
7
|
+
def self.serialize(source)
|
|
8
|
+
return if source.nil?
|
|
9
|
+
output = StringIO.new
|
|
10
|
+
gz = Zlib::GzipWriter.new(output)
|
|
11
|
+
gz.write(source)
|
|
12
|
+
gz.close
|
|
13
|
+
output.string
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.deserialize(source)
|
|
17
|
+
return if source.nil?
|
|
18
|
+
Zlib::GzipReader.wrap(StringIO.new(source), &:read)
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
|
|
3
|
+
module Flipper
|
|
4
|
+
module Serializers
|
|
5
|
+
class Json
|
|
6
|
+
def self.serialize(source)
|
|
7
|
+
return if source.nil?
|
|
8
|
+
JSON.generate(source)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def self.deserialize(source)
|
|
12
|
+
return if source.nil?
|
|
13
|
+
JSON.parse(source)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -4,11 +4,12 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
4
4
|
let(:flipper) { Flipper.new(subject) }
|
|
5
5
|
let(:feature) { flipper[:stats] }
|
|
6
6
|
|
|
7
|
-
let(:boolean_gate)
|
|
8
|
-
let(:
|
|
9
|
-
let(:
|
|
10
|
-
let(:
|
|
11
|
-
let(:
|
|
7
|
+
let(:boolean_gate) { feature.gate(:boolean) }
|
|
8
|
+
let(:expression_gate) { feature.gate(:expression) }
|
|
9
|
+
let(:group_gate) { feature.gate(:group) }
|
|
10
|
+
let(:actor_gate) { feature.gate(:actor) }
|
|
11
|
+
let(:actors_gate) { feature.gate(:percentage_of_actors) }
|
|
12
|
+
let(:time_gate) { feature.gate(:percentage_of_time) }
|
|
12
13
|
|
|
13
14
|
before do
|
|
14
15
|
Flipper.register(:admins) do |actor|
|
|
@@ -66,10 +67,27 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
66
67
|
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true)
|
|
67
68
|
|
|
68
69
|
expect(subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new(false))).to eq(true)
|
|
69
|
-
|
|
70
70
|
expect(subject.get(feature)).to eq(subject.default_config)
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
it 'can enable, disable and get value for expression gate' do
|
|
74
|
+
basic_expression = Flipper.property(:plan).eq("basic")
|
|
75
|
+
age_expression = Flipper.property(:age).gte(21)
|
|
76
|
+
any_expression = Flipper.any(basic_expression, age_expression)
|
|
77
|
+
|
|
78
|
+
expect(subject.enable(feature, expression_gate, any_expression)).to eq(true)
|
|
79
|
+
result = subject.get(feature)
|
|
80
|
+
expect(result[:expression]).to eq(any_expression.value)
|
|
81
|
+
|
|
82
|
+
expect(subject.enable(feature, expression_gate, basic_expression)).to eq(true)
|
|
83
|
+
result = subject.get(feature)
|
|
84
|
+
expect(result[:expression]).to eq(basic_expression.value)
|
|
85
|
+
|
|
86
|
+
expect(subject.disable(feature, expression_gate, basic_expression)).to eq(true)
|
|
87
|
+
result = subject.get(feature)
|
|
88
|
+
expect(result[:expression]).to be(nil)
|
|
89
|
+
end
|
|
90
|
+
|
|
73
91
|
it 'can enable, disable and get value for group gate' do
|
|
74
92
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
|
75
93
|
expect(subject.enable(feature, group_gate, flipper.group(:early_access))).to eq(true)
|
|
@@ -90,19 +108,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
90
108
|
actor22 = Flipper::Actor.new('22')
|
|
91
109
|
actor_asdf = Flipper::Actor.new('asdf')
|
|
92
110
|
|
|
93
|
-
expect(
|
|
94
|
-
expect(
|
|
111
|
+
expect(feature.enable(actor22)).to be(true)
|
|
112
|
+
expect(feature.enable(actor_asdf)).to be(true)
|
|
95
113
|
|
|
96
|
-
|
|
97
|
-
expect(
|
|
114
|
+
expect(feature).to be_enabled(actor22)
|
|
115
|
+
expect(feature).to be_enabled(actor_asdf)
|
|
98
116
|
|
|
99
|
-
expect(
|
|
100
|
-
|
|
101
|
-
expect(
|
|
117
|
+
expect(feature.disable(actor22)).to be(true)
|
|
118
|
+
expect(feature).not_to be_enabled(actor22)
|
|
119
|
+
expect(feature).to be_enabled(actor_asdf)
|
|
102
120
|
|
|
103
|
-
expect(
|
|
104
|
-
|
|
105
|
-
expect(
|
|
121
|
+
expect(feature.disable(actor_asdf)).to eq(true)
|
|
122
|
+
expect(feature).not_to be_enabled(actor22)
|
|
123
|
+
expect(feature).not_to be_enabled(actor_asdf)
|
|
106
124
|
end
|
|
107
125
|
|
|
108
126
|
it 'can enable, disable and get value for percentage of actors gate' do
|
|
@@ -164,9 +182,10 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
164
182
|
end
|
|
165
183
|
|
|
166
184
|
it 'converts the actor value to a string' do
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
185
|
+
actor = Flipper::Actor.new(22)
|
|
186
|
+
expect(feature).not_to be_enabled(actor)
|
|
187
|
+
feature.enable_actor actor
|
|
188
|
+
expect(feature).to be_enabled(actor)
|
|
170
189
|
end
|
|
171
190
|
|
|
172
191
|
it 'converts group value to a string' do
|
|
@@ -256,14 +275,14 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
256
275
|
expect(subject.add(flipper[:stats])).to eq(true)
|
|
257
276
|
expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
|
258
277
|
expect(subject.add(flipper[:search])).to eq(true)
|
|
278
|
+
flipper.enable :analytics, Flipper.property(:plan).eq("pro")
|
|
259
279
|
|
|
260
280
|
result = subject.get_all
|
|
261
|
-
expect(result).to be_instance_of(Hash)
|
|
262
281
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
expect(
|
|
266
|
-
expect(
|
|
282
|
+
expect(result).to be_instance_of(Hash)
|
|
283
|
+
expect(result["stats"]).to eq(subject.default_config.merge(boolean: 'true'))
|
|
284
|
+
expect(result["search"]).to eq(subject.default_config)
|
|
285
|
+
expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}))
|
|
267
286
|
end
|
|
268
287
|
|
|
269
288
|
it 'includes explicitly disabled features when getting all features' do
|
|
@@ -277,9 +296,9 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
|
277
296
|
|
|
278
297
|
it 'can double enable an actor without error' do
|
|
279
298
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
|
280
|
-
expect(
|
|
281
|
-
expect(
|
|
282
|
-
expect(
|
|
299
|
+
expect(feature.enable(actor)).to be(true)
|
|
300
|
+
expect(feature.enable(actor)).to be(true)
|
|
301
|
+
expect(feature).to be_enabled(actor)
|
|
283
302
|
end
|
|
284
303
|
|
|
285
304
|
it 'can double enable a group without error' do
|
|
@@ -7,6 +7,7 @@ module Flipper
|
|
|
7
7
|
@feature = @flipper[:stats]
|
|
8
8
|
@boolean_gate = @feature.gate(:boolean)
|
|
9
9
|
@group_gate = @feature.gate(:group)
|
|
10
|
+
@expression_gate = @feature.gate(:expression)
|
|
10
11
|
@actor_gate = @feature.gate(:actor)
|
|
11
12
|
@actors_gate = @feature.gate(:percentage_of_actors)
|
|
12
13
|
@time_gate = @feature.gate(:percentage_of_time)
|
|
@@ -65,6 +66,24 @@ module Flipper
|
|
|
65
66
|
assert_equal @adapter.default_config, @adapter.get(@feature)
|
|
66
67
|
end
|
|
67
68
|
|
|
69
|
+
def test_can_enable_disable_and_get_value_for_expression_gate
|
|
70
|
+
basic_expression = Flipper.property(:plan).eq("basic")
|
|
71
|
+
age_expression = Flipper.property(:age).gte(21)
|
|
72
|
+
any_expression = Flipper.any(basic_expression, age_expression)
|
|
73
|
+
|
|
74
|
+
assert_equal true, @adapter.enable(@feature, @expression_gate, any_expression)
|
|
75
|
+
result = @adapter.get(@feature)
|
|
76
|
+
assert_equal any_expression.value, result[:expression]
|
|
77
|
+
|
|
78
|
+
assert_equal true, @adapter.enable(@feature, @expression_gate, basic_expression)
|
|
79
|
+
result = @adapter.get(@feature)
|
|
80
|
+
assert_equal basic_expression.value, result[:expression]
|
|
81
|
+
|
|
82
|
+
assert_equal true, @adapter.disable(@feature, @expression_gate, basic_expression)
|
|
83
|
+
result = @adapter.get(@feature)
|
|
84
|
+
assert_nil result[:expression]
|
|
85
|
+
end
|
|
86
|
+
|
|
68
87
|
def test_can_enable_disable_get_value_for_group_gate
|
|
69
88
|
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins))
|
|
70
89
|
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:early_access))
|
|
@@ -85,19 +104,19 @@ module Flipper
|
|
|
85
104
|
actor22 = Flipper::Actor.new('22')
|
|
86
105
|
actor_asdf = Flipper::Actor.new('asdf')
|
|
87
106
|
|
|
88
|
-
assert_equal true, @
|
|
89
|
-
assert_equal true, @
|
|
107
|
+
assert_equal true, @feature.enable(actor22)
|
|
108
|
+
assert_equal true, @feature.enable(actor_asdf)
|
|
90
109
|
|
|
91
|
-
|
|
92
|
-
|
|
110
|
+
assert @feature.enabled?(actor22)
|
|
111
|
+
assert @feature.enabled?(actor_asdf)
|
|
93
112
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
assert_equal true, @feature.disable(actor22)
|
|
114
|
+
refute @feature.enabled?(actor22)
|
|
115
|
+
assert @feature.enabled?(actor_asdf)
|
|
97
116
|
|
|
98
|
-
assert_equal true, @
|
|
99
|
-
|
|
100
|
-
|
|
117
|
+
assert_equal true, @feature.disable(actor_asdf)
|
|
118
|
+
refute @feature.enabled?(actor22)
|
|
119
|
+
refute @feature.enabled?(actor_asdf)
|
|
101
120
|
end
|
|
102
121
|
|
|
103
122
|
def test_can_enable_disable_get_value_for_percentage_of_actors_gate
|
|
@@ -159,10 +178,10 @@ module Flipper
|
|
|
159
178
|
end
|
|
160
179
|
|
|
161
180
|
def test_converts_the_actor_value_to_a_string
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
181
|
+
actor = Flipper::Actor.new(22)
|
|
182
|
+
refute @feature.enabled?(actor)
|
|
183
|
+
@feature.enable_actor actor
|
|
184
|
+
assert @feature.enabled?(actor)
|
|
166
185
|
end
|
|
167
186
|
|
|
168
187
|
def test_converts_group_value_to_a_string
|
|
@@ -252,14 +271,14 @@ module Flipper
|
|
|
252
271
|
assert @adapter.add(@flipper[:stats])
|
|
253
272
|
assert @adapter.enable(@flipper[:stats], @boolean_gate, Flipper::Types::Boolean.new)
|
|
254
273
|
assert @adapter.add(@flipper[:search])
|
|
274
|
+
@flipper.enable :analytics, Flipper.property(:plan).eq("pro")
|
|
255
275
|
|
|
256
276
|
result = @adapter.get_all
|
|
257
|
-
assert_instance_of Hash, result
|
|
258
277
|
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
assert_equal @adapter.default_config
|
|
262
|
-
assert_equal @adapter.default_config,
|
|
278
|
+
assert_instance_of Hash, result
|
|
279
|
+
assert_equal @adapter.default_config.merge(boolean: 'true'), result["stats"]
|
|
280
|
+
assert_equal @adapter.default_config, result["search"]
|
|
281
|
+
assert_equal @adapter.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}), result["analytics"]
|
|
263
282
|
end
|
|
264
283
|
|
|
265
284
|
def test_includes_explicitly_disabled_features_when_getting_all_features
|
|
@@ -273,9 +292,9 @@ module Flipper
|
|
|
273
292
|
|
|
274
293
|
def test_can_double_enable_an_actor_without_error
|
|
275
294
|
actor = Flipper::Actor.new('Flipper::Actor;22')
|
|
276
|
-
assert_equal true, @
|
|
277
|
-
assert_equal true, @
|
|
278
|
-
|
|
295
|
+
assert_equal true, @feature.enable(actor)
|
|
296
|
+
assert_equal true, @feature.enable(actor)
|
|
297
|
+
assert @feature.enabled?(actor)
|
|
279
298
|
end
|
|
280
299
|
|
|
281
300
|
def test_can_double_enable_a_group_without_error
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
module TestHelp
|
|
3
|
+
extend self
|
|
4
|
+
|
|
5
|
+
def flipper_configure
|
|
6
|
+
# Use a shared Memory adapter for all tests. This is instantiated outside of the
|
|
7
|
+
# `configure` block so the same instance is returned in new threads.
|
|
8
|
+
adapter = Flipper::Adapters::Memory.new
|
|
9
|
+
|
|
10
|
+
Flipper.configure do |config|
|
|
11
|
+
config.adapter { adapter }
|
|
12
|
+
config.default { Flipper.new(config.adapter) }
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def flipper_reset
|
|
17
|
+
# Remove all features
|
|
18
|
+
Flipper.features.each(&:remove) rescue nil
|
|
19
|
+
|
|
20
|
+
# Reset previous DSL instance
|
|
21
|
+
Flipper.instance = nil
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
if defined?(RSpec) && RSpec.respond_to?(:configure)
|
|
27
|
+
RSpec.configure do |config|
|
|
28
|
+
config.include Flipper::TestHelp
|
|
29
|
+
config.before(:suite) { Flipper::TestHelp.flipper_configure }
|
|
30
|
+
config.before(:each) { flipper_reset }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
if defined?(ActiveSupport)
|
|
34
|
+
ActiveSupport.on_load(:active_support_test_case) do
|
|
35
|
+
Flipper::TestHelp.flipper_configure
|
|
36
|
+
|
|
37
|
+
ActiveSupport::TestCase.class_eval do
|
|
38
|
+
include Flipper::TestHelp
|
|
39
|
+
|
|
40
|
+
setup :flipper_reset
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
data/lib/flipper/typecast.rb
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
require 'set'
|
|
2
|
+
require "flipper/serializers/json"
|
|
3
|
+
require "flipper/serializers/gzip"
|
|
2
4
|
|
|
3
5
|
module Flipper
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
class Typecast
|
|
7
|
+
TRUTH_MAP = {
|
|
6
8
|
true => true,
|
|
7
9
|
1 => true,
|
|
8
10
|
'true' => true,
|
|
@@ -13,7 +15,7 @@ module Flipper
|
|
|
13
15
|
#
|
|
14
16
|
# Returns true or false.
|
|
15
17
|
def self.to_boolean(value)
|
|
16
|
-
!!
|
|
18
|
+
!!TRUTH_MAP[value]
|
|
17
19
|
end
|
|
18
20
|
|
|
19
21
|
# Internal: Convert value to an integer.
|
|
@@ -36,17 +38,25 @@ module Flipper
|
|
|
36
38
|
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
|
37
39
|
end
|
|
38
40
|
|
|
39
|
-
# Internal: Convert value to a
|
|
41
|
+
# Internal: Convert value to a number.
|
|
40
42
|
#
|
|
41
43
|
# Returns a Integer or Float representation of the value.
|
|
42
44
|
# Raises ArgumentError if conversion is not possible.
|
|
43
|
-
def self.
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
45
|
+
def self.to_number(value)
|
|
46
|
+
case value
|
|
47
|
+
when Numeric
|
|
48
|
+
value
|
|
49
|
+
when String
|
|
50
|
+
value.include?('.') ? to_float(value) : to_integer(value)
|
|
51
|
+
when NilClass
|
|
52
|
+
0
|
|
53
|
+
else
|
|
54
|
+
value.to_f
|
|
55
|
+
end
|
|
47
56
|
rescue NoMethodError
|
|
48
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to a
|
|
57
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a number"
|
|
49
58
|
end
|
|
59
|
+
singleton_class.send(:alias_method, :to_percentage, :to_number)
|
|
50
60
|
|
|
51
61
|
# Internal: Convert value to a set.
|
|
52
62
|
#
|
|
@@ -71,6 +81,8 @@ module Flipper
|
|
|
71
81
|
normalized_value = case value
|
|
72
82
|
when Array, Set
|
|
73
83
|
value.to_set
|
|
84
|
+
when Hash
|
|
85
|
+
value
|
|
74
86
|
else
|
|
75
87
|
value ? value.to_s : value
|
|
76
88
|
end
|
|
@@ -79,5 +91,21 @@ module Flipper
|
|
|
79
91
|
end
|
|
80
92
|
normalized_source
|
|
81
93
|
end
|
|
94
|
+
|
|
95
|
+
def self.to_json(source)
|
|
96
|
+
Serializers::Json.serialize(source)
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def self.from_json(source)
|
|
100
|
+
Serializers::Json.deserialize(source)
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def self.to_gzip(source)
|
|
104
|
+
Serializers::Gzip.serialize(source)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def self.from_gzip(source)
|
|
108
|
+
Serializers::Gzip.deserialize(source)
|
|
109
|
+
end
|
|
82
110
|
end
|
|
83
111
|
end
|
data/lib/flipper/version.rb
CHANGED
|
@@ -1,3 +1,13 @@
|
|
|
1
1
|
module Flipper
|
|
2
|
-
VERSION = '1.
|
|
2
|
+
VERSION = '1.3.6'.freeze
|
|
3
|
+
|
|
4
|
+
REQUIRED_RUBY_VERSION = '2.6'.freeze
|
|
5
|
+
NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
|
|
6
|
+
|
|
7
|
+
REQUIRED_RAILS_VERSION = '5.2'.freeze
|
|
8
|
+
NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
|
|
9
|
+
|
|
10
|
+
def self.deprecated_ruby_version?
|
|
11
|
+
Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
|
|
12
|
+
end
|
|
3
13
|
end
|
data/lib/flipper.rb
CHANGED
|
@@ -57,15 +57,49 @@ module Flipper
|
|
|
57
57
|
# interface of Flipper::DSL.
|
|
58
58
|
def_delegators :instance,
|
|
59
59
|
:enabled?, :enable, :disable,
|
|
60
|
+
:enable_expression, :disable_expression,
|
|
61
|
+
:expression, :add_expression, :remove_expression,
|
|
60
62
|
:enable_actor, :disable_actor,
|
|
61
63
|
:enable_group, :disable_group,
|
|
62
64
|
:enable_percentage_of_actors, :disable_percentage_of_actors,
|
|
63
65
|
:enable_percentage_of_time, :disable_percentage_of_time,
|
|
64
66
|
:features, :feature, :[], :preload, :preload_all,
|
|
65
67
|
:adapter, :add, :exist?, :remove, :import, :export,
|
|
66
|
-
:memoize=, :memoizing?,
|
|
68
|
+
:memoize=, :memoizing?, :read_only?,
|
|
67
69
|
:sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
|
|
68
70
|
|
|
71
|
+
def any(*args)
|
|
72
|
+
Expression.build({ Any: args.flatten })
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def all(*args)
|
|
76
|
+
Expression.build({ All: args.flatten })
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def constant(value)
|
|
80
|
+
Expression.build(value)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def property(name)
|
|
84
|
+
Expression.build({ Property: name })
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def string(value)
|
|
88
|
+
Expression.build({ String: value })
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def number(value)
|
|
92
|
+
Expression.build({ Number: value })
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def boolean(value)
|
|
96
|
+
Expression.build({ Boolean: value })
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def random(max)
|
|
100
|
+
Expression.build({ Random: max })
|
|
101
|
+
end
|
|
102
|
+
|
|
69
103
|
# Public: Use this to register a group by name.
|
|
70
104
|
#
|
|
71
105
|
# name - The Symbol name of the group.
|
|
@@ -140,9 +174,13 @@ end
|
|
|
140
174
|
|
|
141
175
|
require 'flipper/actor'
|
|
142
176
|
require 'flipper/adapter'
|
|
177
|
+
require 'flipper/adapters/wrapper'
|
|
178
|
+
require 'flipper/adapters/actor_limit'
|
|
179
|
+
require 'flipper/adapters/instrumented'
|
|
143
180
|
require 'flipper/adapters/memoizable'
|
|
144
181
|
require 'flipper/adapters/memory'
|
|
145
|
-
require 'flipper/adapters/
|
|
182
|
+
require 'flipper/adapters/strict'
|
|
183
|
+
require 'flipper/adapter_builder'
|
|
146
184
|
require 'flipper/configuration'
|
|
147
185
|
require 'flipper/dsl'
|
|
148
186
|
require 'flipper/errors'
|
|
@@ -155,6 +193,7 @@ require 'flipper/middleware/memoizer'
|
|
|
155
193
|
require 'flipper/middleware/setup_env'
|
|
156
194
|
require 'flipper/poller'
|
|
157
195
|
require 'flipper/registry'
|
|
196
|
+
require 'flipper/expression'
|
|
158
197
|
require 'flipper/type'
|
|
159
198
|
require 'flipper/types/actor'
|
|
160
199
|
require 'flipper/types/boolean'
|