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
@@ -80,7 +80,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
80
80
  context 'with preload: true' do
81
81
  let(:app) do
82
82
  # ensure scoped for builder block, annoying...
83
- instance = flipper
83
+ flipper
84
84
  middleware = described_class
85
85
 
86
86
  Rack::Builder.new do
@@ -141,7 +141,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
141
141
  context 'with preload specific' do
142
142
  let(:app) do
143
143
  # ensure scoped for builder block, annoying...
144
- instance = flipper
144
+ flipper
145
145
  middleware = described_class
146
146
 
147
147
  Rack::Builder.new do
@@ -196,10 +196,77 @@ RSpec.describe Flipper::Middleware::Memoizer do
196
196
  end
197
197
  end
198
198
 
199
+ context 'with preload block' do
200
+ let(:app) do
201
+ app = lambda do |_env|
202
+ flipper[:stats].enabled?
203
+ flipper[:stats].enabled?
204
+ flipper[:shiny].enabled?
205
+ flipper[:shiny].enabled?
206
+ [200, {}, []]
207
+ end
208
+
209
+ described_class.new(app, preload: ->(request) {
210
+ case request.path
211
+ when "/true"
212
+ true
213
+ when "/specific"
214
+ [:stats]
215
+ else
216
+ false
217
+ end
218
+ })
219
+ end
220
+
221
+ include_examples 'flipper middleware'
222
+
223
+ it 'eagerly caches known features for duration of request if block returns true' do
224
+ flipper[:stats].enable
225
+ flipper[:shiny].enable
226
+
227
+ # clear the log of operations
228
+ adapter.reset
229
+
230
+ get '/true', {}, 'flipper' => flipper
231
+
232
+ expect(adapter.operations.size).to be(1)
233
+ expect(adapter.count(:get_all)).to be(1)
234
+ expect(adapter.count(:get)).to be(0)
235
+ end
236
+
237
+ it 'does not eagerly cache known features if block returns false' do
238
+ flipper[:stats].enable
239
+ flipper[:shiny].enable
240
+
241
+ # clear the log of operations
242
+ adapter.reset
243
+
244
+ get '/false', {}, 'flipper' => flipper
245
+
246
+ expect(adapter.operations.size).to be(2)
247
+ expect(adapter.count(:get_all)).to be(0)
248
+ expect(adapter.count(:get)).to be(2)
249
+ end
250
+
251
+ it 'eagerly caches specified features for duration of request if block returns array of specified features' do
252
+ flipper[:stats].enable
253
+ flipper[:shiny].enable
254
+
255
+ # clear the log of operations
256
+ adapter.reset
257
+
258
+ get '/specific', {}, 'flipper' => flipper
259
+
260
+ expect(adapter.operations.size).to be(2)
261
+ expect(adapter.count(:get_multi)).to be(1)
262
+ expect(adapter.count(:get)).to be(1)
263
+ end
264
+ end
265
+
199
266
  context 'with multiple instances' do
200
267
  let(:app) do
201
268
  # ensure scoped for builder block, annoying...
202
- instance = flipper
269
+ flipper
203
270
  middleware = described_class
204
271
 
205
272
  Rack::Builder.new do
@@ -218,7 +285,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
218
285
  end
219
286
 
220
287
  def get(uri, params = {}, env = {}, &block)
221
- silence { super(uri, params, env, &block) }
288
+ capture_output { super(uri, params, env, &block) }
222
289
  end
223
290
 
224
291
  include_examples 'flipper middleware'
@@ -249,7 +316,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
249
316
  context 'with flipper setup in env' do
250
317
  let(:app) do
251
318
  # ensure scoped for builder block, annoying...
252
- instance = flipper
319
+ flipper
253
320
  middleware = described_class
254
321
 
255
322
  Rack::Builder.new do
@@ -391,9 +458,8 @@ RSpec.describe Flipper::Middleware::Memoizer do
391
458
  logged_memory = Flipper::Adapters::OperationLogger.new(memory)
392
459
  cache = ActiveSupport::Cache::MemoryStore.new
393
460
  cache.clear
394
- cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache, expires_in: 10)
461
+ cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache)
395
462
  logged_cached = Flipper::Adapters::OperationLogger.new(cached)
396
- memo = {}
397
463
  flipper = Flipper.new(logged_cached)
