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,46 @@
1
+ {
2
+ "version": 1,
3
+ "features": {
4
+ "search": {
5
+ "boolean": null,
6
+ "actors": [
7
+ "john",
8
+ "another",
9
+ "testing"
10
+ ],
11
+ "percentage_of_actors": null,
12
+ "percentage_of_time": null,
13
+ "groups": [
14
+ "admins"
15
+ ]
16
+ },
17
+ "new_pricing": {
18
+ "boolean": "true",
19
+ "actors": [],
20
+ "percentage_of_actors": null,
21
+ "percentage_of_time": null,
22
+ "groups": []
23
+ },
24
+ "google_analytics_tag": {
25
+ "boolean": null,
26
+ "actors": [],
27
+ "percentage_of_actors": "100",
28
+ "percentage_of_time": null,
29
+ "groups": []
30
+ },
31
+ "help_scout_tag": {
32
+ "boolean": null,
33
+ "actors": [],
34
+ "percentage_of_actors": null,
35
+ "percentage_of_time": "50",
36
+ "groups": []
37
+ },
38
+ "nope": {
39
+ "boolean": null,
40
+ "actors": [],
41
+ "percentage_of_actors": null,
42
+ "percentage_of_time": null,
43
+ "groups": []
44
+ }
45
+ }
46
+ }
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Actor do
4
2
  it 'initializes with and knows flipper_id' do
5
3
  actor = described_class.new("User;235")
@@ -45,4 +43,14 @@ RSpec.describe Flipper::Actor do
45
43
  expect(actor1.==(actor2)).to be(false)
46
44
  end
47
45
  end
46
+
47
+ describe '#hash' do
48
+ it 'returns a hash-value based on the flipper id' do
49
+ h = {
50
+ described_class.new("User;123") => true
51
+ }
52
+ expect(h).to have_key(described_class.new("User;123"))
53
+ expect(h).not_to have_key(described_class.new("User;456"))
54
+ end
55
+ end
48
56
  end
@@ -0,0 +1,72 @@
1
+ RSpec.describe Flipper::AdapterBuilder do
2
+ describe "#initialize" do
3
+ it "instance_eval's block with no arg" do
4
+ called = false
5
+ self_in_block = nil
6
+
7
+ described_class.new do
8
+ called = true
9
+ self_in_block = self
10
+ end
11
+
12
+ expect(self_in_block).to be_instance_of(described_class)
13
+ expect(called).to be(true)
14
+ end
15
+
16
+ it "evals block with arg" do
17
+ called = false
18
+ self_outside_block = self
19
+ self_in_block = nil
20
+
21
+ described_class.new do |arg|
22
+ called = true
23
+ self_in_block = self
24
+ expect(arg).to be_instance_of(described_class)
25
+ end
26
+
27
+ expect(self_in_block).to be(self_outside_block)
28
+ expect(called).to be(true)
29
+ end
30
+ end
31
+
32
+ describe "#use" do
33
+ it "wraps the store adapter with the given adapter" do
34
+ subject.use(Flipper::Adapters::Memoizable)
35
+ subject.use(Flipper::Adapters::Strict, :warn)
36
+
37
+ memoizable_adapter = subject.to_adapter
38
+ strict_adapter = memoizable_adapter.adapter
39
+ memory_adapter = strict_adapter.adapter
40
+
41
+ expect(memoizable_adapter).to be_instance_of(Flipper::Adapters::Memoizable)
42
+ expect(strict_adapter).to be_instance_of(Flipper::Adapters::Strict)
43
+ expect(strict_adapter.handler).to be(:warn)
44
+ expect(memory_adapter).to be_instance_of(Flipper::Adapters::Memory)
45
+ end
46
+
47
+ it "passes block to adapter initializer" do
48
+ expected_block = lambda {}
49
+ adapter_class = double('adapter class')
50
+
51
+ subject.use(adapter_class, &expected_block)
52
+
53
+ expect(adapter_class).to receive(:new) { |&block| expect(block).to be(expected_block) }.and_return(:adapter)
54
+ expect(subject.to_adapter).to be(:adapter)
55
+ end
56
+ end
57
+
58
+ describe "#store" do
59
+ it "defaults to memory adapter" do
60
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
61
+ end
62
+
63
+ it "only saves one store" do
64
+ require "flipper/adapters/pstore"
65
+ subject.store(Flipper::Adapters::PStore)
66
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::PStore)
67
+
68
+ subject.store(Flipper::Adapters::Memory)
69
+ expect(subject.to_adapter).to be_instance_of(Flipper::Adapters::Memory)
70
+ end
71
+ end
72
+ end
@@ -1,5 +1,3 @@
1
- require 'helper'
2
-
3
1
  RSpec.describe Flipper::Adapter do
