flipper 0.16.0 → 1.4.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 (285) hide show
  1. checksums.yaml +5 -5
  2. data/.codeclimate.yml +1 -0
  3. data/.github/FUNDING.yml +1 -0
  4. data/.github/dependabot.yml +6 -0
  5. data/.github/workflows/ci.yml +110 -0
  6. data/.github/workflows/examples.yml +105 -0
  7. data/.github/workflows/release.yml +54 -0
  8. data/.rspec +1 -0
  9. data/CLAUDE.md +87 -0
  10. data/Changelog.md +2 -215
  11. data/Dockerfile +1 -1
  12. data/Gemfile +28 -20
  13. data/README.md +72 -62
  14. data/Rakefile +13 -3
  15. data/benchmark/enabled_ips.rb +10 -0
  16. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  17. data/benchmark/enabled_profile.rb +20 -0
  18. data/benchmark/instrumentation_ips.rb +21 -0
  19. data/benchmark/typecast_ips.rb +27 -0
  20. data/docker-compose.yml +37 -34
  21. data/docs/DockerCompose.md +0 -1
  22. data/docs/README.md +1 -0
  23. data/docs/images/banner.jpg +0 -0
  24. data/docs/images/flipper_cloud.png +0 -0
  25. data/examples/api/basic.ru +18 -0
  26. data/examples/api/custom_memoized.ru +36 -0
  27. data/examples/api/memoized.ru +42 -0
  28. data/examples/basic.rb +1 -12
  29. data/examples/cloud/app.ru +12 -0
  30. data/examples/cloud/backoff_policy.rb +13 -0
  31. data/examples/cloud/basic.rb +22 -0
  32. data/examples/cloud/cloud_setup.rb +20 -0
  33. data/examples/cloud/forked.rb +36 -0
  34. data/examples/cloud/import.rb +17 -0
  35. data/examples/cloud/poll_interval/README.md +111 -0
  36. data/examples/cloud/poll_interval/client.rb +108 -0
  37. data/examples/cloud/poll_interval/server.rb +98 -0
  38. data/examples/cloud/threaded.rb +33 -0
  39. data/examples/configuring_default.rb +2 -5
  40. data/examples/dsl.rb +10 -35
  41. data/examples/enabled_for_actor.rb +10 -15
  42. data/examples/expressions.rb +237 -0
  43. data/examples/group.rb +3 -6
  44. data/examples/group_dynamic_lookup.rb +5 -19
  45. data/examples/group_with_members.rb +4 -14
  46. data/examples/importing.rb +1 -1
  47. data/examples/individual_actor.rb +2 -5
  48. data/examples/instrumentation.rb +2 -2
  49. data/examples/instrumentation_last_accessed_at.rb +38 -0
  50. data/examples/memoizing.rb +35 -0
  51. data/examples/mirroring.rb +59 -0
  52. data/examples/percentage_of_actors.rb +6 -16
  53. data/examples/percentage_of_actors_enabled_check.rb +7 -10
  54. data/examples/percentage_of_actors_group.rb +5 -18
  55. data/examples/percentage_of_time.rb +3 -6
  56. data/examples/strict.rb +18 -0
  57. data/exe/flipper +5 -0
  58. data/flipper-cloud.gemspec +19 -0
  59. data/flipper.gemspec +10 -7
  60. data/lib/flipper/actor.rb +10 -3
  61. data/lib/flipper/adapter.rb +50 -8
  62. data/lib/flipper/adapter_builder.rb +44 -0
  63. data/lib/flipper/adapters/actor_limit.rb +54 -0
  64. data/lib/flipper/adapters/cache_base.rb +161 -0
  65. data/lib/flipper/adapters/dual_write.rb +63 -0
  66. data/lib/flipper/adapters/failover.rb +85 -0
  67. data/lib/flipper/adapters/failsafe.rb +72 -0
  68. data/lib/flipper/adapters/http/client.rb +64 -7
  69. data/lib/flipper/adapters/http/error.rb +19 -1
  70. data/lib/flipper/adapters/http.rb +97 -43
  71. data/lib/flipper/adapters/instrumented.rb +47 -26
  72. data/lib/flipper/adapters/memoizable.rb +44 -40
  73. data/lib/flipper/adapters/memory.rb +75 -111
  74. data/lib/flipper/adapters/operation_logger.rb +22 -78
  75. data/lib/flipper/adapters/poll/poller.rb +2 -0
  76. data/lib/flipper/adapters/poll.rb +52 -0
  77. data/lib/flipper/adapters/pstore.rb +27 -17
  78. data/lib/flipper/adapters/read_only.rb +8 -41
  79. data/lib/flipper/adapters/strict.rb +45 -0
  80. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  81. data/lib/flipper/adapters/sync/interval_synchronizer.rb +2 -7
  82. data/lib/flipper/adapters/sync/synchronizer.rb +13 -6
  83. data/lib/flipper/adapters/sync.rb +23 -29
  84. data/lib/flipper/adapters/wrapper.rb +54 -0
  85. data/lib/flipper/cli.rb +314 -0
  86. data/lib/flipper/cloud/configuration.rb +271 -0
  87. data/lib/flipper/cloud/dsl.rb +27 -0
  88. data/lib/flipper/cloud/message_verifier.rb +95 -0
  89. data/lib/flipper/cloud/middleware.rb +63 -0
  90. data/lib/flipper/cloud/migrate.rb +71 -0
  91. data/lib/flipper/cloud/routes.rb +14 -0
  92. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  93. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  94. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  95. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  96. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  97. data/lib/flipper/cloud/telemetry.rb +191 -0
  98. data/lib/flipper/cloud.rb +54 -0
  99. data/lib/flipper/configuration.rb +54 -7
  100. data/lib/flipper/dsl.rb +58 -47
  101. data/lib/flipper/engine.rb +102 -0
  102. data/lib/flipper/errors.rb +3 -21
  103. data/lib/flipper/export.rb +24 -0
  104. data/lib/flipper/exporter.rb +17 -0
  105. data/lib/flipper/exporters/json/export.rb +32 -0
  106. data/lib/flipper/exporters/json/v1.rb +33 -0
  107. data/lib/flipper/expression/builder.rb +73 -0
  108. data/lib/flipper/expression/constant.rb +25 -0
  109. data/lib/flipper/expression.rb +71 -0
  110. data/lib/flipper/expressions/all.rb +9 -0
  111. data/lib/flipper/expressions/any.rb +9 -0
  112. data/lib/flipper/expressions/boolean.rb +9 -0
  113. data/lib/flipper/expressions/comparable.rb +13 -0
  114. data/lib/flipper/expressions/equal.rb +9 -0
  115. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  116. data/lib/flipper/expressions/greater_than.rb +9 -0
  117. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  118. data/lib/flipper/expressions/less_than.rb +9 -0
  119. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  120. data/lib/flipper/expressions/not_equal.rb +9 -0
  121. data/lib/flipper/expressions/now.rb +9 -0
  122. data/lib/flipper/expressions/number.rb +9 -0
  123. data/lib/flipper/expressions/percentage.rb +9 -0
  124. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  125. data/lib/flipper/expressions/property.rb +9 -0
  126. data/lib/flipper/expressions/random.rb +9 -0
  127. data/lib/flipper/expressions/string.rb +9 -0
  128. data/lib/flipper/expressions/time.rb +16 -0
  129. data/lib/flipper/feature.rb +95 -28
  130. data/lib/flipper/feature_check_context.rb +10 -6
  131. data/lib/flipper/gate.rb +13 -11
  132. data/lib/flipper/gate_values.rb +5 -18
  133. data/lib/flipper/gates/actor.rb +10 -17
  134. data/lib/flipper/gates/boolean.rb +1 -1
  135. data/lib/flipper/gates/expression.rb +75 -0
  136. data/lib/flipper/gates/group.rb +5 -7
  137. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  138. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  139. data/lib/flipper/identifier.rb +17 -0
  140. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  141. data/lib/flipper/instrumentation/statsd.rb +4 -2
  142. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  143. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  144. data/lib/flipper/instrumenters/memory.rb +6 -2
  145. data/lib/flipper/metadata.rb +8 -1
  146. data/lib/flipper/middleware/memoizer.rb +46 -27
  147. data/lib/flipper/middleware/setup_env.rb +13 -3
  148. data/lib/flipper/model/active_record.rb +23 -0
  149. data/lib/flipper/poller.rb +157 -0
  150. data/lib/flipper/serializers/gzip.rb +22 -0
  151. data/lib/flipper/serializers/json.rb +17 -0
  152. data/lib/flipper/spec/shared_adapter_specs.rb +122 -56
  153. data/lib/flipper/test/shared_adapter_test.rb +120 -52
  154. data/lib/flipper/test_help.rb +43 -0
  155. data/lib/flipper/typecast.rb +59 -18
  156. data/lib/flipper/types/actor.rb +19 -13
  157. data/lib/flipper/types/group.rb +12 -5
  158. data/lib/flipper/types/percentage.rb +1 -1
  159. data/lib/flipper/version.rb +11 -1
  160. data/lib/flipper.rb +71 -12
  161. data/lib/generators/flipper/setup_generator.rb +68 -0
  162. data/lib/generators/flipper/templates/initializer.rb +45 -0
  163. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  164. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  165. data/lib/generators/flipper/update_generator.rb +35 -0
  166. data/package-lock.json +41 -0
  167. data/package.json +10 -0
  168. data/spec/fixtures/environment.rb +1 -0
  169. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  170. data/spec/flipper/actor_spec.rb +10 -2
  171. data/spec/flipper/adapter_builder_spec.rb +72 -0
  172. data/spec/flipper/adapter_spec.rb +52 -6
  173. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  174. data/spec/flipper/adapters/dual_write_spec.rb +82 -0
  175. data/spec/flipper/adapters/failover_spec.rb +141 -0
  176. data/spec/flipper/adapters/failsafe_spec.rb +58 -0
  177. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  178. data/spec/flipper/adapters/http_spec.rb +402 -65
  179. data/spec/flipper/adapters/instrumented_spec.rb +31 -13
  180. data/spec/flipper/adapters/memoizable_spec.rb +51 -33
  181. data/spec/flipper/adapters/memory_spec.rb +33 -5
  182. data/spec/flipper/adapters/operation_logger_spec.rb +38 -12
  183. data/spec/flipper/adapters/poll_spec.rb +41 -0
  184. data/spec/flipper/adapters/pstore_spec.rb +0 -2
  185. data/spec/flipper/adapters/read_only_spec.rb +32 -18
  186. data/spec/flipper/adapters/strict_spec.rb +64 -0
  187. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -1
  188. data/spec/flipper/adapters/sync/interval_synchronizer_spec.rb +4 -5
  189. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -1
  190. data/spec/flipper/adapters/sync_spec.rb +17 -6
  191. data/spec/flipper/cli_spec.rb +217 -0
  192. data/spec/flipper/cloud/configuration_spec.rb +257 -0
  193. data/spec/flipper/cloud/dsl_spec.rb +90 -0
  194. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  195. data/spec/flipper/cloud/middleware_spec.rb +307 -0
  196. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  197. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  198. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  199. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  200. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  201. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  202. data/spec/flipper/cloud_spec.rb +186 -0
  203. data/spec/flipper/configuration_spec.rb +37 -3
  204. data/spec/flipper/dsl_spec.rb +67 -80
  205. data/spec/flipper/engine_spec.rb +374 -0
  206. data/spec/flipper/export_spec.rb +13 -0
  207. data/spec/flipper/exporter_spec.rb +16 -0
  208. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  209. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  210. data/spec/flipper/expression/builder_spec.rb +248 -0
  211. data/spec/flipper/expression_spec.rb +188 -0
  212. data/spec/flipper/expressions/all_spec.rb +15 -0
  213. data/spec/flipper/expressions/any_spec.rb +15 -0
  214. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  215. data/spec/flipper/expressions/equal_spec.rb +24 -0
  216. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  217. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  218. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  219. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  220. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  221. data/spec/flipper/expressions/now_spec.rb +11 -0
  222. data/spec/flipper/expressions/number_spec.rb +21 -0
  223. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  224. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  225. data/spec/flipper/expressions/property_spec.rb +13 -0
  226. data/spec/flipper/expressions/random_spec.rb +9 -0
  227. data/spec/flipper/expressions/string_spec.rb +11 -0
  228. data/spec/flipper/expressions/time_spec.rb +29 -0
  229. data/spec/flipper/feature_check_context_spec.rb +18 -20
  230. data/spec/flipper/feature_spec.rb +461 -48
  231. data/spec/flipper/gate_spec.rb +0 -2
  232. data/spec/flipper/gate_values_spec.rb +2 -34
  233. data/spec/flipper/gates/actor_spec.rb +0 -2
  234. data/spec/flipper/gates/boolean_spec.rb +1 -3
  235. data/spec/flipper/gates/expression_spec.rb +190 -0
  236. data/spec/flipper/gates/group_spec.rb +2 -5
  237. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -7
  238. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -4
  239. data/spec/flipper/identifier_spec.rb +12 -0
  240. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -7
  241. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -3
  242. data/spec/flipper/instrumenters/memory_spec.rb +18 -1
  243. data/spec/flipper/instrumenters/noop_spec.rb +14 -8
  244. data/spec/flipper/middleware/memoizer_spec.rb +199 -62
  245. data/spec/flipper/middleware/setup_env_spec.rb +23 -5
  246. data/spec/flipper/model/active_record_spec.rb +72 -0
  247. data/spec/flipper/poller_spec.rb +390 -0
  248. data/spec/flipper/registry_spec.rb +0 -1
  249. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  250. data/spec/flipper/serializers/json_spec.rb +13 -0
  251. data/spec/flipper/typecast_spec.rb +121 -7
  252. data/spec/flipper/types/actor_spec.rb +63 -47
  253. data/spec/flipper/types/boolean_spec.rb +0 -1
  254. data/spec/flipper/types/group_spec.rb +24 -3
  255. data/spec/flipper/types/percentage_of_actors_spec.rb +0 -1
  256. data/spec/flipper/types/percentage_of_time_spec.rb +0 -1
  257. data/spec/flipper/types/percentage_spec.rb +0 -1
  258. data/spec/{integration_spec.rb → flipper_integration_spec.rb} +301 -59
  259. data/spec/flipper_spec.rb +123 -29
  260. data/spec/{helper.rb → spec_helper.rb} +23 -21
  261. data/spec/support/actor_names.yml +1 -0
  262. data/spec/support/descriptions.yml +1 -0
  263. data/spec/support/fail_on_output.rb +8 -0
  264. data/spec/support/fake_backoff_policy.rb +15 -0
  265. data/spec/support/skippable.rb +18 -0
  266. data/spec/support/spec_helpers.rb +53 -6
  267. data/test/adapters/actor_limit_test.rb +20 -0
  268. data/test/test_helper.rb +2 -1
  269. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  270. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  271. data/test_rails/helper.rb +31 -0
  272. data/test_rails/system/test_help_test.rb +52 -0
  273. metadata +200 -82
  274. data/.rubocop.yml +0 -54
  275. data/.rubocop_todo.yml +0 -199
  276. data/docs/Adapters.md +0 -124
  277. data/docs/Caveats.md +0 -4
  278. data/docs/Gates.md +0 -167
  279. data/docs/Instrumentation.md +0 -27
  280. data/docs/Optimization.md +0 -114
  281. data/docs/api/README.md +0 -849
  282. data/docs/http/README.md +0 -35
  283. data/docs/read-only/README.md +0 -21
  284. data/examples/example_setup.rb +0 -8
  285. data/test/helper.rb +0 -11