398
464
  flipper[:stats].enable
399
465
  flipper[:shiny].enable
@@ -404,15 +470,18 @@ RSpec.describe Flipper::Middleware::Memoizer do
404
470
 
405
471
  get '/', {}, 'flipper' => flipper
406
472
  expect(logged_cached.count(:get_all)).to be(1)
407
- expect(logged_memory.count(:get_all)).to be(1)
473
+ expect(logged_memory.count(:features)).to be(1)
474
+ expect(logged_memory.count(:get_multi)).to be(1)
408
475
 
409
476
  get '/', {}, 'flipper' => flipper
410
477
  expect(logged_cached.count(:get_all)).to be(2)
411
- expect(logged_memory.count(:get_all)).to be(1)
478
+ expect(logged_memory.count(:features)).to be(1)
479
+ expect(logged_memory.count(:get_multi)).to be(1)
412
480
 
413
481
  get '/', {}, 'flipper' => flipper
414
482
  expect(logged_cached.count(:get_all)).to be(3)
415
- expect(logged_memory.count(:get_all)).to be(1)
483
+ expect(logged_memory.count(:features)).to be(1)
484
+ expect(logged_memory.count(:get_multi)).to be(1)
416
485
  end
417
486
  end
418
487
  end
@@ -0,0 +1,72 @@
1
+ require 'active_record'
2
+ require 'flipper/model/active_record'
3
+
4
+ # Turn off migration logging for specs
5
+ ActiveRecord::Migration.verbose = false
6
+
7
+ RSpec.describe Flipper::Model::ActiveRecord do
8
+ before(:all) do
9
+ ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
10
+ end
11
+
12
+ before(:each) do
13
+ ActiveRecord::Base.connection.execute <<-SQL
14
+ CREATE TABLE users (
15
+ id integer PRIMARY KEY,
16
+ name string NOT NULL,
17
+ age integer,
18
+ is_confirmed boolean,
19
+ created_at datetime NOT NULL,
20
+ updated_at datetime NOT NULL
21
+ )
22
+ SQL
23
+ end
24
+
25
+ after(:each) do
26
+ ActiveRecord::Base.connection.execute("DROP table IF EXISTS `users`")
27
+ end
28
+
29
+ class User < ActiveRecord::Base
30
+ include Flipper::Model::ActiveRecord
31
+ end
32
+
33
+ class DelegatedUser < DelegateClass(User)
34
+ end
35
+
36
+ class Admin < User
37
+ end
38
+
39
+ it "doesn't warn for to_ary" do
40
+ # looks like we should remove this but you are wrong, we have specs that
41
+ # fail if there are warnings and if this regresses it will print a warning
42
+ # so it is in fact testing something
43
+ user = User.create!(name: "Test")
44
+ Flipper.enabled?(:something, DelegatedUser.new(user))
45
+ end
46
+
47
+ describe "flipper_id" do
48
+ it "returns class name and id" do
49
+ expect(User.new(id: 1).flipper_id).to eq("User;1")
50
+ end
51
+
52
+ it "uses base class name" do
53
+ expect(Admin.new(id: 2).flipper_id).to eq("User;2")
54
+ end
55
+ end
56
+
57
+ describe "flipper_properties" do
58
+ subject { User.create!(name: "Test", age: 22, is_confirmed: true) }
59
+
60
+ it "includes all attributes" do
61
+ expect(subject.flipper_properties).to eq({
62
+ "type" => "User",
63
+ "id" => subject.id,
64
+ "name" => "Test",
65
+ "age" => 22,
66
+ "is_confirmed" => true,
67
+ "created_at" => subject.created_at,
68
+ "updated_at" => subject.updated_at
69
+ })
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,13 @@
1
+ require 'flipper/serializers/gzip'
2
+
3
+ RSpec.describe Flipper::Serializers::Gzip do
4
+ it "serializes and deserializes" do
5
+ serialized = described_class.serialize("my data")
6
+ expect(described_class.deserialize(serialized)).to eq("my data")
7
+ end
8
+
9
+ it "doesn't fail with nil" do
10
+ expect(described_class.serialize(nil)).to be(nil)
11
+ expect(described_class.deserialize(nil)).to be(nil)
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'flipper/serializers/json'
2
+
3
+ RSpec.describe Flipper::Serializers::Json do
4
+ it "serializes and deserializes" do
5
+ serialized = described_class.serialize("my data")
6
+ expect(described_class.deserialize(serialized)).to eq("my data")
7
+ end
8
+
9
+ it "doesn't fail with nil" do
10
+ expect(described_class.serialize(nil)).to be(nil)
11
+ expect(described_class.deserialize(nil)).to be(nil)
12
+ end
13
+ end
@@ -56,7 +56,7 @@ RSpec.describe Flipper::Typecast do
56
56
  nil => 0,