4
2
  let(:source_flipper) { build_flipper }
5
3
  let(:destination_flipper) { build_flipper }
@@ -8,6 +6,7 @@ RSpec.describe Flipper::Adapter do
8
6
  boolean: nil,
9
7
  groups: Set.new,
10
8
  actors: Set.new,
9
+ expression: nil,
11
10
  percentage_of_actors: nil,
12
11
  percentage_of_time: nil,
13
12
  }
@@ -16,7 +15,7 @@ RSpec.describe Flipper::Adapter do
16
15
  describe '.default_config' do
17
16
  it 'returns default config' do
18
17
  adapter_class = Class.new do
19
- include Flipper::Adapter # rubocop:disable RSpec/DescribedClass
18
+ include Flipper::Adapter
20
19
  end
21
20
  expect(adapter_class.default_config).to eq(default_config)
22
21
  end
@@ -25,16 +24,16 @@ RSpec.describe Flipper::Adapter do
25
24
  describe '#default_config' do
26
25
  it 'returns default config' do
27
26
  adapter_class = Class.new do
28
- include Flipper::Adapter # rubocop:disable RSpec/DescribedClass
27
+ include Flipper::Adapter
29
28
  end
30
29
  expect(adapter_class.new.default_config).to eq(default_config)
31
30
  end
32
31
  end
33
32
 
34
33
  describe '#import' do
35
- it 'returns nothing' do
34
+ it 'returns true' do
36
35
  result = destination_flipper.import(source_flipper)
37
- expect(result).to be(nil)
36
+ expect(result).to be(true)
38
37
  end
39
38
 
40
39
  it 'can import from one adapter to another' do
@@ -116,5 +115,52 @@ RSpec.describe Flipper::Adapter do
116
115
  destination_flipper.import(source_flipper)
117
116
  expect(destination_flipper.features.map(&:key)).to eq([])
118
117
  end
118
+
119
+ it 'can import an export' do
120
+ source_flipper.enable(:search)
121
+ source_flipper.enable(:google_analytics, Flipper::Actor.new("User;1"))
122
+
123
+ destination_flipper.import(source_flipper.export)
124
+
125
+ feature = destination_flipper[:search]
126
+ expect(feature.boolean_value).to be(true)
127
+
128
+ feature = destination_flipper[:google_analytics]
129
+ expect(feature.actors_value).to eq(Set["User;1"])
130
+ end
131
+ end
132
+
133
+ describe "#export" do
134
+ it "exports features" do
135
+ source_flipper.enable(:search)
136
+ export = source_flipper.export
137
+ expect(export.features.dig("search", :boolean)).to eq("true")
138
+ end
139
+
140
+ it "exports with arguments" do
141
+ source_flipper.enable(:search)
142
+ export = source_flipper.export(format: :json, version: 1)
143
+ expect(export.features.dig("search", :boolean)).to eq("true")
144
+ end
145
+ end
146
+
147
+ describe "#adapter_stack" do
148
+ it "returns the adapter name for a simple adapter" do
149
+ adapter = Flipper::Adapters::Memory.new
150
+ expect(adapter.adapter_stack).to eq("memory")
151
+ end
152
+
153
+ it "returns the chain for wrapped adapters" do
154
+ memory = Flipper::Adapters::Memory.new
155
+ memoizable = Flipper::Adapters::Memoizable.new(memory)
156
+ expect(memoizable.adapter_stack).to eq("memoizable -> memory")
157
+ end
158
+
159
+ it "returns the chain for deeply nested adapters" do
160
+ memory = Flipper::Adapters::Memory.new
161
+ strict = Flipper::Adapters::Strict.new(memory)
162
+ memoizable = Flipper::Adapters::Memoizable.new(strict)
163
+ expect(memoizable.adapter_stack).to eq("memoizable -> strict -> memory")
164
+ end
119
165
  end
