flipper 1.0.0 → 1.1.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (140) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +7 -3
  4. data/.github/workflows/examples.yml +27 -5
  5. data/Changelog.md +42 -0
  6. data/Gemfile +4 -4
  7. data/README.md +13 -11
  8. data/benchmark/typecast_ips.rb +8 -0
  9. data/docs/images/flipper_cloud.png +0 -0
  10. data/examples/cloud/backoff_policy.rb +13 -0
  11. data/examples/cloud/cloud_setup.rb +16 -0
  12. data/examples/cloud/forked.rb +7 -2
  13. data/examples/cloud/threaded.rb +15 -18
  14. data/examples/expressions.rb +213 -0
  15. data/examples/strict.rb +18 -0
  16. data/flipper.gemspec +1 -2
  17. data/lib/flipper/actor.rb +6 -3
  18. data/lib/flipper/adapter.rb +10 -0
  19. data/lib/flipper/adapter_builder.rb +44 -0
  20. data/lib/flipper/adapters/dual_write.rb +1 -3
  21. data/lib/flipper/adapters/failover.rb +0 -4
  22. data/lib/flipper/adapters/failsafe.rb +0 -4
  23. data/lib/flipper/adapters/http/client.rb +26 -7
  24. data/lib/flipper/adapters/http/error.rb +1 -1
  25. data/lib/flipper/adapters/http.rb +18 -13
  26. data/lib/flipper/adapters/instrumented.rb +0 -4
  27. data/lib/flipper/adapters/memoizable.rb +14 -19
  28. data/lib/flipper/adapters/memory.rb +4 -6
  29. data/lib/flipper/adapters/operation_logger.rb +0 -4
  30. data/lib/flipper/adapters/poll.rb +1 -3
  31. data/lib/flipper/adapters/pstore.rb +17 -11
  32. data/lib/flipper/adapters/read_only.rb +4 -4
  33. data/lib/flipper/adapters/strict.rb +47 -0
  34. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  35. data/lib/flipper/adapters/sync.rb +0 -4
  36. data/lib/flipper/cloud/configuration.rb +121 -52
  37. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  38. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  39. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  40. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  41. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  42. data/lib/flipper/cloud/telemetry.rb +183 -0
  43. data/lib/flipper/configuration.rb +25 -4
  44. data/lib/flipper/dsl.rb +51 -0
  45. data/lib/flipper/engine.rb +28 -3
  46. data/lib/flipper/exporters/json/export.rb +1 -1
  47. data/lib/flipper/exporters/json/v1.rb +1 -1
  48. data/lib/flipper/expression/builder.rb +73 -0
  49. data/lib/flipper/expression/constant.rb +25 -0
  50. data/lib/flipper/expression.rb +71 -0
  51. data/lib/flipper/expressions/all.rb +11 -0
  52. data/lib/flipper/expressions/any.rb +9 -0
  53. data/lib/flipper/expressions/boolean.rb +9 -0
  54. data/lib/flipper/expressions/comparable.rb +13 -0
  55. data/lib/flipper/expressions/duration.rb +28 -0
  56. data/lib/flipper/expressions/equal.rb +9 -0
  57. data/lib/flipper/expressions/greater_than.rb +9 -0
  58. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  59. data/lib/flipper/expressions/less_than.rb +9 -0
  60. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  61. data/lib/flipper/expressions/not_equal.rb +9 -0
  62. data/lib/flipper/expressions/now.rb +9 -0
  63. data/lib/flipper/expressions/number.rb +9 -0
  64. data/lib/flipper/expressions/percentage.rb +9 -0
  65. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  66. data/lib/flipper/expressions/property.rb +9 -0
  67. data/lib/flipper/expressions/random.rb +9 -0
  68. data/lib/flipper/expressions/string.rb +9 -0
  69. data/lib/flipper/expressions/time.rb +9 -0
  70. data/lib/flipper/feature.rb +55 -0
  71. data/lib/flipper/gate.rb +1 -0
  72. data/lib/flipper/gate_values.rb +5 -2
  73. data/lib/flipper/gates/expression.rb +75 -0
  74. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  75. data/lib/flipper/middleware/memoizer.rb +29 -13
  76. data/lib/flipper/poller.rb +1 -1
  77. data/lib/flipper/serializers/gzip.rb +24 -0
  78. data/lib/flipper/serializers/json.rb +19 -0
  79. data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
  80. data/lib/flipper/test/shared_adapter_test.rb +24 -5
  81. data/lib/flipper/typecast.rb +34 -6
  82. data/lib/flipper/types/percentage.rb +1 -1
  83. data/lib/flipper/version.rb +1 -1
  84. data/lib/flipper.rb +38 -1
  85. data/spec/flipper/adapter_builder_spec.rb +73 -0
  86. data/spec/flipper/adapter_spec.rb +1 -0
  87. data/spec/flipper/adapters/http_spec.rb +39 -5
  88. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  89. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  90. data/spec/flipper/adapters/strict_spec.rb +62 -0
  91. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  92. data/spec/flipper/cloud/configuration_spec.rb +6 -23
  93. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  94. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  95. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  96. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  97. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  98. data/spec/flipper/cloud_spec.rb +12 -12
  99. data/spec/flipper/configuration_spec.rb +17 -0
  100. data/spec/flipper/dsl_spec.rb +39 -0
  101. data/spec/flipper/engine_spec.rb +108 -7
  102. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  103. data/spec/flipper/expression/builder_spec.rb +248 -0
  104. data/spec/flipper/expression_spec.rb +188 -0
  105. data/spec/flipper/expressions/all_spec.rb +15 -0
  106. data/spec/flipper/expressions/any_spec.rb +15 -0
  107. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  108. data/spec/flipper/expressions/duration_spec.rb +43 -0
  109. data/spec/flipper/expressions/equal_spec.rb +24 -0
  110. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  111. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  112. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  113. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  114. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  115. data/spec/flipper/expressions/now_spec.rb +11 -0
  116. data/spec/flipper/expressions/number_spec.rb +21 -0
  117. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  118. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  119. data/spec/flipper/expressions/property_spec.rb +13 -0
  120. data/spec/flipper/expressions/random_spec.rb +9 -0
  121. data/spec/flipper/expressions/string_spec.rb +11 -0
  122. data/spec/flipper/expressions/time_spec.rb +13 -0
  123. data/spec/flipper/feature_spec.rb +360 -1
  124. data/spec/flipper/gate_values_spec.rb +2 -2
  125. data/spec/flipper/gates/expression_spec.rb +108 -0
  126. data/spec/flipper/identifier_spec.rb +4 -5
  127. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
  128. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  129. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  130. data/spec/flipper/serializers/json_spec.rb +13 -0
  131. data/spec/flipper/typecast_spec.rb +43 -7
  132. data/spec/flipper/types/actor_spec.rb +18 -1
  133. data/spec/flipper_integration_spec.rb +102 -4
  134. data/spec/flipper_spec.rb +89 -1
  135. data/spec/spec_helper.rb +5 -0
  136. data/spec/support/actor_names.yml +1 -0
  137. data/spec/support/fake_backoff_policy.rb +15 -0
  138. data/spec/support/spec_helpers.rb +11 -3
  139. metadata +104 -18
  140. data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class NotEqual < Comparable