@@ -1,9 +1,9 @@
1
- require 'helper'
2
1
  require 'rack/test'
3
2
  require 'active_support/cache'
4
- require 'active_support/cache/dalli_store'
5
3
  require 'flipper/adapters/active_support_cache_store'
6
4
  require 'flipper/adapters/operation_logger'
5
+ require 'flipper/adapters/actor_limit'
6
+ require 'flipper/adapters/sync'
7
7
 
8
8
  RSpec.describe Flipper::Middleware::Memoizer do
9
9
  include Rack::Test::Methods
@@ -15,10 +15,6 @@ RSpec.describe Flipper::Middleware::Memoizer do
15
15
  let(:flipper) { Flipper.new(adapter) }
16
16
  let(:env) { { 'flipper' => flipper } }
17
17
 
18
- after do
19
- flipper.memoize = nil
20
- end
21
-
22
18
  it 'raises if initialized with app and flipper instance' do
23
19
  expect do
24
20
  described_class.new(app, flipper)
@@ -44,26 +40,6 @@ RSpec.describe Flipper::Middleware::Memoizer do
44
40
  expect(called).to eq(true)
45
41
  end
46
42
 
47
- it 'disables local cache after body close' do
48
- app = ->(_env) { [200, {}, []] }
49
- middleware = described_class.new(app)
50
- body = middleware.call(env).last
51
-
52
- expect(flipper.memoizing?).to eq(true)
53
- body.close
54
- expect(flipper.memoizing?).to eq(false)
55
- end
56
-
57
- it 'clears local cache after body close' do
58
- app = ->(_env) { [200, {}, []] }
59
- middleware = described_class.new(app)
60
- body = middleware.call(env).last
61
-
62
- flipper.adapter.cache['hello'] = 'world'
63
- body.close
64
- expect(flipper.adapter.cache).to be_empty
65
- end
66
-
67
43
  it 'clears the local cache with a successful request' do
