flipper 0.24.1 → 1.3.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (226) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/dependabot.yml +6 -0
  4. data/.github/workflows/ci.yml +45 -14
  5. data/.github/workflows/examples.yml +39 -16
  6. data/Changelog.md +2 -443
  7. data/Gemfile +19 -11
  8. data/README.md +31 -27
  9. data/Rakefile +6 -4
  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/instrumentation.rb +1 -0
  31. data/examples/instrumentation_last_accessed_at.rb +1 -0
  32. data/examples/mirroring.rb +59 -0
  33. data/examples/strict.rb +18 -0
  34. data/exe/flipper +5 -0
  35. data/flipper-cloud.gemspec +19 -0
  36. data/flipper.gemspec +10 -6
  37. data/lib/flipper/actor.rb +6 -3
  38. data/lib/flipper/adapter.rb +33 -7
  39. data/lib/flipper/adapter_builder.rb +44 -0
  40. data/lib/flipper/adapters/actor_limit.rb +28 -0
  41. data/lib/flipper/adapters/cache_base.rb +143 -0
  42. data/lib/flipper/adapters/dual_write.rb +1 -3
  43. data/lib/flipper/adapters/failover.rb +0 -4
  44. data/lib/flipper/adapters/failsafe.rb +72 -0
  45. data/lib/flipper/adapters/http/client.rb +44 -20
  46. data/lib/flipper/adapters/http/error.rb +1 -1
  47. data/lib/flipper/adapters/http.rb +31 -16
  48. data/lib/flipper/adapters/instrumented.rb +25 -6
  49. data/lib/flipper/adapters/memoizable.rb +33 -21
  50. data/lib/flipper/adapters/memory.rb +81 -46
  51. data/lib/flipper/adapters/operation_logger.rb +17 -78
  52. data/lib/flipper/adapters/poll/poller.rb +2 -0
  53. data/lib/flipper/adapters/poll.rb +37 -0
  54. data/lib/flipper/adapters/pstore.rb +17 -11
  55. data/lib/flipper/adapters/read_only.rb +8 -41
  56. data/lib/flipper/adapters/strict.rb +45 -0
  57. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  58. data/lib/flipper/adapters/sync.rb +0 -4
  59. data/lib/flipper/adapters/wrapper.rb +54 -0
  60. data/lib/flipper/cli.rb +263 -0
  61. data/lib/flipper/cloud/configuration.rb +263 -0
  62. data/lib/flipper/cloud/dsl.rb +27 -0
  63. data/lib/flipper/cloud/message_verifier.rb +95 -0
  64. data/lib/flipper/cloud/middleware.rb +63 -0
  65. data/lib/flipper/cloud/routes.rb +14 -0
  66. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  67. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  68. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  69. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  70. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  71. data/lib/flipper/cloud/telemetry.rb +191 -0
  72. data/lib/flipper/cloud.rb +53 -0
  73. data/lib/flipper/configuration.rb +25 -4
  74. data/lib/flipper/dsl.rb +46 -45
  75. data/lib/flipper/engine.rb +102 -0
  76. data/lib/flipper/errors.rb +3 -20
  77. data/lib/flipper/export.rb +26 -0
  78. data/lib/flipper/exporter.rb +17 -0
  79. data/lib/flipper/exporters/json/export.rb +32 -0
  80. data/lib/flipper/exporters/json/v1.rb +33 -0
  81. data/lib/flipper/expression/builder.rb +73 -0
  82. data/lib/flipper/expression/constant.rb +25 -0
  83. data/lib/flipper/expression.rb +71 -0
  84. data/lib/flipper/expressions/all.rb +11 -0
  85. data/lib/flipper/expressions/any.rb +9 -0
  86. data/lib/flipper/expressions/boolean.rb +9 -0
  87. data/lib/flipper/expressions/comparable.rb +13 -0
  88. data/lib/flipper/expressions/duration.rb +28 -0
  89. data/lib/flipper/expressions/equal.rb +9 -0
  90. data/lib/flipper/expressions/greater_than.rb +9 -0
  91. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  92. data/lib/flipper/expressions/less_than.rb +9 -0
  93. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  94. data/lib/flipper/expressions/not_equal.rb +9 -0
  95. data/lib/flipper/expressions/now.rb +9 -0
  96. data/lib/flipper/expressions/number.rb +9 -0
  97. data/lib/flipper/expressions/percentage.rb +9 -0
  98. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  99. data/lib/flipper/expressions/property.rb +9 -0
  100. data/lib/flipper/expressions/random.rb +9 -0
  101. data/lib/flipper/expressions/string.rb +9 -0
  102. data/lib/flipper/expressions/time.rb +9 -0
  103. data/lib/flipper/feature.rb +87 -26
  104. data/lib/flipper/feature_check_context.rb +10 -6
  105. data/lib/flipper/gate.rb +13 -11
  106. data/lib/flipper/gate_values.rb +5 -18
  107. data/lib/flipper/gates/actor.rb +10 -17
  108. data/lib/flipper/gates/boolean.rb +1 -1
  109. data/lib/flipper/gates/expression.rb +75 -0
  110. data/lib/flipper/gates/group.rb +5 -7
  111. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  112. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  113. data/lib/flipper/identifier.rb +2 -2
  114. data/lib/flipper/instrumentation/log_subscriber.rb +34 -6
  115. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  116. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  117. data/lib/flipper/metadata.rb +7 -1
  118. data/lib/flipper/middleware/memoizer.rb +28 -22
  119. data/lib/flipper/model/active_record.rb +23 -0
  120. data/lib/flipper/poller.rb +118 -0
  121. data/lib/flipper/serializers/gzip.rb +22 -0
  122. data/lib/flipper/serializers/json.rb +17 -0
  123. data/lib/flipper/spec/shared_adapter_specs.rb +105 -63
  124. data/lib/flipper/test/shared_adapter_test.rb +101 -58
  125. data/lib/flipper/test_help.rb +43 -0
  126. data/lib/flipper/typecast.rb +59 -18
  127. data/lib/flipper/types/actor.rb +13 -13
  128. data/lib/flipper/types/group.rb +4 -4
  129. data/lib/flipper/types/percentage.rb +1 -1
  130. data/lib/flipper/version.rb +11 -1
  131. data/lib/flipper.rb +50 -11
  132. data/lib/generators/flipper/setup_generator.rb +63 -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/failsafe_spec.rb +58 -0
  145. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  146. data/spec/flipper/adapters/http_spec.rb +137 -55
  147. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  148. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  149. data/spec/flipper/adapters/memory_spec.rb +14 -3
  150. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  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 +164 -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 +181 -0
  165. data/spec/flipper/configuration_spec.rb +17 -0
  166. data/spec/flipper/dsl_spec.rb +54 -73
  167. data/spec/flipper/engine_spec.rb +373 -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 +436 -33
  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 +23 -6
  202. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  203. data/spec/flipper/middleware/memoizer_spec.rb +74 -24
  204. data/spec/flipper/model/active_record_spec.rb +61 -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 +93 -29
  213. data/spec/spec_helper.rb +8 -14
  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 +23 -8
  219. data/test/adapters/actor_limit_test.rb +20 -0
  220. data/test_rails/generators/flipper/setup_generator_test.rb +64 -0
  221. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  222. data/test_rails/helper.rb +19 -2
  223. data/test_rails/system/test_help_test.rb +51 -0
  224. metadata +223 -19
  225. data/lib/flipper/railtie.rb +0 -47
  226. data/spec/flipper/railtie_spec.rb +0 -73