4
+ def self.operator
5
+ :!=
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Now
4
+ def self.call
5
+ ::Time.now.utc
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Number
4
+ def self.call(value)
5
+ Flipper::Typecast.to_number(value)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Percentage
4
+ def self.call(value)
5
+ value.to_f.clamp(0, 100)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Property
4
+ def self.call(key, context:)
5
+ context.dig(:properties, key.to_s)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Random
4
+ def self.call(max = 0)
5
+ rand max
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class String
4
+ def self.call(value)
5
+ value.to_s
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,9 @@
1
+ module Flipper
2
+ module Expressions
3
+ class Time
4
+ def self.call(value)
5
+ ::Time.parse(value)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -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
@@ -60,3 +60,4 @@ require 'flipper/gates/boolean'
60
60
  require 'flipper/gates/group'
61
61
  require 'flipper/gates/percentage_of_actors'
62
62
  require 'flipper/gates/percentage_of_time'
63
+ require 'flipper/gates/expression'
@@ -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
- @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors])
17
- @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
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
- if self.class.client
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.increment metric if 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(env)
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(env)
56
- reset_on_body_close = false
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
- flipper.memoize = true
72
+ begin
73
+ flipper.memoize = true
66
74
 
67
- case @opts[:preload]
68
- when true then flipper.preload_all
69
- when Array then flipper.preload(@opts[:preload])
70
- end
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
- @app.call(env)
73
- ensure
74
- flipper.memoize = false if flipper
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
@@ -17,7 +17,7 @@ module Flipper
17
17
  end
18
18
 
19
19
  def self.reset
20
- instances.each {|_,poller| poller.stop }.clear
20
+ instances.each {|_, instance| instance.stop }.clear
21
21
  end
22
22
 
23
23
  def initialize(options = {})
@@ -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) { feature.gate(:boolean) }
8
- let(:group_gate) { feature.gate(:group) }
9
- let(:actor_gate) { feature.gate(:actor) }
10
- let(:actors_gate) { feature.gate(:percentage_of_actors) }
11
- let(:time_gate) { feature.gate(:percentage_of_time) }
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
- stats = result["stats"]
264
- search = result["search"]
265
- expect(stats).to eq(subject.default_config.merge(boolean: 'true'))
266
- expect(search).to eq(subject.default_config)
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
- stats = result["stats"]
260
- search = result["search"]
261
- assert_equal @adapter.default_config.merge(boolean: 'true'), stats
262
- assert_equal @adapter.default_config, search
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
@@ -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 percentage.
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.to_percentage(value)
44
- result_to_f = value.to_f
45
- result_to_i = result_to_f.to_i
46
- result_to_f == result_to_i ? result_to_i : result_to_f
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 percentage"
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
@@ -4,7 +4,7 @@ module Flipper
4
4
  module Types
5
5
  class Percentage < Type
6
6
  def initialize(value)
7
- value = Typecast.to_percentage(value)
7
+ value = Typecast.to_number(value)
8
8
 
9
9
  if value < 0 || value > 100
10
10
  raise ArgumentError,
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.1.0'.freeze
3
3
  end