68
44
  flipper.adapter.cache['hello'] = 'world'
69
45
  get '/', {}, 'flipper' => flipper
@@ -103,14 +79,14 @@ RSpec.describe Flipper::Middleware::Memoizer do
103
79
  end
104
80
  end
105
81
 
106
- context 'with preload_all' do
82
+ context 'with preload: true' do
107
83
  let(:app) do
108
84
  # ensure scoped for builder block, annoying...
109
- instance = flipper
85
+ flipper
110
86
  middleware = described_class
111
87
 
112
88
  Rack::Builder.new do
113
- use middleware, preload_all: true
89
+ use middleware, preload: true
114
90
 
115
91
  map '/' do
116
92
  run ->(_env) { [200, {}, []] }
@@ -139,7 +115,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
139
115
  [200, {}, []]
140
116
  end
141
117
 
142
- middleware = described_class.new(app, preload_all: true)
118
+ middleware = described_class.new(app, preload: true)
143
119
  middleware.call(env)
144
120
 
145
121
  expect(adapter.operations.size).to be(1)
@@ -156,7 +132,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
156
132
  [200, {}, []]
157
133
  end
158
134
 
159
- middleware = described_class.new(app, preload_all: true)
135
+ middleware = described_class.new(app, preload: true)
160
136
  middleware.call(env)