57
57
  '' => 0,
58
58
  0 => 0,
59
- 0.0 => 0,
59
+ 0.0 => 0.0,
60
60
  1 => 1,
61
61
  1.1 => 1.1,
62
62
  '0.01' => 0.01,
@@ -65,9 +65,9 @@ RSpec.describe Flipper::Typecast do
65
65
  '99' => 99,
66
66
  '99.9' => 99.9,
67
67
  }.each do |value, expected|
68
- context "#to_percentage for #{value.inspect}" do
68
+ context "#to_number for #{value.inspect}" do
69
69
  it "returns #{expected}" do
70
- expect(described_class.to_percentage(value)).to be(expected)
70
+ expect(described_class.to_number(value)).to be(expected)
71
71
  end
72
72
  end
73
73
  end
@@ -99,14 +99,14 @@ RSpec.describe Flipper::Typecast do
99
99
 
100
100
  it 'raises argument error for bad integer percentage' do
101
101
  expect do
102
- described_class.to_percentage(['asdf'])
103
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
102
+ described_class.to_number(['asdf'])
103
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
104
104
  end
105
105
 
106
106
  it 'raises argument error for bad float percentage' do
107
107
  expect do
108
- described_class.to_percentage(['asdf.0'])
109
- end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a percentage))
108
+ described_class.to_number(['asdf.0'])
109
+ end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a number))
110
110
  end
111
111
 
112
112
  it 'raises argument error for set value that cannot be converted to a set' do
@@ -127,6 +127,30 @@ RSpec.describe Flipper::Typecast do
127
127
  expect(result["search"]).not_to be(hash["search"])
128
128
  end
129
129
 
130
+ it "converts does not convert expressions" do
131
+ hash = {
132
+ "search" => {
133
+ boolean: nil,
134
+ expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
135
+ groups: ['a', 'b'],
136
+ actors: ['User;1'],
137
+ percentage_of_actors: nil,
138
+ percentage_of_time: nil,
139
+ },
140
+ }
141
+ result = described_class.features_hash(hash)
142
+ expect(result).to eq({
143
+ "search" => {
144
+ boolean: nil,
145
+ expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
146
+ groups: Set['a', 'b'],
147
+ actors: Set['User;1'],
148
+ percentage_of_actors: nil,
149
+ percentage_of_time: nil,
150
+ },
151
+ })
152
+ end
153
+
130
154
  it "converts gate value arrays to sets" do
131
155
  hash = {
132
156
  "search" => {
@@ -193,4 +217,16 @@ RSpec.describe Flipper::Typecast do
193
217
  })
194
218
  end
195
219
  end
220
+
221
+ it "converts to and from json" do
222
+ source = {"foo" => "bar"}
223
+ output = described_class.to_json(source)
224
+ expect(described_class.from_json(output)).to eq(source)
225
+ end
226
+
227
+ it "converts to and from gzip" do
228
+ source = "foo bar"
229
+ output = described_class.to_gzip(source)
230
+ expect(described_class.from_gzip(output)).to eq(source)
231
+ end
196
232
  end
@@ -11,12 +11,19 @@ RSpec.describe Flipper::Types::Actor do
11
11
  attr_reader :flipper_id
12
12
 
13
13
  def initialize(flipper_id)
14
- @flipper_id = flipper_id
14
+ @flipper_id = flipper_id.to_s
15
15
  end
16
16
 
17
17
  def admin?
18
18
  true
19
19
  end
20
+
21
+ def flipper_properties
22
+ {
23
+ "flipper_id" => flipper_id,
24
+ "admin" => admin?,
25
+ }
26
+ end
20
27
  end
21
28
  end
22
29
 
@@ -87,6 +94,15 @@ RSpec.describe Flipper::Types::Actor do
87
94
  expect(actor.admin?).to eq(true)
