flipper 1.0.0 → 1.3.6

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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +50 -7
  4. data/.github/workflows/examples.yml +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -8
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/typecast_ips.rb +8 -0
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/images/flipper_cloud.png +0 -0
  13. data/examples/cloud/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -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
@@ -0,0 +1,23 @@
1
+ module Flipper
2
+ module Model
3
+ module ActiveRecord
4
+ # The id of the record when used as an actor.
5
+ #
6
+ # class User < ActiveRecord::Base
7
+ # end
8
+ #
9
+ # user = User.first
10
+ # Flipper.enable :some_feature, user
11
+ # Flipper.enabled? :some_feature, user #=> true
12
+ #
13
+ def flipper_id
14
+ "#{self.class.base_class.name};#{id}"
15
+ end
16
+
17
+ # Properties used to evaluate expressions
18
+ def flipper_properties
19
+ {"type" => self.class.name}.merge(attributes)
20
+ end
21
+ end
22
+ end
23
+ end
@@ -17,9 +17,11 @@ 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
+ MINIMUM_POLL_INTERVAL = 10
24
+
23
25
  def initialize(options = {})
24
26
  @thread = nil
25
27
  @pid = Process.pid
@@ -30,9 +32,9 @@ module Flipper
30
32
  @last_synced_at = Concurrent::AtomicFixnum.new(0)
31
33
  @adapter = Adapters::Memory.new(nil, threadsafe: true)
32
34
 
33
- if @interval < 1
34
- warn "Flipper::Cloud poll interval must be greater than or equal to 1 but was #{@interval}. Setting @interval to 1."
35
- @interval = 1
35
+ if @interval < MINIMUM_POLL_INTERVAL
36
+ warn "Flipper::Cloud poll interval must be greater than or equal to #{MINIMUM_POLL_INTERVAL} but was #{@interval}. Setting @interval to #{MINIMUM_POLL_INTERVAL}."
37
+ @interval = MINIMUM_POLL_INTERVAL
36
38
  end
37
39
 
38
40
  @start_automatically = options.fetch(:start_automatically, true)
@@ -57,15 +59,14 @@ module Flipper
57
59
  def run
58
60
  loop do
59
61
  sleep jitter
60
- start = Concurrent.monotonic_time
62
+
61
63
  begin
62
64
  sync
63
- rescue => exception
65
+ rescue
64
66
  # you can instrument these using poller.flipper
65
67
  end
66
68
 
67
- sleep_interval = interval - (Concurrent.monotonic_time - start)
68
- sleep sleep_interval if sleep_interval.positive?
69
+ sleep interval
69
70
  end
70
71
  end
71
72
 
@@ -0,0 +1,22 @@
1
+ require "zlib"
2
+ require "stringio"
3
+
4
+ module Flipper
5
+ module Serializers
6
+ class Gzip
7
+ def self.serialize(source)
8
+ return if source.nil?
9
+ output = StringIO.new
10
+ gz = Zlib::GzipWriter.new(output)
11
+ gz.write(source)
12
+ gz.close
13
+ output.string
14
+ end
15
+
16
+ def self.deserialize(source)
17
+ return if source.nil?
18
+ Zlib::GzipReader.wrap(StringIO.new(source), &:read)
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,17 @@
1
+ require "json"
2
+
3
+ module Flipper
4
+ module Serializers
5
+ class Json
6
+ def self.serialize(source)
7
+ return if source.nil?
8
+ JSON.generate(source)
9
+ end
10
+
11
+ def self.deserialize(source)
12
+ return if source.nil?
13
+ JSON.parse(source)
14
+ end
15
+ end
16
+ end
17
+ 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)
@@ -90,19 +108,19 @@ RSpec.shared_examples_for 'a flipper adapter' do
90
108
  actor22 = Flipper::Actor.new('22')
91
109
  actor_asdf = Flipper::Actor.new('asdf')
92
110
 
93
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
94
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor_asdf))).to eq(true)
111
+ expect(feature.enable(actor22)).to be(true)
112
+ expect(feature.enable(actor_asdf)).to be(true)
95
113
 