161
137
 
162
138
  expect(adapter.count(:get)).to be(1)
@@ -167,7 +143,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
167
143
  context 'with preload specific' do
168
144
  let(:app) do
169
145
  # ensure scoped for builder block, annoying...
170
- instance = flipper
146
+ flipper
171
147
  middleware = described_class
172
148
 
173
149
  Rack::Builder.new do
@@ -222,6 +198,111 @@ RSpec.describe Flipper::Middleware::Memoizer do
222
198
  end
223
199
  end
224
200
 
201
+ context 'with preload block' do
202
+ let(:app) do
203
+ app = lambda do |_env|
204
+ flipper[:stats].enabled?
205
+ flipper[:stats].enabled?
206
+ flipper[:shiny].enabled?
207
+ flipper[:shiny].enabled?
208
+ [200, {}, []]
209
+ end
210
+
211
+ described_class.new(app, preload: ->(request) {
212
+ case request.path
213
+ when "/true"
214
+ true
215
+ when "/specific"
216
+ [:stats]
217
+ else
218
+ false
219
+ end
220
+ })
221
+ end
222
+
223
+ include_examples 'flipper middleware'
224
+
225
+ it 'eagerly caches known features for duration of request if block returns true' do
226
+ flipper[:stats].enable
227
+ flipper[:shiny].enable
228
+
229
+ # clear the log of operations
230
+ adapter.reset
231
+
232
+ get '/true', {}, 'flipper' => flipper
233
+
234
+ expect(adapter.operations.size).to be(1)
235
+ expect(adapter.count(:get_all)).to be(1)
236
+ expect(adapter.count(:get)).to be(0)
237
+ end
238
+
239
+ it 'does not eagerly cache known features if block returns false' do
240
+ flipper[:stats].enable
241
+ flipper[:shiny].enable
242
+
243
+ # clear the log of operations
244
+ adapter.reset
245
+
246
+ get '/false', {}, 'flipper' => flipper
247
+
248
+ expect(adapter.operations.size).to be(2)
249
+ expect(adapter.count(:get_all)).to be(0)
250
+ expect(adapter.count(:get)).to be(2)
251
+ end
252
+
253
+ it 'eagerly caches specified features for duration of request if block returns array of specified features' do
254
+ flipper[:stats].enable
255
+ flipper[:shiny].enable
256
+
257
+ # clear the log of operations
258
+ adapter.reset
259
+
260
+ get '/specific', {}, 'flipper' => flipper
261
+
262
+ expect(adapter.operations.size).to be(2)
263
+ expect(adapter.count(:get_multi)).to be(1)
264
+ expect(adapter.count(:get)).to be(1)
265
+ end
266
+ end
267
+
268
+ context 'with multiple instances' do
269
+ let(:app) do
270
+ # ensure scoped for builder block, annoying...
271
+ flipper
272
+ middleware = described_class
273
+
274
+ Rack::Builder.new do
275
+ use middleware, preload: %i(stats)
276
+ # Second instance should be a noop
277
+ use middleware, preload: true
278
+
279
+ map '/' do
280
+ run ->(_env) { [200, {}, []] }
281
+ end
282
+
283
+ map '/fail' do
284
+ run ->(_env) { raise 'FAIL!' }
285
+ end
286
+ end.to_app
287
+ end
288
+
289
+ def get(uri, params = {}, env = {}, &block)
290
+ capture_output { super(uri, params, env, &block) }
291
+ end
292
+
293
+ include_examples 'flipper middleware'
294
+
295
+ it 'does not call preload in second instance' do
296
+ expect(flipper).not_to receive(:preload_all)
297
+
298
+ output = get '/', {}, 'flipper' => flipper
299
+
300
+ expect(output).to match(/Flipper::Middleware::Memoizer appears to be running twice/)
301
+ expect(adapter.count(:get_multi)).to be(1)
302
+ expect(adapter.last(:get_multi).args).to eq([[flipper[:stats]]])
303
+ end
304
+ end
305
+
225
306
  context 'when an app raises an exception' do