@@ -1,77 +1,159 @@
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
+ dir = FlipperRoot.join('tmp').tap(&:mkpath)
32
+ log_path = dir.join('flipper_adapters_http_spec.log')
33
+ @pstore_file = dir.join('flipper.pstore')
34
+ @pstore_file.unlink if @pstore_file.exist?
35
+
36
+ api_adapter = Flipper::Adapters::PStore.new(@pstore_file)
37
+ flipper_api = Flipper.new(api_adapter)
38
+ app = Flipper::Api.app(flipper_api)
39
+ server_options = {
40
+ Port: FLIPPER_SPEC_API_PORT,
41
+ StartCallback: -> { @started = true },
42
+ Logger: WEBrick::Log.new(log_path.to_s, WEBrick::Log::INFO),
43
+ AccessLog: [
44
+ [log_path.open('w'), WEBrick::AccessLog::COMBINED_LOG_FORMAT],
45
+ ],
46
+ }
47
+ @server = WEBrick::HTTPServer.new(server_options)
48
+ @server.mount '/', rack_handler, app
49
+
50
+ Thread.new { @server.start }
51
+ Timeout.timeout(1) { :wait until @started }
52
+ end
53
+
54
+ after :all do
55
+ @server.shutdown if @server
56
+ end
57
+
58
+ before(:each) do
59
+ @pstore_file.unlink if @pstore_file.exist?
60
+ end
61
+
62
+ it_should_behave_like 'a flipper adapter'
63
+
64
+ it "can enable and disable unregistered group" do
65
+ flipper = Flipper.new(subject)
66
+ expect(flipper[:search].enable_group(:some_made_up_group)).to be(true)
67
+ expect(flipper[:search].groups_value).to eq(Set["some_made_up_group"])
68
+
69
+ expect(flipper[:search].disable_group(:some_made_up_group)).to be(true)
70
+ expect(flipper[:search].groups_value).to eq(Set.new)
71
+ end
72
+
73
+ it "can import" do
74
+ adapter = Flipper::Adapters::Memory.new
75
+ source_flipper = Flipper.new(adapter)
76
+ source_flipper.enable_percentage_of_actors :search, 10
77
+ source_flipper.enable_percentage_of_time :search, 15
78
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
79
+ source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
80
+ source_flipper.enable_group :search, :admins
81
+ source_flipper.enable_group :search, :employees
82
+ source_flipper.enable :plausible
83
+ source_flipper.disable :google_analytics
84
+
85
+ flipper = Flipper.new(subject)
86
+ flipper.import(source_flipper)
87
+ expect(flipper[:search].percentage_of_actors_value).to be(10)
88
+ expect(flipper[:search].percentage_of_time_value).to be(15)
89
+ expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
90
+ expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
91
+ expect(flipper[:plausible].boolean_value).to be(true)
92
+ expect(flipper[:google_analytics].boolean_value).to be(false)
93
+ end
11
94
  end