88
95
  end
89
96
 
97
+ it 'proxies flipper_properties to actor' do
98
+ actor = actor_class.new(10)
99
+ actor = described_class.new(actor)
100
+ expect(actor.flipper_properties).to eq({
101
+ "flipper_id" => "10",
102
+ "admin" => true,
103
+ })
104
+ end
105
+
90
106
  it 'exposes actor' do
91
107
  actor = actor_class.new(10)
92
108
  actor_type_instance = described_class.new(actor)
@@ -104,6 +120,7 @@ RSpec.describe Flipper::Types::Actor do
104
120
  actor = actor_class.new(10)
105
121
  actor_type_instance = described_class.new(actor)
106
122
  expect(actor_type_instance.respond_to?(:admin?)).to eq(true)
123
+ expect(actor_type_instance.respond_to?(:flipper_properties)).to eq(true)
107
124
  end
108
125
 
109
126
  it 'returns false if does not respond to method and actor does not respond to method' do
@@ -8,17 +8,24 @@ RSpec.describe Flipper do
8
8
  let(:dev_group) { flipper.group(:devs) }
9
9
 
10
10
  let(:admin_actor) do
11
- double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false
11
+ double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false, flipper_properties: {"admin" => true, "dev" => false}
12
12
  end
13
13
  let(:dev_actor) do
14
- double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true
14
+ double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true, flipper_properties: {"admin" => false, "dev" => true}
15
15
  end
16
16
 
17
17
  let(:admin_truthy_actor) do
18
- double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false
18
+ double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false, flipper_properties: {"admin" => "true-ish", "dev" => false}
19
19
  end
20
20
  let(:admin_falsey_actor) do
21
- double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false
21
+ double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false, flipper_properties: {"admin" => nil, "dev" => false}
22
+ end
23
+
24
+ let(:basic_plan_actor) do
25
+ double 'Non Flipper Thing', flipper_id: 1, flipper_properties: {"plan" => "basic"}
26
+ end
27
+ let(:premium_plan_actor) do
28
+ double 'Non Flipper Thing', flipper_id: 10, flipper_properties: {"plan" => "premium"}
22
29
  end
23
30
 
24
31
  let(:pitt) { Flipper::Actor.new(1) }
@@ -70,10 +77,12 @@ RSpec.describe Flipper do
70
77
 
71
78
  it 'enables feature for flipper actor in group' do
72
79
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_actor))).to eq(true)
80
+ expect(feature.enabled?(admin_actor)).to eq(true)
73
81
  end
74
82
 
75
83
  it 'does not enable for flipper actor not in group' do
76
84
  expect(feature.enabled?(Flipper::Types::Actor.new(dev_actor))).to eq(false)
85
+ expect(feature.enabled?(dev_actor)).to eq(false)
77
86
  end
78
87
 
79
88
  it 'does not enable feature for all' do
@@ -257,10 +266,12 @@ RSpec.describe Flipper do
257
266
 
258
267
  it 'disables feature for flipper actor in group' do
259
268
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_actor))).to eq(false)
269
+ expect(feature.enabled?(admin_actor)).to eq(false)
260
270
  end
261
271
 
262
272
  it 'does not disable feature for flipper actor in other groups' do
263
273
  expect(feature.enabled?(Flipper::Types::Actor.new(dev_actor))).to eq(true)
274
+ expect(feature.enabled?(dev_actor)).to eq(true)
264
275
  end
265
276
 
266
277
  it 'adds feature to set of features' do
@@ -379,6 +390,7 @@ RSpec.describe Flipper do
379
390
 
380
391
  it 'returns true for truthy block values' do
381
392
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_truthy_actor))).to eq(true)
393
+ expect(feature.enabled?(admin_truthy_actor)).to eq(true)
382
394
  end
383
395
 
384
396
  it 'returns true if any actor is in enabled group' do
@@ -394,6 +406,7 @@ RSpec.describe Flipper do
394
406
 
395
407
  it 'returns false for falsey block values' do
396
408
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_falsey_actor))).to eq(false)
409
+ expect(feature.enabled?(admin_falsey_actor)).to eq(false)
397
410
  end
398
411
  end
399
412
 
@@ -549,4 +562,89 @@ RSpec.describe Flipper do
549
562
  expect(feature.enabled?(dev_actor)).to eq(false)