226
307
  it 'resets memoize' do
227
308
  begin
@@ -237,7 +318,7 @@ RSpec.describe Flipper::Middleware::Memoizer do
237
318
  context 'with flipper setup in env' do
238
319
  let(:app) do
239
320
  # ensure scoped for builder block, annoying...
240
- instance = flipper
321
+ flipper
241
322
  middleware = described_class
242
323
 
243
324
  Rack::Builder.new do
@@ -259,10 +340,9 @@ RSpec.describe Flipper::Middleware::Memoizer do
259
340
  context 'with Flipper setup in env' do
260
341
  it 'caches getting a feature for duration of request' do
261
342
  Flipper.configure do |config|
262
- config.default do
343
+ config.adapter do
263
344
  memory = Flipper::Adapters::Memory.new
264
- logged_adapter = Flipper::Adapters::OperationLogger.new(memory)
265
- Flipper.new(logged_adapter)
345
+ Flipper::Adapters::OperationLogger.new(memory)
266
346
  end
267
347
  end
268
348
  Flipper.enable(:stats)
@@ -308,14 +388,16 @@ RSpec.describe Flipper::Middleware::Memoizer do
308
388
  end
309
389
  end
310
390
 
311
- context 'with preload_all and unless option' do
391
+ context 'with preload:true' do
392
+ let(:options) { {preload: true} }
393
+
312
394
  let(:app) do
