flipper 0.28.3 → 1.4.2
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 +56 -10
- data/.github/workflows/examples.yml +54 -11
- data/.github/workflows/release.yml +54 -0
- data/.superset/config.json +4 -0
- data/CLAUDE.md +93 -0
- data/Changelog.md +1 -559
- data/Gemfile +17 -8
- data/README.md +34 -29
- 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/app.ru +12 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/basic.rb +22 -0
- data/examples/cloud/cloud_setup.rb +20 -0
- data/examples/cloud/forked.rb +36 -0
- data/examples/cloud/import.rb +17 -0
- data/examples/cloud/poll_interval/README.md +111 -0
- data/examples/cloud/poll_interval/client.rb +108 -0
- data/examples/cloud/poll_interval/server.rb +98 -0
- data/examples/cloud/threaded.rb +33 -0
- data/examples/dsl.rb +0 -14
- data/examples/expressions.rb +237 -0
- data/examples/strict.rb +18 -0
- data/exe/flipper +5 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +8 -4
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +27 -1
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/actor_limit.rb +54 -0
- data/lib/flipper/adapters/cache_base.rb +161 -0
- data/lib/flipper/adapters/dual_write.rb +7 -5
- data/lib/flipper/adapters/failover.rb +9 -7
- data/lib/flipper/adapters/failsafe.rb +2 -6
- data/lib/flipper/adapters/http/client.rb +53 -14
- data/lib/flipper/adapters/http/error.rb +2 -2
- data/lib/flipper/adapters/http.rb +56 -16
- data/lib/flipper/adapters/instrumented.rb +2 -6
- data/lib/flipper/adapters/memoizable.rb +17 -22
- data/lib/flipper/adapters/memory.rb +5 -7
- data/lib/flipper/adapters/operation_logger.rb +18 -92
- data/lib/flipper/adapters/poll.rb +16 -3
- data/lib/flipper/adapters/pstore.rb +18 -12
- data/lib/flipper/adapters/read_only.rb +8 -41
- data/lib/flipper/adapters/strict.rb +75 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
- data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
- data/lib/flipper/adapters/sync.rb +7 -7
- data/lib/flipper/adapters/wrapper.rb +54 -0
- data/lib/flipper/cli.rb +314 -0
- data/lib/flipper/cloud/configuration.rb +267 -0
- data/lib/flipper/cloud/dsl.rb +27 -0
- data/lib/flipper/cloud/message_verifier.rb +95 -0
- data/lib/flipper/cloud/middleware.rb +63 -0
- data/lib/flipper/cloud/migrate.rb +71 -0
- data/lib/flipper/cloud/routes.rb +14 -0
- 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 +54 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +48 -43
- data/lib/flipper/engine.rb +102 -0
- 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/equal.rb +9 -0
- data/lib/flipper/expressions/feature_enabled.rb +34 -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 +16 -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 +8 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/model/active_record.rb +23 -0
- data/lib/flipper/poller.rb +54 -12
- data/lib/flipper/serializers/gzip.rb +22 -0
- data/lib/flipper/serializers/json.rb +17 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
- data/lib/flipper/test/shared_adapter_test.rb +77 -58
- 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 +61 -8
- 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 +21 -0
- data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
- data/spec/flipper/adapters/dual_write_spec.rb +15 -2
- data/spec/flipper/adapters/failover_spec.rb +12 -0
- data/spec/flipper/adapters/http/client_spec.rb +61 -0
- data/spec/flipper/adapters/http_spec.rb +375 -74
- data/spec/flipper/adapters/instrumented_spec.rb +1 -1
- data/spec/flipper/adapters/memoizable_spec.rb +21 -21
- data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
- data/spec/flipper/adapters/poll_spec.rb +41 -0
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- data/spec/flipper/adapters/strict_spec.rb +122 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
- data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
- data/spec/flipper/adapters/sync_spec.rb +13 -0
- data/spec/flipper/cli_spec.rb +217 -0
- data/spec/flipper/cloud/configuration_spec.rb +251 -0
- data/spec/flipper/cloud/dsl_spec.rb +82 -0
- data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
- data/spec/flipper/cloud/middleware_spec.rb +289 -0
- data/spec/flipper/cloud/migrate_spec.rb +160 -0
- 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 +186 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +34 -73
- data/spec/flipper/engine_spec.rb +374 -0
- 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/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 +29 -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 +190 -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 +107 -7
- data/spec/flipper/model/active_record_spec.rb +72 -0
- data/spec/flipper/poller_spec.rb +347 -4
- 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 +247 -16
- data/spec/flipper_spec.rb +92 -29
- data/spec/spec_helper.rb +24 -17
- 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 +139 -75
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -109
|
@@ -0,0 +1,237 @@
|
|
|
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
|
+
org = Org.new(1, {
|
|
35
|
+
"type" => "Org",
|
|
36
|
+
"id" => 1,
|
|
37
|
+
})
|
|
38
|
+
|
|
39
|
+
user = User.new(1, {
|
|
40
|
+
"type" => "User",
|
|
41
|
+
"id" => 1,
|
|
42
|
+
"plan" => "basic",
|
|
43
|
+
"age" => 39,
|
|
44
|
+
"team_user" => true,
|
|
45
|
+
})
|
|
46
|
+
|
|
47
|
+
admin_user = User.new(2, {
|
|
48
|
+
"type" => "User",
|
|
49
|
+
"id" => 2,
|
|
50
|
+
"admin" => true,
|
|
51
|
+
"team_user" => true,
|
|
52
|
+
})
|
|
53
|
+
|
|
54
|
+
other_user = User.new(3, {
|
|
55
|
+
"type" => "User",
|
|
56
|
+
"id" => 3,
|
|
57
|
+
"plan" => "plus",
|
|
58
|
+
"age" => 18,
|
|
59
|
+
"org_admin" => true,
|
|
60
|
+
})
|
|
61
|
+
|
|
62
|
+
age_expression = Flipper.property(:age).gte(21)
|
|
63
|
+
plan_expression = Flipper.property(:plan).eq("basic")
|
|
64
|
+
admin_expression = Flipper.property(:admin).eq(true)
|
|
65
|
+
|
|
66
|
+
puts "Single Expression"
|
|
67
|
+
refute Flipper.enabled?(:something, user)
|
|
68
|
+
|
|
69
|
+
puts "Enabling single expression"
|
|
70
|
+
Flipper.enable :something, plan_expression
|
|
71
|
+
assert Flipper.enabled?(:something, user)
|
|
72
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
73
|
+
refute Flipper.enabled?(:something, other_user)
|
|
74
|
+
|
|
75
|
+
puts "Disabling single expression"
|
|
76
|
+
reset
|
|
77
|
+
refute Flipper.enabled?(:something, user)
|
|
78
|
+
|
|
79
|
+
puts "\n\nAny Expression"
|
|
80
|
+
any_expression = Flipper.any(plan_expression, age_expression)
|
|
81
|
+
refute Flipper.enabled?(:something, user)
|
|
82
|
+
|
|
83
|
+
puts "Enabling any expression"
|
|
84
|
+
Flipper.enable :something, any_expression
|
|
85
|
+
assert Flipper.enabled?(:something, user)
|
|
86
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
87
|
+
refute Flipper.enabled?(:something, other_user)
|
|
88
|
+
|
|
89
|
+
puts "Disabling any expression"
|
|
90
|
+
reset
|
|
91
|
+
refute Flipper.enabled?(:something, user)
|
|
92
|
+
|
|
93
|
+
puts "\n\nAll Expression"
|
|
94
|
+
all_expression = Flipper.all(plan_expression, age_expression)
|
|
95
|
+
refute Flipper.enabled?(:something, user)
|
|
96
|
+
|
|
97
|
+
puts "Enabling all expression"
|
|
98
|
+
Flipper.enable :something, all_expression
|
|
99
|
+
assert Flipper.enabled?(:something, user)
|
|
100
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
101
|
+
refute Flipper.enabled?(:something, other_user)
|
|
102
|
+
|
|
103
|
+
puts "Disabling all expression"
|
|
104
|
+
reset
|
|
105
|
+
refute Flipper.enabled?(:something, user)
|
|
106
|
+
|
|
107
|
+
puts "\n\nNested Expression"
|
|
108
|
+
nested_expression = Flipper.any(admin_expression, all_expression)
|
|
109
|
+
refute Flipper.enabled?(:something, user)
|
|
110
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
111
|
+
refute Flipper.enabled?(:something, other_user)
|
|
112
|
+
|
|
113
|
+
puts "Enabling nested expression"
|
|
114
|
+
Flipper.enable :something, nested_expression
|
|
115
|
+
assert Flipper.enabled?(:something, user)
|
|
116
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
117
|
+
refute Flipper.enabled?(:something, other_user)
|
|
118
|
+
|
|
119
|
+
puts "Disabling nested expression"
|
|
120
|
+
reset
|
|
121
|
+
refute Flipper.enabled?(:something, user)
|
|
122
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
123
|
+
refute Flipper.enabled?(:something, other_user)
|
|
124
|
+
|
|
125
|
+
puts "\n\nBoolean Expression"
|
|
126
|
+
boolean_expression = Flipper.boolean(true)
|
|
127
|
+
Flipper.enable :something, boolean_expression
|
|
128
|
+
assert Flipper.enabled?(:something)
|
|
129
|
+
assert Flipper.enabled?(:something, user)
|
|
130
|
+
reset
|
|
131
|
+
|
|
132
|
+
puts "\n\nSet of Actors Expression"
|
|
133
|
+
set_of_actors_expression = Flipper.any(
|
|
134
|
+
Flipper.property(:flipper_id).eq("User;1"),
|
|
135
|
+
Flipper.property(:flipper_id).eq("User;3"),
|
|
136
|
+
)
|
|
137
|
+
Flipper.enable :something, set_of_actors_expression
|
|
138
|
+
assert Flipper.enabled?(:something, user)
|
|
139
|
+
assert Flipper.enabled?(:something, other_user)
|
|
140
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
141
|
+
reset
|
|
142
|
+
|
|
143
|
+
puts "\n\n% of Actors Expression"
|
|
144
|
+
percentage_of_actors = Flipper.property(:flipper_id).percentage_of_actors(30)
|
|
145
|
+
Flipper.enable :something, percentage_of_actors
|
|
146
|
+
refute Flipper.enabled?(:something, user)
|
|
147
|
+
refute Flipper.enabled?(:something, other_user)
|
|
148
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
149
|
+
reset
|
|
150
|
+
|
|
151
|
+
puts "\n\n% of Actors Per Type Expression"
|
|
152
|
+
percentage_of_actors_per_type = Flipper.any(
|
|
153
|
+
Flipper.all(
|
|
154
|
+
Flipper.property(:type).eq("User"),
|
|
155
|
+
Flipper.property(:flipper_id).percentage_of_actors(40),
|
|
156
|
+
),
|
|
157
|
+
Flipper.all(
|
|
158
|
+
Flipper.property(:type).eq("Org"),
|
|
159
|
+
Flipper.property(:flipper_id).percentage_of_actors(10),
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
Flipper.enable :something, percentage_of_actors_per_type
|
|
163
|
+
refute Flipper.enabled?(:something, user) # not in the 40% enabled for Users
|
|
164
|
+
assert Flipper.enabled?(:something, other_user)
|
|
165
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
166
|
+
refute Flipper.enabled?(:something, org) # not in the 10% of enabled for Orgs
|
|
167
|
+
reset
|
|
168
|
+
|
|
169
|
+
puts "\n\nPercentage of Time Expression"
|
|
170
|
+
percentage_of_time_expression = Flipper.random(100).lt(50)
|
|
171
|
+
Flipper.enable :something, percentage_of_time_expression
|
|
172
|
+
results = (1..10000).map { |n| Flipper.enabled?(:something, user) }
|
|
173
|
+
enabled, disabled = results.partition { |r| r }
|
|
174
|
+
p enabled: enabled.size
|
|
175
|
+
p disabled: disabled.size
|
|
176
|
+
assert (4_700..5_200).include?(enabled.size)
|
|
177
|
+
assert (4_700..5_200).include?(disabled.size)
|
|
178
|
+
reset
|
|
179
|
+
|
|
180
|
+
puts "\n\nChanging single expression to all expression"
|
|
181
|
+
Flipper.enable :something, plan_expression
|
|
182
|
+
Flipper.enable :something, Flipper.expression(:something).all.add(age_expression)
|
|
183
|
+
assert Flipper.enabled?(:something, user)
|
|
184
|
+
refute Flipper.enabled?(:something, admin_user)
|
|
185
|
+
refute Flipper.enabled?(:something, other_user)
|
|
186
|
+
|
|
187
|
+
puts "\n\nChanging single expression to any expression"
|
|
188
|
+
Flipper.enable :something, plan_expression
|
|
189
|
+
Flipper.enable :something, Flipper.expression(:something).any.add(age_expression, admin_expression)
|
|
190
|
+
assert Flipper.enabled?(:something, user)
|
|
191
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
192
|
+
refute Flipper.enabled?(:something, other_user)
|
|
193
|
+
|
|
194
|
+
puts "\n\nChanging single expression to any expression by adding to condition"
|
|
195
|
+
Flipper.enable :something, plan_expression
|
|
196
|
+
Flipper.enable :something, Flipper.expression(:something).add(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\nEnabling based on time (epoch)"
|
|
202
|
+
scheduled_epoch = Flipper.now.gte(Flipper.time(Time.now.to_i - 86_400))
|
|
203
|
+
Flipper.enable :something, scheduled_epoch
|
|
204
|
+
assert Flipper.enabled?(:something, user)
|
|
205
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
206
|
+
assert Flipper.enabled?(:something, other_user)
|
|
207
|
+
reset
|
|
208
|
+
|
|
209
|
+
puts "\n\nEnabling based on time (datetime)"
|
|
210
|
+
past_time = (Time.now.utc - 86_400).iso8601
|
|
211
|
+
scheduled_datetime = Flipper.now.gte(Flipper.time(past_time))
|
|
212
|
+
Flipper.enable :something, scheduled_datetime
|
|
213
|
+
assert Flipper.enabled?(:something, user)
|
|
214
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
215
|
+
assert Flipper.enabled?(:something, other_user)
|
|
216
|
+
reset
|
|
217
|
+
|
|
218
|
+
puts "\n\nDisabling after a time (expiring feature)"
|
|
219
|
+
future_time = (Time.now.utc + 86_400).iso8601
|
|
220
|
+
expiring_expression = Flipper.now.lt(Flipper.time(future_time))
|
|
221
|
+
Flipper.enable :something, expiring_expression
|
|
222
|
+
assert Flipper.enabled?(:something, user)
|
|
223
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
224
|
+
assert Flipper.enabled?(:something, other_user)
|
|
225
|
+
reset
|
|
226
|
+
|
|
227
|
+
puts "\n\nEnabling within a time window"
|
|
228
|
+
start_time = (Time.now.utc - 86_400).iso8601
|
|
229
|
+
end_time = (Time.now.utc + 86_400).iso8601
|
|
230
|
+
time_window = Flipper.all(
|
|
231
|
+
Flipper.now.gte(Flipper.time(start_time)),
|
|
232
|
+
Flipper.now.lt(Flipper.time(end_time))
|
|
233
|
+
)
|
|
234
|
+
Flipper.enable :something, time_window
|
|
235
|
+
assert Flipper.enabled?(:something, user)
|
|
236
|
+
assert Flipper.enabled?(:something, admin_user)
|
|
237
|
+
assert 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/exe/flipper
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
|
2
|
+
require File.expand_path('../lib/flipper/version', __FILE__)
|
|
3
|
+
require File.expand_path('../lib/flipper/metadata', __FILE__)
|
|
4
|
+
|
|
5
|
+
Gem::Specification.new do |gem|
|
|
6
|
+
gem.authors = ['John Nunemaker']
|
|
7
|
+
gem.email = 'support@flippercloud.io'
|
|
8
|
+
gem.summary = '[DEPRECATED] This gem has been merged into the `flipper` gem'
|
|
9
|
+
gem.license = 'MIT'
|
|
10
|
+
gem.homepage = 'https://www.flippercloud.io'
|
|
11
|
+
|
|
12
|
+
gem.files = [ 'lib/flipper-cloud.rb', 'lib/flipper/version.rb' ]
|
|
13
|
+
gem.name = 'flipper-cloud'
|
|
14
|
+
gem.require_paths = ['lib']
|
|
15
|
+
gem.version = Flipper::VERSION
|
|
16
|
+
gem.metadata = Flipper::METADATA
|
|
17
|
+
|
|
18
|
+
gem.add_dependency 'flipper', "~> #{Flipper::VERSION}"
|
|
19
|
+
end
|
data/flipper.gemspec
CHANGED
|
@@ -6,7 +6,7 @@ plugin_files = []
|
|
|
6
6
|
plugin_test_files = []
|
|
7
7
|
|
|
8
8
|
Dir['flipper-*.gemspec'].map do |gemspec|
|
|
9
|
-
spec =
|
|
9
|
+
spec = Gem::Specification.load(gemspec)
|
|
10
10
|
plugin_files << spec.files
|
|
11
11
|
plugin_test_files << spec.files
|
|
12
12
|
end
|
|
@@ -22,11 +22,13 @@ ignored_test_files.flatten!.uniq!
|
|
|
22
22
|
|
|
23
23
|
Gem::Specification.new do |gem|
|
|
24
24
|
gem.authors = ['John Nunemaker']
|
|
25
|
-
gem.email =
|
|
26
|
-
gem.summary = '
|
|
27
|
-
gem.homepage = 'https://
|
|
25
|
+
gem.email = 'support@flippercloud.io'
|
|
26
|
+
gem.summary = 'Beautiful, performant feature flags for Ruby and Rails.'
|
|
27
|
+
gem.homepage = 'https://www.flippercloud.io/docs'
|
|
28
28
|
gem.license = 'MIT'
|
|
29
29
|
|
|
30
|
+
gem.bindir = "exe"
|
|
31
|
+
gem.executables = `git ls-files -- exe/*`.split("\n").map { |f| File.basename(f) }
|
|
30
32
|
gem.files = `git ls-files`.split("\n") - ignored_files + ['lib/flipper/version.rb']
|
|
31
33
|
gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n") - ignored_test_files
|
|
32
34
|
gem.name = 'flipper'
|
|
@@ -35,4 +37,6 @@ Gem::Specification.new do |gem|
|
|
|
35
37
|
gem.metadata = Flipper::METADATA
|
|
36
38
|
|
|
37
39
|
gem.add_dependency 'concurrent-ruby', '< 2'
|
|
40
|
+
|
|
41
|
+
gem.required_ruby_version = ">= #{Flipper::REQUIRED_RUBY_VERSION}"
|
|
38
42
|
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,10 +24,14 @@ 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.
|
|
29
|
-
def get_all
|
|
34
|
+
def get_all(**kwargs)
|
|
30
35
|
instances = features.map { |key| Flipper::Feature.new(key, self) }
|
|
31
36
|
get_multi(instances)
|
|
32
37
|
end
|
|
@@ -63,6 +68,27 @@ 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
|
|
76
|
+
|
|
77
|
+
# Public: Returns a string representation of the adapter stack for debugging.
|
|
78
|
+
# Shows the full chain of wrapped adapters.
|
|
79
|
+
#
|
|
80
|
+
# Examples:
|
|
81
|
+
# "memoizable -> active_support_cache_store -> active_record"
|
|
82
|
+
# "memoizable -> failover(primary: redis, secondary: memory)"
|
|
83
|
+
#
|
|
84
|
+
# Returns a String.
|
|
85
|
+
def adapter_stack
|
|
86
|
+
if respond_to?(:adapter) && adapter
|
|
87
|
+
"#{name} -> #{adapter.adapter_stack}"
|
|
88
|
+
else
|
|
89
|
+
name.to_s
|
|
90
|
+
end
|
|
91
|
+
end
|
|
66
92
|
end
|
|
67
93
|
end
|
|
68
94
|
|
|
@@ -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::Memoizable
|
|
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
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
require "flipper/adapters/wrapper"
|
|
2
|
+
|
|
3
|
+
module Flipper
|
|
4
|
+
module Adapters
|
|
5
|
+
class ActorLimit < Wrapper
|
|
6
|
+
LimitExceeded = Class.new(Flipper::Error)
|
|
7
|
+
|
|
8
|
+
attr_reader :limit
|
|
9
|
+
|
|
10
|
+
class << self
|
|
11
|
+
# Returns whether sync mode is enabled for the current thread.
|
|
12
|
+
# When sync mode is enabled, actor limits are not enforced,
|
|
13
|
+
# allowing sync operations to bring local state in line with
|
|
14
|
+
# remote state regardless of limits.
|
|
15
|
+
def sync_mode
|
|
16
|
+
Thread.current[:flipper_actor_limit_sync_mode]
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def sync_mode=(value)
|
|
20
|
+
Thread.current[:flipper_actor_limit_sync_mode] = value
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
# Executes a block with sync mode enabled. Actor limits will
|
|
24
|
+
# not be enforced within the block.
|
|
25
|
+
def with_sync_mode
|
|
26
|
+
old_value = sync_mode
|
|
27
|
+
self.sync_mode = true
|
|
28
|
+
yield
|
|
29
|
+
ensure
|
|
30
|
+
self.sync_mode = old_value
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def initialize(adapter, limit = 100)
|
|
35
|
+
super(adapter)
|
|
36
|
+
@limit = limit
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def enable(feature, gate, resource)
|
|
40
|
+
if gate.is_a?(Flipper::Gates::Actor) && !self.class.sync_mode && over_limit?(feature)
|
|
41
|
+
raise LimitExceeded, "Actor limit of #{@limit} exceeded for feature #{feature.key}. See https://www.flippercloud.io/docs/features/actors#limitations"
|
|
42
|
+
else
|
|
43
|
+
super
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def over_limit?(feature)
|
|
50
|
+
feature.actors_value.size >= @limit
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,161 @@
|
|
|
1
|
+
module Flipper
|
|
2
|
+
module Adapters
|
|
3
|
+
# Base class for caching adapters. Inherit from this and then override
|
|
4
|
+
# cache_fetch, cache_read_multi, cache_write, and cache_delete.
|
|
5
|
+
class CacheBase
|
|
6
|
+
include ::Flipper::Adapter
|
|
7
|
+
|
|
8
|
+
# Public: The adapter being cached.
|
|
9
|
+
attr_reader :adapter
|
|
10
|
+
|
|
11
|
+
# Public: The ActiveSupport::Cache::Store to cache with.
|
|
12
|
+
attr_reader :cache
|
|
13
|
+
|
|
14
|
+
# Public: The ttl for all cached data.
|
|
15
|
+
attr_reader :ttl
|
|
16
|
+
|
|
17
|
+
# Public: The cache key where the set of known features is cached.
|
|
18
|
+
attr_reader :features_cache_key
|
|
19
|
+
|
|
20
|
+
# Public: The cache key where the set of all features with gates is cached.
|
|
21
|
+
attr_reader :get_all_cache_key
|
|
22
|
+
|
|
23
|
+
# Public: Alias expires_in to ttl for compatibility.
|
|
24
|
+
alias_method :expires_in, :ttl
|
|
25
|
+
|
|
26
|
+
def initialize(adapter, cache, ttl = 300, prefix: nil)
|
|
27
|
+
@adapter = adapter
|
|
28
|
+
@cache = cache
|
|
29
|
+
@ttl = ttl
|
|
30
|
+
|
|
31
|
+
@cache_version = 'v1'.freeze
|
|
32
|
+
@namespace = "flipper/#{@cache_version}"
|
|
33
|
+
@namespace = @namespace.prepend(prefix) if prefix
|
|
34
|
+
@features_cache_key = "#{@namespace}/features"
|
|
35
|
+
@get_all_cache_key = "#{@namespace}/get_all"
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Public: Expire the cache for the set of all features with gates.
|
|
39
|
+
def expire_get_all_cache
|
|
40
|
+
cache_delete @get_all_cache_key
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Public: Expire the cache for the set of known feature names.
|
|
44
|
+
def expire_features_cache
|
|
45
|
+
cache_delete @features_cache_key
|
|
46
|
+
expire_get_all_cache
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Public: Expire the cache for a given feature.
|
|
50
|
+
def expire_feature_cache(key)
|
|
51
|
+
cache_delete feature_cache_key(key)
|
|
52
|
+
expire_get_all_cache
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
# Public
|
|
56
|
+
def features
|
|
57
|
+
read_feature_keys
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
# Public
|
|
61
|
+
def add(feature)
|
|
62
|
+
result = @adapter.add(feature)
|
|
63
|
+
expire_features_cache
|
|
64
|
+
result
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
# Public
|
|
68
|
+
def remove(feature)
|
|
69
|
+
result = @adapter.remove(feature)
|
|
70
|
+
expire_features_cache
|
|
71
|
+
expire_feature_cache(feature.key)
|
|
72
|
+
result
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
# Public
|
|
76
|
+
def clear(feature)
|
|
77
|
+
result = @adapter.clear(feature)
|
|
78
|
+
expire_feature_cache(feature.key)
|
|
79
|
+
result
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Public
|
|
83
|
+
def get(feature)
|
|
84
|
+
read_feature(feature)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
# Public
|
|
88
|
+
def get_multi(features)
|
|
89
|
+
read_many_features(features)
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
# Public
|
|
93
|
+
def get_all(**kwargs)
|
|
94
|
+
cache_fetch(@get_all_cache_key) {
|
|
95
|
+
result = read_all_features(**kwargs)
|
|
96
|
+
cache_write @features_cache_key, result.keys.to_set
|
|
97
|
+
result
|
|
98
|
+
}
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Public
|
|
102
|
+
def enable(feature, gate, thing)
|
|
103
|
+
result = @adapter.enable(feature, gate, thing)
|
|
104
|
+
expire_feature_cache(feature.key)
|
|
105
|
+
result
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
# Public
|
|
109
|
+
def disable(feature, gate, thing)
|
|
110
|
+
result = @adapter.disable(feature, gate, thing)
|
|
111
|
+
expire_feature_cache(feature.key)
|
|
112
|
+
result
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
# Public: Generate the cache key for a given feature.
|
|
116
|
+
#
|
|
117
|
+
# key - The String or Symbol feature key.
|
|
118
|
+
def feature_cache_key(key)
|
|
119
|
+
"#{@namespace}/feature/#{key}"
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
private
|
|
123
|
+
|
|
124
|
+
def read_all_features(**kwargs)
|
|
125
|
+
@adapter.get_all(**kwargs)
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Private: Returns the Set of known feature keys.
|
|
129
|
+
def read_feature_keys
|
|
130
|
+
cache_fetch(@features_cache_key) { @adapter.features }
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
# Private: Read through caching for a single feature.
|
|
134
|
+
def read_feature(feature)
|
|
135
|
+
cache_fetch(feature_cache_key(feature.key)) { @adapter.get(feature) }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
# Private: Given an array of features, attempts to read through cache in
|
|
139
|
+
# as few network calls as possible.
|
|
140
|
+
def read_many_features(features)
|
|
141
|
+
keys = features.map { |feature| feature_cache_key(feature.key) }
|
|
142
|
+
cache_result = cache_read_multi(keys)
|
|
143
|
+
uncached_features = features.reject { |feature| cache_result[feature_cache_key(feature)] }
|
|
144
|
+
|
|
145
|
+
if uncached_features.any?
|
|
146
|
+
response = @adapter.get_multi(uncached_features)
|
|
147
|
+
response.each do |key, value|
|
|
148
|
+
cache_write feature_cache_key(key), value
|
|
149
|
+
cache_result[feature_cache_key(key)] = value
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
result = {}
|
|
154
|
+
features.each do |feature|
|
|
155
|
+
result[feature.key] = cache_result[feature_cache_key(feature.key)]
|
|
156
|
+
end
|
|
157
|
+
result
|
|
158
|
+
end
|
|
159
|
+
end
|
|
160
|
+
end
|
|
161
|
+
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,11 +11,14 @@ 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
|
|
19
17
|
|
|
18
|
+
def adapter_stack
|
|
19
|
+
"#{name}(local: #{@local.adapter_stack}, remote: #{@remote.adapter_stack})"
|
|
20
|
+
end
|
|
21
|
+
|
|
20
22
|
def features
|
|
21
23
|
@local.features
|
|
22
24
|
end
|
|
@@ -29,8 +31,8 @@ module Flipper
|
|
|
29
31
|
@local.get_multi(features)
|
|
30
32
|
end
|
|
31
33
|
|
|
32
|
-
def get_all
|
|
33
|
-
@local.get_all
|
|
34
|
+
def get_all(**kwargs)
|
|
35
|
+
@local.get_all(**kwargs)
|
|
34
36
|
end
|
|
35
37
|
|
|
36
38
|
def add(feature)
|