120
166
  end
@@ -0,0 +1,75 @@
1
+ require "flipper/adapters/actor_limit"
2
+
3
+ RSpec.describe Flipper::Adapters::ActorLimit do
4
+ it_should_behave_like 'a flipper adapter' do
5
+ let(:limit) { 5 }
6
+ let(:adapter) { Flipper::Adapters::ActorLimit.new(Flipper::Adapters::Memory.new, limit) }
7
+
8
+ subject { adapter }
9
+
10
+ describe '#enable' do
11
+ it "fails when limit exceeded" do
12
+ 5.times { |i| feature.enable Flipper::Actor.new("User;#{i}") }
13
+
14
+ expect {
15
+ feature.enable Flipper::Actor.new("User;6")
16
+ }.to raise_error(Flipper::Adapters::ActorLimit::LimitExceeded)
17
+ end
18
+
19
+ it "allows exceeding limit when in sync mode" do
20
+ 5.times { |i| feature.enable Flipper::Actor.new("User;#{i}") }
21
+
22
+ described_class.with_sync_mode do
23
+ expect {
24
+ feature.enable Flipper::Actor.new("User;6")
25
+ }.not_to raise_error
26
+ end
27
+ end
28
+ end
29
+ end
30
+
31
+ describe '.sync_mode' do
32
+ after do
33
+ described_class.sync_mode = nil
34
+ end
35
+
36
+ it 'defaults to nil/falsy' do
37
+ expect(described_class.sync_mode).to be_falsy
38
+ end
39
+
40
+ it 'can be set and read' do
41
+ described_class.sync_mode = true
42
+ expect(described_class.sync_mode).to be true
43
+ end
44
+ end
45
+
46
+ describe '.with_sync_mode' do
47
+ after do
48
+ described_class.sync_mode = nil
49
+ end
50
+
51
+ it 'sets sync_mode to true within block' do
52
+ described_class.with_sync_mode do
53
+ expect(described_class.sync_mode).to be true
54
+ end
55
+ end
56
+
57
+ it 'restores previous value after block' do
58
+ expect(described_class.sync_mode).to be_falsy
59
+ described_class.with_sync_mode { }
60
+ expect(described_class.sync_mode).to be_falsy
61
+ end
62
+
63
+ it 'restores previous value even on exception' do
64
+ expect {
65
+ described_class.with_sync_mode { raise "boom" }
66
+ }.to raise_error("boom")
67
+ expect(described_class.sync_mode).to be_falsy
68
+ end
69
+
70
+ it 'returns the block result' do
71
+ result = described_class.with_sync_mode { 42 }
72
+ expect(result).to eq(42)
73
+ end
74
+ end
75
+ end
@@ -0,0 +1,82 @@
1
+ require 'flipper/adapters/dual_write'
2
+ require 'flipper/adapters/operation_logger'
3
+ require 'active_support/notifications'
4
+
5
+ RSpec.describe Flipper::Adapters::DualWrite do
6
+ let(:local_adapter) do
7
+ Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
8
+ end
9
+ let(:remote_adapter) do
10
+ Flipper::Adapters::OperationLogger.new Flipper::Adapters::Memory.new
11
+ end
12
+ let(:local) { Flipper.new(local_adapter) }
13
+ let(:remote) { Flipper.new(remote_adapter) }
14
+ let(:sync) { Flipper.new(subject) }
15
+
16
+ subject do
17
+ described_class.new(local_adapter, remote_adapter)
18
+ end
19
+
20
+ it_should_behave_like 'a flipper adapter'
21
+
22
+ it 'only uses local for #features' do
23
+ subject.features
24
+ end
25
+
26
+ it 'only uses local for #get' do
27
+ subject.get sync[:search]
28
+ end
29
+
30
+ it 'only uses local for #get_multi' do
31
+ subject.get_multi [sync[:search]]
32
+ end
33
+
34
+ it 'only uses local for #get_all' do
35
+ subject.get_all
36
+ end
37
+
38
+ it 'updates remote and local for #add' do
39
+ subject.add sync[:search]
40
+ expect(remote_adapter.count(:add)).to be(1)
41
+ expect(local_adapter.count(:add)).to be(1)
42
+ end
43
+
44
+ it 'updates remote and local for #remove' do
45
+ subject.remove sync[:search]
46
+ expect(remote_adapter.count(:remove)).to be(1)
47
+ expect(local_adapter.count(:remove)).to be(1)
48
+ end
49
+
50
+ it 'updates remote and local for #clear' do
51
+ subject.clear sync[:search]
52
+ expect(remote_adapter.count(:clear)).to be(1)
53
+ expect(local_adapter.count(:clear)).to be(1)
54
+ end
55
+
56
+ it 'updates remote and local for #enable' do
57
+ feature = sync[:search]
58
+ subject.enable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(true)
59
+ expect(remote_adapter.count(:enable)).to be(1)
60
+ expect(local_adapter.count(:enable)).to be(1)
61
+ end
62
+
63
+ it 'updates remote and local for #disable' do
64
+ feature = sync[:search]
65
+ subject.disable feature, feature.gate(:boolean), Flipper::Types::Boolean.new(false)
66
+ expect(remote_adapter.count(:disable)).to be(1)
67
+ expect(local_adapter.count(:disable)).to be(1)
68
+ end
69
+
70
+ describe '#adapter_stack' do
71
+ it 'returns the tree representation' do
72
+ expect(subject.adapter_stack).to eq("dual_write(local: operation_logger -> memory, remote: operation_logger -> memory)")
73
+ end
74
+
75
+ it 'shows nested adapters in the tree' do
76
+ memory = Flipper::Adapters::Memory.new
77
+ strict = Flipper::Adapters::Strict.new(Flipper::Adapters::Memory.new)
78
+ adapter = described_class.new(memory, strict)
79
+ expect(adapter.adapter_stack).to eq("dual_write(local: memory, remote: strict -> memory)")
80
+ end
81
+ end
82
+ end
@@ -0,0 +1,141 @@
1
+ require 'dalli'
2
+ require 'flipper/adapters/failover'
3
+ require 'net/http'
4
+ require 'pstore'
5
+ require 'redis'
6
+
7
+ RSpec.describe Flipper::Adapters::Failover do
8
+ subject { described_class.new(primary, secondary, options) }
9
+
10
+ let(:primary) { Flipper::Adapters::Memory.new }
11
+ let(:secondary) { Flipper::Adapters::Memory.new }
12
+ let(:options) { {} }
13
+ let(:flipper) { Flipper.new(subject) }
14
+
15
+ context 'when the primary is a functioning adapter' do
16
+ it_should_behave_like 'a flipper adapter'
17
+
18
+ it 'should not call the secondary' do
19
+ expect(secondary).not_to receive(:features)
20
+ subject.features
21
+ end
22
+
23
+ it 'should not write to secondary' do
24
+ expect(secondary).not_to receive(:add)
25
+ expect(secondary).not_to receive(:enable)
26
+
27
+ flipper[:flag].enable
28
+ end
29
+
30
+ context 'when dual_write is enabled' do
31
+ let(:options) { { dual_write: true } }
32
+
33
+ it_should_behave_like 'a flipper adapter'
34
+
35
+ it 'writes to both primary and secondary' do
36
+ expect(primary).to receive(:add).and_call_original
37
+ expect(primary).to receive(:enable).and_call_original
38
+
39
+ expect(secondary).to receive(:add)
40
+ expect(secondary).to receive(:enable)
41
+
42
+ flipper[:flag].enable
43
+ end
44
+ end
45
+ end
46
+
47
+ context 'when primary fails during read operations' do
48
+ before do
49
+ allow(primary).to receive(:features).and_raise(Redis::ConnectionError)
50
+ allow(primary).to receive(:get).and_raise(Dalli::NetworkError)
51
+ end
52
+
53
+ it 'fails over to the secondary adapter for reads' do
54
+ expect(secondary).to receive(:features)
55
+ subject.features
56
+
57
+ flipper[:flag].enable
58
+ expect(secondary).to receive(:get).and_call_original
59
+ flipper[:flag].enabled?
60
+ end
61
+
62
+ context 'when dual_write is enabled' do
63
+ let(:options) { { dual_write: true } }
64
+
65
+ it_should_behave_like 'a flipper adapter'
66
+ end
67
+ end
68
+
69
+ context 'when primary fails during write operations' do
70
+ before do
71
+ allow(primary).to receive(:add).and_raise(PStore::Error)
72
+ end
73
+
74
+ let(:options) { { dual_write: true } }
75
+
76
+ it 'fails and does not write to secondary adapter' do
77
+ expect(secondary).not_to receive(:add)
78
+ expect(secondary).not_to receive(:enable)
79
+
80
+ expect { flipper[:flag].enable }.to raise_error(PStore::Error)
81
+ end
82
+ end
83
+
84
+ context 'when primary is instrumented and fails' do
85
+ before do
86
+ allow(memory_adapter).to receive(:get).and_raise(Net::ReadTimeout)
87
+ end
88
+
89
+ let(:memory_adapter) { Flipper::Adapters::Memory.new }
90
+ let(:primary) do
91
+ Flipper::Adapters::Instrumented.new(
92
+ memory_adapter,
93
+ instrumenter: instrumenter,
94
+ )
95
+ end
96
+ let(:instrumenter) { Flipper::Instrumenters::Memory.new }
97
+
98
+ it 'logs the raised exception' do
99
+ flipper[:flag].enabled?
100
+
101
+ expect(instrumenter.events.count).to be 1
102
+
103
+ payload = instrumenter.events[0].payload
104
+ expect(payload.keys).to include(:exception, :exception_object)
105
+ expect(payload[:exception_object]).to be_a Net::ReadTimeout
106
+ end
107
+ end
108
+
109
+ context 'when adapter raises a SyntaxError' do
110
+ before do
111
+ allow(primary).to receive(:features).and_raise(SyntaxError)
112
+ end
113
+
114
+ it 'does not rescue this type by default' do
115
+ expect {
116
+ subject.features
117
+ }.to raise_error(SyntaxError)
118
+ end
119
+
120
+ context 'when Failover adapter is configured to catch SyntaxError' do
121
+ let(:options) { { errors: [ SyntaxError ] } }
122
+
123
+ it 'fails over to secondary adapter' do
124
+ expect(secondary).to receive(:features)
125
+ subject.features
126
+ end
127
+ end
128
+ end
129
+
130
+ describe '#adapter_stack' do
131
+ it 'returns the tree representation' do
132
+ expect(subject.adapter_stack).to eq("failover(primary: memory, secondary: memory)")
133
+ end
134
+
135
+ it 'shows nested adapters in the tree' do
136
+ strict_primary = Flipper::Adapters::Strict.new(primary)
137
+ adapter = described_class.new(strict_primary, secondary)
138
+ expect(adapter.adapter_stack).to eq("failover(primary: strict -> memory, secondary: memory)")
139
+ end
140
+ end
141
+ end
@@ -0,0 +1,58 @@
1
+ require 'flipper/adapters/failsafe'
2
+
3
+ RSpec.describe Flipper::Adapters::Failsafe do
4
+ subject { described_class.new(memory_adapter, options) }
5
+
6
+ let(:memory_adapter) { Flipper::Adapters::Memory.new }
7
+ let(:options) { {} }
8
+ let(:flipper) { Flipper.new(subject) }
9
+
10
+ it_should_behave_like 'a flipper adapter'
11
+
12
+ context 'when disaster strikes' do
13
+ before do
14
+ expect(flipper[feature.name].enable).to be(true)
15
+
16
+ (subject.methods - Object.methods).each do |method_name|
17
+ allow(memory_adapter).to receive(method_name).and_raise(IOError)
18
+ end
19
+ end
20
+
21
+ let(:feature) { Flipper::Feature.new(:my_feature, subject) }
22
+
23
+ it { expect(subject.features).to eq(Set.new) }
24
+ it { expect(feature.add).to eq(false) }
25
+ it { expect(feature.remove).to eq(false) }
26
+ it { expect(feature.clear).to eq(false) }
27
+ it { expect(subject.get(feature)).to eq({}) }
28
+ it { expect(subject.get_multi([feature])).to eq({}) }
29
+ it { expect(subject.get_all).to eq({}) }
30
+ it { expect(feature.enable).to eq(false) }
31
+ it { expect(feature.disable).to eq(false) }
32
+
33
+ context 'when used via Flipper' do
34
+ it { expect(flipper.features).to eq(Set.new) }
35
+ it { expect(flipper[feature.name].enabled?).to eq(false) }
36
+ it { expect(flipper[feature.name].enable).to eq(false) }
37
+ it { expect(flipper[feature.name].disable).to eq(false) }
38
+ end
39
+
40
+ context 'when there is a syntax error' do
41
+ let(:test) { flipper[feature.name].enabled? }
42
+
43
+ before do
44
+ expect(memory_adapter).to receive(:get).and_raise(SyntaxError)
45
+ end
46
+
47
+ it 'does not catch this type of error' do
48
+ expect { test }.to raise_error(SyntaxError)
49
+ end
50
+
51
+ context 'when configured to catch SyntaxError' do
52
+ let(:options) { { errors: [SyntaxError] } }
53
+
54
+ it { expect(test).to eq(false) }
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,61 @@
1
+ require "flipper/adapters/http/client"
2
+
3
+ RSpec.describe Flipper::Adapters::Http::Client do
4
+ describe "#initialize" do
5
+ it "requires url" do
6
+ expect { described_class.new }.to raise_error(KeyError, "key not found: :url")
7
+ end
8
+
9
+ it "sets default headers" do
10
+ client = described_class.new(url: "http://example.com")
11
+ expect(client.headers).to eq({
12
+ 'content-type' => 'application/json',
13
+ 'accept' => 'application/json',
14
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
15
+ })
16
+ end
17
+
18
+ it "adds custom headers" do
19
+ client = described_class.new(url: "http://example.com", headers: {'custom-header' => 'value'})
20
+ expect(client.headers).to include('custom-header' => 'value')
21
+ end
22
+
23
+ it "overrides default headers with custom headers" do
24
+ client = described_class.new(url: "http://example.com", headers: {'content-type' => 'text/plain'})
25
+ expect(client.headers['content-type']).to eq('text/plain')
26
+ end
27
+ end
28
+
29
+ describe "#add_header" do
30
+ it "can add string header" do
31
+ client = described_class.new(url: "http://example.com")
32
+ client.add_header("key", "value")
33
+ expect(client.headers.fetch("key")).to eq("value")
34
+ end
35
+
36
+ it "standardizes key to lowercase" do
37
+ client = described_class.new(url: "http://example.com")
38
+ client.add_header("Content-Type", "value")
39
+ expect(client.headers.fetch("content-type")).to eq("value")
40
+ end
41
+
42
+ it "standardizes key to dashes" do
43
+ client = described_class.new(url: "http://example.com")
44
+ client.add_header(:content_type, "value")
45
+ expect(client.headers.fetch("content-type")).to eq("value")
46
+ end
47
+
48
+ it "can add symbol header" do
49
+ client = described_class.new(url: "http://example.com")
50
+ client.add_header(:key, "value")
51
+ expect(client.headers.fetch("key")).to eq("value")
52
+ end
53
+
54
+ it "overrides existing header" do
55
+ client = described_class.new(url: "http://example.com")
56
+ client.add_header("key", "value 1")
57
+ client.add_header("key", "value 2")
58
+ expect(client.headers.fetch("key")).to eq("value 2")
59
+ end
60
+ end
61
+ end