313
395
  # ensure scoped for builder block, annoying...
314
396
  middleware = described_class
397
+ opts = options
315
398
 
316
399
  Rack::Builder.new do
317
- use middleware, preload_all: true,
318
- unless: ->(request) { request.path.start_with?("/assets") }
400
+ use middleware, opts
319
401
 
320
402
  map '/' do
321
403
  run ->(_env) { [200, {}, []] }
@@ -327,26 +409,59 @@ RSpec.describe Flipper::Middleware::Memoizer do
327
409
  end.to_app
328
410
  end
329
411
 
330
- it 'does NOT preload_all if request matches unless block' do
331
- expect(flipper).to receive(:preload_all).never
332
- get '/assets/foo.png', {}, 'flipper' => flipper
412
+ context 'and unless option' do
413
+ before do
414
+ options[:unless] = ->(request) { request.path.start_with?("/assets") }
415
+ end
416
+
417
+ it 'does NOT preload if request matches unless block' do
418
+ expect(flipper).to receive(:preload_all).never
419
+ get '/assets/foo.png', {}, 'flipper' => flipper
420
+ end
421
+
422
+ it 'does preload if request does NOT match unless block' do
423
+ expect(flipper).to receive(:preload_all).once
424
+ get '/some/other/path', {}, 'flipper' => flipper
425
+ end
333
426
  end