550
563
  end
551
564
  end
565
+
566
+ context "for expression" do
567
+ it "works" do
568
+ feature.enable Flipper.property(:plan).eq("basic")
569
+
570
+ expect(feature.enabled?).to be(false)
571
+ expect(feature.enabled?(basic_plan_actor)).to be(true)
572
+ expect(feature.enabled?(premium_plan_actor)).to be(false)
573
+ expect(feature.enabled?(admin_actor)).to be(false)
574
+ end
575
+
576
+ it "works for true expression with no actor" do
577
+ feature.enable Flipper.boolean(true)
578
+ expect(feature.enabled?).to be(true)
579
+ end
580
+
581
+ it "works for multiple actors" do
582
+ feature.enable Flipper.property(:plan).eq("basic")
583
+
584
+ expect(feature.enabled?(basic_plan_actor, premium_plan_actor)).to be(true)
585
+ expect(feature.enabled?(premium_plan_actor, basic_plan_actor)).to be(true)
586
+ expect(feature.enabled?(premium_plan_actor, admin_actor)).to be(false)
587
+ end
588
+ end
589
+
590
+ context "for Any" do
591
+ it "works" do
592
+ expression = Flipper.any(
593
+ Flipper.property(:plan).eq("basic"),
594
+ Flipper.property(:plan).eq("plus"),
595
+ )
596
+ feature.enable expression
597
+
598
+ expect(feature.enabled?(basic_plan_actor)).to be(true)
599
+ expect(feature.enabled?(premium_plan_actor)).to be(false)
600
+ end
601
+ end
602
+
603
+ context "for All" do
604
+ it "works" do
605
+ true_actor = Flipper::Actor.new("User;1", {
606
+ "plan" => "basic",
607
+ "age" => 21,
608
+ })
609
+ false_actor = Flipper::Actor.new("User;1", {
610
+ "plan" => "basic",
611
+ "age" => 20,
612
+ })
613
+ expression = Flipper.all(
614
+ Flipper.property(:plan).eq("basic"),
615
+ Flipper.property(:age).eq(21)
616
+ )
617
+ feature.enable expression
618
+
619
+ expect(feature.enabled?(true_actor)).to be(true)
620
+ expect(feature.enabled?(false_actor)).to be(false)
621
+ end
622
+
623
+ it "works when nested" do
624
+ admin_actor = Flipper::Actor.new("User;1", {
625
+ "admin" => true,
626
+ })
627
+ true_actor = Flipper::Actor.new("User;1", {
628
+ "plan" => "basic",
629
+ "age" => 21,
630
+ })
631
+ false_actor = Flipper::Actor.new("User;1", {
632
+ "plan" => "basic",
633
+ "age" => 20,
634
+ })
635
+ expression = Flipper.any(
636
+ Flipper.property(:admin).eq(true),
637
+ Flipper.all(
638
+ Flipper.property(:plan).eq("basic"),
639
+ Flipper.property(:age).eq(21)
640
+ )
641
+ )
642
+
643
+ feature.enable expression
644
+
645
+ expect(feature.enabled?(admin_actor)).to be(true)
646
+ expect(feature.enabled?(true_actor)).to be(true)
647
+ expect(feature.enabled?(false_actor)).to be(false)
648
+ end
649
+ end
552
650
  end
data/spec/flipper_spec.rb CHANGED
@@ -64,7 +64,12 @@ RSpec.describe Flipper do
64
64
 
65
65
  describe "delegation to instance" do
66
66
  let(:group) { Flipper::Types::Group.new(:admins) }
67
- let(:actor) { Flipper::Actor.new("1") }
67
+ let(:actor) {
68
+ Flipper::Actor.new("1", {
69
+ "plan" => "basic",
70
+ })
71
+ }
72
+ let(:expression) { Flipper.property(:plan).eq("basic") }
68
73
 
69
74
  before do
70
75
  described_class.configure do |config|
@@ -88,6 +93,37 @@ RSpec.describe Flipper do
88
93
  expect(described_class.instance.enabled?(:search)).to be(false)
89
94
  end
90
95
 