96
- result = subject.get(feature)
97
- expect(result[:actors]).to eq(Set['22', 'asdf'])
114
+ expect(feature).to be_enabled(actor22)
115
+ expect(feature).to be_enabled(actor_asdf)
98
116
 
99
- expect(subject.disable(feature, actor_gate, Flipper::Types::Actor.new(actor22))).to eq(true)
100
- result = subject.get(feature)
101
- expect(result[:actors]).to eq(Set['asdf'])
117
+ expect(feature.disable(actor22)).to be(true)
118
+ expect(feature).not_to be_enabled(actor22)
119
+ expect(feature).to be_enabled(actor_asdf)
102
120
 
103
- expect(subject.disable(feature, actor_gate, Flipper::Types::Actor.new(actor_asdf))).to eq(true)
104
- result = subject.get(feature)
105
- expect(result[:actors]).to eq(Set.new)
121
+ expect(feature.disable(actor_asdf)).to eq(true)
122
+ expect(feature).not_to be_enabled(actor22)
123
+ expect(feature).not_to be_enabled(actor_asdf)
106
124
  end
107
125
 
108
126
  it 'can enable, disable and get value for percentage of actors gate' do
@@ -164,9 +182,10 @@ RSpec.shared_examples_for 'a flipper adapter' do
164
182
  end
165
183
 
166
184
  it 'converts the actor value to a string' do
167
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22)))).to eq(true)
168
- result = subject.get(feature)
169
- expect(result[:actors]).to eq(Set['22'])
185
+ actor = Flipper::Actor.new(22)
186
+ expect(feature).not_to be_enabled(actor)
187
+ feature.enable_actor actor
188
+ expect(feature).to be_enabled(actor)
170
189
  end
171
190
 
172
191
  it 'converts group value to a string' do
@@ -256,14 +275,14 @@ RSpec.shared_examples_for 'a flipper adapter' do
256
275
  expect(subject.add(flipper[:stats])).to eq(true)
257
276
  expect(subject.enable(flipper[:stats], boolean_gate, Flipper::Types::Boolean.new)).to eq(true)
258
277
  expect(subject.add(flipper[:search])).to eq(true)
278
+ flipper.enable :analytics, Flipper.property(:plan).eq("pro")
259
279
 
260
280
  result = subject.get_all
261
- expect(result).to be_instance_of(Hash)
262
281
 
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)
282
+ expect(result).to be_instance_of(Hash)
283
+ expect(result["stats"]).to eq(subject.default_config.merge(boolean: 'true'))
284
+ expect(result["search"]).to eq(subject.default_config)
285
+ expect(result["analytics"]).to eq(subject.default_config.merge(expression: {"Equal"=>[{"Property"=>["plan"]}, "pro"]}))
267
286
  end
268
287
 
269
288
  it 'includes explicitly disabled features when getting all features' do
@@ -277,9 +296,9 @@ RSpec.shared_examples_for 'a flipper adapter' do
277
296
 
278
297
  it 'can double enable an actor without error' do
279
298
  actor = Flipper::Actor.new('Flipper::Actor;22')
280
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))).to eq(true)
281
- expect(subject.enable(feature, actor_gate, Flipper::Types::Actor.new(actor))).to eq(true)
282
- expect(subject.get(feature).fetch(:actors)).to eq(Set['Flipper::Actor;22'])
299
+ expect(feature.enable(actor)).to be(true)
300
+ expect(feature.enable(actor)).to be(true)
301
+ expect(feature).to be_enabled(actor)
283
302
  end
284
303
 
285
304
  it 'can double enable a group without error' 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))
@@ -85,19 +104,19 @@ module Flipper
85
104
  actor22 = Flipper::Actor.new('22')
86
105
  actor_asdf = Flipper::Actor.new('asdf')
87
106
 
88
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22))
89
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf))
107
+ assert_equal true, @feature.enable(actor22)
108
+ assert_equal true, @feature.enable(actor_asdf)
90
109
 
