flipper 1.0.0 → 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- 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 +326 -272
- 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 +27 -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/model/active_record.rb +23 -0
- 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/model/active_record_spec.rb +61 -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 +107 -18
- data/lib/flipper/cloud/instrumenter.rb +0 -48
data/Gemfile
CHANGED
@@ -27,8 +27,8 @@ gem 'flamegraph'
|
|
27
27
|
gem 'climate_control'
|
28
28
|
|
29
29
|
group(:guard) do
|
30
|
-
gem 'guard'
|
31
|
-
gem 'guard-rspec'
|
32
|
-
gem 'guard-bundler'
|
33
|
-
gem 'rb-fsevent'
|
30
|
+
gem 'guard'
|
31
|
+
gem 'guard-rspec'
|
32
|
+
gem 'guard-bundler'
|
33
|
+
gem 'rb-fsevent'
|
34
34
|
end
|
data/README.md
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
[![Flipper Mark](docs/images/banner.jpg)](https://www.flippercloud.io)
|
2
2
|
|
3
|
-
[Website](https://flippercloud.io) | [Documentation](https://flippercloud.io/docs) | [Examples](examples) | [Twitter](https://twitter.com/flipper_cloud)
|
3
|
+
[Website](https://flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=website_link) | [Documentation](https://flippercloud.io/docs?utm_source=oss&utm_medium=readme&utm_campaign=docs_link) | [Examples](examples) | [Twitter](https://twitter.com/flipper_cloud) | [Ruby.social](https://ruby.social/@flipper)
|
4
4
|
|
5
5
|
# Flipper
|
6
6
|
|
@@ -35,7 +35,7 @@ Or install it yourself with:
|
|
35
35
|
|
36
36
|
## Subscribe & Ship
|
37
37
|
|
38
|
-
[💌 Subscribe](https://
|
38
|
+
[💌 Subscribe](https://blog.flippercloud.io/#/portal/signup) - we'll send you short and sweet emails when we release new versions ([examples](https://blog.flippercloud.io/tag/releases/)).
|
39
39
|
|
40
40
|
## Getting Started
|
41
41
|
|
@@ -43,7 +43,7 @@ Use `Flipper#enabled?` in your app to check if a feature is enabled.
|
|
43
43
|
|
44
44
|
```ruby
|
45
45
|
# check if search is enabled
|
46
|
-
if Flipper.enabled?
|
46
|
+
if Flipper.enabled?(:search, current_user)
|
47
47
|
puts 'Search away!'
|
48
48
|
else
|
49
49
|
puts 'No search for you!'
|
@@ -66,24 +66,26 @@ Flipper.enable_group :search, :admin
|
|
66
66
|
Flipper.enable_percentage_of_actors :search, 2
|
67
67
|
```
|
68
68
|
|
69
|
-
Read more about [getting started with Flipper](https://flippercloud.io/docs) and [enabling features](https://flippercloud.io/docs/features).
|
69
|
+
Read more about [getting started with Flipper](https://flippercloud.io/docs?utm_source=oss&utm_medium=readme&utm_campaign=getting_started) and [enabling features](https://flippercloud.io/docs/features?utm_source=oss&utm_medium=readme&utm_campaign=enabling_features).
|
70
70
|
|
71
71
|
## Flipper Cloud
|
72
72
|
|
73
|
-
Like Flipper and want more? Check out [Flipper Cloud](https://www.flippercloud.io), which comes with:
|
73
|
+
Like Flipper and want more? Check out [Flipper Cloud](https://www.flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=check_out), which comes with:
|
74
74
|
|
75
|
-
* **
|
76
|
-
* **
|
77
|
-
* **
|
78
|
-
* **personal environments** — no more rake scripts or manual enable/disable to get your laptop to look like production. Every developer gets a personal environment that inherits from production that they can override as they please ([read more](https://www.johnnunemaker.com/flipper-cloud-environments/)).
|
79
|
-
* **no maintenance** — we'll keep the lights on for you. We also have handy webhooks for keeping your app in sync with Cloud, so **our availability won't affect yours**. All your feature flag reads are local to your app.
|
75
|
+
* **multiple environments** — production, staging, per continent, whatever you need. Every environment inherits from production by default and every project comes with a [project overview page](https://blog.flippercloud.io/project-overview/) that shows each feature and its status in each environment.
|
76
|
+
* **personal environments** — everyone on your team gets a personal environment (that inherits from production) which they can modify however they want without stepping on anyone else's toes.
|
77
|
+
* **permissions** — grant access to everyone in your organization or lockdown each project to particular people. You can even limit access to a particular environment (like production) to specific people.
|
80
78
|
* **audit history** — every feature change and who made it.
|
81
79
|
* **rollbacks** — enable or disable a feature accidentally? No problem. You can roll back to any point in the audit history with a single click.
|
80
|
+
* **maintenance** — we'll keep the lights on for you. We also have handy webhooks and background polling for keeping your app in sync with Cloud, so **our availability won't affect yours**. All your feature flag reads are local to your app.
|
81
|
+
* **everything in one place** — no need to bounce around from different application UIs or IRB consoles.
|
82
82
|
|
83
|
-
[![Flipper Cloud Screenshot](docs/images/flipper_cloud.png)](https://www.flippercloud.io)
|
83
|
+
[![Flipper Cloud Screenshot](docs/images/flipper_cloud.png)](https://www.flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=screenshot)
|
84
84
|
|
85
85
|
Cloud is super simple to integrate with Rails ([demo app](https://github.com/fewerandfaster/flipper-rails-demo)), Sinatra or any other framework.
|
86
86
|
|
87
|
+
We also have a [free plan](https://www.flippercloud.io?utm_source=oss&utm_medium=readme&utm_campaign=free_plan) that you can use forever.
|
88
|
+
|
87
89
|
## Contributing
|
88
90
|
|
89
91
|
1. Fork it
|
data/benchmark/typecast_ips.rb
CHANGED
@@ -16,4 +16,12 @@ Benchmark.ips do |x|
|
|
16
16
|
x.report("Typecast.to_float '1'") { Flipper::Typecast.to_float('1'.freeze) }
|
17
17
|
x.report("Typecast.to_float 1.01") { Flipper::Typecast.to_float(1) }
|
18
18
|
x.report("Typecast.to_float '1.01'") { Flipper::Typecast.to_float('1'.freeze) }
|
19
|
+
|
20
|
+
x.report("Typecast.to_number 1") { Flipper::Typecast.to_number(1) }
|
21
|
+
x.report("Typecast.to_number 1.1") { Flipper::Typecast.to_number(1.1) }
|
22
|
+
x.report("Typecast.to_number '1'") { Flipper::Typecast.to_number('1'.freeze) }
|
23
|
+
x.report("Typecast.to_number '1.1'") { Flipper::Typecast.to_number('1.1'.freeze) }
|
24
|
+
x.report("Typecast.to_number nil") { Flipper::Typecast.to_number(nil) }
|
25
|
+
time = Time.now
|
26
|
+
x.report("Typecast.to_number Time.now") { Flipper::Typecast.to_number(time) }
|
19
27
|
end
|
Binary file
|
@@ -0,0 +1,13 @@
|
|
1
|
+
# Just a simple example that shows how the backoff policy works.
|
2
|
+
require 'bundler/setup'
|
3
|
+
require 'flipper/cloud/telemetry/backoff_policy'
|
4
|
+
|
5
|
+
intervals = []
|
6
|
+
policy = Flipper::Cloud::Telemetry::BackoffPolicy.new
|
7
|
+
|
8
|
+
10.times do |n|
|
9
|
+
intervals << policy.next_interval
|
10
|
+
end
|
11
|
+
|
12
|
+
pp intervals.map { |i| i.round(2) }
|
13
|
+
puts "Total: #{intervals.sum.round(2)}ms (#{(intervals.sum/1_000.0).round(2)} sec)"
|
@@ -2,3 +2,19 @@ if ENV["FLIPPER_CLOUD_TOKEN"].nil? || ENV["FLIPPER_CLOUD_TOKEN"].empty?
|
|
2
2
|
warn "FLIPPER_CLOUD_TOKEN missing so skipping cloud example."
|
3
3
|
exit
|
4
4
|
end
|
5
|
+
|
6
|
+
matrix_key = if ENV["CI"]
|
7
|
+
suffix_rails = ENV["RAILS_VERSION"].split(".").take(2).join
|
8
|
+
suffix_ruby = RUBY_VERSION.split(".").take(2).join
|
9
|
+
"FLIPPER_CLOUD_TOKEN_#{suffix_ruby}_#{suffix_rails}"
|
10
|
+
else
|
11
|
+
"FLIPPER_CLOUD_TOKEN"
|
12
|
+
end
|
13
|
+
|
14
|
+
if matrix_token = ENV[matrix_key]
|
15
|
+
puts "Using #{matrix_key} for FLIPPER_CLOUD_TOKEN"
|
16
|
+
ENV["FLIPPER_CLOUD_TOKEN"] = matrix_token
|
17
|
+
else
|
18
|
+
warn "Missing #{matrix_key}. Go create an environment in flipper cloud and set #{matrix_key} to the adapter token for that environment in github actions secrets."
|
19
|
+
exit 1
|
20
|
+
end
|
data/examples/cloud/forked.rb
CHANGED
@@ -5,11 +5,16 @@ require_relative "./cloud_setup"
|
|
5
5
|
require 'bundler/setup'
|
6
6
|
require 'flipper/cloud'
|
7
7
|
|
8
|
-
|
8
|
+
puts Process.pid
|
9
|
+
|
10
|
+
# Make a call in the parent process so we can detect forking.
|
11
|
+
Flipper.enabled?(:stats)
|
12
|
+
|
13
|
+
pids = 2.times.map do |n|
|
9
14
|
fork {
|
10
15
|
# Check every second to see if the feature is enabled
|
11
16
|
threads = []
|
12
|
-
|
17
|
+
2.times do
|
13
18
|
threads << Thread.new do
|
14
19
|
loop do
|
15
20
|
sleep rand
|
data/examples/cloud/threaded.rb
CHANGED
@@ -4,33 +4,30 @@
|
|
4
4
|
require_relative "./cloud_setup"
|
5
5
|
require 'bundler/setup'
|
6
6
|
require 'flipper/cloud'
|
7
|
-
require "active_support/notifications"
|
8
|
-
require "active_support/isolated_execution_state"
|
9
7
|
|
10
|
-
|
11
|
-
p args: args
|
12
|
-
end
|
8
|
+
puts Process.pid
|
13
9
|
|
14
10
|
Flipper.configure do |config|
|
15
11
|
config.default {
|
16
|
-
Flipper::Cloud.new(
|
12
|
+
Flipper::Cloud.new(
|
13
|
+
local_adapter: config.adapter,
|
14
|
+
debug_output: STDOUT,
|
15
|
+
)
|
17
16
|
}
|
18
17
|
end
|
19
18
|
|
19
|
+
# You might want to do this at some point to see different results:
|
20
|
+
# Flipper.enable(:search)
|
21
|
+
# Flipper.disable(:stats)
|
22
|
+
|
20
23
|
# Check every second to see if the feature is enabled
|
21
|
-
|
22
|
-
|
23
|
-
threads << Thread.new do
|
24
|
+
5.times.map { |i|
|
25
|
+
Thread.new {
|
24
26
|
loop do
|
25
27
|
sleep rand
|
26
28
|
|
27
|
-
|
28
|
-
|
29
|
-
else
|
30
|
-
puts "#{Time.now.to_i} Disabled!"
|
31
|
-
end
|
29
|
+
Flipper.enabled?(:stats)
|
30
|
+
Flipper.enabled?(:search)
|
32
31
|
end
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
threads.map(&:join)
|
32
|
+
}
|
33
|
+
}.each(&:join)
|
@@ -0,0 +1,213 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'flipper'
|
3
|
+
|
4
|
+
def assert(value)
|
5
|
+
if value
|
6
|
+
p value
|
7
|
+
else
|
8
|
+
puts "Expected true but was #{value}. Please correct."
|
9
|
+
exit 1
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
def refute(value)
|
14
|
+
if value
|
15
|
+
puts "Expected false but was #{value}. Please correct."
|
16
|
+
exit 1
|
17
|
+
else
|
18
|
+
p value
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def reset
|
23
|
+
Flipper.disable_expression :something
|
24
|
+
end
|
25
|
+
|
26
|
+
class User < Struct.new(:id, :flipper_properties)
|
27
|
+
include Flipper::Identifier
|
28
|
+
end
|
29
|
+
|
30
|
+
class Org < Struct.new(:id, :flipper_properties)
|
31
|
+
include Flipper::Identifier
|
32
|
+
end
|
33
|
+
|
34
|
+
NOW = Time.now.to_i
|
35
|
+
DAY = 60 * 60 * 24
|
36
|
+
|
37
|
+
org = Org.new(1, {
|
38
|
+
"type" => "Org",
|
39
|
+
"id" => 1,
|
40
|
+
"now" => NOW,
|
41
|
+
})
|
42
|
+
|
43
|
+
user = User.new(1, {
|
44
|
+
"type" => "User",
|
45
|
+
"id" => 1,
|
46
|
+
"plan" => "basic",
|
47
|
+
"age" => 39,
|
48
|
+
"team_user" => true,
|
49
|
+
"now" => NOW,
|
50
|
+
})
|
51
|
+
|
52
|
+
admin_user = User.new(2, {
|
53
|
+
"type" => "User",
|
54
|
+
"id" => 2,
|
55
|
+
"admin" => true,
|
56
|
+
"team_user" => true,
|
57
|
+
"now" => NOW,
|
58
|
+
})
|
59
|
+
|
60
|
+
other_user = User.new(3, {
|
61
|
+
"type" => "User",
|
62
|
+
"id" => 3,
|
63
|
+
"plan" => "plus",
|
64
|
+
"age" => 18,
|
65
|
+
"org_admin" => true,
|
66
|
+
"now" => NOW - DAY,
|
67
|
+
})
|
68
|
+
|
69
|
+
age_expression = Flipper.property(:age).gte(21)
|
70
|
+
plan_expression = Flipper.property(:plan).eq("basic")
|
71
|
+
admin_expression = Flipper.property(:admin).eq(true)
|
72
|
+
|
73
|
+
puts "Single Expression"
|
74
|
+
refute Flipper.enabled?(:something, user)
|
75
|
+
|
76
|
+
puts "Enabling single expression"
|
77
|
+
Flipper.enable :something, plan_expression
|
78
|
+
assert Flipper.enabled?(:something, user)
|
79
|
+
refute Flipper.enabled?(:something, admin_user)
|
80
|
+
refute Flipper.enabled?(:something, other_user)
|
81
|
+
|
82
|
+
puts "Disabling single expression"
|
83
|
+
reset
|
84
|
+
refute Flipper.enabled?(:something, user)
|
85
|
+
|
86
|
+
puts "\n\nAny Expression"
|
87
|
+
any_expression = Flipper.any(plan_expression, age_expression)
|
88
|
+
refute Flipper.enabled?(:something, user)
|
89
|
+
|
90
|
+
puts "Enabling any expression"
|
91
|
+
Flipper.enable :something, any_expression
|
92
|
+
assert Flipper.enabled?(:something, user)
|
93
|
+
refute Flipper.enabled?(:something, admin_user)
|
94
|
+
refute Flipper.enabled?(:something, other_user)
|
95
|
+
|
96
|
+
puts "Disabling any expression"
|
97
|
+
reset
|
98
|
+
refute Flipper.enabled?(:something, user)
|
99
|
+
|
100
|
+
puts "\n\nAll Expression"
|
101
|
+
all_expression = Flipper.all(plan_expression, age_expression)
|
102
|
+
refute Flipper.enabled?(:something, user)
|
103
|
+
|
104
|
+
puts "Enabling all expression"
|
105
|
+
Flipper.enable :something, all_expression
|
106
|
+
assert Flipper.enabled?(:something, user)
|
107
|
+
refute Flipper.enabled?(:something, admin_user)
|
108
|
+
refute Flipper.enabled?(:something, other_user)
|
109
|
+
|
110
|
+
puts "Disabling all expression"
|
111
|
+
reset
|
112
|
+
refute Flipper.enabled?(:something, user)
|
113
|
+
|
114
|
+
puts "\n\nNested Expression"
|
115
|
+
nested_expression = Flipper.any(admin_expression, all_expression)
|
116
|
+
refute Flipper.enabled?(:something, user)
|
117
|
+
refute Flipper.enabled?(:something, admin_user)
|
118
|
+
refute Flipper.enabled?(:something, other_user)
|
119
|
+
|
120
|
+
puts "Enabling nested expression"
|
121
|
+
Flipper.enable :something, nested_expression
|
122
|
+
assert Flipper.enabled?(:something, user)
|
123
|
+
assert Flipper.enabled?(:something, admin_user)
|
124
|
+
refute Flipper.enabled?(:something, other_user)
|
125
|
+
|
126
|
+
puts "Disabling nested expression"
|
127
|
+
reset
|
128
|
+
refute Flipper.enabled?(:something, user)
|
129
|
+
refute Flipper.enabled?(:something, admin_user)
|
130
|
+
refute Flipper.enabled?(:something, other_user)
|
131
|
+
|
132
|
+
puts "\n\nBoolean Expression"
|
133
|
+
boolean_expression = Flipper.boolean(true)
|
134
|
+
Flipper.enable :something, boolean_expression
|
135
|
+
assert Flipper.enabled?(:something)
|
136
|
+
assert Flipper.enabled?(:something, user)
|
137
|
+
reset
|
138
|
+
|
139
|
+
puts "\n\nSet of Actors Expression"
|
140
|
+
set_of_actors_expression = Flipper.any(
|
141
|
+
Flipper.property(:flipper_id).eq("User;1"),
|
142
|
+
Flipper.property(:flipper_id).eq("User;3"),
|
143
|
+
)
|
144
|
+
Flipper.enable :something, set_of_actors_expression
|
145
|
+
assert Flipper.enabled?(:something, user)
|
146
|
+
assert Flipper.enabled?(:something, other_user)
|
147
|
+
refute Flipper.enabled?(:something, admin_user)
|
148
|
+
reset
|
149
|
+
|
150
|
+
puts "\n\n% of Actors Expression"
|
151
|
+
percentage_of_actors = Flipper.property(:flipper_id).percentage_of_actors(30)
|
152
|
+
Flipper.enable :something, percentage_of_actors
|
153
|
+
refute Flipper.enabled?(:something, user)
|
154
|
+
refute Flipper.enabled?(:something, other_user)
|
155
|
+
assert Flipper.enabled?(:something, admin_user)
|
156
|
+
reset
|
157
|
+
|
158
|
+
puts "\n\n% of Actors Per Type Expression"
|
159
|
+
percentage_of_actors_per_type = Flipper.any(
|
160
|
+
Flipper.all(
|
161
|
+
Flipper.property(:type).eq("User"),
|
162
|
+
Flipper.property(:flipper_id).percentage_of_actors(40),
|
163
|
+
),
|
164
|
+
Flipper.all(
|
165
|
+
Flipper.property(:type).eq("Org"),
|
166
|
+
Flipper.property(:flipper_id).percentage_of_actors(10),
|
167
|
+
)
|
168
|
+
)
|
169
|
+
Flipper.enable :something, percentage_of_actors_per_type
|
170
|
+
refute Flipper.enabled?(:something, user) # not in the 40% enabled for Users
|
171
|
+
assert Flipper.enabled?(:something, other_user)
|
172
|
+
assert Flipper.enabled?(:something, admin_user)
|
173
|
+
refute Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs
|
174
|
+
reset
|
175
|
+
|
176
|
+
puts "\n\nPercentage of Time Expression"
|
177
|
+
percentage_of_time_expression = Flipper.random(100).lt(50)
|
178
|
+
Flipper.enable :something, percentage_of_time_expression
|
179
|
+
results = (1..10000).map { |n| Flipper.enabled?(:something, user) }
|
180
|
+
enabled, disabled = results.partition { |r| r }
|
181
|
+
p enabled: enabled.size
|
182
|
+
p disabled: disabled.size
|
183
|
+
assert (4_700..5_200).include?(enabled.size)
|
184
|
+
assert (4_700..5_200).include?(disabled.size)
|
185
|
+
reset
|
186
|
+
|
187
|
+
puts "\n\nChanging single expression to all expression"
|
188
|
+
Flipper.enable :something, plan_expression
|
189
|
+
Flipper.enable :something, Flipper.expression(:something).all.add(age_expression)
|
190
|
+
assert Flipper.enabled?(:something, user)
|
191
|
+
refute Flipper.enabled?(:something, admin_user)
|
192
|
+
refute Flipper.enabled?(:something, other_user)
|
193
|
+
|
194
|
+
puts "\n\nChanging single expression to any expression"
|
195
|
+
Flipper.enable :something, plan_expression
|
196
|
+
Flipper.enable :something, Flipper.expression(:something).any.add(age_expression, admin_expression)
|
197
|
+
assert Flipper.enabled?(:something, user)
|
198
|
+
assert Flipper.enabled?(:something, admin_user)
|
199
|
+
refute Flipper.enabled?(:something, other_user)
|
200
|
+
|
201
|
+
puts "\n\nChanging single expression to any expression by adding to condition"
|
202
|
+
Flipper.enable :something, plan_expression
|
203
|
+
Flipper.enable :something, Flipper.expression(:something).add(admin_expression)
|
204
|
+
assert Flipper.enabled?(:something, user)
|
205
|
+
assert Flipper.enabled?(:something, admin_user)
|
206
|
+
refute Flipper.enabled?(:something, other_user)
|
207
|
+
|
208
|
+
puts "\n\nEnabling based on time"
|
209
|
+
scheduled_time_expression = Flipper.property(:now).gte(NOW)
|
210
|
+
Flipper.enable :something, scheduled_time_expression
|
211
|
+
assert Flipper.enabled?(:something, user)
|
212
|
+
assert Flipper.enabled?(:something, admin_user)
|
213
|
+
refute Flipper.enabled?(:something, other_user)
|
data/examples/strict.rb
ADDED
@@ -0,0 +1,18 @@
|
|
1
|
+
require 'bundler/setup'
|
2
|
+
require 'flipper'
|
3
|
+
|
4
|
+
adapter = Flipper::Adapters::Strict.new(Flipper::Adapters::Memory.new)
|
5
|
+
flipper = Flipper.new(adapter)
|
6
|
+
|
7
|
+
begin
|
8
|
+
puts "Checking :unknown_feature, which should raise an error."
|
9
|
+
flipper.enabled?(:unknown_feature)
|
10
|
+
warn "An error was not raised, but should have been"
|
11
|
+
exit 1
|
12
|
+
rescue Flipper::Adapters::Strict::NotFound => exception
|
13
|
+
puts "Ok, the exepcted error was raised: #{exception.message}"
|
14
|
+
end
|
15
|
+
|
16
|
+
puts "Flipper.add(:new_feature)"
|
17
|
+
flipper.add(:new_feature)
|
18
|
+
puts "Flipper.enabled?(:new_feature) => #{flipper.enabled?(:new_feature)}"
|
data/flipper.gemspec
CHANGED
@@ -23,7 +23,7 @@ ignored_test_files.flatten!.uniq!
|
|
23
23
|
Gem::Specification.new do |gem|
|
24
24
|
gem.authors = ['John Nunemaker']
|
25
25
|
gem.email = 'support@flippercloud.io'
|
26
|
-
gem.summary = '
|
26
|
+
gem.summary = 'Beautiful, performant feature flags for Ruby.'
|
27
27
|
gem.homepage = 'https://www.flippercloud.io/docs'
|
28
28
|
gem.license = 'MIT'
|
29
29
|
|
@@ -35,5 +35,4 @@ Gem::Specification.new do |gem|
|
|
35
35
|
gem.metadata = Flipper::METADATA
|
36
36
|
|
37
37
|
gem.add_dependency 'concurrent-ruby', '< 2'
|
38
|
-
gem.add_dependency 'brow', '~> 0.4.1'
|
39
38
|
end
|
data/lib/flipper/actor.rb
CHANGED
@@ -2,14 +2,17 @@
|
|
2
2
|
# to Flipper::Feature#enabled?.
|
3
3
|
module Flipper
|
4
4
|
class Actor
|
5
|
-
attr_reader :flipper_id
|
5
|
+
attr_reader :flipper_id, :flipper_properties
|
6
6
|
|
7
|
-
def initialize(flipper_id)
|
7
|
+
def initialize(flipper_id, flipper_properties = {})
|
8
8
|
@flipper_id = flipper_id
|
9
|
+
@flipper_properties = flipper_properties
|
9
10
|
end
|
10
11
|
|
11
12
|
def eql?(other)
|
12
|
-
self.class.eql?(other.class) &&
|
13
|
+
self.class.eql?(other.class) &&
|
14
|
+
@flipper_id == other.flipper_id &&
|
15
|
+
@flipper_properties == other.flipper_properties
|
13
16
|
end
|
14
17
|
alias_method :==, :eql?
|
15
18
|
|
data/lib/flipper/adapter.rb
CHANGED
@@ -12,6 +12,7 @@ module Flipper
|
|
12
12
|
boolean: nil,
|
13
13
|
groups: Set.new,
|
14
14
|
actors: Set.new,
|
15
|
+
expression: nil,
|
15
16
|
percentage_of_actors: nil,
|
16
17
|
percentage_of_time: nil,
|
17
18
|
}
|
@@ -23,6 +24,10 @@ module Flipper
|
|
23
24
|
end
|
24
25
|
end
|
25
26
|
|
27
|
+
def read_only?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
26
31
|
# Public: Get all features and gate values in one call. Defaults to one call
|
27
32
|
# to features and another to get_multi. Feel free to override per adapter to
|
28
33
|
# make this more efficient.
|
@@ -63,6 +68,11 @@ module Flipper
|
|
63
68
|
def default_config
|
64
69
|
self.class.default_config
|
65
70
|
end
|
71
|
+
|
72
|
+
# Public: default name of the adapter
|
73
|
+
def name
|
74
|
+
@name ||= self.class.name.split('::').last.split(/(?=[A-Z])/).join('_').downcase.to_sym
|
75
|
+
end
|
66
76
|
end
|
67
77
|
end
|
68
78
|
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Flipper
|
2
|
+
# Builds an adapter from a stack of adapters.
|
3
|
+
#
|
4
|
+
# adapter = Flipper::AdapterBuilder.new do
|
5
|
+
# use Flipper::Adapters::Strict
|
6
|
+
# use Flipper::Adapters::Memoizer
|
7
|
+
# store Flipper::Adapters::Memory
|
8
|
+
# end.to_adapter
|
9
|
+
#
|
10
|
+
class AdapterBuilder
|
11
|
+
def initialize(&block)
|
12
|
+
@stack = []
|
13
|
+
|
14
|
+
# Default to memory adapter
|
15
|
+
store Flipper::Adapters::Memory
|
16
|
+
|
17
|
+
block.arity == 0 ? instance_eval(&block) : block.call(self) if block
|
18
|
+
end
|
19
|
+
|
20
|
+
if RUBY_VERSION >= '3.0'
|
21
|
+
def use(klass, *args, **kwargs, &block)
|
22
|
+
@stack.push ->(adapter) { klass.new(adapter, *args, **kwargs, &block) }
|
23
|
+
end
|
24
|
+
else
|
25
|
+
def use(klass, *args, &block)
|
26
|
+
@stack.push ->(adapter) { klass.new(adapter, *args, &block) }
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
if RUBY_VERSION >= '3.0'
|
31
|
+
def store(adapter, *args, **kwargs, &block)
|
32
|
+
@store = adapter.respond_to?(:call) ? adapter : -> { adapter.new(*args, **kwargs, &block) }
|
33
|
+
end
|
34
|
+
else
|
35
|
+
def store(adapter, *args, &block)
|
36
|
+
@store = adapter.respond_to?(:call) ? adapter : -> { adapter.new(*args, &block) }
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
def to_adapter
|
41
|
+
@stack.reverse.inject(@store.call) { |adapter, wrapper| wrapper.call(adapter) }
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -3,8 +3,7 @@ module Flipper
|
|
3
3
|
class DualWrite
|
4
4
|
include ::Flipper::Adapter
|
5
5
|
|
6
|
-
|
7
|
-
attr_reader :name, :local, :remote
|
6
|
+
attr_reader :local, :remote
|
8
7
|
|
9
8
|
# Public: Build a new sync instance.
|
10
9
|
#
|
@@ -12,7 +11,6 @@ module Flipper
|
|
12
11
|
# remote - The remote flipper adapter that writes should go to first (in
|
13
12
|
# addition to the local adapter).
|
14
13
|
def initialize(local, remote, options = {})
|
15
|
-
@name = :dual_write
|
16
14
|
@local = local
|
17
15
|
@remote = remote
|
18
16
|
end
|
@@ -3,9 +3,6 @@ module Flipper
|
|
3
3
|
class Failover
|
4
4
|
include ::Flipper::Adapter
|
5
5
|
|
6
|
-
# Public: The name of the adapter.
|
7
|
-
attr_reader :name
|
8
|
-
|
9
6
|
# Public: Build a new failover instance.
|
10
7
|
#
|
11
8
|
# primary - The primary flipper adapter.
|
@@ -17,7 +14,6 @@ module Flipper
|
|
17
14
|
# :errors - Array of exception types for which to failover
|
18
15
|
|
19
16
|
def initialize(primary, secondary, options = {})
|
20
|
-
@name = :failover
|
21
17
|
@primary = primary
|
22
18
|
@secondary = secondary
|
23
19
|
|
@@ -3,9 +3,6 @@ module Flipper
|
|
3
3
|
class Failsafe
|
4
4
|
include ::Flipper::Adapter
|
5
5
|
|
6
|
-
# Public: The name of the adapter.
|
7
|
-
attr_reader :name
|
8
|
-
|
9
6
|
# Public: Build a new Failsafe instance.
|
10
7
|
#
|
11
8
|
# adapter - Flipper adapter to guard.
|
@@ -15,7 +12,6 @@ module Flipper
|
|
15
12
|
def initialize(adapter, options = {})
|
16
13
|
@adapter = adapter
|
17
14
|
@errors = options.fetch(:errors, [StandardError])
|
18
|
-
@name = :failsafe
|
19
15
|
end
|
20
16
|
|
21
17
|
def features
|
@@ -14,6 +14,12 @@ module Flipper
|
|
14
14
|
|
15
15
|
HTTPS_SCHEME = "https".freeze
|
16
16
|
|
17
|
+
CLIENT_FRAMEWORKS = {
|
18
|
+
rails: -> { Rails.version if defined?(Rails) },
|
19
|
+
sinatra: -> { Sinatra::VERSION if defined?(Sinatra) },
|
20
|
+
hanami: -> { Hanami::VERSION if defined?(Hanami) },
|
21
|
+
}
|
22
|
+
|
17
23
|
attr_reader :uri, :headers
|
18
24
|
attr_reader :basic_auth_username, :basic_auth_password
|
19
25
|
attr_reader :read_timeout, :open_timeout, :write_timeout, :max_retries, :debug_output
|
@@ -30,6 +36,10 @@ module Flipper
|
|
30
36
|
@debug_output = options[:debug_output]
|
31
37
|
end
|
32
38
|
|
39
|
+
def add_header(key, value)
|
40
|
+
@headers[key] = value
|
41
|
+
end
|
42
|
+
|
33
43
|
def get(path)
|
34
44
|
perform Net::HTTP::Get, path, @headers
|
35
45
|
end
|
@@ -77,18 +87,23 @@ module Flipper
|
|
77
87
|
|
78
88
|
def build_request(http_method, uri, headers, options)
|
79
89
|
request_headers = {
|
80
|
-
|
81
|
-
|
82
|
-
|
83
|
-
|
84
|
-
|
85
|
-
|
86
|
-
|
90
|
+
client_language: "ruby",
|
91
|
+
client_language_version: "#{RUBY_VERSION} p#{RUBY_PATCHLEVEL} (#{RUBY_RELEASE_DATE})",
|
92
|
+
client_platform: RUBY_PLATFORM,
|
93
|
+
client_engine: defined?(RUBY_ENGINE) ? RUBY_ENGINE : "",
|
94
|
+
client_pid: Process.pid.to_s,
|
95
|
+
client_thread: Thread.current.object_id.to_s,
|
96
|
+
client_hostname: Socket.gethostname,
|
87
97
|
}.merge(headers)
|
88
98
|
|
89
99
|
body = options[:body]
|
90
100
|
request = http_method.new(uri.request_uri)
|
91
101
|
request.initialize_http_header(request_headers)
|
102
|
+
|
103
|
+
client_frameworks.each do |framework, version|
|
104
|
+
request.add_field("Client-Framework", [framework, version].join("="))
|
105
|
+
end
|
106
|
+
|
92
107
|
request.body = body if body
|
93
108
|
|
94
109
|
if @basic_auth_username && @basic_auth_password
|
@@ -97,6 +112,10 @@ module Flipper
|
|
97
112
|
|
98
113
|
request
|
99
114
|
end
|
115
|
+
|
116
|
+
def client_frameworks
|
117
|
+
CLIENT_FRAMEWORKS.transform_values { |detect| detect.call rescue nil }.compact
|
118
|
+
end
|
100
119
|
end
|
101
120
|
end
|
102
121
|
end
|