flipper 1.0.0 → 1.1.0
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 +42 -0
- data/Gemfile +4 -4
- data/README.md +13 -11
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/flipper.gemspec +1 -2
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +26 -7
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +18 -13
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +0 -4
- data/lib/flipper/adapters/poll.rb +1 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +4 -4
- data/lib/flipper/adapters/strict.rb +47 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/cloud/configuration.rb +121 -52
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
- data/lib/flipper/cloud/telemetry.rb +183 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +28 -3
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +11 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +55 -0
- data/lib/flipper/gate.rb +1 -0
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/poller.rb +1 -1
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
- data/lib/flipper/test/shared_adapter_test.rb +24 -5
- data/lib/flipper/typecast.rb +34 -6
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +38 -1
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/http_spec.rb +39 -5
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +62 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -23
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +156 -0
- data/spec/flipper/cloud_spec.rb +12 -12
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -0
- data/spec/flipper/engine_spec.rb +108 -7
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_spec.rb +360 -1
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +89 -1
- data/spec/spec_helper.rb +5 -0
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +104 -18
- data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -0,0 +1,183 @@
|
|
1
|
+
require "concurrent/map"
|
2
|
+
require "concurrent/timer_task"
|
3
|
+
require "concurrent/executor/fixed_thread_pool"
|
4
|
+
require "flipper/typecast"
|
5
|
+
require "flipper/cloud/telemetry/metric"
|
6
|
+
require "flipper/cloud/telemetry/metric_storage"
|
7
|
+
require "flipper/cloud/telemetry/submitter"
|
8
|
+
|
9
|
+
module Flipper
|
10
|
+
module Cloud
|
11
|
+
class Telemetry
|
12
|
+
# Internal: Map of instances of telemetry.
|
13
|
+
def self.instances
|
14
|
+
@instances ||= Concurrent::Map.new
|
15
|
+
end
|
16
|
+
private_class_method :instances
|
17
|
+
|
18
|
+
def self.reset
|
19
|
+
instances.each { |_, instance| instance.stop }.clear
|
20
|
+
end
|
21
|
+
|
22
|
+
# Internal: Fetch an instance of telemetry once per process per url +
|
23
|
+
# token (aka cloud endpoint). Should only ever be one instance unless you
|
24
|
+
# are doing some funky stuff.
|
25
|
+
def self.instance_for(cloud_configuration)
|
26
|
+
instances.compute_if_absent(cloud_configuration.url + cloud_configuration.token) do
|
27
|
+
new(cloud_configuration)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Public: The cloud configuration to use for this telemetry instance.
|
32
|
+
attr_reader :cloud_configuration
|
33
|
+
|
34
|
+
# Internal: Where the metrics are stored between cloud submissions.
|
35
|
+
attr_reader :metric_storage
|
36
|
+
|
37
|
+
# Internal: The pool of background threads that submits metrics to cloud.
|
38
|
+
attr_reader :pool
|
39
|
+
|
40
|
+
# Internal: The timer that triggers draining the metrics to the pool.
|
41
|
+
attr_reader :timer
|
42
|
+
|
43
|
+
# Internal: The interval in seconds for how often telemetry should be sent to cloud.
|
44
|
+
attr_reader :interval
|
45
|
+
|
46
|
+
# Internal: The timeout in seconds for how long to wait for the pool to shutdown.
|
47
|
+
attr_reader :shutdown_timeout
|
48
|
+
|
49
|
+
# Internal: The proc that is called to submit metrics to cloud.
|
50
|
+
attr_accessor :submitter
|
51
|
+
|
52
|
+
def initialize(cloud_configuration)
|
53
|
+
@pid = $$
|
54
|
+
@cloud_configuration = cloud_configuration
|
55
|
+
self.interval = ENV.fetch("FLIPPER_TELEMETRY_INTERVAL", 60).to_f
|
56
|
+
self.shutdown_timeout = ENV.fetch("FLIPPER_TELEMETRY_SHUTDOWN_TIMEOUT", 5).to_f
|
57
|
+
self.submitter = ->(drained) { Submitter.new(@cloud_configuration).call(drained) }
|
58
|
+
start
|
59
|
+
at_exit { stop }
|
60
|
+
end
|
61
|
+
|
62
|
+
# Public: Records telemetry events based on active support notifications.
|
63
|
+
def record(name, payload)
|
64
|
+
return unless name == Flipper::Feature::InstrumentationName
|
65
|
+
return unless payload[:operation] == :enabled?
|
66
|
+
detect_forking
|
67
|
+
|
68
|
+
metric = Metric.new(payload[:feature_name].to_s.freeze, payload[:result])
|
69
|
+
@metric_storage.increment metric
|
70
|
+
end
|
71
|
+
|
72
|
+
# Public: Start all the tasks and setup new metric storage.
|
73
|
+
def start
|
74
|
+
info "action=start"
|
75
|
+
|
76
|
+
@metric_storage = MetricStorage.new
|
77
|
+
|
78
|
+
@pool = Concurrent::FixedThreadPool.new(2, {
|
79
|
+
max_queue: 5,
|
80
|
+
fallback_policy: :discard,
|
81
|
+
name: "flipper-telemetry-post-to-cloud-pool".freeze,
|
82
|
+
})
|
83
|
+
|
84
|
+
@timer = Concurrent::TimerTask.execute({
|
85
|
+
execution_interval: interval,
|
86
|
+
name: "flipper-telemetry-post-to-pool-timer".freeze,
|
87
|
+
}) { post_to_pool }
|
88
|
+
end
|
89
|
+
|
90
|
+
# Public: Shuts down all the tasks and tries to flush any remaining info to Cloud.
|
91
|
+
def stop
|
92
|
+
info "action=stop"
|
93
|
+
|
94
|
+
if @timer
|
95
|
+
debug "action=timer_shutdown_start"
|
96
|
+
@timer.shutdown
|
97
|
+
# no need to wait long for timer, all it does is drain in memory metric
|
98
|
+
# storage and post to the pool of background workers
|
99
|
+
timer_termination_result = @timer.wait_for_termination(1)
|
100
|
+
@timer.kill unless timer_termination_result
|
101
|
+
debug "action=timer_shutdown_end result=#{timer_termination_result}"
|
102
|
+
end
|
103
|
+
|
104
|
+
if @pool
|
105
|
+
post_to_pool # one last drain
|
106
|
+
debug "action=pool_shutdown_start"
|
107
|
+
@pool.shutdown
|
108
|
+
pool_termination_result = @pool.wait_for_termination(@shutdown_timeout)
|
109
|
+
@pool.kill unless pool_termination_result
|
110
|
+
debug "action=pool_shutdown_end result=#{pool_termination_result}"
|
111
|
+
end
|
112
|
+
end
|
113
|
+
|
114
|
+
# Public: Restart all the tasks and reset the storage.
|
115
|
+
def restart
|
116
|
+
stop
|
117
|
+
start
|
118
|
+
end
|
119
|
+
|
120
|
+
# Internal: Sets the interval in seconds for how often telemetry should be sent to cloud.
|
121
|
+
def interval=(value)
|
122
|
+
new_interval = [Typecast.to_float(value), 10].max
|
123
|
+
@timer&.execution_interval = new_interval
|
124
|
+
@interval = new_interval
|
125
|
+
end
|
126
|
+
|
127
|
+
# Internal: Sets the timeout in seconds for how long to wait for the pool to shutdown.
|
128
|
+
def shutdown_timeout=(value)
|
129
|
+
new_shutdown_timeout = [Typecast.to_float(value), 0.1].max
|
130
|
+
@shutdown_timeout = new_shutdown_timeout
|
131
|
+
end
|
132
|
+
|
133
|
+
private
|
134
|
+
|
135
|
+
def detect_forking
|
136
|
+
if @pid != $$
|
137
|
+
info "action=fork_detected pid_was#{@pid} pid_is=#{$$}"
|
138
|
+
restart
|
139
|
+
@pid = $$
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
# Drains the metric storage and enqueues the metrics to be posted to cloud.
|
144
|
+
def post_to_pool
|
145
|
+
drained = @metric_storage.drain
|
146
|
+
return if drained.empty?
|
147
|
+
debug "action=post_to_pool metrics=#{drained.size}"
|
148
|
+
@pool.post { post_to_cloud(drained) }
|
149
|
+
rescue => error
|
150
|
+
error "action=post_to_pool error=#{error.inspect}"
|
151
|
+
end
|
152
|
+
|
153
|
+
# Posts the drained metrics to cloud.
|
154
|
+
def post_to_cloud(drained)
|
155
|
+
debug "action=post_to_cloud metrics=#{drained.size}"
|
156
|
+
response, error = submitter.call(drained)
|
157
|
+
debug "action=post_to_cloud response=#{response.inspect} body=#{response&.body.inspect} error=#{error.inspect}"
|
158
|
+
|
159
|
+
# Some of the errors are response code errors which have a response and
|
160
|
+
# thus may have a telemetry-interval header for us to respect.
|
161
|
+
response ||= error.response if error && error.respond_to?(:response)
|
162
|
+
|
163
|
+
if response && interval = response["telemetry-interval"]
|
164
|
+
self.interval = interval.to_f
|
165
|
+
end
|
166
|
+
rescue => error
|
167
|
+
error "action=post_to_cloud error=#{error.inspect}"
|
168
|
+
end
|
169
|
+
|
170
|
+
def error(message)
|
171
|
+
@cloud_configuration.log message, level: :error
|
172
|
+
end
|
173
|
+
|
174
|
+
def info(message)
|
175
|
+
@cloud_configuration.log message, level: :info
|
176
|
+
end
|
177
|
+
|
178
|
+
def debug(message)
|
179
|
+
@cloud_configuration.log message
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
end
|
@@ -1,8 +1,8 @@
|
|
1
1
|
module Flipper
|
2
2
|
class Configuration
|
3
3
|
def initialize(options = {})
|
4
|
-
@
|
5
|
-
@
|
4
|
+
@builder = AdapterBuilder.new { store Flipper::Adapters::Memory }
|
5
|
+
@default = -> { Flipper.new(@builder.to_adapter) }
|
6
6
|
end
|
7
7
|
|
8
8
|
# The default adapter to use.
|
@@ -24,9 +24,20 @@ module Flipper
|
|
24
24
|
#
|
25
25
|
def adapter(&block)
|
26
26
|
if block_given?
|
27
|
-
@
|
27
|
+
@builder.store(block)
|
28
28
|
else
|
29
|
-
@
|
29
|
+
@builder.to_adapter
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
# An adapter to use to augment the primary storage adapter. See `AdapterBuilder#use`
|
34
|
+
if RUBY_VERSION >= '3.0'
|
35
|
+
def use(klass, *args, **kwargs, &block)
|
36
|
+
@builder.use(klass, *args, **kwargs, &block)
|
37
|
+
end
|
38
|
+
else
|
39
|
+
def use(klass, *args, &block)
|
40
|
+
@builder.use(klass, *args, &block)
|
30
41
|
end
|
31
42
|
end
|
32
43
|
|
@@ -54,5 +65,15 @@ module Flipper
|
|
54
65
|
@default.call
|
55
66
|
end
|
56
67
|
end
|
68
|
+
|
69
|
+
def statsd
|
70
|
+
require 'flipper/instrumentation/statsd_subscriber'
|
71
|
+
Flipper::Instrumentation::StatsdSubscriber.client
|
72
|
+
end
|
73
|
+
|
74
|
+
def statsd=(client)
|
75
|
+
require "flipper/instrumentation/statsd"
|
76
|
+
Flipper::Instrumentation::StatsdSubscriber.client = client
|
77
|
+
end
|
57
78
|
end
|
58
79
|
end
|
data/lib/flipper/dsl.rb
CHANGED
@@ -46,6 +46,25 @@ module Flipper
|
|
46
46
|
feature(name).enable(*args)
|
47
47
|
end
|
48
48
|
|
49
|
+
# Public: Enable a feature for an expression.
|
50
|
+
#
|
51
|
+
# name - The String or Symbol name of the feature.
|
52
|
+
# expression - a Flipper::Expression instance or a Hash.
|
53
|
+
#
|
54
|
+
# Returns result of Feature#enable.
|
55
|
+
def enable_expression(name, expression)
|
56
|
+
feature(name).enable_expression(expression)
|
57
|
+
end
|
58
|
+
|
59
|
+
# Public: Add an expression to a feature.
|
60
|
+
#
|
61
|
+
# expression - an expression or Hash that can be converted to an expression.
|
62
|
+
#
|
63
|
+
# Returns result of enable.
|
64
|
+
def add_expression(name, expression)
|
65
|
+
feature(name).add_expression(expression)
|
66
|
+
end
|
67
|
+
|
49
68
|
# Public: Enable a feature for an actor.
|
50
69
|
#
|
51
70
|
# name - The String or Symbol name of the feature.
|
@@ -100,6 +119,24 @@ module Flipper
|
|
100
119
|
feature(name).disable(*args)
|
101
120
|
end
|
102
121
|
|
122
|
+
# Public: Disable expression for feature.
|
123
|
+
#
|
124
|
+
# name - The String or Symbol name of the feature.
|
125
|
+
#
|
126
|
+
# Returns result of Feature#disable.
|
127
|
+
def disable_expression(name)
|
128
|
+
feature(name).disable_expression
|
129
|
+
end
|
130
|
+
|
131
|
+
# Public: Remove an expression from a feature.
|
132
|
+
#
|
133
|
+
# expression - an Expression or Hash that can be converted to an expression.
|
134
|
+
#
|
135
|
+
# Returns result of enable.
|
136
|
+
def remove_expression(name, expression)
|
137
|
+
feature(name).remove_expression(expression)
|
138
|
+
end
|
139
|
+
|
103
140
|
# Public: Disable a feature for an actor.
|
104
141
|
#
|
105
142
|
# name - The String or Symbol name of the feature.
|
@@ -219,6 +256,15 @@ module Flipper
|
|
219
256
|
Flipper.group(name)
|
220
257
|
end
|
221
258
|
|
259
|
+
# Public: Gets the expression for the feature.
|
260
|
+
#
|
261
|
+
# name - The String or Symbol name of the feature.
|
262
|
+
#
|
263
|
+
# Returns an instance of Flipper::Expression.
|
264
|
+
def expression(name)
|
265
|
+
feature(name).expression
|
266
|
+
end
|
267
|
+
|
222
268
|
# Public: Returns a Set of the known features for this adapter.
|
223
269
|
#
|
224
270
|
# Returns Set of Flipper::Feature instances.
|
@@ -226,6 +272,11 @@ module Flipper
|
|
226
272
|
adapter.features.map { |name| feature(name) }.to_set
|
227
273
|
end
|
228
274
|
|
275
|
+
# Public: Does this adapter support writes or not.
|
276
|
+
def read_only?
|
277
|
+
adapter.read_only?
|
278
|
+
end
|
279
|
+
|
229
280
|
# Cloud DSL method that does nothing for open source version.
|
230
281
|
def sync
|
231
282
|
end
|
data/lib/flipper/engine.rb
CHANGED
@@ -9,20 +9,31 @@ module Flipper
|
|
9
9
|
preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
|
10
10
|
instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
|
11
11
|
log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
|
12
|
-
cloud_path: "_flipper"
|
12
|
+
cloud_path: "_flipper",
|
13
|
+
strict: default_strict_value
|
13
14
|
)
|
14
15
|
end
|
15
16
|
|
16
|
-
initializer "flipper.
|
17
|
+
initializer "flipper.properties" do
|
18
|
+
require "flipper/model/active_record"
|
19
|
+
|
17
20
|
ActiveSupport.on_load(:active_record) do
|
18
|
-
ActiveRecord::Base.include Flipper::
|
21
|
+
ActiveRecord::Base.include Flipper::Model::ActiveRecord
|
19
22
|
end
|
20
23
|
end
|
21
24
|
|
22
25
|
initializer "flipper.default", before: :load_config_initializers do |app|
|
26
|
+
# Load cloud secrets from Rails credentials
|
27
|
+
ENV["FLIPPER_CLOUD_TOKEN"] ||= app.credentials.dig(:flipper, :cloud_token)
|
28
|
+
ENV["FLIPPER_CLOUD_SYNC_SECRET"] ||= app.credentials.dig(:flipper, :cloud_sync_secret)
|
29
|
+
|
23
30
|
require 'flipper/cloud' if cloud?
|
24
31
|
|
25
32
|
Flipper.configure do |config|
|
33
|
+
if app.config.flipper.strict
|
34
|
+
config.use Flipper::Adapters::Strict, app.config.flipper.strict
|
35
|
+
end
|
36
|
+
|
26
37
|
config.default do
|
27
38
|
if cloud?
|
28
39
|
Flipper::Cloud.new(
|
@@ -59,5 +70,19 @@ module Flipper
|
|
59
70
|
def cloud?
|
60
71
|
!!ENV["FLIPPER_CLOUD_TOKEN"]
|
61
72
|
end
|
73
|
+
|
74
|
+
def default_strict_value
|
75
|
+
value = ENV["FLIPPER_STRICT"]
|
76
|
+
if value.in?(["warn", "raise", "noop"])
|
77
|
+
value.to_sym
|
78
|
+
elsif value
|
79
|
+
Typecast.to_boolean(value) ? :raise : false
|
80
|
+
elsif Rails.env.production?
|
81
|
+
false
|
82
|
+
else
|
83
|
+
# Warn for now. Future versions will default to :raise in development and test
|
84
|
+
:warn
|
85
|
+
end
|
86
|
+
end
|
62
87
|
end
|
63
88
|
end
|
@@ -18,7 +18,7 @@ module Flipper
|
|
18
18
|
# Public: The features hash identical to calling get_all on adapter.
|
19
19
|
def features
|
20
20
|
@features ||= begin
|
21
|
-
features =
|
21
|
+
features = Typecast.from_json(contents).fetch("features")
|
22
22
|
Typecast.features_hash(features)
|
23
23
|
rescue JSON::ParserError
|
24
24
|
raise JsonError
|
@@ -0,0 +1,73 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Expression
|
3
|
+
module Builder
|
4
|
+
def build(object)
|
5
|
+
Expression.build(object)
|
6
|
+
end
|
7
|
+
|
8
|
+
def add(*expressions)
|
9
|
+
group? ? build(name => args + expressions.flatten) : any.add(*expressions)
|
10
|
+
end
|
11
|
+
|
12
|
+
def remove(*expressions)
|
13
|
+
group? ? build(name => args - expressions.flatten) : any.remove(*expressions)
|
14
|
+
end
|
15
|
+
|
16
|
+
def any
|
17
|
+
any? ? self : build({ Any: [self] })
|
18
|
+
end
|
19
|
+
|
20
|
+
def all
|
21
|
+
all? ? self : build({ All: [self] })
|
22
|
+
end
|
23
|
+
|
24
|
+
def equal(object)
|
25
|
+
build({ Equal: [self, object] })
|
26
|
+
end
|
27
|
+
alias eq equal
|
28
|
+
|
29
|
+
def not_equal(object)
|
30
|
+
build({ NotEqual: [self, object] })
|
31
|
+
end
|
32
|
+
alias neq not_equal
|
33
|
+
|
34
|
+
def greater_than(object)
|
35
|
+
build({ GreaterThan: [self, object] })
|
36
|
+
end
|
37
|
+
alias gt greater_than
|
38
|
+
|
39
|
+
def greater_than_or_equal_to(object)
|
40
|
+
build({ GreaterThanOrEqualTo: [self, object] })
|
41
|
+
end
|
42
|
+
alias gte greater_than_or_equal_to
|
43
|
+
alias greater_than_or_equal greater_than_or_equal_to
|
44
|
+
|
45
|
+
def less_than(object)
|
46
|
+
build({ LessThan: [self, object] })
|
47
|
+
end
|
48
|
+
alias lt less_than
|
49
|
+
|
50
|
+
def less_than_or_equal_to(object)
|
51
|
+
build({ LessThanOrEqualTo: [self, object] })
|
52
|
+
end
|
53
|
+
alias lte less_than_or_equal_to
|
54
|
+
alias less_than_or_equal less_than_or_equal_to
|
55
|
+
|
56
|
+
def percentage_of_actors(object)
|
57
|
+
build({ PercentageOfActors: [self, build(object)] })
|
58
|
+
end
|
59
|
+
|
60
|
+
def any?
|
61
|
+
is_a?(Expression) && function == Expressions::Any
|
62
|
+
end
|
63
|
+
|
64
|
+
def all?
|
65
|
+
is_a?(Expression) && function == Expressions::All
|
66
|
+
end
|
67
|
+
|
68
|
+
def group?
|
69
|
+
any? || all?
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Expression
|
3
|
+
# Public: A constant value like a "string", Number (1, 3.5), Boolean (true, false).
|
4
|
+
#
|
5
|
+
# Implements the same interface as Expression
|
6
|
+
class Constant
|
7
|
+
include Expression::Builder
|
8
|
+
|
9
|
+
attr_reader :value
|
10
|
+
|
11
|
+
def initialize(value)
|
12
|
+
@value = value
|
13
|
+
end
|
14
|
+
|
15
|
+
def evaluate(_ = nil)
|
16
|
+
value
|
17
|
+
end
|
18
|
+
|
19
|
+
def eql?(other)
|
20
|
+
other.is_a?(self.class) && other.value == value
|
21
|
+
end
|
22
|
+
alias_method :==, :eql?
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,71 @@
|
|
1
|
+
require "flipper/expression/builder"
|
2
|
+
require "flipper/expression/constant"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
class Expression
|
6
|
+
include Builder
|
7
|
+
|
8
|
+
def self.build(object)
|
9
|
+
return object if object.is_a?(self) || object.is_a?(Constant)
|
10
|
+
|
11
|
+
case object
|
12
|
+
when Hash
|
13
|
+
name = object.keys.first
|
14
|
+
args = object.values.first
|
15
|
+
unless name
|
16
|
+
raise ArgumentError, "#{object.inspect} cannot be converted into an expression"
|
17
|
+
end
|
18
|
+
|
19
|
+
new(name, Array(args).map { |o| build(o) })
|
20
|
+
when String, Numeric, FalseClass, TrueClass
|
21
|
+
Expression::Constant.new(object)
|
22
|
+
when Symbol
|
23
|
+
Expression::Constant.new(object.to_s)
|
24
|
+
else
|
25
|
+
raise ArgumentError, "#{object.inspect} cannot be converted into an expression"
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
# Use #build
|
30
|
+
private_class_method :new
|
31
|
+
|
32
|
+
attr_reader :name, :function, :args
|
33
|
+
|
34
|
+
def initialize(name, args = [])
|
35
|
+
@name = name.to_s
|
36
|
+
@function = Expressions.const_get(name)
|
37
|
+
@args = args
|
38
|
+
end
|
39
|
+
|
40
|
+
def evaluate(context = {})
|
41
|
+
if call_with_context?
|
42
|
+
function.call(*args.map {|arg| arg.evaluate(context) }, context: context)
|
43
|
+
else
|
44
|
+
function.call(*args.map {|arg| arg.evaluate(context) })
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def eql?(other)
|
49
|
+
other.is_a?(self.class) && @function == other.function && @args == other.args
|
50
|
+
end
|
51
|
+
alias_method :==, :eql?
|
52
|
+
|
53
|
+
def value
|
54
|
+
{
|
55
|
+
name => args.map(&:value)
|
56
|
+
}
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def call_with_context?
|
62
|
+
function.method(:call).parameters.any? do |type, name|
|
63
|
+
name == :context && [:key, :keyreq].include?(type)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
Dir[File.join(File.dirname(__FILE__), 'expressions', '*.rb')].sort.each do |file|
|
70
|
+
require "flipper/expressions/#{File.basename(file, '.rb')}"
|
71
|
+
end
|
@@ -0,0 +1,13 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Expressions
|
3
|
+
class Comparable
|
4
|
+
def self.operator
|
5
|
+
raise NotImplementedError
|
6
|
+
end
|
7
|
+
|
8
|
+
def self.call(left, right)
|
9
|
+
left.respond_to?(operator) && right.respond_to?(operator) && left.public_send(operator, right)
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Expressions
|
3
|
+
class Duration
|
4
|
+
SECONDS_PER = {
|
5
|
+
"second" => 1,
|
6
|
+
"minute" => 60,
|
7
|
+
"hour" => 3600,
|
8
|
+
"day" => 86400,
|
9
|
+
"week" => 604_800,
|
10
|
+
"month" => 2_629_746, # 1/12 of a gregorian year
|
11
|
+
"year" => 31_556_952 # length of a gregorian year (365.2425 days)
|
12
|
+
}.freeze
|
13
|
+
|
14
|
+
def self.call(scalar, unit = 'second')
|
15
|
+
unit = unit.to_s.downcase.chomp("s")
|
16
|
+
|
17
|
+
unless scalar.is_a?(Numeric)
|
18
|
+
raise ArgumentError.new("Duration value must be a number but was #{scalar.inspect}")
|
19
|
+
end
|
20
|
+
unless SECONDS_PER[unit]
|
21
|
+
raise ArgumentError.new("Duration unit #{unit.inspect} must be one of: #{SECONDS_PER.keys.join(', ')}")
|
22
|
+
end
|
23
|
+
|
24
|
+
scalar * SECONDS_PER[unit]
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|