91
- result = @adapter.get(@feature)
92
- assert_equal Set['22', 'asdf'], result[:actors]
110
+ assert @feature.enabled?(actor22)
111
+ assert @feature.enabled?(actor_asdf)
93
112
 
94
- assert true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor22))
95
- result = @adapter.get(@feature)
96
- assert_equal Set['asdf'], result[:actors]
113
+ assert_equal true, @feature.disable(actor22)
114
+ refute @feature.enabled?(actor22)
115
+ assert @feature.enabled?(actor_asdf)
97
116
 
98
- assert_equal true, @adapter.disable(@feature, @actor_gate, Flipper::Types::Actor.new(actor_asdf))
99
- result = @adapter.get(@feature)
100
- assert_equal Set.new, result[:actors]
117
+ assert_equal true, @feature.disable(actor_asdf)
118
+ refute @feature.enabled?(actor22)
119
+ refute @feature.enabled?(actor_asdf)
101
120
  end
102
121
 
103
122
  def test_can_enable_disable_get_value_for_percentage_of_actors_gate
@@ -159,10 +178,10 @@ module Flipper
159
178
  end
160
179
 
161
180
  def test_converts_the_actor_value_to_a_string
162
- assert_equal true,
163
- @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(Flipper::Actor.new(22)))
164
- result = @adapter.get(@feature)
165
- assert_equal Set['22'], result[:actors]
181
+ actor = Flipper::Actor.new(22)
182
+ refute @feature.enabled?(actor)
183
+ @feature.enable_actor actor
184
+ assert @feature.enabled?(actor)
166
185
  end
167
186
 
168
187
  def test_converts_group_value_to_a_string
@@ -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
@@ -273,9 +292,9 @@ module Flipper
273
292
 
274
293
  def test_can_double_enable_an_actor_without_error
275
294
  actor = Flipper::Actor.new('Flipper::Actor;22')
276
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor))
277
- assert_equal true, @adapter.enable(@feature, @actor_gate, Flipper::Types::Actor.new(actor))
278
- assert_equal Set['Flipper::Actor;22'], @adapter.get(@feature).fetch(:actors)
295
+ assert_equal true, @feature.enable(actor)
296
+ assert_equal true, @feature.enable(actor)
297
+ assert @feature.enabled?(actor)
279
298
  end
280
299
 
281
300
  def test_can_double_enable_a_group_without_error
@@ -0,0 +1,43 @@
1
+ module Flipper
2
+ module TestHelp
3
+ extend self
4
+
5
+ def flipper_configure
6
+ # Use a shared Memory adapter for all tests. This is instantiated outside of the
7
+ # `configure` block so the same instance is returned in new threads.
8
+ adapter = Flipper::Adapters::Memory.new
9
+
10
+ Flipper.configure do |config|
11
+ config.adapter { adapter }
12
+ config.default { Flipper.new(config.adapter) }
13
+ end
14
+ end
15
+
16
+ def flipper_reset
17
+ # Remove all features
18
+ Flipper.features.each(&:remove) rescue nil
19
+
20
+ # Reset previous DSL instance
21
+ Flipper.instance = nil
22
+ end
23
+ end
24
+ end
25
+
26
+ if defined?(RSpec) && RSpec.respond_to?(:configure)
27
+ RSpec.configure do |config|
28
+ config.include Flipper::TestHelp
29
+ config.before(:suite) { Flipper::TestHelp.flipper_configure }
30
+ config.before(:each) { flipper_reset }
31
+ end
32
+ end
33
+ if defined?(ActiveSupport)
34
+ ActiveSupport.on_load(:active_support_test_case) do
35
+ Flipper::TestHelp.flipper_configure
36
+
37
+ ActiveSupport::TestCase.class_eval do
38
+ include Flipper::TestHelp
39
+
40
+ setup :flipper_reset
41
+ end
42
+ end
43
+ end
@@ -1,8 +1,10 @@
1
1
  require 'set'
2
+ require "flipper/serializers/json"
3
+ require "flipper/serializers/gzip"
2
4
 
3
5
  module Flipper