95
+ end
12
96
 
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
97
+ it "sends default headers" do
98
+ headers = {
99
+ 'accept' => 'application/json',
100
+ 'content-type' => 'application/json',
101
+ 'user-agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
102
+ }
103
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
104
+ .with(headers: headers)
105
+ .to_return(status: 404)
36
106
 
37
- after :all do
38
- @server.shutdown if @server
39
- end
107
+ adapter = described_class.new(url: 'http://app.com/flipper')
108
+ adapter.get(flipper[:feature_panel])
109
+ end
40
110
 
41
- before(:each) do
42
- @pstore_file.unlink if @pstore_file.exist?
43
- end
111
+ it "sends framework versions" do
112
+ stub_const("Rails", double(version: "7.1.0"))
113
+ stub_const("Sinatra::VERSION", "3.1.0")
114
+ stub_const("Hanami::VERSION", "0.7.2")
115
+ stub_const("GoodJob::VERSION", "3.21.5")
116
+ stub_const("Sidekiq::VERSION", "7.2.0")
44
117
 
45
- it_should_behave_like 'a flipper adapter'
118
+ headers = {
119
+ "client-framework" => [
120
+ "rails=7.1.0",
121
+ "sinatra=3.1.0",
122
+ "hanami=0.7.2",
123
+ "good_job=3.21.5",
124
+ "sidekiq=7.2.0",
125
+ ]
126
+ }
46
127
 
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"])
128
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
129
+ .with(headers: headers)
130
+ .to_return(status: 404)
51
131
 
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
132
+ adapter = described_class.new(url: 'http://app.com/flipper')
133
+ adapter.get(flipper[:feature_panel])
55
134
  end
56
135
 
57
- it "sends default headers" do
136
+ it "does not send undefined framework versions" do
137
+ stub_const("Rails", double(version: "7.1.0"))
138
+ stub_const("Sinatra::VERSION", "3.1.0")
139
+
58
140
  headers = {
59
- 'Accept' => 'application/json',
60
- 'Content-Type' => 'application/json',
61
- 'User-Agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
141
+ "client-framework" => ["rails=7.1.0", "sinatra=3.1.0"]
62
142
  }
143
+
63
144
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
64
145
  .with(headers: headers)
65
- .to_return(status: 404, body: "", headers: {})
146
+ .to_return(status: 404)
66
147
 
67
148
  adapter = described_class.new(url: 'http://app.com/flipper')
68
149
  adapter.get(flipper[:feature_panel])
69
150
  end
70
151
 
152
+
71
153
  describe "#get" do
72
154
  it "raises error when not successful response" do
73
155
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
74
- .to_return(status: 503, body: "", headers: {})
156
+ .to_return(status: 503)
75
157
 
76
158
  adapter = described_class.new(url: 'http://app.com/flipper')
77
159
  expect {
@@ -82,8 +164,8 @@ RSpec.describe Flipper::Adapters::Http do
82
164
 
83
165
  describe "#get_multi" do
84
166
  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: {})
167
+ stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
168
+ .to_return(status: 503)
87
169
 
88
170
  adapter = described_class.new(url: 'http://app.com/flipper')
