flipper 0.28.3 → 1.4.2

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 (204) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +56 -10
  4. data/.github/workflows/examples.yml +54 -11
  5. data/.github/workflows/release.yml +54 -0
  6. data/.superset/config.json +4 -0
  7. data/CLAUDE.md +93 -0
  8. data/Changelog.md +1 -559
  9. data/Gemfile +17 -8
  10. data/README.md +34 -29
  11. data/Rakefile +2 -2
  12. data/benchmark/typecast_ips.rb +8 -0
  13. data/docs/images/banner.jpg +0 -0
  14. data/docs/images/flipper_cloud.png +0 -0
  15. data/examples/cloud/app.ru +12 -0
  16. data/examples/cloud/backoff_policy.rb +13 -0
  17. data/examples/cloud/basic.rb +22 -0
  18. data/examples/cloud/cloud_setup.rb +20 -0
  19. data/examples/cloud/forked.rb +36 -0
  20. data/examples/cloud/import.rb +17 -0
  21. data/examples/cloud/poll_interval/README.md +111 -0
  22. data/examples/cloud/poll_interval/client.rb +108 -0
  23. data/examples/cloud/poll_interval/server.rb +98 -0
  24. data/examples/cloud/threaded.rb +33 -0
  25. data/examples/dsl.rb +0 -14
  26. data/examples/expressions.rb +237 -0
  27. data/examples/strict.rb +18 -0
  28. data/exe/flipper +5 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +8 -4
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +27 -1
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/actor_limit.rb +54 -0
  35. data/lib/flipper/adapters/cache_base.rb +161 -0
  36. data/lib/flipper/adapters/dual_write.rb +7 -5
  37. data/lib/flipper/adapters/failover.rb +9 -7
  38. data/lib/flipper/adapters/failsafe.rb +2 -6
  39. data/lib/flipper/adapters/http/client.rb +53 -14
  40. data/lib/flipper/adapters/http/error.rb +2 -2
  41. data/lib/flipper/adapters/http.rb +56 -16
  42. data/lib/flipper/adapters/instrumented.rb +2 -6
  43. data/lib/flipper/adapters/memoizable.rb +17 -22
  44. data/lib/flipper/adapters/memory.rb +5 -7
  45. data/lib/flipper/adapters/operation_logger.rb +18 -92
  46. data/lib/flipper/adapters/poll.rb +16 -3
  47. data/lib/flipper/adapters/pstore.rb +18 -12
  48. data/lib/flipper/adapters/read_only.rb +8 -41
  49. data/lib/flipper/adapters/strict.rb +75 -0
  50. data/lib/flipper/adapters/sync/feature_synchronizer.rb +14 -1
  51. data/lib/flipper/adapters/sync/synchronizer.rb +13 -5
  52. data/lib/flipper/adapters/sync.rb +7 -7
  53. data/lib/flipper/adapters/wrapper.rb +54 -0
  54. data/lib/flipper/cli.rb +314 -0
  55. data/lib/flipper/cloud/configuration.rb +267 -0
  56. data/lib/flipper/cloud/dsl.rb +27 -0
  57. data/lib/flipper/cloud/message_verifier.rb +95 -0
  58. data/lib/flipper/cloud/middleware.rb +63 -0
  59. data/lib/flipper/cloud/migrate.rb +71 -0
  60. data/lib/flipper/cloud/routes.rb +14 -0
  61. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  62. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  63. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  64. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  65. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  66. data/lib/flipper/cloud/telemetry.rb +191 -0
  67. data/lib/flipper/cloud.rb +54 -0
  68. data/lib/flipper/configuration.rb +25 -4
  69. data/lib/flipper/dsl.rb +48 -43
  70. data/lib/flipper/engine.rb +102 -0
  71. data/lib/flipper/export.rb +0 -2
  72. data/lib/flipper/exporters/json/export.rb +1 -1
  73. data/lib/flipper/exporters/json/v1.rb +1 -1
  74. data/lib/flipper/expression/builder.rb +73 -0
  75. data/lib/flipper/expression/constant.rb +25 -0
  76. data/lib/flipper/expression.rb +71 -0
  77. data/lib/flipper/expressions/all.rb +9 -0
  78. data/lib/flipper/expressions/any.rb +9 -0
  79. data/lib/flipper/expressions/boolean.rb +9 -0
  80. data/lib/flipper/expressions/comparable.rb +13 -0
  81. data/lib/flipper/expressions/equal.rb +9 -0
  82. data/lib/flipper/expressions/feature_enabled.rb +34 -0
  83. data/lib/flipper/expressions/greater_than.rb +9 -0
  84. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  85. data/lib/flipper/expressions/less_than.rb +9 -0
  86. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  87. data/lib/flipper/expressions/not_equal.rb +9 -0
  88. data/lib/flipper/expressions/now.rb +9 -0
  89. data/lib/flipper/expressions/number.rb +9 -0
  90. data/lib/flipper/expressions/percentage.rb +9 -0
  91. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  92. data/lib/flipper/expressions/property.rb +9 -0
  93. data/lib/flipper/expressions/random.rb +9 -0
  94. data/lib/flipper/expressions/string.rb +9 -0
  95. data/lib/flipper/expressions/time.rb +16 -0
  96. data/lib/flipper/feature.rb +63 -1
  97. data/lib/flipper/gate.rb +2 -1
  98. data/lib/flipper/gate_values.rb +5 -2
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  101. data/lib/flipper/instrumentation/statsd.rb +4 -2
  102. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  103. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  104. data/lib/flipper/metadata.rb +8 -1
  105. data/lib/flipper/middleware/memoizer.rb +30 -14
  106. data/lib/flipper/model/active_record.rb +23 -0
  107. data/lib/flipper/poller.rb +54 -12
  108. data/lib/flipper/serializers/gzip.rb +22 -0
  109. data/lib/flipper/serializers/json.rb +17 -0
  110. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  111. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  112. data/lib/flipper/test_help.rb +43 -0
  113. data/lib/flipper/typecast.rb +37 -9
  114. data/lib/flipper/types/percentage.rb +1 -1
  115. data/lib/flipper/version.rb +11 -1
  116. data/lib/flipper.rb +61 -8
  117. data/lib/generators/flipper/setup_generator.rb +68 -0
  118. data/lib/generators/flipper/templates/initializer.rb +45 -0
  119. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  120. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  121. data/lib/generators/flipper/update_generator.rb +35 -0
  122. data/package-lock.json +41 -0
  123. data/package.json +10 -0
  124. data/spec/fixtures/environment.rb +1 -0
  125. data/spec/flipper/adapter_builder_spec.rb +72 -0
  126. data/spec/flipper/adapter_spec.rb +21 -0
  127. data/spec/flipper/adapters/actor_limit_spec.rb +75 -0
  128. data/spec/flipper/adapters/dual_write_spec.rb +15 -2
  129. data/spec/flipper/adapters/failover_spec.rb +12 -0
  130. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  131. data/spec/flipper/adapters/http_spec.rb +375 -74
  132. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  133. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  134. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  135. data/spec/flipper/adapters/poll_spec.rb +41 -0
  136. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  137. data/spec/flipper/adapters/strict_spec.rb +122 -0
  138. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +39 -0
  139. data/spec/flipper/adapters/sync/synchronizer_spec.rb +87 -0
  140. data/spec/flipper/adapters/sync_spec.rb +13 -0
  141. data/spec/flipper/cli_spec.rb +217 -0
  142. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  143. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  144. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  145. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  146. data/spec/flipper/cloud/migrate_spec.rb +160 -0
  147. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  148. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  149. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  150. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  151. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  152. data/spec/flipper/cloud_spec.rb +186 -0
  153. data/spec/flipper/configuration_spec.rb +17 -0
  154. data/spec/flipper/dsl_spec.rb +34 -73
  155. data/spec/flipper/engine_spec.rb +374 -0
  156. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  157. data/spec/flipper/expression/builder_spec.rb +248 -0
  158. data/spec/flipper/expression_spec.rb +188 -0
  159. data/spec/flipper/expressions/all_spec.rb +15 -0
  160. data/spec/flipper/expressions/any_spec.rb +15 -0
  161. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  162. data/spec/flipper/expressions/equal_spec.rb +24 -0
  163. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  164. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  165. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  166. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  167. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  168. data/spec/flipper/expressions/now_spec.rb +11 -0
  169. data/spec/flipper/expressions/number_spec.rb +21 -0
  170. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  171. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  172. data/spec/flipper/expressions/property_spec.rb +13 -0
  173. data/spec/flipper/expressions/random_spec.rb +9 -0
  174. data/spec/flipper/expressions/string_spec.rb +11 -0
  175. data/spec/flipper/expressions/time_spec.rb +29 -0
  176. data/spec/flipper/feature_spec.rb +380 -10
  177. data/spec/flipper/gate_values_spec.rb +2 -2
  178. data/spec/flipper/gates/expression_spec.rb +190 -0
  179. data/spec/flipper/identifier_spec.rb +4 -5
  180. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  181. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  182. data/spec/flipper/middleware/memoizer_spec.rb +107 -7
  183. data/spec/flipper/model/active_record_spec.rb +72 -0
  184. data/spec/flipper/poller_spec.rb +347 -4
  185. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  186. data/spec/flipper/serializers/json_spec.rb +13 -0
  187. data/spec/flipper/typecast_spec.rb +43 -7
  188. data/spec/flipper/types/actor_spec.rb +18 -1
  189. data/spec/flipper_integration_spec.rb +247 -16
  190. data/spec/flipper_spec.rb +92 -29
  191. data/spec/spec_helper.rb +24 -17
  192. data/spec/support/actor_names.yml +1 -0
  193. data/spec/support/fail_on_output.rb +8 -0
  194. data/spec/support/fake_backoff_policy.rb +15 -0
  195. data/spec/support/spec_helpers.rb +34 -8
  196. data/test/adapters/actor_limit_test.rb +20 -0
  197. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  198. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  199. data/test_rails/helper.rb +22 -2
  200. data/test_rails/system/test_help_test.rb +52 -0
  201. metadata +139 -75
  202. data/.tool-versions +0 -1
  203. data/lib/flipper/railtie.rb +0 -47
  204. data/spec/flipper/railtie_spec.rb +0 -109
