flipper 0.26.0 → 1.3.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (228) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +61 -16
  4. data/.github/workflows/examples.yml +55 -18
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -486
  7. data/Gemfile +23 -11
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/enabled_ips.rb +10 -0
  11. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  12. data/benchmark/enabled_profile.rb +20 -0
  13. data/benchmark/instrumentation_ips.rb +21 -0
  14. data/benchmark/typecast_ips.rb +27 -0
  15. data/docs/images/banner.jpg +0 -0
  16. data/docs/images/flipper_cloud.png +0 -0
  17. data/examples/api/basic.ru +3 -4
  18. data/examples/api/custom_memoized.ru +3 -4
  19. data/examples/api/memoized.ru +3 -4
  20. data/examples/cloud/app.ru +12 -0
  21. data/examples/cloud/backoff_policy.rb +13 -0
  22. data/examples/cloud/basic.rb +22 -0
  23. data/examples/cloud/cloud_setup.rb +20 -0
  24. data/examples/cloud/forked.rb +36 -0
  25. data/examples/cloud/import.rb +17 -0
  26. data/examples/cloud/threaded.rb +33 -0
  27. data/examples/dsl.rb +1 -15
  28. data/examples/enabled_for_actor.rb +4 -2
  29. data/examples/expressions.rb +213 -0
  30. data/examples/mirroring.rb +59 -0
  31. data/examples/strict.rb +18 -0
  32. data/exe/flipper +5 -0
  33. data/flipper-cloud.gemspec +19 -0
  34. data/flipper.gemspec +8 -6
  35. data/lib/flipper/actor.rb +6 -3
  36. data/lib/flipper/adapter.rb +33 -7
  37. data/lib/flipper/adapter_builder.rb +44 -0
  38. data/lib/flipper/adapters/actor_limit.rb +28 -0
  39. data/lib/flipper/adapters/cache_base.rb +143 -0
  40. data/lib/flipper/adapters/dual_write.rb +1 -3
  41. data/lib/flipper/adapters/failover.rb +0 -4
  42. data/lib/flipper/adapters/failsafe.rb +0 -4
  43. data/lib/flipper/adapters/http/client.rb +40 -12
  44. data/lib/flipper/adapters/http/error.rb +2 -2
  45. data/lib/flipper/adapters/http.rb +30 -17
  46. data/lib/flipper/adapters/instrumented.rb +25 -6
  47. data/lib/flipper/adapters/memoizable.rb +33 -21
  48. data/lib/flipper/adapters/memory.rb +81 -46
  49. data/lib/flipper/adapters/operation_logger.rb +17 -78
  50. data/lib/flipper/adapters/poll/poller.rb +2 -125
  51. data/lib/flipper/adapters/poll.rb +20 -3
  52. data/lib/flipper/adapters/pstore.rb +17 -11
  53. data/lib/flipper/adapters/read_only.rb +8 -41
  54. data/lib/flipper/adapters/strict.rb +45 -0
  55. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  56. data/lib/flipper/adapters/sync.rb +0 -4
  57. data/lib/flipper/adapters/wrapper.rb +54 -0
  58. data/lib/flipper/cli.rb +263 -0
  59. data/lib/flipper/cloud/configuration.rb +266 -0
  60. data/lib/flipper/cloud/dsl.rb +27 -0
  61. data/lib/flipper/cloud/message_verifier.rb +95 -0
  62. data/lib/flipper/cloud/middleware.rb +63 -0
  63. data/lib/flipper/cloud/routes.rb +14 -0
  64. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  65. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  66. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  67. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  68. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  69. data/lib/flipper/cloud/telemetry.rb +191 -0
  70. data/lib/flipper/cloud.rb +53 -0
  71. data/lib/flipper/configuration.rb +25 -4
  72. data/lib/flipper/dsl.rb +46 -45
  73. data/lib/flipper/engine.rb +102 -0
  74. data/lib/flipper/errors.rb +3 -3
  75. data/lib/flipper/export.rb +24 -0
  76. data/lib/flipper/exporter.rb +17 -0
  77. data/lib/flipper/exporters/json/export.rb +32 -0
  78. data/lib/flipper/exporters/json/v1.rb +33 -0
  79. data/lib/flipper/expression/builder.rb +73 -0
  80. data/lib/flipper/expression/constant.rb +25 -0
  81. data/lib/flipper/expression.rb +71 -0
  82. data/lib/flipper/expressions/all.rb +9 -0
  83. data/lib/flipper/expressions/any.rb +9 -0
  84. data/lib/flipper/expressions/boolean.rb +9 -0
  85. data/lib/flipper/expressions/comparable.rb +13 -0
  86. data/lib/flipper/expressions/duration.rb +28 -0
  87. data/lib/flipper/expressions/equal.rb +9 -0
  88. data/lib/flipper/expressions/greater_than.rb +9 -0
  89. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  90. data/lib/flipper/expressions/less_than.rb +9 -0
  91. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/not_equal.rb +9 -0
  93. data/lib/flipper/expressions/now.rb +9 -0
  94. data/lib/flipper/expressions/number.rb +9 -0
  95. data/lib/flipper/expressions/percentage.rb +9 -0
  96. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  97. data/lib/flipper/expressions/property.rb +9 -0
  98. data/lib/flipper/expressions/random.rb +9 -0
  99. data/lib/flipper/expressions/string.rb +9 -0
  100. data/lib/flipper/expressions/time.rb +9 -0
  101. data/lib/flipper/feature.rb +94 -26
  102. data/lib/flipper/feature_check_context.rb +10 -6
  103. data/lib/flipper/gate.rb +13 -11
  104. data/lib/flipper/gate_values.rb +5 -18
  105. data/lib/flipper/gates/actor.rb +10 -17
  106. data/lib/flipper/gates/boolean.rb +1 -1
  107. data/lib/flipper/gates/expression.rb +75 -0
  108. data/lib/flipper/gates/group.rb +5 -7
  109. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  110. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  111. data/lib/flipper/identifier.rb +2 -2
  112. data/lib/flipper/instrumentation/log_subscriber.rb +35 -8
  113. data/lib/flipper/instrumentation/statsd.rb +4 -2
  114. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  115. data/lib/flipper/instrumentation/subscriber.rb +8 -5
  116. data/lib/flipper/metadata.rb +8 -1
  117. data/lib/flipper/middleware/memoizer.rb +30 -14
  118. data/lib/flipper/model/active_record.rb +23 -0
  119. data/lib/flipper/poller.rb +118 -0
  120. data/lib/flipper/serializers/gzip.rb +22 -0
  121. data/lib/flipper/serializers/json.rb +17 -0
  122. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  123. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  124. data/lib/flipper/test_help.rb +43 -0
  125. data/lib/flipper/typecast.rb +59 -18
  126. data/lib/flipper/types/actor.rb +13 -13
  127. data/lib/flipper/types/group.rb +4 -4
  128. data/lib/flipper/types/percentage.rb +1 -1
  129. data/lib/flipper/version.rb +11 -1
  130. data/lib/flipper.rb +50 -11
  131. data/lib/generators/flipper/setup_generator.rb +68 -0
  132. data/lib/generators/flipper/templates/initializer.rb +45 -0
  133. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  134. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  135. data/lib/generators/flipper/update_generator.rb +35 -0
  136. data/package-lock.json +41 -0
  137. data/package.json +10 -0
  138. data/spec/fixtures/environment.rb +1 -0
  139. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  140. data/spec/flipper/adapter_builder_spec.rb +72 -0
  141. data/spec/flipper/adapter_spec.rb +30 -2
  142. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  143. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  144. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  145. data/spec/flipper/adapters/http_spec.rb +138 -55
  146. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  147. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  148. data/spec/flipper/adapters/memory_spec.rb +14 -3
  149. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  150. data/spec/flipper/adapters/poll_spec.rb +41 -0
  151. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  152. data/spec/flipper/adapters/strict_spec.rb +64 -0
  153. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  154. data/spec/flipper/cli_spec.rb +166 -0
  155. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  156. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  157. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  158. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  159. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  160. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  161. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  162. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  163. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  164. data/spec/flipper/cloud_spec.rb +186 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -76
  167. data/spec/flipper/engine_spec.rb +374 -0
  168. data/spec/flipper/export_spec.rb +13 -0
  169. data/spec/flipper/exporter_spec.rb +16 -0
  170. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  171. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  172. data/spec/flipper/expression/builder_spec.rb +248 -0
  173. data/spec/flipper/expression_spec.rb +188 -0
  174. data/spec/flipper/expressions/all_spec.rb +15 -0
  175. data/spec/flipper/expressions/any_spec.rb +15 -0
  176. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  177. data/spec/flipper/expressions/duration_spec.rb +43 -0
  178. data/spec/flipper/expressions/equal_spec.rb +24 -0
  179. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  180. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  181. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  182. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  183. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  184. data/spec/flipper/expressions/now_spec.rb +11 -0
  185. data/spec/flipper/expressions/number_spec.rb +21 -0
  186. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  187. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  188. data/spec/flipper/expressions/property_spec.rb +13 -0
  189. data/spec/flipper/expressions/random_spec.rb +9 -0
  190. data/spec/flipper/expressions/string_spec.rb +11 -0
  191. data/spec/flipper/expressions/time_spec.rb +13 -0
  192. data/spec/flipper/feature_check_context_spec.rb +17 -17
  193. data/spec/flipper/feature_spec.rb +453 -39
  194. data/spec/flipper/gate_values_spec.rb +2 -33
  195. data/spec/flipper/gates/boolean_spec.rb +1 -1
  196. data/spec/flipper/gates/expression_spec.rb +108 -0
  197. data/spec/flipper/gates/group_spec.rb +2 -3
  198. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  199. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  200. data/spec/flipper/identifier_spec.rb +4 -5
  201. data/spec/flipper/instrumentation/log_subscriber_spec.rb +24 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +26 -2
  203. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  204. data/spec/flipper/model/active_record_spec.rb +72 -0
  205. data/spec/flipper/poller_spec.rb +47 -0
  206. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  207. data/spec/flipper/serializers/json_spec.rb +13 -0
  208. data/spec/flipper/typecast_spec.rb +121 -6
  209. data/spec/flipper/types/actor_spec.rb +63 -46
  210. data/spec/flipper/types/group_spec.rb +2 -2
  211. data/spec/flipper_integration_spec.rb +168 -58
  212. data/spec/flipper_spec.rb +94 -30
  213. data/spec/spec_helper.rb +18 -18
  214. data/spec/support/actor_names.yml +1 -0
  215. data/spec/support/fail_on_output.rb +8 -0
  216. data/spec/support/fake_backoff_policy.rb +15 -0
  217. data/spec/support/skippable.rb +18 -0
  218. data/spec/support/spec_helpers.rb +34 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +22 -2
  223. data/test_rails/system/test_help_test.rb +52 -0
  224. metadata +203 -20
  225. data/.github/workflows/release.yml +0 -44
  226. data/.tool-versions +0 -1
  227. data/lib/flipper/railtie.rb +0 -47
  228. data/spec/flipper/railtie_spec.rb +0 -109