89
171
  expect {
@@ -94,8 +176,8 @@ RSpec.describe Flipper::Adapters::Http do
94
176
 
95
177
  describe "#get_all" do
96
178
  it "raises error when not successful response" do
97
- stub_request(:get, "http://app.com/flipper/features")
98
- .to_return(status: 503, body: "", headers: {})
179
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
180
+ .to_return(status: 503)
99
181
 
100
182
  adapter = described_class.new(url: 'http://app.com/flipper')
101
183
  expect {
@@ -106,8 +188,8 @@ RSpec.describe Flipper::Adapters::Http do
106
188
 
107
189
  describe "#features" do
108
190
  it "raises error when not successful response" do
109
- stub_request(:get, "http://app.com/flipper/features")
110
- .to_return(status: 503, body: "", headers: {})
191
+ stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
192
+ .to_return(status: 503)
111
193
 
112
194
  adapter = described_class.new(url: 'http://app.com/flipper')
113
195
  expect {
@@ -224,7 +306,7 @@ RSpec.describe Flipper::Adapters::Http do
224
306
  let(:options) do
225
307
  {
226
308
  url: 'http://app.com/mount-point',
227
- headers: { 'X-Custom-Header' => 'foo' },
309
+ headers: { 'x-custom-header' => 'foo' },
228
310
  basic_auth_username: 'username',
229
311
  basic_auth_password: 'password',
230
312
  read_timeout: 100,
@@ -245,7 +327,7 @@ RSpec.describe Flipper::Adapters::Http do
245
327
  subject.get(feature)
246
328
  expect(
247
329
  a_request(:get, 'http://app.com/mount-point/features/feature_panel')
248
- .with(headers: { 'X-Custom-Header' => 'foo' })
330
+ .with(headers: { 'x-custom-header' => 'foo' })
249
331
  ).to have_been_made.once
250
332
  end
251
333
 
@@ -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
@@ -5,11 +5,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
5
5
  let(:flipper) { Flipper.new(subject) }
6
6
  let(:feature) { flipper[:stats] }
7
7
 
8
- let(:boolean_gate) { feature.gate(:boolean) }
9
- let(:group_gate) { feature.gate(:group) }
10
- let(:actor_gate) { feature.gate(:actor) }
11
- let(:actors_gate) { feature.gate(:percentage_of_actors) }
12
- let(:time_gate) { feature.gate(:percentage_of_time) }
8
+ let(:boolean_gate) { feature.gate(:boolean) }
9
+ let(:group_gate) { feature.gate(:group) }
10
+ let(:actor_gate) { feature.gate(:actor) }
11
+ let(:expression_gate) { feature.gate(:expression) }
12
+ let(:actors_gate) { feature.gate(:percentage_of_actors) }
13
+ let(:time_gate) { feature.gate(:percentage_of_time) }
13
14
 
14
15
  subject { described_class.new(adapter) }
15
16
 
@@ -41,18 +42,28 @@ RSpec.describe Flipper::Adapters::ReadOnly do
41
42
  end
42
43
 
43
44
  it 'can get feature' do
45
+ expression = Flipper.property(:plan).eq("basic")
44
46
  actor22 = Flipper::Actor.new('22')
45
- adapter.enable(feature, boolean_gate, flipper.boolean)
47
+ adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
46
48
  adapter.enable(feature, group_gate, flipper.group(:admins))
47
- adapter.enable(feature, actor_gate, flipper.actor(actor22))
48
- adapter.enable(feature, actors_gate, flipper.actors(25))
49
- adapter.enable(feature, time_gate, flipper.time(45))
50
-
51
- expect(subject.get(feature)).to eq(boolean: 'true',
52
- groups: Set['admins'],
53
- actors: Set['22'],
54
- percentage_of_actors: '25',
55
- percentage_of_time: '45')
49
+ adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
50
+ adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
51
+ adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(45))
52
+ adapter.enable(feature, expression_gate, expression)
53
+
54
+ expect(subject.get(feature)).to eq({
55
+ boolean: 'true',
56
+ groups: Set['admins'],
57
+ actors: Set['22'],
58
+ expression: {
59
+ "Equal" => [
60
+ {"Property" => ["plan"]},
61
+ "basic",
62
+ ]
63
+ },
64
+ percentage_of_actors: '25',
65
+ percentage_of_time: '45',
66
+ })
56
67
  end
57
68
 
58
69
  it 'can get features' do
@@ -61,6 +72,10 @@ RSpec.describe Flipper::Adapters::ReadOnly do
61
72
  expect(subject.features).to eq(Set['stats'])
62
73
  end
63
74
 
75
+ it 'is configured as read only' do
76
+ expect(subject.read_only?).to eq(true)
77
+ end
78
+
64
79
  it 'raises error on add' do
65
80
  expect { subject.add(feature) }.to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
66
81
  end
@@ -74,12 +89,12 @@ RSpec.describe Flipper::Adapters::ReadOnly do
74
89
  end
75
90
 
76
91
  it 'raises error on enable' do
77
- expect { subject.enable(feature, boolean_gate, flipper.boolean) }
92
+ expect { subject.enable(feature, boolean_gate, Flipper::Types::Boolean.new) }
78
93
  .to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
79
94
  end
80
95
 
81
96
  it 'raises error on disable' do
82
- expect { subject.disable(feature, boolean_gate, flipper.boolean) }
97
+ expect { subject.disable(feature, boolean_gate, Flipper::Types::Boolean.new) }
83
98
  .to raise_error(Flipper::Adapters::ReadOnly::WriteAttempted)
84
99
  end
85
100
  end