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.
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