334
427
 
335
- it 'does preload_all if request does NOT match unless block' do
336
- expect(flipper).to receive(:preload_all).once
337
- get '/some/other/path', {}, 'flipper' => flipper
428
+ context 'and if option' do
429
+ before do
430
+ options[:if] = ->(request) { !request.path.start_with?("/assets") }
431
+ end
432
+
433
+ it 'does NOT preload if request does not match if block' do
434
+ expect(flipper).to receive(:preload_all).never
435
+ get '/assets/foo.png', {}, 'flipper' => flipper
436
+ end
437
+
438
+ it 'does preload if request matches if block' do
439
+ expect(flipper).to receive(:preload_all).once
440
+ get '/some/other/path', {}, 'flipper' => flipper
441
+ end
338
442
  end
339
443
  end
340
444
 
341
- context 'with preload_all and caching adapter' do
445
+ context 'with preload:true and caching adapter' do
446
+ let(:app) do
447
+ app = lambda do |_env|
448
+ flipper[:stats].enabled?
449
+ flipper[:stats].enabled?
450
+ flipper[:shiny].enabled?
451
+ flipper[:shiny].enabled?
452
+ [200, {}, []]
453
+ end
454
+
455
+ described_class.new(app, preload: true)
456
+ end
457
+
342
458
  it 'eagerly caches known features for duration of request' do
343
459
  memory = Flipper::Adapters::Memory.new
344
460
  logged_memory = Flipper::Adapters::OperationLogger.new(memory)
345
- cache = ActiveSupport::Cache::DalliStore.new
461
+ cache = ActiveSupport::Cache::MemoryStore.new
346
462
  cache.clear
347
- cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache, expires_in: 10)
463
+ cached = Flipper::Adapters::ActiveSupportCacheStore.new(logged_memory, cache)
348
464
  logged_cached = Flipper::Adapters::OperationLogger.new(cached)
349
- memo = {}
350
465
  flipper = Flipper.new(logged_cached)
351
466
  flipper[:stats].enable
352
467
  flipper[:shiny].enable
@@ -355,27 +470,49 @@ RSpec.describe Flipper::Middleware::Memoizer do
355
470
  logged_memory.reset
356
471
  logged_cached.reset
357
472
 
358
- app = lambda do |_env|
359
- flipper[:stats].enabled?
360
- flipper[:stats].enabled?
361
- flipper[:shiny].enabled?
362
- flipper[:shiny].enabled?
363
- [200, {}, []]
364
- end
365
-
366
- middleware = described_class.new(app, preload_all: true)
367
-
368
- middleware.call('flipper' => flipper)
473
+ get '/', {}, 'flipper' => flipper
369
474
  expect(logged_cached.count(:get_all)).to be(1)
