flipper 1.0.0 → 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/FUNDING.yml +1 -0
- data/.github/workflows/ci.yml +7 -3
- data/.github/workflows/examples.yml +27 -5
- data/Changelog.md +42 -0
- data/Gemfile +4 -4
- data/README.md +13 -11
- data/benchmark/typecast_ips.rb +8 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/cloud/backoff_policy.rb +13 -0
- data/examples/cloud/cloud_setup.rb +16 -0
- data/examples/cloud/forked.rb +7 -2
- data/examples/cloud/threaded.rb +15 -18
- data/examples/expressions.rb +213 -0
- data/examples/strict.rb +18 -0
- data/flipper.gemspec +1 -2
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +10 -0
- data/lib/flipper/adapter_builder.rb +44 -0
- data/lib/flipper/adapters/dual_write.rb +1 -3
- data/lib/flipper/adapters/failover.rb +0 -4
- data/lib/flipper/adapters/failsafe.rb +0 -4
- data/lib/flipper/adapters/http/client.rb +26 -7
- data/lib/flipper/adapters/http/error.rb +1 -1
- data/lib/flipper/adapters/http.rb +18 -13
- data/lib/flipper/adapters/instrumented.rb +0 -4
- data/lib/flipper/adapters/memoizable.rb +14 -19
- data/lib/flipper/adapters/memory.rb +4 -6
- data/lib/flipper/adapters/operation_logger.rb +0 -4
- data/lib/flipper/adapters/poll.rb +1 -3
- data/lib/flipper/adapters/pstore.rb +17 -11
- data/lib/flipper/adapters/read_only.rb +4 -4
- data/lib/flipper/adapters/strict.rb +47 -0
- data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
- data/lib/flipper/adapters/sync.rb +0 -4
- data/lib/flipper/cloud/configuration.rb +121 -52
- data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
- data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
- data/lib/flipper/cloud/telemetry/metric.rb +39 -0
- data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
- data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
- data/lib/flipper/cloud/telemetry.rb +183 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +51 -0
- data/lib/flipper/engine.rb +28 -3
- data/lib/flipper/exporters/json/export.rb +1 -1
- data/lib/flipper/exporters/json/v1.rb +1 -1
- data/lib/flipper/expression/builder.rb +73 -0
- data/lib/flipper/expression/constant.rb +25 -0
- data/lib/flipper/expression.rb +71 -0
- data/lib/flipper/expressions/all.rb +11 -0
- data/lib/flipper/expressions/any.rb +9 -0
- data/lib/flipper/expressions/boolean.rb +9 -0
- data/lib/flipper/expressions/comparable.rb +13 -0
- data/lib/flipper/expressions/duration.rb +28 -0
- data/lib/flipper/expressions/equal.rb +9 -0
- data/lib/flipper/expressions/greater_than.rb +9 -0
- data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/less_than.rb +9 -0
- data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
- data/lib/flipper/expressions/not_equal.rb +9 -0
- data/lib/flipper/expressions/now.rb +9 -0
- data/lib/flipper/expressions/number.rb +9 -0
- data/lib/flipper/expressions/percentage.rb +9 -0
- data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
- data/lib/flipper/expressions/property.rb +9 -0
- data/lib/flipper/expressions/random.rb +9 -0
- data/lib/flipper/expressions/string.rb +9 -0
- data/lib/flipper/expressions/time.rb +9 -0
- data/lib/flipper/feature.rb +55 -0
- data/lib/flipper/gate.rb +1 -0
- data/lib/flipper/gate_values.rb +5 -2
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/middleware/memoizer.rb +29 -13
- data/lib/flipper/poller.rb +1 -1
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
- data/lib/flipper/test/shared_adapter_test.rb +24 -5
- data/lib/flipper/typecast.rb +34 -6
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +38 -1
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +1 -0
- data/spec/flipper/adapters/http_spec.rb +39 -5
- data/spec/flipper/adapters/memoizable_spec.rb +15 -15
- data/spec/flipper/adapters/read_only_spec.rb +26 -11
- data/spec/flipper/adapters/strict_spec.rb +62 -0
- data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
- data/spec/flipper/cloud/configuration_spec.rb +6 -23
- data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
- data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
- data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
- data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
- data/spec/flipper/cloud/telemetry_spec.rb +156 -0
- data/spec/flipper/cloud_spec.rb +12 -12
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +39 -0
- data/spec/flipper/engine_spec.rb +108 -7
- data/spec/flipper/exporters/json/v1_spec.rb +3 -3
- data/spec/flipper/expression/builder_spec.rb +248 -0
- data/spec/flipper/expression_spec.rb +188 -0
- data/spec/flipper/expressions/all_spec.rb +15 -0
- data/spec/flipper/expressions/any_spec.rb +15 -0
- data/spec/flipper/expressions/boolean_spec.rb +15 -0
- data/spec/flipper/expressions/duration_spec.rb +43 -0
- data/spec/flipper/expressions/equal_spec.rb +24 -0
- data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/greater_than_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
- data/spec/flipper/expressions/less_than_spec.rb +32 -0
- data/spec/flipper/expressions/not_equal_spec.rb +15 -0
- data/spec/flipper/expressions/now_spec.rb +11 -0
- data/spec/flipper/expressions/number_spec.rb +21 -0
- data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
- data/spec/flipper/expressions/percentage_spec.rb +15 -0
- data/spec/flipper/expressions/property_spec.rb +13 -0
- data/spec/flipper/expressions/random_spec.rb +9 -0
- data/spec/flipper/expressions/string_spec.rb +11 -0
- data/spec/flipper/expressions/time_spec.rb +13 -0
- data/spec/flipper/feature_spec.rb +360 -1
- data/spec/flipper/gate_values_spec.rb +2 -2
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- data/spec/flipper/serializers/gzip_spec.rb +13 -0
- data/spec/flipper/serializers/json_spec.rb +13 -0
- data/spec/flipper/typecast_spec.rb +43 -7
- data/spec/flipper/types/actor_spec.rb +18 -1
- data/spec/flipper_integration_spec.rb +102 -4
- data/spec/flipper_spec.rb +89 -1
- data/spec/spec_helper.rb +5 -0
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +104 -18
- data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -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
|