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,12 @@
|
|
1
|
+
module Flipper
|
2
|
+
module Expressions
|
3
|
+
class PercentageOfActors
|
4
|
+
SCALING_FACTOR = 1_000
|
5
|
+
|
6
|
+
def self.call(text, percentage, context: {})
|
7
|
+
prefix = context[:feature_name] || ""
|
8
|
+
Zlib.crc32("#{prefix}#{text}") % (100 * SCALING_FACTOR) < percentage * SCALING_FACTOR
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
data/lib/flipper/feature.rb
CHANGED
@@ -120,6 +120,28 @@ module Flipper
|
|
120
120
|
end
|
121
121
|
end
|
122
122
|
|
123
|
+
# Public: Enables an expression_to_add for a feature.
|
124
|
+
#
|
125
|
+
# expression - an Expression or Hash that can be converted to an expression.
|
126
|
+
#
|
127
|
+
# Returns result of enable.
|
128
|
+
def enable_expression(expression)
|
129
|
+
enable Expression.build(expression)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Public: Add an expression for a feature.
|
133
|
+
#
|
134
|
+
# expression_to_add - an expression or Hash that can be converted to an expression.
|
135
|
+
#
|
136
|
+
# Returns result of enable.
|
137
|
+
def add_expression(expression_to_add)
|
138
|
+
if (current_expression = expression)
|
139
|
+
enable current_expression.add(expression_to_add)
|
140
|
+
else
|
141
|
+
enable expression_to_add
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
123
145
|
# Public: Enables a feature for an actor.
|
124
146
|
#
|
125
147
|
# actor - a Flipper::Types::Actor instance or an object that responds
|
@@ -160,6 +182,27 @@ module Flipper
|
|
160
182
|
enable Types::PercentageOfActors.wrap(percentage)
|
161
183
|
end
|
162
184
|
|
185
|
+
# Public: Disables an expression for a feature.
|
186
|
+
#
|
187
|
+
# expression - an expression or Hash that can be converted to an expression.
|
188
|
+
#
|
189
|
+
# Returns result of disable.
|
190
|
+
def disable_expression
|
191
|
+
disable Flipper.all # just need an expression to clear
|
192
|
+
end
|
193
|
+
|
194
|
+
# Public: Remove an expression from a feature. Does nothing if no expression is
|
195
|
+
# currently enabled.
|
196
|
+
#
|
197
|
+
# expression - an Expression or Hash that can be converted to an expression.
|
198
|
+
#
|
199
|
+
# Returns result of enable or nil (if no expression enabled).
|
200
|
+
def remove_expression(expression_to_remove)
|
201
|
+
if (current_expression = expression)
|
202
|
+
enable current_expression.remove(expression_to_remove)
|
203
|
+
end
|
204
|
+
end
|
205
|
+
|
163
206
|
# Public: Disables a feature for an actor.
|
164
207
|
#
|
165
208
|
# actor - a Flipper::Types::Actor instance or an object that responds
|
@@ -252,6 +295,10 @@ module Flipper
|
|
252
295
|
Flipper.groups - enabled_groups
|
253
296
|
end
|
254
297
|
|
298
|
+
def expression
|
299
|
+
Flipper::Expression.build(expression_value) if expression_value
|
300
|
+
end
|
301
|
+
|
255
302
|
# Public: Get the adapter value for the groups gate.
|
256
303
|
#
|
257
304
|
# Returns Set of String group names.
|
@@ -259,6 +306,13 @@ module Flipper
|
|
259
306
|
gate_values.groups
|
260
307
|
end
|
261
308
|
|
309
|
+
# Public: Get the adapter value for the expression gate.
|
310
|
+
#
|
311
|
+
# Returns expression.
|
312
|
+
def expression_value
|
313
|
+
gate_values.expression
|
314
|
+
end
|
315
|
+
|
262
316
|
# Public: Get the adapter value for the actors gate.
|
263
317
|
#
|
264
318
|
# Returns Set of String flipper_id's.
|
@@ -347,6 +401,7 @@ module Flipper
|
|
347
401
|
def gates_hash
|
348
402
|
@gates_hash ||= {
|
349
403
|
boolean: Gates::Boolean.new,
|
404
|
+
expression: Gates::Expression.new,
|
350
405
|
actor: Gates::Actor.new,
|
351
406
|
percentage_of_actors: Gates::PercentageOfActors.new,
|
352
407
|
percentage_of_time: Gates::PercentageOfTime.new,
|
data/lib/flipper/gate.rb
CHANGED
data/lib/flipper/gate_values.rb
CHANGED
@@ -6,6 +6,7 @@ module Flipper
|
|
6
6
|
attr_reader :boolean
|
7
7
|
attr_reader :actors
|
8
8
|
attr_reader :groups
|
9
|
+
attr_reader :expression
|
9
10
|
attr_reader :percentage_of_actors
|
10
11
|
attr_reader :percentage_of_time
|
11
12
|
|
@@ -13,8 +14,9 @@ module Flipper
|
|
13
14
|
@boolean = Typecast.to_boolean(adapter_values[:boolean])
|
14
15
|
@actors = Typecast.to_set(adapter_values[:actors])
|
15
16
|
@groups = Typecast.to_set(adapter_values[:groups])
|
16
|
-
@
|
17
|
-
@
|
17
|
+
@expression = adapter_values[:expression]
|
18
|
+
@percentage_of_actors = Typecast.to_number(adapter_values[:percentage_of_actors])
|
19
|
+
@percentage_of_time = Typecast.to_number(adapter_values[:percentage_of_time])
|
18
20
|
end
|
19
21
|
|
20
22
|
def eql?(other)
|
@@ -22,6 +24,7 @@ module Flipper
|
|
22
24
|
boolean == other.boolean &&
|
23
25
|
actors == other.actors &&
|
24
26
|
groups == other.groups &&
|
27
|
+
expression == other.expression &&
|
25
28
|
percentage_of_actors == other.percentage_of_actors &&
|
26
29
|
percentage_of_time == other.percentage_of_time
|
27
30
|
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
require "flipper/expression"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Gates
|
5
|
+
class Expression < Gate
|
6
|
+
# Internal: The name of the gate. Used for instrumentation, etc.
|
7
|
+
def name
|
8
|
+
:expression
|
9
|
+
end
|
10
|
+
|
11
|
+
# Internal: Name converted to value safe for adapter.
|
12
|
+
def key
|
13
|
+
:expression
|
14
|
+
end
|
15
|
+
|
16
|
+
def data_type
|
17
|
+
:json
|
18
|
+
end
|
19
|
+
|
20
|
+
def enabled?(value)
|
21
|
+
!value.nil? && !value.empty?
|
22
|
+
end
|
23
|
+
|
24
|
+
# Internal: Checks if the gate is open for a thing.
|
25
|
+
#
|
26
|
+
# Returns true if gate open for thing, false if not.
|
27
|
+
def open?(context)
|
28
|
+
data = context.values.expression
|
29
|
+
return false if data.nil? || data.empty?
|
30
|
+
expression = Flipper::Expression.build(data)
|
31
|
+
|
32
|
+
if context.actors.nil? || context.actors.empty?
|
33
|
+
!!expression.evaluate(feature_name: context.feature_name, properties: DEFAULT_PROPERTIES)
|
34
|
+
else
|
35
|
+
context.actors.any? do |actor|
|
36
|
+
!!expression.evaluate(feature_name: context.feature_name, properties: properties(actor))
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def protects?(thing)
|
42
|
+
thing.is_a?(Flipper::Expression) || thing.is_a?(Hash)
|
43
|
+
end
|
44
|
+
|
45
|
+
def wrap(thing)
|
46
|
+
Flipper::Expression.build(thing)
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
# Internal
|
52
|
+
DEFAULT_PROPERTIES = {}.freeze
|
53
|
+
|
54
|
+
def properties(actor)
|
55
|
+
return DEFAULT_PROPERTIES if actor.nil?
|
56
|
+
|
57
|
+
properties = {}
|
58
|
+
|
59
|
+
if actor.respond_to?(:flipper_properties)
|
60
|
+
properties.update(actor.flipper_properties)
|
61
|
+
else
|
62
|
+
warn "#{actor.inspect} does not respond to `flipper_properties` but should."
|
63
|
+
end
|
64
|
+
|
65
|
+
properties.transform_keys!(&:to_s)
|
66
|
+
|
67
|
+
if actor.respond_to?(:flipper_id)
|
68
|
+
properties["flipper_id".freeze] = actor.flipper_id
|
69
|
+
end
|
70
|
+
|
71
|
+
properties
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -12,13 +12,11 @@ module Flipper
|
|
12
12
|
end
|
13
13
|
|
14
14
|
def update_timer(metric)
|
15
|
-
|
16
|
-
self.class.client.timing metric, (@duration * 1_000).round
|
17
|
-
end
|
15
|
+
self.class.client&.timing metric, (@duration * 1_000).round
|
18
16
|
end
|
19
17
|
|
20
18
|
def update_counter(metric)
|
21
|
-
self.class.client
|
19
|
+
self.class.client&.increment metric
|
22
20
|
end
|
23
21
|
end
|
24
22
|
end
|
@@ -20,6 +20,14 @@ module Flipper
|
|
20
20
|
# # using with preload specific features
|
21
21
|
# use Flipper::Middleware::Memoizer, preload: [:stats, :search, :some_feature]
|
22
22
|
#
|
23
|
+
# # using with preload block that returns true/false
|
24
|
+
# use Flipper::Middleware::Memoizer, preload: ->(request) { !request.path.start_with?('/assets') }
|
25
|
+
#
|
26
|
+
# # using with preload block that returns specific features
|
27
|
+
# use Flipper::Middleware::Memoizer, preload: ->(request) {
|
28
|
+
# request.path.starts_with?('/admin') ? [:stats, :search] : false
|
29
|
+
# }
|
30
|
+
#
|
23
31
|
def initialize(app, opts = {})
|
24
32
|
if opts.is_a?(Flipper::DSL) || opts.is_a?(Proc)
|
25
33
|
raise 'Flipper::Middleware::Memoizer no longer initializes with a flipper instance or block. Read more at: https://git.io/vSo31.'
|
@@ -34,7 +42,7 @@ module Flipper
|
|
34
42
|
request = Rack::Request.new(env)
|
35
43
|
|
36
44
|
if memoize?(request)
|
37
|
-
memoized_call(
|
45
|
+
memoized_call(request)
|
38
46
|
else
|
39
47
|
@app.call(env)
|
40
48
|
end
|
@@ -52,26 +60,34 @@ module Flipper
|
|
52
60
|
end
|
53
61
|
end
|
54
62
|
|
55
|
-
def memoized_call(
|
56
|
-
|
57
|
-
flipper = env.fetch(@env_key) { Flipper }
|
63
|
+
def memoized_call(request)
|
64
|
+
flipper = request.env.fetch(@env_key) { Flipper }
|
58
65
|
|
59
66
|
# Already memoizing. This instance does not need to do anything.
|
60
67
|
if flipper.memoizing?
|
61
68
|
warn "Flipper::Middleware::Memoizer appears to be running twice. Read how to resolve this at https://github.com/flippercloud/flipper/pull/523"
|
62
|
-
return @app.call(env)
|
69
|
+
return @app.call(request.env)
|
63
70
|
end
|
64
71
|
|
65
|
-
|
72
|
+
begin
|
73
|
+
flipper.memoize = true
|
66
74
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
75
|
+
# Preloading is pointless without memoizing.
|
76
|
+
preload = if @opts[:preload].respond_to?(:call)
|
77
|
+
@opts[:preload].call(request)
|
78
|
+
else
|
79
|
+
@opts[:preload]
|
80
|
+
end
|
71
81
|
|
72
|
-
|
73
|
-
|
74
|
-
|
82
|
+
case preload
|
83
|
+
when true then flipper.preload_all
|
84
|
+
when Array then flipper.preload(preload)
|
85
|
+
end
|
86
|
+
|
87
|
+
@app.call(request.env)
|
88
|
+
ensure
|
89
|
+
flipper.memoize = false
|
90
|
+
end
|
75
91
|
end
|
76
92
|
end
|
77
93
|
end
|
data/lib/flipper/poller.rb
CHANGED
@@ -0,0 +1,24 @@
|
|
1
|
+
require "zlib"
|
2
|
+
require "stringio"
|
3
|
+
|
4
|
+
module Flipper
|
5
|
+
module Serializers
|
6
|
+
module Gzip
|
7
|
+
module_function
|
8
|
+
|
9
|
+
def serialize(source)
|
10
|
+
return if source.nil?
|
11
|
+
output = StringIO.new
|
12
|
+
gz = Zlib::GzipWriter.new(output)
|
13
|
+
gz.write(source)
|
14
|
+
gz.close
|
15
|
+
output.string
|
16
|
+
end
|
17
|
+
|
18
|
+
def deserialize(source)
|
19
|
+
return if source.nil?
|
20
|
+
Zlib::GzipReader.wrap(StringIO.new(source), &:read)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
require "json"
|
2
|
+
|
3
|
+
module Flipper
|
4
|
+
module Serializers
|
5
|
+
module Json
|
6
|
+
module_function
|
7
|
+
|
8
|
+
def serialize(source)
|
9
|
+
return if source.nil?
|
10
|
+
JSON.generate(source)
|
11
|
+
end
|
12
|
+
|
13
|
+
def deserialize(source)
|
14
|
+
return if source.nil?
|
15
|
+
JSON.parse(source)
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
19
|
+
end
|
@@ -4,11 +4,12 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
4
4
|
let(:flipper) { Flipper.new(subject) }
|
5
5
|
let(:feature) { flipper[:stats] }
|
6
6
|
|
7
|
-
let(:boolean_gate)
|
8
|
-
let(:
|
9
|
-
let(:
|
10
|
-
let(:
|
11
|
-
let(:
|
7
|
+
let(:boolean_gate) { feature.gate(:boolean) }
|
8
|
+
let(:expression_gate) { feature.gate(:expression) }
|
9
|
+
let(:group_gate) { feature.gate(:group) }
|
10
|
+
let(:actor_gate) { feature.gate(:actor) }
|
11
|
+
let(:actors_gate) { feature.gate(:percentage_of_actors) }
|
12
|
+
let(:time_gate) { feature.gate(:percentage_of_time) }
|
12
13
|
|
13
14
|
before do
|
14
15
|
Flipper.register(:admins) do |actor|
|
@@ -66,10 +67,27 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
66
67
|
expect(subject.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))).to eq(true)
|
67
68
|
|
68
69
|
expect(subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new(false))).to eq(true)
|
69
|
-
|
70
70
|
expect(subject.get(feature)).to eq(subject.default_config)
|
71
71
|
end
|
72
72
|
|
73
|
+
it 'can enable, disable and get value for expression gate' do
|
74
|
+
basic_expression = Flipper.property(:plan).eq("basic")
|
75
|
+
age_expression = Flipper.property(:age).gte(21)
|
76
|
+
any_expression = Flipper.any(basic_expression, age_expression)
|
77
|
+
|
78
|
+
expect(subject.enable(feature, expression_gate, any_expression)).to eq(true)
|
79
|
+
result = subject.get(feature)
|
80
|
+
expect(result[:expression]).to eq(any_expression.value)
|
81
|
+
|
82
|
+
expect(subject.enable(feature, expression_gate, basic_expression)).to eq(true)
|
83
|
+
result = subject.get(feature)
|
84
|
+
expect(result[:expression]).to eq(basic_expression.value)
|
85
|
+
|
86
|
+
expect(subject.disable(feature, expression_gate, basic_expression)).to eq(true)
|
87
|
+
result = subject.get(feature)
|
88
|
+
expect(result[:expression]).to be(nil)
|
89
|
+
end
|
90
|
+
|
73
91
|
it 'can enable, disable and get value for group gate' do
|
74
92
|
expect(subject.enable(feature, group_gate, flipper.group(:admins))).to eq(true)
|
75
93
|
expect(subject.enable(feature, group_gate, flipper.group(:early_access))).to eq(true)
|
@@ -256,14 +274,14 @@ RSpec.shared_examples_for 'a flipper adapter' do
|
|
256
274
|
expect(subject.add(flipper[:stats])).to eq(true)
|
257
275
|
expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
|
258
276
|
expect(subject.add(flipper[:search])).to eq(true)
|
277
|
+
flipper.enable :analytics, Flipper.property(:plan).eq("pro")
|
259
278
|
|
260
279
|
result = subject.get_all
|
261
|
-
expect(result).to be_instance_of(Hash)
|
262
280
|
|
263
|
-
|
264
|
-
|
265
|
-
expect(
|
266
|
-
expect(
|
281
|
+
expect(result).to be_instance_of(Hash)
|
282
|
+
expect(result["stats"]).to eq(subject.default_config.merge(boolean: 'true'))
|
283
|
+
expect(result["search"]).to eq(subject.default_config)
|
284
|
+
expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}))
|
267
285
|
end
|
268
286
|
|
269
287
|
it 'includes explicitly disabled features when getting all features' do
|
@@ -7,6 +7,7 @@ module Flipper
|
|
7
7
|
@feature = @flipper[:stats]
|
8
8
|
@boolean_gate = @feature.gate(:boolean)
|
9
9
|
@group_gate = @feature.gate(:group)
|
10
|
+
@expression_gate = @feature.gate(:expression)
|
10
11
|
@actor_gate = @feature.gate(:actor)
|
11
12
|
@actors_gate = @feature.gate(:percentage_of_actors)
|
12
13
|
@time_gate = @feature.gate(:percentage_of_time)
|
@@ -65,6 +66,24 @@ module Flipper
|
|
65
66
|
assert_equal @adapter.default_config, @adapter.get(@feature)
|
66
67
|
end
|
67
68
|
|
69
|
+
def test_can_enable_disable_and_get_value_for_expression_gate
|
70
|
+
basic_expression = Flipper.property(:plan).eq("basic")
|
71
|
+
age_expression = Flipper.property(:age).gte(21)
|
72
|
+
any_expression = Flipper.any(basic_expression, age_expression)
|
73
|
+
|
74
|
+
assert_equal true, @adapter.enable(@feature, @expression_gate, any_expression)
|
75
|
+
result = @adapter.get(@feature)
|
76
|
+
assert_equal any_expression.value, result[:expression]
|
77
|
+
|
78
|
+
assert_equal true, @adapter.enable(@feature, @expression_gate, basic_expression)
|
79
|
+
result = @adapter.get(@feature)
|
80
|
+
assert_equal basic_expression.value, result[:expression]
|
81
|
+
|
82
|
+
assert_equal true, @adapter.disable(@feature, @expression_gate, basic_expression)
|
83
|
+
result = @adapter.get(@feature)
|
84
|
+
assert_nil result[:expression]
|
85
|
+
end
|
86
|
+
|
68
87
|
def test_can_enable_disable_get_value_for_group_gate
|
69
88
|
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:admins))
|
70
89
|
assert_equal true, @adapter.enable(@feature, @group_gate, @flipper.group(:early_access))
|
@@ -252,14 +271,14 @@ module Flipper
|
|
252
271
|
assert @adapter.add(@flipper[:stats])
|
253
272
|
assert @adapter.enable(@flipper[:stats], @boolean_gate, Flipper::Types::Boolean.new)
|
254
273
|
assert @adapter.add(@flipper[:search])
|
274
|
+
@flipper.enable :analytics, Flipper.property(:plan).eq("pro")
|
255
275
|
|
256
276
|
result = @adapter.get_all
|
257
|
-
assert_instance_of Hash, result
|
258
277
|
|
259
|
-
|
260
|
-
|
261
|
-
assert_equal @adapter.default_config
|
262
|
-
assert_equal @adapter.default_config,
|
278
|
+
assert_instance_of Hash, result
|
279
|
+
assert_equal @adapter.default_config.merge(boolean: 'true'), result["stats"]
|
280
|
+
assert_equal @adapter.default_config, result["search"]
|
281
|
+
assert_equal @adapter.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}), result["analytics"]
|
263
282
|
end
|
264
283
|
|
265
284
|
def test_includes_explicitly_disabled_features_when_getting_all_features
|
data/lib/flipper/typecast.rb
CHANGED
@@ -1,4 +1,6 @@
|
|
1
1
|
require 'set'
|
2
|
+
require "flipper/serializers/json"
|
3
|
+
require "flipper/serializers/gzip"
|
2
4
|
|
3
5
|
module Flipper
|
4
6
|
module Typecast
|
@@ -36,17 +38,25 @@ module Flipper
|
|
36
38
|
raise ArgumentError, "#{value.inspect} cannot be converted to a float"
|
37
39
|
end
|
38
40
|
|
39
|
-
# Internal: Convert value to a
|
41
|
+
# Internal: Convert value to a number.
|
40
42
|
#
|
41
43
|
# Returns a Integer or Float representation of the value.
|
42
44
|
# Raises ArgumentError if conversion is not possible.
|
43
|
-
def self.
|
44
|
-
|
45
|
-
|
46
|
-
|
45
|
+
def self.to_number(value)
|
46
|
+
case value
|
47
|
+
when Numeric
|
48
|
+
value
|
49
|
+
when String
|
50
|
+
value.include?('.') ? to_float(value) : to_integer(value)
|
51
|
+
when NilClass
|
52
|
+
0
|
53
|
+
else
|
54
|
+
value.to_f
|
55
|
+
end
|
47
56
|
rescue NoMethodError
|
48
|
-
raise ArgumentError, "#{value.inspect} cannot be converted to a
|
57
|
+
raise ArgumentError, "#{value.inspect} cannot be converted to a number"
|
49
58
|
end
|
59
|
+
singleton_class.send(:alias_method, :to_percentage, :to_number)
|
50
60
|
|
51
61
|
# Internal: Convert value to a set.
|
52
62
|
#
|
@@ -71,6 +81,8 @@ module Flipper
|
|
71
81
|
normalized_value = case value
|
72
82
|
when Array, Set
|
73
83
|
value.to_set
|
84
|
+
when Hash
|
85
|
+
value
|
74
86
|
else
|
75
87
|
value ? value.to_s : value
|
76
88
|
end
|
@@ -79,5 +91,21 @@ module Flipper
|
|
79
91
|
end
|
80
92
|
normalized_source
|
81
93
|
end
|
94
|
+
|
95
|
+
def self.to_json(source)
|
96
|
+
Serializers::Json.serialize(source)
|
97
|
+
end
|
98
|
+
|
99
|
+
def self.from_json(source)
|
100
|
+
Serializers::Json.deserialize(source)
|
101
|
+
end
|
102
|
+
|
103
|
+
def self.to_gzip(source)
|
104
|
+
Serializers::Gzip.serialize(source)
|
105
|
+
end
|
106
|
+
|
107
|
+
def self.from_gzip(source)
|
108
|
+
Serializers::Gzip.deserialize(source)
|
109
|
+
end
|
82
110
|
end
|
83
111
|
end
|
data/lib/flipper/version.rb
CHANGED