@@ -1,19 +1,23 @@
1
1
  require "flipper/poller"
2
+ require "flipper/adapters/http"
2
3
 
3
4
  RSpec.describe Flipper::Poller do
4
- let(:remote_adapter) { Flipper::Adapters::Memory.new }
5
- let(:remote) { Flipper.new(remote_adapter) }
5
+ let(:url) { "http://app.com/flipper" }
6
+ let(:remote_adapter) { Flipper::Adapters::Http.new(url: url) }
6
7
  let(:local) { Flipper.new(subject.adapter) }
7
8
 
8
9
  subject do
9
10
  described_class.new(
10
11
  remote_adapter: remote_adapter,
11
12
  start_automatically: false,
12
- interval: Float::INFINITY
13
+ interval: 3600 # 1 hour
13
14
  )
14
15
  end
15
16
 
16
17
  before do
18
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
19
+ .to_return(status: 200, body: JSON.generate(features: []))
20
+
17
21
  allow(subject).to receive(:loop).and_yield # Make loop just call once
18
22
  allow(subject).to receive(:sleep) # Disable sleep
19
23
  allow(Thread).to receive(:new).and_yield # Disable separate thread
@@ -28,12 +32,320 @@ RSpec.describe Flipper::Poller do
28
32
 
29
33
  describe "#sync" do