370
475
  expect(logged_memory.count(:get_all)).to be(1)
371
476
 
372
- middleware.call('flipper' => flipper)
477
+ get '/', {}, 'flipper' => flipper
373
478
  expect(logged_cached.count(:get_all)).to be(2)
374
479
  expect(logged_memory.count(:get_all)).to be(1)
375
480
 
376
- middleware.call('flipper' => flipper)
481
+ get '/', {}, 'flipper' => flipper
377
482
  expect(logged_cached.count(:get_all)).to be(3)
378
483
  expect(logged_memory.count(:get_all)).to be(1)
379
484
  end
380
485
  end
486
+
487
+ context 'with preload:true and Sync adapter wrapped with ActorLimit' do
488
+ it 'preloads even when remote has more actors than local limit' do
489
+ local = Flipper::Adapters::Memory.new
490
+ remote = Flipper::Adapters::Memory.new
491
+ remote_flipper = Flipper.new(remote)
492
+
493
+ # Remote has more actors than limit allows (actor-only enables, not boolean)
494
+ 10.times { |i| remote_flipper[:stats].enable_actor Flipper::Actor.new("User;#{i}") }
495
+
496
+ # Sync adapter will sync from remote to local, then ActorLimit wraps it
497
+ # Use interval: 0 to force sync on every call
498
+ sync_adapter = Flipper::Adapters::Sync.new(local, remote, interval: 0)
499
+ limited_adapter = Flipper::Adapters::ActorLimit.new(sync_adapter, 5)
500
+ test_flipper = Flipper.new(limited_adapter)
501
+
502
+ app = lambda do |env|
503
+ f = env['flipper']
504
+ f[:stats].enabled?
505
+ [200, {}, []]
506
+ end
507
+ middleware = described_class.new(app, preload: true)
508
+
509
+ # Preload should work without raising ActorLimit::LimitExceeded
510
+ expect {
511
+ middleware.call('flipper' => test_flipper)
512
+ }.not_to raise_error
513
+
514
+ # Verify actors were synced (all 10, not just 5)
515
+ expect(test_flipper[:stats].actors_value.size).to eq(10)
516
+ end
517
+ end
381
518
  end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Middleware::SetupEnv do
4
2
  context 'with flipper instance' do
5
3
  let(:app) do
@@ -56,21 +54,41 @@ RSpec.describe Flipper::Middleware::SetupEnv do
56
54
  end
57
55
  end
58
56
 
59
- context 'when flipper instance is nil' do
57
+ context 'when flipper instance or block are nil but env flipper is configured' do
60
58
  let(:app) do
61
59
  app = lambda do |env|
62
60
  [200, { 'Content-Type' => 'text/html' }, [env['flipper'].object_id.to_s]]
63
61
  end
64
62
  builder = Rack::Builder.new
65
- builder.use described_class, nil
63
+ builder.use described_class
66
64
  builder.run app
67
65
  builder
68
66
  end
69
67
 
70
- it 'leaves env flipper alone' do
68
+ it 'can use env flipper' do
71
69
  env_flipper = build_flipper
72
70
  get '/', {}, 'flipper' => env_flipper
73
71
  expect(last_response.body).to eq(env_flipper.object_id.to_s)
74
72
  end
75
73
  end
74
+
75
+ context 'when flipper instance or block are nil and default Flipper is configured' do
76
+ let(:app) do
77
+ Flipper.configure do |config|
78
+ config.default { flipper }
79
+ end
80
+ app = lambda do |env|
81
+ [200, { 'Content-Type' => 'text/html' }, [env['flipper'].object_id.to_s]]
82
+ end
83
+ builder = Rack::Builder.new
84
+ builder.use described_class
85
+ builder.run app
86
+ builder
87
+ end
88
+
89
+ it 'can use env flipper' do
90
+ get '/', {}, {}
91
+ expect(last_response.body).to eq(Flipper.object_id.to_s)
92
+ end
93
+ end
76
94
  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