96
+ it 'delegates expression to instance' do
97
+ expect(described_class.expression(:search)).to be(nil)
98
+
99
+ expression = Flipper.property(:plan).eq("basic")
100
+ Flipper.instance.enable_expression :search, expression
101
+
102
+ expect(described_class.expression(:search)).to eq(expression)
103
+ end
104
+
105
+ it 'delegates enable_expression to instance' do
106
+ described_class.enable_expression(:search, expression)
107
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
108
+ end
109
+
110
+ it 'delegates disable_expression to instance' do
111
+ described_class.disable_expression(:search)
112
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
113
+ end
114
+
115
+ it 'delegates add_expression to instance' do
116
+ described_class.add_expression(:search, expression)
117
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
118
+ end
119
+
120
+ it 'delegates remove_expression to instance' do
121
+ described_class.enable_expression(:search, Flipper.any(expression))
122
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
123
+ described_class.remove_expression(:search, expression)
124
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
125
+ end
126
+
91
127
  it 'delegates enable_actor to instance' do
92
128
  described_class.enable_actor(:search, actor)
93
129
  expect(described_class.instance.enabled?(:search, actor)).to be(true)
@@ -192,6 +228,10 @@ RSpec.describe Flipper do
192
228
  expect(described_class.memoizing?).to eq(described_class.adapter.memoizing?)
193
229
  end
194
230
 
231
+ it 'delegates read_only? to instance' do
232
+ expect(described_class.read_only?).to eq(described_class.adapter.read_only?)
233
+ end
234
+
195
235
  it 'delegates sync stuff to instance and does nothing' do
196
236
  expect(described_class.sync).to be(nil)
197
237
  expect(described_class.sync_secret).to be(nil)
@@ -201,7 +241,7 @@ RSpec.describe Flipper do
201
241
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
202
242
  with({
203
243
  headers: {
204
- 'Flipper-Cloud-Token'=>'asdf',
244
+ 'flipper-cloud-token'=>'asdf',
205
245
  },
206
246
  }).to_return(status: 200, body: '{"features": {}}', headers: {})
207
247
  cloud_configuration = Flipper::Cloud::Configuration.new({
@@ -273,7 +313,7 @@ RSpec.describe Flipper do
273
313
 
274
314
  describe '.group_exists' do
275
315
  it 'returns true if the group is already created' do
276
- group = described_class.register('admins', &:admin?)
316
+ described_class.register('admins', &:admin?)
277
317
  expect(described_class.group_exists?(:admins)).to eq(true)
278
318
  end
279
319
 
@@ -326,4 +366,52 @@ RSpec.describe Flipper do
326
366
  expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
327
367
  end
328
368
  end
369
+
370
+ describe ".constant" do
371
+ it "returns Flipper::Expression::Constant instance" do
372
+ expect(described_class.constant(false)).to eq(Flipper::Expression::Constant.new(false))
373
+ expect(described_class.constant("string")).to eq(Flipper::Expression::Constant.new("string"))
374
+ end
375
+ end
376
+
377
+ describe ".property" do
378
+ it "returns Flipper::Expressions::Property expression" do
379
+ expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name"))
380
+ end
381
+ end
382
+
383
+ describe ".boolean" do
384
+ it "returns Flipper::Expressions::Boolean expression" do
385
+ expect(described_class.boolean(true)).to eq(Flipper::Expression.build(Boolean: true))
386
+ expect(described_class.boolean(false)).to eq(Flipper::Expression.build(Boolean: false))
387
+ end
388
+ end
389
+
390
+ describe ".random" do
391
+ it "returns Flipper::Expressions::Random expression" do
392
+ expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100))
393
+ end
394
+ end
395
+
396
+ describe ".any" do
397
+ let(:age_expression) { Flipper.property(:age).gte(21) }
398
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
399
+
400
+ it "returns Flipper::Expressions::Any instance" do
401
+ expect(Flipper.any(age_expression, plan_expression)).to eq(
402
+ Flipper::Expression.build({Any: [age_expression, plan_expression]})
403
+ )
404
+ end
405
+ end
406
+
407
+ describe ".all" do
408
+ let(:age_expression) { Flipper.property(:age).gte(21) }
409
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
410
+
411
+ it "returns Flipper::Expressions::All instance" do
412
+ expect(Flipper.all(age_expression, plan_expression)).to eq(
413
+ Flipper::Expression.build({All: [age_expression, plan_expression]})
414
+ )
415
+ end
416
+ end
329
417
  end