30
34
  it "syncs remote adapter to local adapter" do
31
- remote.enable :polling
35
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
36
+ .to_return(status: 200, body: JSON.generate(
37
+ features: [
38
+ {
39
+ key: "polling",
40
+ gates: [
41
+ { key: "boolean", value: true }
42
+ ]
43
+ }
44
+ ]
45
+ ))
32
46
 
33
47
  expect(local.enabled?(:polling)).to be(false)
34
48
  subject.sync
35
49
  expect(local.enabled?(:polling)).to be(true)
36
50
  end
51
+
52
+ context "when poll-shutdown header is present" do
53
+ before do
54
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
55
+ .to_return(
56
+ status: 200,
57
+ body: JSON.generate(
58
+ features: [
59
+ {
60
+ key: "polling",
61
+ gates: [
62
+ { key: "boolean", value: true }
63
+ ]
64
+ }
65
+ ]
66
+ ),
67
+ headers: { "poll-shutdown" => "true" }
68
+ )
69
+ end
70
+
71
+ it "stops the poller when poll-shutdown header is true" do
72
+ expect(subject).to receive(:stop).and_call_original
73
+ subject.sync
74
+ end
75
+
76
+ it "prevents poller from restarting after shutdown" do
77
+ subject.sync # This should trigger shutdown
78
+
79
+ # Try to start again - should be a no-op
80
+ expect(Thread).not_to receive(:new)
81
+ subject.start
82
+ end
83
+
84
+ it "instruments the shutdown_requested event" do
85
+ instrumenter = subject.instance_variable_get(:@instrumenter)
86
+
87
+ expect(instrumenter).to receive(:instrument).with(
88
+ "poller.#{Flipper::InstrumentationNamespace}",
89
+ { operation: :poll }
90
+ ).and_call_original
91
+
92
+ expect(instrumenter).to receive(:instrument).with(
93
+ "poller.#{Flipper::InstrumentationNamespace}",
94
+ { operation: :shutdown_requested }
95
+ ).and_call_original
96
+
97
+ expect(instrumenter).to receive(:instrument).with(
98
+ "poller.#{Flipper::InstrumentationNamespace}",
99
+ { operation: :stop }
100
+ )
101
+
102
+ subject.sync
103
+ end
104
+ end
105
+
106
+ context "when poll-shutdown header is present on error response" do
107
+ before do
108
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
109
+ .to_return(
110
+ status: 404,
111
+ body: JSON.generate({ error: "Not found" }),
112
+ headers: { "poll-shutdown" => "true" }
113
+ )
114
+ end
115
+
116
+ it "stops polling even when sync fails with error response" do
117
+ # sync will raise an error, but should still check shutdown header
118
+ expect { subject.sync }.to raise_error(Flipper::Adapters::Http::Error)
119
+
120
+ # Verify shutdown was triggered
121
+ expect(Thread).not_to receive(:new)
122
+ subject.start
123
+ end
124
+ end
125
+
126
+ context "when poll-shutdown header is false" do
127
+ before do
128
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
129
+ .to_return(
130
+ status: 200,
131
+ body: JSON.generate(
132
+ features: [
133
+ {
134
+ key: "polling",
135
+ gates: [
136
+ { key: "boolean", value: true }
137
+ ]
138
+ }
139
+ ]
140
+ ),
141
+ headers: { "poll-shutdown" => "false" }
142
+ )
143
+ end
144
+
145
+ it "does not stop the poller" do
146
+ expect(subject).not_to receive(:stop)
147
+ subject.sync
148
+ end
149
+ end
150
+
151
+ context "when poll-shutdown header is missing" do
152
+ before do
153
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
154
+ .to_return(
155
+ status: 200,
156
+ body: JSON.generate(
157
+ features: [
158
+ {
159
+ key: "polling",
160
+ gates: [
161
+ { key: "boolean", value: true }
162
+ ]
163
+ }
164
+ ]
165
+ )
166
+ )
167
+ end
168
+
169
+ it "does not stop the poller" do
170
+ expect(subject).not_to receive(:stop)
171
+ subject.sync
172
+ end
173
+ end
174
+
175
+ context "when poll-interval header is lower than initial interval" do
176
+ before do
177
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
178
+ .to_return(
179
+ status: 200,
180
+ body: JSON.generate(
181
+ features: [
182
+ {
183
+ key: "polling",
184
+ gates: [
185
+ { key: "boolean", value: true }
186
+ ]
187
+ }
188
+ ]
189
+ ),
190
+ headers: { "poll-interval" => "30" }
191
+ )
192
+ end
193
+
194
+ it "uses the initial interval as minimum" do
195
+ expect(subject.interval).to eq(3600.0)
196
+ subject.sync
197
+ expect(subject.interval).to eq(3600.0) # Keeps 3600 because it's the initial interval
198
+ end
199
+ end
200
+
201
+ context "when poll-interval header is below minimum" do
202
+ subject do
203
+ described_class.new(
204
+ remote_adapter: remote_adapter,
205
+ start_automatically: false,
206
+ interval: 10 # Set initial to minimum
207
+ )
208
+ end
209
+
210
+ before do
211
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
212
+ .to_return(
213
+ status: 200,
214
+ body: JSON.generate(
215
+ features: [
216
+ {
217
+ key: "polling",
218
+ gates: [
219
+ { key: "boolean", value: true }
220
+ ]
221
+ }
222
+ ]
223
+ ),
224
+ headers: { "poll-interval" => "5" }
225
+ )
226
+ end
227
+
228
+ it "enforces minimum poll interval" do
229
+ expect(subject.interval).to eq(10.0)
230
+ subject.sync
231
+ # Header says 5, minimum is 10, initial is 10, so max(5->10, 10) = 10
232
+ expect(subject.interval).to eq(Flipper::Poller::MINIMUM_POLL_INTERVAL)
233
+ end
234
+ end
235
+
236
+ context "when poll-interval header is higher than initial interval" do
237
+ subject do
238
+ described_class.new(
239
+ remote_adapter: remote_adapter,
240
+ start_automatically: false,
241
+ interval: 20
242
+ )
243
+ end
244
+
245
+ before do
246
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
247
+ .to_return(
248
+ status: 200,
249
+ body: JSON.generate(
250
+ features: [
251
+ {
252
+ key: "polling",
253
+ gates: [
254
+ { key: "boolean", value: true }
255
+ ]
256
+ }
257
+ ]
258
+ ),
259
+ headers: { "poll-interval" => "60" }
260
+ )
261
+ end
262
+
263
+ it "updates to the higher interval from header" do
264
+ expect(subject.interval).to eq(20.0)
265
+ subject.sync
266
+ expect(subject.interval).to eq(60.0) # Uses 60 because it's higher than initial 20
267
+ end
268
+ end
269
+
270
+ context "when poll-interval header can decrease back to initial interval" do
271
+ subject do
272
+ described_class.new(
273
+ remote_adapter: remote_adapter,
274
+ start_automatically: false,
275
+ interval: 10
276
+ )
277
+ end
278
+
279
+ before do
280
+ # First sync increases interval to 60
281
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
282
+ .to_return(
283
+ status: 200,
284
+ body: JSON.generate(
285
+ features: [
286
+ {
287
+ key: "polling",
288
+ gates: [
289
+ { key: "boolean", value: true }
290
+ ]
291
+ }
292
+ ]
293
+ ),
294
+ headers: { "poll-interval" => "60" }
295
+ ).times(1).then
296
+ .to_return(
297
+ status: 200,
298
+ body: JSON.generate(
299
+ features: [
300
+ {
301
+ key: "polling",
302
+ gates: [
303
+ { key: "boolean", value: true }
304
+ ]
305
+ }
306
+ ]
307
+ ),
308
+ headers: { "poll-interval" => "10" }
309
+ )
310
+ end
311
+
312
+ it "allows interval to go back down to initial after being increased" do
313
+ expect(subject.interval).to eq(10.0)
314
+
315
+ # First sync: header says 60, initial is 10, so use 60
316
+ subject.sync
317
+ expect(subject.interval).to eq(60.0)
318
+
319
+ # Second sync: header says 10, initial is 10, so use 10
320
+ subject.sync
321
+ expect(subject.interval).to eq(10.0)
322
+ end
323
+ end
324
+
325
+ context "when poll-interval header is missing" do
326
+ before do
327
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
328
+ .to_return(
329
+ status: 200,
330
+ body: JSON.generate(
331
+ features: [
332
+ {
333
+ key: "polling",
334
+ gates: [
335
+ { key: "boolean", value: true }
336
+ ]
337
+ }
338
+ ]
339
+ )
340
+ )
341
+ end
342
+
343
+ it "does not change the interval" do
344
+ original_interval = subject.interval
345
+ subject.sync
346
+ expect(subject.interval).to eq(original_interval)
347
+ end
348
+ end
37
349
  end
38
350
 
39
351
  describe "#start" do
@@ -43,5 +355,36 @@ RSpec.describe Flipper::Poller do
43
355
  expect(subject).to receive(:sync)
44
356
  subject.start
45
357
  end
358
+
359
+ context "after shutdown_requested" do
360
+ before do
361
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
362
+ .to_return(
363
+ status: 200,
364
+ body: JSON.generate(features: []),
365
+ headers: { "poll-shutdown" => "true" }
366
+ )
367
+ end
368
+
369
+ it "does not start when shutdown was requested" do
370
+ subject.sync # This triggers shutdown
371
+
372
+ expect(Thread).not_to receive(:new)
373
+ subject.start
374
+ end
375
+
376
+ it "allows starting after a fork" do
377
+ subject.sync # This triggers shutdown
378
+
379
+ # Simulate fork by changing PID
380
+ allow(Process).to receive(:pid).and_return(subject.instance_variable_get(:@pid) + 1)
381
+
382
+ # After fork, start should work again
383
+ expect(Thread).to receive(:new).and_yield
384
+ expect(subject).to receive(:loop).and_yield
385
+ expect(subject).to receive(:sync)
386
+ subject.start
387
+ end
388
+ end
46
389
  end
47
390
  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