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
@@ -0,0 +1,390 @@
1
+ require "flipper/poller"
2
+ require "flipper/adapters/http"
3
+
4
+ RSpec.describe Flipper::Poller do
5
+ let(:url) { "http://app.com/flipper" }
6
+ let(:remote_adapter) { Flipper::Adapters::Http.new(url: url) }
7
+ let(:local) { Flipper.new(subject.adapter) }
8
+
9
+ subject do
10
+ described_class.new(
11
+ remote_adapter: remote_adapter,
12
+ start_automatically: false,
13
+ interval: 3600 # 1 hour
14
+ )
15
+ end
16
+
17
+ before do
18
+ stub_request(:get, "#{url}/features?exclude_gate_names=true")
19
+ .to_return(status: 200, body: JSON.generate(features: []))
20
+
21
+ allow(subject).to receive(:loop).and_yield # Make loop just call once
22
+ allow(subject).to receive(:sleep) # Disable sleep
23
+ allow(Thread).to receive(:new).and_yield # Disable separate thread
24
+ end
25
+
26
+ describe "#adapter" do
27
+ it "always returns same memory adapter instance" do
28
+ expect(subject.adapter).to be_a(Flipper::Adapters::Memory)
29
+ expect(subject.adapter.object_id).to eq(subject.adapter.object_id)
30
+ end
31
+ end
32
+
33
+ describe "#sync" do
34
+ it "syncs remote adapter to local adapter" do
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
+ ))
46
+
47
+ expect(local.enabled?(:polling)).to be(false)
48
+ subject.sync
49
+ expect(local.enabled?(:polling)).to be(true)
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
349
+ end
350
+
351
+ describe "#start" do
352
+ it "starts the poller thread" do
353
+ expect(Thread).to receive(:new).and_yield
354
+ expect(subject).to receive(:loop).and_yield
355
+ expect(subject).to receive(:sync)
356
+ subject.start
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
389
+ end
390
+ end
@@ -1,4 +1,3 @@
1
- require 'helper'
2
1
  require 'flipper/registry'
3
2
 
4
3
  RSpec.describe Flipper::Registry do
@@ -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
@@ -1,4 +1,3 @@
1
- require 'helper'
2
1
  require 'flipper/typecast'
3
2
 
4
3
  RSpec.describe Flipper::Typecast do
@@ -66,9 +65,9 @@ RSpec.describe Flipper::Typecast do
66
65
  '99' => 99,
67
66
  '99.9' => 99.9,
68
67
  }.each do |value, expected|
69
- context "#to_percentage for #{value.inspect}" do
68
+ context "#to_number for #{value.inspect}" do
70
69
  it "returns #{expected}" do
71
- expect(described_class.to_percentage(value)).to be(expected)
70
+ expect(described_class.to_number(value)).to be(expected)
72
71
  end
73
72
  end
74
73
  end
@@ -100,14 +99,14 @@ RSpec.describe Flipper::Typecast do
100
99
 
101
100
  it 'raises argument error for bad integer percentage' do
102
101
  expect do
103
- described_class.to_percentage(['asdf'])
104
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
102
+ described_class.to_number(['asdf'])
103
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
105
104
  end
106
105
 
107
106
  it 'raises argument error for bad float percentage' do
108
107
  expect do
109
- described_class.to_percentage(['asdf.0'])
110
- end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a float))
108
+ described_class.to_number(['asdf.0'])
109
+ end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a number))
111
110
  end
112
111
 
113
112
  it 'raises argument error for set value that cannot be converted to a set' do
@@ -115,4 +114,119 @@ RSpec.describe Flipper::Typecast do
115
114
  described_class.to_set('asdf')
116
115
  end.to raise_error(ArgumentError, %("asdf" cannot be converted to a set))
117
116
  end
117
+
118
+ describe "#features_hash" do
119
+ it "returns new hash" do
120
+ hash = {
121
+ "search" => {
122
+ boolean: nil,
123
+ }
124
+ }
125
+ result = described_class.features_hash(hash)
126
+ expect(result).not_to be(hash)
127
+ expect(result["search"]).not_to be(hash["search"])
128
+ end
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
+
154
+ it "converts gate value arrays to sets" do
155
+ hash = {
156
+ "search" => {
157
+ boolean: nil,
158
+ groups: ['a', 'b'],
159
+ actors: ['User;1'],
160
+ percentage_of_actors: nil,
161
+ percentage_of_time: nil,
162
+ },
163
+ }
164
+ result = described_class.features_hash(hash)
165
+ expect(result).to eq({
166
+ "search" => {
167
+ boolean: nil,
168
+ groups: Set['a', 'b'],
169
+ actors: Set['User;1'],
170
+ percentage_of_actors: nil,
171
+ percentage_of_time: nil,
172
+ },
173
+ })
174
+ end
175
+
176
+ it "converts gate value boolean and integers to strings" do
177
+ hash = {
178
+ "search" => {
179
+ boolean: true,
180
+ groups: Set.new,
181
+ actors: Set.new,
182
+ percentage_of_actors: 10,
183
+ percentage_of_time: 15,
184
+ },
185
+ }
186
+ result = described_class.features_hash(hash)
187
+ expect(result).to eq({
188
+ "search" => {
189
+ boolean: "true",
190
+ groups: Set.new,
191
+ actors: Set.new,
192
+ percentage_of_actors: "10",
193
+ percentage_of_time: "15",
194
+ },
195
+ })
196
+ end
197
+
198
+ it "converts string gate keys to symbols" do
199
+ hash = {
200
+ "search" => {
201
+ "boolean" => nil,
202
+ "groups" => Set.new,
203
+ "actors" => Set.new,
204
+ "percentage_of_actors" => nil,
205
+ "percentage_of_time" => nil,
206
+ },
207
+ }
208
+ result = described_class.features_hash(hash)
209
+ expect(result).to eq({
210
+ "search" => {
211
+ boolean: nil,
212
+ groups: Set.new,
213
+ actors: Set.new,
214
+ percentage_of_actors: nil,
215
+ percentage_of_time: nil,
216
+ },
217
+ })
218
+ end
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
118
232
  end