4
- module Typecast
5
- TruthMap = {
6
+ class Typecast
7
+ TRUTH_MAP = {
6
8
  true => true,
7
9
  1 => true,
8
10
  'true' => true,
@@ -13,7 +15,7 @@ module Flipper
13
15
  #
14
16
  # Returns true or false.
15
17
  def self.to_boolean(value)
16
- !!TruthMap[value]
18
+ !!TRUTH_MAP[value]
17
19
  end
18
20
 
19
21
  # Internal: Convert value to an integer.
@@ -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,13 @@
1
1
  module Flipper
2
- VERSION = '1.0.0'.freeze
2
+ VERSION = '1.3.6'.freeze
3
+
4
+ REQUIRED_RUBY_VERSION = '2.6'.freeze
5
+ NEXT_REQUIRED_RUBY_VERSION = '3.0'.freeze
6
+
7
+ REQUIRED_RAILS_VERSION = '5.2'.freeze
8
+ NEXT_REQUIRED_RAILS_VERSION = '6.1.0'.freeze
9
+
10
+ def self.deprecated_ruby_version?
11
+ Gem::Version.new(RUBY_VERSION) < Gem::Version.new(NEXT_REQUIRED_RUBY_VERSION)
12
+ end
3
13
  end
data/lib/flipper.rb CHANGED
@@ -57,15 +57,49 @@ module Flipper
57
57
  # interface of Flipper::DSL.
58
58
  def_delegators :instance,
59
59
  :enabled?, :enable, :disable,
60
+ :enable_expression, :disable_expression,
61
+ :expression, :add_expression, :remove_expression,
60
62
  :enable_actor, :disable_actor,
61
63
  :enable_group, :disable_group,
62
64
  :enable_percentage_of_actors, :disable_percentage_of_actors,
63
65
  :enable_percentage_of_time, :disable_percentage_of_time,
64
66
  :features, :feature, :[], :preload, :preload_all,
65
67
  :adapter, :add, :exist?, :remove, :import, :export,
66
- :memoize=, :memoizing?,
68
+ :memoize=, :memoizing?, :read_only?,
67
69
  :sync, :sync_secret # For Flipper::Cloud. Will error for OSS Flipper.
68
70
 
71
+ def any(*args)
72
+ Expression.build({ Any: args.flatten })
73
+ end
74
+
75
+ def all(*args)
76
+ Expression.build({ All: args.flatten })
77
+ end
78
+
79
+ def constant(value)
80
+ Expression.build(value)
81
+ end
82
+
83
+ def property(name)
84
+ Expression.build({ Property: name })
85
+ end
86
+
87
+ def string(value)
88
+ Expression.build({ String: value })
89
+ end
90
+
91
+ def number(value)
92
+ Expression.build({ Number: value })
93
+ end
94
+
95
+ def boolean(value)
96
+ Expression.build({ Boolean: value })
97
+ end
98
+
99
+ def random(max)
100
+ Expression.build({ Random: max })
101
+ end
102
+
69
103
  # Public: Use this to register a group by name.
70
104
  #
71
105
  # name - The Symbol name of the group.
@@ -140,9 +174,13 @@ end
140
174
 
141
175
  require 'flipper/actor'
142
176
  require 'flipper/adapter'
177
+ require 'flipper/adapters/wrapper'
178
+ require 'flipper/adapters/actor_limit'
179
+ require 'flipper/adapters/instrumented'
143
180
  require 'flipper/adapters/memoizable'
144
181
  require 'flipper/adapters/memory'
145
- require 'flipper/adapters/instrumented'
182
+ require 'flipper/adapters/strict'
183
+ require 'flipper/adapter_builder'
146
184
  require 'flipper/configuration'
147
185
  require 'flipper/dsl'
148
186
  require 'flipper/errors'
@@ -155,6 +193,7 @@ require 'flipper/middleware/memoizer'
155
193
  require 'flipper/middleware/setup_env'
156
194
  require 'flipper/poller'
157
195
  require 'flipper/registry'
196
+ require 'flipper/expression'
158
197
  require 'flipper/type'
159
198
  require 'flipper/types/actor'
160
199
  require 'flipper/types/boolean'