@@ -1,77 +1,160 @@
1
1
  require 'flipper/adapters/http'
2
2
  require 'flipper/adapters/pstore'
3
- require 'rack/handler/webrick'
3
+
4
+ rack_handler = begin
5
+ # Rack 3+
6
+ require 'rackup/handler/webrick'
7
+ Rackup::Handler::WEBrick
8
+ rescue LoadError
9
+ require 'rack/handler/webrick'
10
+ Rack::Handler::WEBrick
11
+ end
12
+
4
13
 
5
14
  FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
6
15
 
7
16
  RSpec.describe Flipper::Adapters::Http do
8
- context 'adapter' do
9
- subject do
10
- described_class.new(url: "http://localhost:#{FLIPPER_SPEC_API_PORT}")
17
+ default_options = {
18
+ url: "http://localhost:#{FLIPPER_SPEC_API_PORT}",
19
+ }
20
+
21
+ {
22
+ basic: default_options.dup,
23
+ gzip: default_options.dup.merge(headers: { 'accept-encoding': 'gzip' }),
24
+ }.each do |name, options|
25
+ context "adapter (#{name} #{options.inspect})" do
26
+ subject do
27
+ described_class.new(options)
28
+ end
29
+
30
+ before :all do
31
+ @started = false
32
+ dir = FlipperRoot.join('tmp').tap(&:mkpath)
33
+ log_path = dir.join('flipper_adapters_http_spec.log')
34
+ @pstore_file = dir.join('flipper.pstore')
35
+ @pstore_file.unlink if @pstore_file.exist?
36
+
37
+ api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
38
+ flipper_api = Flipper.new(api_adapter)
39
+ app = Flipper::Api.app(flipper_api)
40
+ server_options = {
41
+ Port: FLIPPER_SPEC_API_PORT,
42
+ StartCallback: -> { @started = true },
43
+ Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
44
+ AccessLog: [
45
+ [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
46
+ ],
47
+ }
48
+ @server = WEBrick::HTTPServer.new(server_options)
49
+ @server.mount '/', rack_handler, app
50
+
51
+ Thread.new { @server.start }
52
+ Timeout.timeout(1) { :wait until @started }
53
+ end
54
+
55
+ after :all do
56
+ @server.shutdown if @server
57
+ end
58
+
59
+ before(:each) do
60
+ @pstore_file.unlink if @pstore_file.exist?
61
+ end
62
+
63
+ it_should_behave_like 'a flipper adapter'
64
+
65
+ it "can enable and disable unregistered group" do
66
+ flipper = Flipper.new(subject)
67
+ expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
68
+ expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
69
+
70
+ expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
71
+ expect(flipper[:search].groups_value).to eq(Set.new)
72
+ end
73
+
74
+ it "can import" do
75
+ adapter = Flipper::Adapters::Memory.new
76
+ source_flipper = Flipper.new(adapter)
77
+ source_flipper.enable_percentage_of_actors :search, 10
78
+ source_flipper.enable_percentage_of_time :search, 15
79
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
80
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
81
+ source_flipper.enable_group :search, :admins
82
+ source_flipper.enable_group :search, :employees
83
+ source_flipper.enable :plausible
84
+ source_flipper.disable :google_analytics
85
+
86
+ flipper = Flipper.new(subject)
87
+ flipper.import(source_flipper)
88
+ expect(flipper[:search].percentage_of_actors_value).to be(10)
89
+ expect(flipper[:search].percentage_of_time_value).to be(15)
90
+ expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
91
+ expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
92
+ expect(flipper[:plausible].boolean_value).to be(true)
93
+ expect(flipper[:google_analytics].boolean_value).to be(false)
94
+ end
11
95
  end
96
+ end
12
97
 
13
- before :all do
14
- dir = FlipperRoot.join('tmp').tap(&:mkpath)
15
- log_path = dir.join('flipper_adapters_http_spec.log')
16
- @pstore_file = dir.join('flipper.pstore')
17
- @pstore_file.unlink if @pstore_file.exist?
18
-
19
- api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
20
- flipper_api = Flipper.new(api_adapter)
21
- app = Flipper::Api.app(flipper_api)
22
- server_options = {
23
- Port: FLIPPER_SPEC_API_PORT,
24
- StartCallback: -> { @started = true },
25
- Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
26
- AccessLog: [
27
- [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
28
- ],
29
- }
30
- @server = WEBrick::HTTPServer.new(server_options)
31
- @server.mount '/', Rack::Handler::WEBrick, app
32
-
33
- Thread.new { @server.start }
34
- Timeout.timeout(1) { :wait until @started }
35
- end
98
+ it "sends default headers" do
99
+ headers = {
100
+ 'accept' => 'application/json',
101
+ 'content-type' => 'application/json',
102
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
103
+ }
104
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
105
+ .with(headers: headers)
106
+ .to_return(status: 404)
36
107
 
37
- after :all do
38
- @server.shutdown if @server
39
- end
108
+ adapter = described_class.new(url: 'http://app.com/flipper')
109
+ adapter.get(flipper[:feature_panel])
110
+ end
40
111
 
41
- before(:each) do
42
- @pstore_file.unlink if @pstore_file.exist?
43
- end
112
+ it "sends framework versions" do
113
+ stub_const("Rails", double(version: "7.1.0"))
114
+ stub_const("Sinatra::VERSION", "3.1.0")
115
+ stub_const("Hanami::VERSION", "0.7.2")
116
+ stub_const("GoodJob::VERSION", "3.21.5")
117
+ stub_const("Sidekiq::VERSION", "7.2.0")
44
118
 
45
- it_should_behave_like 'a flipper adapter'
119
+ headers = {
120
+ "client-framework" => [
121
+ "rails=7.1.0",
122
+ "sinatra=3.1.0",
123
+ "hanami=0.7.2",
124
+ "good_job=3.21.5",
125
+ "sidekiq=7.2.0",
126
+ ]
127
+ }
46
128
 
47
- it "can enable and disable unregistered group" do
48
- flipper = Flipper.new(subject)
49
- expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
50
- expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
129
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
130
+ .with(headers: headers)
131
+ .to_return(status: 404)
51
132
 
52
- expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
53
- expect(flipper[:search].groups_value).to eq(Set.new)
54
- end
133
+ adapter = described_class.new(url: 'http://app.com/flipper')
134
+ adapter.get(flipper[:feature_panel])
55
135
  end
56
136
 
57
- it "sends default headers" do
137
+ it "does not send undefined framework versions" do
138
+ stub_const("Rails", double(version: "7.1.0"))
139
+ stub_const("Sinatra::VERSION", "3.1.0")
140
+
58
141
  headers = {
59
- 'Accept' => 'application/json',
60
- 'Content-Type' => 'application/json',
61
- 'User-Agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
142
+ "client-framework" => ["rails=7.1.0", "sinatra=3.1.0"]
62
143
  }
144
+
63
145
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
64
146
  .with(headers: headers)
65
- .to_return(status: 404, body: "", headers: {})
147
+ .to_return(status: 404)
66
148
 
67
149
  adapter = described_class.new(url: 'http://app.com/flipper')
68
150
  adapter.get(flipper[:feature_panel])
69
151
  end
70
152
 
153
+
71
154
  describe "#get" do
72
155
  it "raises error when not successful response" do
73
156
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
74
- .to_return(status: 503, body: "", headers: {})
157
+ .to_return(status: 503)
75
158
 
76
159
  adapter = described_class.new(url: 'http://app.com/flipper')
77
160
  expect {
@@ -82,8 +165,8 @@ RSpec.describe Flipper::Adapters::Http do
82
165
 
83
166
  describe "#get_multi" do
84
167
  it "raises error when not successful response" do
85
- stub_request(:get, "http://app.com/flipper/features?keys=feature_panel")
86
- .to_return(status: 503, body: "", headers: {})
168
+ stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
169
+ .to_return(status: 503)
87
170
 
88
171
  adapter = described_class.new(url: 'http://app.com/flipper')
89
172
  expect {
@@ -94,8 +177,8 @@ RSpec.describe Flipper::Adapters::Http do
94
177
 
95
178
  describe "#get_all" do
96
179
  it "raises error when not successful response" do
97
- stub_request(:get, "http://app.com/flipper/features")
98
- .to_return(status: 503, body: "", headers: {})
180
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
181
+ .to_return(status: 503)
99
182
 
100
183
  adapter = described_class.new(url: 'http://app.com/flipper')
101
184
  expect {
@@ -106,8 +189,8 @@ RSpec.describe Flipper::Adapters::Http do
106
189
 
107
190
  describe "#features" do
108
191
  it "raises error when not successful response" do
109
- stub_request(:get, "http://app.com/flipper/features")
110
- .to_return(status: 503, body: "", headers: {})
192
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
193
+ .to_return(status: 503)
111
194
 
112
195
  adapter = described_class.new(url: 'http://app.com/flipper')
113
196
  expect {
@@ -224,7 +307,7 @@ RSpec.describe Flipper::Adapters::Http do
224
307
  let(:options) do
225
308
  {
226
309
  url: 'http://app.com/mount-point',
227
- headers: { 'X-Custom-Header' => 'foo' },
310
+ headers: { 'x-custom-header' => 'foo' },
228
311
  basic_auth_username: 'username',
229
312
  basic_auth_password: 'password',
230
313
  read_timeout: 100,
@@ -245,7 +328,7 @@ RSpec.describe Flipper::Adapters::Http do
245
328
  subject.get(feature)
246
329
  expect(
247
330
  a_request(:get, 'http://app.com/mount-point/features/feature_panel')
248
- .with(headers: { 'X-Custom-Header' => 'foo' })
331
+ .with(headers: { 'x-custom-header' => 'foo' })
249
332
  ).to have_been_made.once
250
333
  end
251
334
 
@@ -8,7 +8,7 @@ RSpec.describe Flipper::Adapters::Instrumented do
8
8
 
9
9
  let(:feature) { flipper[:stats] }
10
10
  let(:gate) { feature.gate(:percentage_of_actors) }
11
- let(:thing) { flipper.actors(22) }
11
+ let(:thing) { Flipper::Types::PercentageOfActors.new(22) }
12
12
 
13
13
  subject do
14
14
  described_class.new(adapter, instrumenter: instrumenter)
@@ -16,16 +16,6 @@ RSpec.describe Flipper::Adapters::Instrumented do
16
16
 
17
17
  it_should_behave_like 'a flipper adapter'
18
18
 
19
- it 'forwards missing methods to underlying adapter' do
20
- adapter = Class.new do
21
- def foo
22
- :foo
23
- end
24
- end.new
25
- instrumented = described_class.new(adapter)
26
- expect(instrumented.foo).to eq(:foo)
27
- end
28
-
29
19
  describe '#name' do
30
20
  it 'is instrumented' do
31
21
  expect(subject.name).to be(:instrumented)
@@ -146,4 +136,32 @@ RSpec.describe Flipper::Adapters::Instrumented do
146
136
  expect(event.payload[:result]).to be(result)
147
137
  end
148
138
  end
139
+
140
+ describe '#import' do
141
+ it 'records instrumentation' do
142
+ result = subject.import(Flipper::Adapters::Memory.new)
143
+
144
+ event = instrumenter.events.last
145
+ expect(event).not_to be_nil
146
+ expect(event.name).to eq('adapter_operation.flipper')
147
+ expect(event.payload[:operation]).to eq(:import)
148
+ expect(event.payload[:adapter_name]).to eq(:memory)
149
+ expect(event.payload[:result]).to be(result)
150
+ end
151
+ end
152
+
153
+ describe '#export' do
154
+ it 'records instrumentation' do
155
+ result = subject.export(format: :json, version: 1)
156
+
157
+ event = instrumenter.events.last
158
+ expect(event).not_to be_nil
159
+ expect(event.name).to eq('adapter_operation.flipper')
160
+ expect(event.payload[:operation]).to eq(:export)
161
+ expect(event.payload[:adapter_name]).to eq(:memory)
162
+ expect(event.payload[:format]).to be(:json)
163
+ expect(event.payload[:version]).to be(1)
164
+ expect(event.payload[:result]).to be(result)
165
+ end
166
+ end
149
167
  end
@@ -2,7 +2,7 @@ require 'flipper/adapters/memoizable'
2
2
  require 'flipper/adapters/operation_logger'
3
3
 
4
4
  RSpec.describe Flipper::Adapters::Memoizable do
5
- let(:features_key) { described_class::FeaturesKey }
5
+ let(:features_key) { :flipper_features }
6
6
  let(:adapter) { Flipper::Adapters::Memory.new }
7
7
  let(:flipper) { Flipper.new(adapter) }
8
8
  let(:cache) { {} }
@@ -11,16 +11,6 @@ RSpec.describe Flipper::Adapters::Memoizable do
11
11
 
12
12
  it_should_behave_like 'a flipper adapter'
13
13
 
14
- it 'forwards missing methods to underlying adapter' do
15
- adapter = Class.new do
16
- def foo
17
- :foo
18
- end
19
- end.new
20
- memoizable = described_class.new(adapter)
21
- expect(memoizable.foo).to eq(:foo)
22
- end
23
-
24
14
  describe '#name' do
25
15
  it 'is instrumented' do
26
16
  expect(subject.name).to be(:memoizable)
@@ -64,7 +54,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
64
54
  it 'memoizes feature' do
65
55
  feature = flipper[:stats]
66
56
  result = subject.get(feature)
67
- expect(cache[described_class.key_for(feature.key)]).to be(result)
57
+ expect(cache["feature/#{feature.key}"]).to be(result)
68
58
  end
69
59
  end
70
60
 
@@ -93,8 +83,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
93
83
  features = names.map { |name| flipper[name] }
94
84
  results = subject.get_multi(features)
95
85
  features.each do |feature|
96
- expect(cache[described_class.key_for(feature.key)]).not_to be(nil)
97
- expect(cache[described_class.key_for(feature.key)]).to be(results[feature.key])
86
+ expect(cache["feature/#{feature.key}"]).not_to be(nil)
87
+ expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
98
88
  end
99
89
  end
100
90
  end
@@ -125,10 +115,10 @@ RSpec.describe Flipper::Adapters::Memoizable do
125
115
  features = names.map { |name| flipper[name].tap(&:enable) }
126
116
  results = subject.get_all
127
117
  features.each do |feature|
128
- expect(cache[described_class.key_for(feature.key)]).not_to be(nil)
129
- expect(cache[described_class.key_for(feature.key)]).to be(results[feature.key])
118
+ expect(cache["feature/#{feature.key}"]).not_to be(nil)
119
+ expect(cache["feature/#{feature.key}"]).to be(results[feature.key])
130
120
  end
131
- expect(cache[subject.class::FeaturesKey]).to eq(names.map(&:to_s).to_set)
121
+ expect(cache[:flipper_features]).to eq(names.map(&:to_s).to_set)
132
122
  end
133
123
 
134
124
  it 'only calls get_all once for memoized adapter' do
@@ -198,9 +188,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
198
188
  it 'unmemoizes feature' do
199
189
  feature = flipper[:stats]
200
190
  gate = feature.gate(:boolean)
201
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
202
- subject.enable(feature, gate, flipper.bool)
203
- expect(cache[described_class.key_for(feature.key)]).to be_nil
191
+ cache["feature/#{feature.key}"] = { some: 'thing' }
192
+ subject.enable(feature, gate, Flipper::Types::Boolean.new)
193
+ expect(cache["feature/#{feature.key}"]).to be_nil
204
194
  end
205
195
  end
206
196
 
@@ -212,8 +202,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
212
202
  it 'returns result' do
213
203
  feature = flipper[:stats]
214
204
  gate = feature.gate(:boolean)
215
- result = subject.enable(feature, gate, flipper.bool)
216
- adapter_result = adapter.enable(feature, gate, flipper.bool)
205
+ result = subject.enable(feature, gate, Flipper::Types::Boolean.new)
206
+ adapter_result = adapter.enable(feature, gate, Flipper::Types::Boolean.new)
217
207
  expect(result).to eq(adapter_result)
218
208
  end
219
209
  end
@@ -228,9 +218,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
228
218
  it 'unmemoizes feature' do
229
219
  feature = flipper[:stats]
230
220
  gate = feature.gate(:boolean)
231
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
232
- subject.disable(feature, gate, flipper.bool)
233
- expect(cache[described_class.key_for(feature.key)]).to be_nil
221
+ cache["feature/#{feature.key}"] = { some: 'thing' }
222
+ subject.disable(feature, gate, Flipper::Types::Boolean.new)
223
+ expect(cache["feature/#{feature.key}"]).to be_nil
234
224
  end
235
225
  end
236
226
 
@@ -242,13 +232,43 @@ RSpec.describe Flipper::Adapters::Memoizable do
242
232
  it 'returns result' do
243
233
  feature = flipper[:stats]
244
234
  gate = feature.gate(:boolean)
245
- result = subject.disable(feature, gate, flipper.bool)
246
- adapter_result = adapter.disable(feature, gate, flipper.bool)
235
+ result = subject.disable(feature, gate, Flipper::Types::Boolean.new)
236
+ adapter_result = adapter.disable(feature, gate, Flipper::Types::Boolean.new)
247
237
  expect(result).to eq(adapter_result)
248
238
  end
249
239
  end
250
240
  end
251
241
 
242
+ describe "#import" do
243
+ context "with memoization enabled" do
244
+ before do
245
+ subject.memoize = true
246
+ end
247
+
248
+ it "unmemoizes features" do
249
+ cache[:foo] = "bar"
250
+ flipper[:stats].enable
251
+ flipper[:search].disable
252
+ subject.import(Flipper::Adapters::Memory.new)
253
+ expect(cache).to be_empty
254
+ end
255
+ end
256
+
257
+ context "with memoization disabled" do
258
+ before do
259
+ subject.memoize = false
260
+ end
261
+
262
+ it "does not unmemoize features" do
263
+ cache[:foo] = "bar"
264
+ flipper[:stats].enable
265
+ flipper[:search].disable
266
+ subject.import(Flipper::Adapters::Memory.new)
267
+ expect(cache).not_to be_empty
268
+ end
269
+ end
270
+ end
271
+
252
272
  describe '#features' do
253
273
  context 'with memoization enabled' do
254
274
  before do
@@ -312,9 +332,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
312
332
 
313
333
  it 'unmemoizes the feature' do
314
334
  feature = flipper[:stats]
315
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
335
+ cache["feature/#{feature.key}"] = { some: 'thing' }
316
336
  subject.remove(feature)
317
- expect(cache[described_class.key_for(feature.key)]).to be_nil
337
+ expect(cache["feature/#{feature.key}"]).to be_nil
318
338
  end
319
339
  end
320
340
 
@@ -337,9 +357,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
337
357
 
338
358
  it 'unmemoizes feature' do
339
359
  feature = flipper[:stats]
340
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
360
+ cache["feature/#{feature.key}"] = { some: 'thing' }
341
361
  subject.clear(feature)
342
- expect(cache[described_class.key_for(feature.key)]).to be_nil
362
+ expect(cache["feature/#{feature.key}"]).to be_nil
343
363
  end
344
364
  end
345
365
 
@@ -1,8 +1,17 @@
1
1
  RSpec.describe Flipper::Adapters::Memory do
2
2
  let(:source) { {} }
3
- subject { described_class.new(source) }
4
3
 
5
- it_should_behave_like 'a flipper adapter'
4
+ context 'threadsafe: true' do
5
+ subject { described_class.new(source, threadsafe: true) }
6
+
7
+ it_should_behave_like 'a flipper adapter'
8
+ end
9
+
10
+ context 'threadsafe: false' do
11
+ subject { described_class.new(source, threadsafe: false) }
12
+
13
+ it_should_behave_like 'a flipper adapter'
14
+ end
6
15
 
7
16
  it "can initialize from big hash" do
8
17
  flipper = Flipper.new(subject)
@@ -14,7 +23,9 @@ RSpec.describe Flipper::Adapters::Memory do
14
23
  flipper.enable_actor :following, Flipper::Actor.new('3')
15
24
  flipper.enable_group :following, Flipper::Types::Group.new(:staff)
16
25
 
17
- expect(source).to eq({
26
+ dup = described_class.new(subject.get_all)
27
+
28
+ expect(dup.get_all).to eq({
18
29
  "subscriptions" => subject.default_config.merge(boolean: "true"),
19
30
  "search" => subject.default_config,
20
31
  "logging" => subject.default_config.merge(:percentage_of_time => "30"),
@@ -18,16 +18,6 @@ RSpec.describe Flipper::Adapters::OperationLogger do
18
18
  expect(output).to match(/@adapter=#<Flipper::Adapters::Memory/)
19
19
  end
20
20
 
21
- it 'forwards missing methods to underlying adapter' do
22
- adapter = Class.new do
23
- def foo
24
- :foo
25
- end
26
- end.new
27
- operation_logger = described_class.new(adapter)
28
- expect(operation_logger.foo).to eq(:foo)
29
- end
30
-
31
21
  describe '#get' do
32
22
  before do
33
23
  @feature = flipper[:stats]
@@ -47,7 +37,7 @@ RSpec.describe Flipper::Adapters::OperationLogger do
47
37
  before do
48
38
  @feature = flipper[:stats]
49
39
  @gate = @feature.gate(:boolean)
50
- @thing = flipper.bool
40
+ @thing = Flipper::Types::Boolean.new
51
41
  @result = subject.enable(@feature, @gate, @thing)
52
42
  end
53
43
 
@@ -64,7 +54,7 @@ RSpec.describe Flipper::Adapters::OperationLogger do
64
54
  before do
65
55
  @feature = flipper[:stats]
66
56
  @gate = @feature.gate(:boolean)
67
- @thing = flipper.bool
57
+ @thing = Flipper::Types::Boolean.new
68
58
  @result = subject.disable(@feature, @gate, @thing)
69
59
  end
70
60
 
@@ -106,4 +96,33 @@ RSpec.describe Flipper::Adapters::OperationLogger do
106
96
  expect(@result).to eq(adapter.add(@feature))
107
97
  end
108
98
  end
99
+
100
+ describe '#import' do
101
+ before do
102
+ @source = Flipper::Adapters::Memory.new
103
+ @result = subject.import(@source)
104
+ end
105
+
106
+ it 'logs operation' do
107
+ expect(subject.count(:import)).to be(1)
108
+ end
109
+
110
+ it 'returns result' do
111
+ expect(@result).to eq(adapter.import(@source))
112
+ end
113
+ end
114
+
115
+ describe '#export' do
116
+ before do
117
+ @result = subject.export(format: :json, version: 1)
118
+ end
119
+
120
+ it 'logs operation' do
121
+ expect(subject.count(:export)).to be(1)
122
+ end
123
+
124
+ it 'returns result' do
125
+ expect(@result).to eq(adapter.export(format: :json, version: 1))
126
+ end
127
+ end
109
128
  end
@@ -0,0 +1,41 @@
1
+ require 'flipper/adapters/poll'
2
+
3
+ RSpec.describe Flipper::Adapters::Poll do
4
+ let(:remote_adapter) {
5
+ adapter = Flipper::Adapters::Memory.new(threadsafe: true)
6
+ flipper = Flipper.new(adapter)
7
+ flipper.enable(:search)
8
+ flipper.enable(:analytics)
9
+ adapter
10
+ }
11
+ let(:local_adapter) { Flipper::Adapters::Memory.new(threadsafe: true) }
12
+ let(:poller) {
13
+ Flipper::Poller.get("for_spec", {
14
+ start_automatically: false,
15
+ remote_adapter: remote_adapter,
16
+ })
17
+ }
18
+
19
+ it "syncs in main thread if local adapter is empty" do
20
+ instance = described_class.new(poller, local_adapter)
21
+ instance.features # call something to force sync
22
+ expect(local_adapter.features).to eq(remote_adapter.features)
23
+ end
24
+
25
+ it "does not sync in main thread if local adapter is not empty" do
26
+ # make local not empty by importing remote
27
+ flipper = Flipper.new(local_adapter)
28
+ flipper.import(remote_adapter)
29
+
30
+ # make a fake poller to verify calls
31
+ poller = double("Poller", last_synced_at: Concurrent::AtomicFixnum.new(0))
32
+ expect(poller).to receive(:start).twice
33
+ expect(poller).not_to receive(:sync)
34
+
35
+ # create new instance and call something to force sync
36
+ instance = described_class.new(poller, local_adapter)
37
+ instance.features # call something to force sync
38
+
39
+ expect(local_adapter.features).to eq(remote_adapter.features)
40
+ end
41
+ end