flipper 0.26.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 +19 -13
- data/.github/workflows/examples.yml +32 -15
- data/Changelog.md +294 -154
- data/Gemfile +15 -10
- data/README.md +13 -11
- data/benchmark/enabled_ips.rb +10 -0
- data/benchmark/enabled_multiple_actors_ips.rb +20 -0
- data/benchmark/enabled_profile.rb +20 -0
- data/benchmark/instrumentation_ips.rb +21 -0
- data/benchmark/typecast_ips.rb +27 -0
- data/docs/images/flipper_cloud.png +0 -0
- data/examples/api/basic.ru +3 -4
- data/examples/api/custom_memoized.ru +3 -4
- data/examples/api/memoized.ru +3 -4
- 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/threaded.rb +33 -0
- data/examples/dsl.rb +1 -15
- data/examples/enabled_for_actor.rb +4 -2
- data/examples/expressions.rb +213 -0
- data/examples/mirroring.rb +59 -0
- data/examples/strict.rb +18 -0
- data/flipper-cloud.gemspec +19 -0
- data/flipper.gemspec +3 -5
- data/lib/flipper/actor.rb +6 -3
- data/lib/flipper/adapter.rb +33 -7
- 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 +29 -16
- data/lib/flipper/adapters/instrumented.rb +25 -6
- data/lib/flipper/adapters/memoizable.rb +33 -21
- data/lib/flipper/adapters/memory.rb +81 -46
- data/lib/flipper/adapters/operation_logger.rb +16 -7
- data/lib/flipper/adapters/poll/poller.rb +2 -125
- data/lib/flipper/adapters/poll.rb +5 -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 +258 -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/routes.rb +14 -0
- 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/cloud.rb +53 -0
- data/lib/flipper/configuration.rb +25 -4
- data/lib/flipper/dsl.rb +46 -45
- data/lib/flipper/engine.rb +88 -0
- data/lib/flipper/errors.rb +3 -3
- data/lib/flipper/export.rb +26 -0
- data/lib/flipper/exporter.rb +17 -0
- data/lib/flipper/exporters/json/export.rb +32 -0
- data/lib/flipper/exporters/json/v1.rb +33 -0
- 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 +87 -26
- data/lib/flipper/feature_check_context.rb +10 -6
- data/lib/flipper/gate.rb +13 -11
- data/lib/flipper/gate_values.rb +5 -18
- data/lib/flipper/gates/actor.rb +10 -17
- data/lib/flipper/gates/boolean.rb +1 -1
- data/lib/flipper/gates/expression.rb +75 -0
- data/lib/flipper/gates/group.rb +5 -7
- data/lib/flipper/gates/percentage_of_actors.rb +10 -13
- data/lib/flipper/gates/percentage_of_time.rb +1 -2
- data/lib/flipper/identifier.rb +2 -2
- data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
- data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
- data/lib/flipper/instrumentation/subscriber.rb +8 -1
- data/lib/flipper/metadata.rb +5 -1
- data/lib/flipper/middleware/memoizer.rb +30 -14
- data/lib/flipper/poller.rb +117 -0
- data/lib/flipper/serializers/gzip.rb +24 -0
- data/lib/flipper/serializers/json.rb +19 -0
- data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
- data/lib/flipper/test/shared_adapter_test.rb +91 -48
- data/lib/flipper/typecast.rb +56 -15
- data/lib/flipper/types/actor.rb +13 -13
- data/lib/flipper/types/group.rb +4 -4
- data/lib/flipper/types/percentage.rb +1 -1
- data/lib/flipper/version.rb +1 -1
- data/lib/flipper.rb +47 -10
- data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
- data/spec/flipper/adapter_builder_spec.rb +73 -0
- data/spec/flipper/adapter_spec.rb +30 -2
- data/spec/flipper/adapters/dual_write_spec.rb +2 -2
- data/spec/flipper/adapters/http_spec.rb +64 -8
- data/spec/flipper/adapters/instrumented_spec.rb +29 -11
- data/spec/flipper/adapters/memoizable_spec.rb +51 -31
- data/spec/flipper/adapters/memory_spec.rb +14 -3
- data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
- data/spec/flipper/adapters/read_only_spec.rb +32 -17
- 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 +252 -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/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 +180 -0
- data/spec/flipper/configuration_spec.rb +17 -0
- data/spec/flipper/dsl_spec.rb +54 -73
- data/spec/flipper/engine_spec.rb +291 -0
- data/spec/flipper/export_spec.rb +13 -0
- data/spec/flipper/exporter_spec.rb +16 -0
- data/spec/flipper/exporters/json/export_spec.rb +60 -0
- data/spec/flipper/exporters/json/v1_spec.rb +33 -0
- 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_check_context_spec.rb +17 -17
- data/spec/flipper/feature_spec.rb +436 -33
- data/spec/flipper/gate_values_spec.rb +2 -33
- data/spec/flipper/gates/boolean_spec.rb +1 -1
- data/spec/flipper/gates/expression_spec.rb +108 -0
- data/spec/flipper/gates/group_spec.rb +2 -3
- data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
- data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
- data/spec/flipper/identifier_spec.rb +4 -5
- data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
- data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
- data/spec/flipper/middleware/memoizer_spec.rb +67 -0
- data/spec/flipper/poller_spec.rb +47 -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 +121 -6
- data/spec/flipper/types/actor_spec.rb +63 -46
- data/spec/flipper/types/group_spec.rb +2 -2
- data/spec/flipper_integration_spec.rb +168 -58
- data/spec/flipper_spec.rb +92 -28
- data/spec/spec_helper.rb +6 -13
- data/spec/support/actor_names.yml +1 -0
- data/spec/support/climate_control.rb +7 -0
- data/spec/support/fake_backoff_policy.rb +15 -0
- data/spec/support/skippable.rb +18 -0
- data/spec/support/spec_helpers.rb +11 -3
- metadata +166 -13
- data/.github/workflows/release.yml +0 -44
- data/.tool-versions +0 -1
- data/lib/flipper/railtie.rb +0 -47
- data/spec/flipper/railtie_spec.rb +0 -109
data/lib/flipper/dsl.rb
CHANGED
@@ -10,7 +10,7 @@ module Flipper
|
|
10
10
|
# Private: What is being used to instrument all the things.
|
11
11
|
attr_reader :instrumenter
|
12
12
|
|
13
|
-
def_delegators :@adapter, :memoize=, :memoizing
|
13
|
+
def_delegators :@adapter, :memoize=, :memoizing?, :import, :export
|
14
14
|
|
15
15
|
# Public: Returns a new instance of the DSL.
|
16
16
|
#
|
@@ -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.
|
@@ -210,22 +247,6 @@ module Flipper
|
|
210
247
|
# Returns an instance of Flipper::Feature.
|
211
248
|
alias_method :[], :feature
|
212
249
|
|
213
|
-
# Public: Shortcut for getting a boolean type instance.
|
214
|
-
#
|
215
|
-
# value - The true or false value for the boolean.
|
216
|
-
#
|
217
|
-
# Returns a Flipper::Types::Boolean instance.
|
218
|
-
def boolean(value = true)
|
219
|
-
Types::Boolean.new(value)
|
220
|
-
end
|
221
|
-
|
222
|
-
# Public: Even shorter shortcut for getting a boolean type instance.
|
223
|
-
#
|
224
|
-
# value - The true or false value for the boolean.
|
225
|
-
#
|
226
|
-
# Returns a Flipper::Types::Boolean instance.
|
227
|
-
alias_method :bool, :boolean
|
228
|
-
|
229
250
|
# Public: Access a flipper group by name.
|
230
251
|
#
|
231
252
|
# name - The String or Symbol name of the feature.
|
@@ -235,35 +256,14 @@ module Flipper
|
|
235
256
|
Flipper.group(name)
|
236
257
|
end
|
237
258
|
|
238
|
-
# Public:
|
239
|
-
#
|
240
|
-
# thing - The object that you would like to wrap.
|
259
|
+
# Public: Gets the expression for the feature.
|
241
260
|
#
|
242
|
-
#
|
243
|
-
# Raises ArgumentError if thing does not respond to `flipper_id`.
|
244
|
-
def actor(thing)
|
245
|
-
Types::Actor.new(thing)
|
246
|
-
end
|
247
|
-
|
248
|
-
# Public: Shortcut for getting a percentage of time instance.
|
249
|
-
#
|
250
|
-
# number - The percentage of time that should be enabled.
|
251
|
-
#
|
252
|
-
# Returns Flipper::Types::PercentageOfTime.
|
253
|
-
def time(number)
|
254
|
-
Types::PercentageOfTime.new(number)
|
255
|
-
end
|
256
|
-
alias_method :percentage_of_time, :time
|
257
|
-
|
258
|
-
# Public: Shortcut for getting a percentage of actors instance.
|
259
|
-
#
|
260
|
-
# number - The percentage of actors that should be enabled.
|
261
|
+
# name - The String or Symbol name of the feature.
|
261
262
|
#
|
262
|
-
# Returns Flipper::
|
263
|
-
def
|
264
|
-
|
263
|
+
# Returns an instance of Flipper::Expression.
|
264
|
+
def expression(name)
|
265
|
+
feature(name).expression
|
265
266
|
end
|
266
|
-
alias_method :percentage_of_actors, :actors
|
267
267
|
|
268
268
|
# Public: Returns a Set of the known features for this adapter.
|
269
269
|
#
|
@@ -272,8 +272,9 @@ module Flipper
|
|
272
272
|
adapter.features.map { |name| feature(name) }.to_set
|
273
273
|
end
|
274
274
|
|
275
|
-
|
276
|
-
|
275
|
+
# Public: Does this adapter support writes or not.
|
276
|
+
def read_only?
|
277
|
+
adapter.read_only?
|
277
278
|
end
|
278
279
|
|
279
280
|
# Cloud DSL method that does nothing for open source version.
|
@@ -0,0 +1,88 @@
|
|
1
|
+
module Flipper
|
2
|
+
class Engine < Rails::Engine
|
3
|
+
paths["config/routes.rb"] = ["lib/flipper/cloud/routes.rb"]
|
4
|
+
|
5
|
+
config.before_configuration do
|
6
|
+
config.flipper = ActiveSupport::OrderedOptions.new.update(
|
7
|
+
env_key: ENV.fetch('FLIPPER_ENV_KEY', 'flipper'),
|
8
|
+
memoize: ENV.fetch('FLIPPER_MEMOIZE', 'true').casecmp('true').zero?,
|
9
|
+
preload: ENV.fetch('FLIPPER_PRELOAD', 'true').casecmp('true').zero?,
|
10
|
+
instrumenter: ENV.fetch('FLIPPER_INSTRUMENTER', 'ActiveSupport::Notifications').constantize,
|
11
|
+
log: ENV.fetch('FLIPPER_LOG', 'true').casecmp('true').zero?,
|
12
|
+
cloud_path: "_flipper",
|
13
|
+
strict: default_strict_value
|
14
|
+
)
|
15
|
+
end
|
16
|
+
|
17
|
+
initializer "flipper.properties" do
|
18
|
+
require "flipper/model/active_record"
|
19
|
+
|
20
|
+
ActiveSupport.on_load(:active_record) do
|
21
|
+
ActiveRecord::Base.include Flipper::Model::ActiveRecord
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
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
|
+
|
30
|
+
require 'flipper/cloud' if cloud?
|
31
|
+
|
32
|
+
Flipper.configure do |config|
|
33
|
+
if app.config.flipper.strict
|
34
|
+
config.use Flipper::Adapters::Strict, app.config.flipper.strict
|
35
|
+
end
|
36
|
+
|
37
|
+
config.default do
|
38
|
+
if cloud?
|
39
|
+
Flipper::Cloud.new(
|
40
|
+
local_adapter: config.adapter,
|
41
|
+
instrumenter: app.config.flipper.instrumenter
|
42
|
+
)
|
43
|
+
else
|
44
|
+
Flipper.new(config.adapter, instrumenter: app.config.flipper.instrumenter)
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
initializer "flipper.log", after: :load_config_initializers do |app|
|
51
|
+
flipper = app.config.flipper
|
52
|
+
|
53
|
+
if flipper.log && flipper.instrumenter == ActiveSupport::Notifications
|
54
|
+
require "flipper/instrumentation/log_subscriber"
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
initializer "flipper.memoizer", after: :load_config_initializers do |app|
|
59
|
+
flipper = app.config.flipper
|
60
|
+
|
61
|
+
if flipper.memoize
|
62
|
+
app.middleware.use Flipper::Middleware::Memoizer, {
|
63
|
+
env_key: flipper.env_key,
|
64
|
+
preload: flipper.preload,
|
65
|
+
if: flipper.memoize.respond_to?(:call) ? flipper.memoize : nil
|
66
|
+
}
|
67
|
+
end
|
68
|
+
end
|
69
|
+
|
70
|
+
def cloud?
|
71
|
+
!!ENV["FLIPPER_CLOUD_TOKEN"]
|
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
|
87
|
+
end
|
88
|
+
end
|
data/lib/flipper/errors.rb
CHANGED
@@ -2,10 +2,10 @@ module Flipper
|
|
2
2
|
# Top level error that all other errors inherit from.
|
3
3
|
class Error < StandardError; end
|
4
4
|
|
5
|
-
# Raised when gate can not be found for
|
5
|
+
# Raised when gate can not be found for an actor.
|
6
6
|
class GateNotFound < Error
|
7
|
-
def initialize(
|
8
|
-
super "Could not find gate for #{
|
7
|
+
def initialize(actor)
|
8
|
+
super "Could not find gate for #{actor.inspect}"
|
9
9
|
end
|
10
10
|
end
|
11
11
|
|
@@ -0,0 +1,26 @@
|
|
1
|
+
require "flipper/adapters/memory"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
class Export
|
5
|
+
attr_reader :contents, :format, :version
|
6
|
+
|
7
|
+
def initialize(contents:, format: :json, version: 1)
|
8
|
+
@contents = contents
|
9
|
+
@format = format
|
10
|
+
@version = version
|
11
|
+
end
|
12
|
+
|
13
|
+
def features
|
14
|
+
raise NotImplementedError
|
15
|
+
end
|
16
|
+
|
17
|
+
def adapter
|
18
|
+
@adapter ||= Flipper::Adapters::Memory.new(features)
|
19
|
+
end
|
20
|
+
|
21
|
+
def eql?(other)
|
22
|
+
self.class.eql?(other.class) && @contents == other.contents && @format == other.format && @version == other.version
|
23
|
+
end
|
24
|
+
alias_method :==, :eql?
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
require "flipper/exporters/json/v1"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Exporter
|
5
|
+
extend self
|
6
|
+
|
7
|
+
FORMATTERS = {
|
8
|
+
json: {
|
9
|
+
1 => Flipper::Exporters::Json::V1,
|
10
|
+
}
|
11
|
+
}.freeze
|
12
|
+
|
13
|
+
def build(format: :json, version: 1)
|
14
|
+
FORMATTERS.fetch(format).fetch(version).new
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
require "flipper/export"
|
2
|
+
require "flipper/typecast"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
# Raised when the contents of the export are not valid.
|
8
|
+
class InvalidError < StandardError; end
|
9
|
+
class JsonError < InvalidError; end
|
10
|
+
|
11
|
+
# Internal: JSON export class that knows how to build features hash
|
12
|
+
# from data.
|
13
|
+
class Export < ::Flipper::Export
|
14
|
+
def initialize(contents:, version: 1)
|
15
|
+
super contents: contents, version: version, format: :json
|
16
|
+
end
|
17
|
+
|
18
|
+
# Public: The features hash identical to calling get_all on adapter.
|
19
|
+
def features
|
20
|
+
@features ||= begin
|
21
|
+
features = Typecast.from_json(contents).fetch("features")
|
22
|
+
Typecast.features_hash(features)
|
23
|
+
rescue JSON::ParserError
|
24
|
+
raise JsonError
|
25
|
+
rescue
|
26
|
+
raise InvalidError
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require "json"
|
2
|
+
require "flipper/exporters/json/export"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Exporters
|
6
|
+
module Json
|
7
|
+
class V1
|
8
|
+
VERSION = 1
|
9
|
+
|
10
|
+
def call(adapter)
|
11
|
+
features = adapter.get_all
|
12
|
+
|
13
|
+
# Convert sets to arrays for json
|
14
|
+
features.each do |feature_key, gates|
|
15
|
+
gates.each do |key, value|
|
16
|
+
case value
|
17
|
+
when Set
|
18
|
+
features[feature_key][key] = value.to_a
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
json = Typecast.to_json({
|
24
|
+
version: VERSION,
|
25
|
+
features: features,
|
26
|
+
})
|
27
|
+
|
28
|
+
Json::Export.new(contents: json, version: VERSION)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -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
|