flipper 1.0.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 (180) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +50 -7
  4. data/.github/workflows/examples.yml +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -8
  8. data/README.md +31 -27
  9. data/Rakefile +2 -2
  10. data/benchmark/typecast_ips.rb +8 -0
  11. data/docs/images/banner.jpg +0 -0
  12. data/docs/images/flipper_cloud.png +0 -0
  13. data/examples/cloud/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -1,99 +1,160 @@
1
1
  require 'flipper/adapters/http'
2
2
  require 'flipper/adapters/pstore'
3
- require 'rack/handler/webrick'
4
3
 
5
- FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
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
6
12
 
7
- RSpec.describe Flipper::Adapters::Http do
8
- context 'adapter' do
9
- subject do
10
- described_class.new(url: "http://localhost:#{FLIPPER_SPEC_API_PORT}")
11
- end
12
13
 
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
14
+ FLIPPER_SPEC_API_PORT = ENV.fetch('FLIPPER_SPEC_API_PORT', 9001).to_i
32
15
 
33
- Thread.new { @server.start }
34
- Timeout.timeout(1) { :wait until @started }
16
+ RSpec.describe Flipper::Adapters::Http do
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
35
95
  end
96
+ end
36
97
 
37
- after :all do
38
- @server.shutdown if @server
39
- 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)
40
107
 
41
- before(:each) do
42
- @pstore_file.unlink if @pstore_file.exist?
43
- end
108
+ adapter = described_class.new(url: 'http://app.com/flipper')
109
+ adapter.get(flipper[:feature_panel])
110
+ end
44
111
 
45
- it_should_behave_like 'a flipper adapter'
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")
46
118
 
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"])
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
+ }
51
128
 
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
129
+ stub_request(:get, "http://app.com/flipper/features/feature_panel")
130
+ .with(headers: headers)
131
+ .to_return(status: 404)
55
132
 
56
- it "can import" do
57
- adapter = Flipper::Adapters::Memory.new
58
- source_flipper = Flipper.new(adapter)
59
- source_flipper.enable_percentage_of_actors :search, 10
60
- source_flipper.enable_percentage_of_time :search, 15
61
- source_flipper.enable_actor :search, Flipper::Actor.new('User;1')
62
- source_flipper.enable_actor :search, Flipper::Actor.new('User;100')
63
- source_flipper.enable_group :search, :admins
64
- source_flipper.enable_group :search, :employees
65
- source_flipper.enable :plausible
66
- source_flipper.disable :google_analytics
67
-
68
- flipper = Flipper.new(subject)
69
- flipper.import(source_flipper)
70
- expect(flipper[:search].percentage_of_actors_value).to be(10)
71
- expect(flipper[:search].percentage_of_time_value).to be(15)
72
- expect(flipper[:search].actors_value).to eq(Set["User;1", "User;100"])
73
- expect(flipper[:search].groups_value).to eq(Set["admins", "employees"])
74
- expect(flipper[:plausible].boolean_value).to be(true)
75
- expect(flipper[:google_analytics].boolean_value).to be(false)
76
- end
133
+ adapter = described_class.new(url: 'http://app.com/flipper')
134
+ adapter.get(flipper[:feature_panel])
77
135
  end
78
136
 
79
- 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
+
80
141
  headers = {
81
- 'Accept' => 'application/json',
82
- 'Content-Type' => 'application/json',
83
- 'User-Agent' => "Flipper HTTP Adapter v#{Flipper::VERSION}",
142
+ "client-framework" => ["rails=7.1.0", "sinatra=3.1.0"]
84
143
  }
144
+
85
145
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
86
146
  .with(headers: headers)
87
- .to_return(status: 404, body: "", headers: {})
147
+ .to_return(status: 404)
88
148
 
89
149
  adapter = described_class.new(url: 'http://app.com/flipper')
90
150
  adapter.get(flipper[:feature_panel])
91
151
  end
92
152
 
153
+
93
154
  describe "#get" do
94
155
  it "raises error when not successful response" do
95
156
  stub_request(:get, "http://app.com/flipper/features/feature_panel")
96
- .to_return(status: 503, body: "", headers: {})
157
+ .to_return(status: 503)
97
158
 
98
159
  adapter = described_class.new(url: 'http://app.com/flipper')
99
160
  expect {
@@ -105,7 +166,7 @@ RSpec.describe Flipper::Adapters::Http do
105
166
  describe "#get_multi" do
106
167
  it "raises error when not successful response" do
107
168
  stub_request(:get, "http://app.com/flipper/features?keys=feature_panel&exclude_gate_names=true")
108
- .to_return(status: 503, body: "", headers: {})
169
+ .to_return(status: 503)
109
170
 
110
171
  adapter = described_class.new(url: 'http://app.com/flipper')
111
172
  expect {
@@ -117,7 +178,7 @@ RSpec.describe Flipper::Adapters::Http do
117
178
  describe "#get_all" do
118
179
  it "raises error when not successful response" do
119
180
  stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
120
- .to_return(status: 503, body: "", headers: {})
181
+ .to_return(status: 503)
121
182
 
122
183
  adapter = described_class.new(url: 'http://app.com/flipper')
123
184
  expect {
@@ -129,7 +190,7 @@ RSpec.describe Flipper::Adapters::Http do
129
190
  describe "#features" do
130
191
  it "raises error when not successful response" do
131
192
  stub_request(:get, "http://app.com/flipper/features?exclude_gate_names=true")
132
- .to_return(status: 503, body: "", headers: {})
193
+ .to_return(status: 503)
133
194
 
134
195
  adapter = described_class.new(url: 'http://app.com/flipper')
135
196
  expect {
@@ -246,7 +307,7 @@ RSpec.describe Flipper::Adapters::Http do
246
307
  let(:options) do
247
308
  {
248
309
  url: 'http://app.com/mount-point',
249
- headers: { 'X-Custom-Header' => 'foo' },
310
+ headers: { 'x-custom-header' => 'foo' },
250
311
  basic_auth_username: 'username',
251
312
  basic_auth_password: 'password',
252
313
  read_timeout: 100,
@@ -267,7 +328,7 @@ RSpec.describe Flipper::Adapters::Http do
267
328
  subject.get(feature)
268
329
  expect(
269
330
  a_request(:get, 'http://app.com/mount-point/features/feature_panel')
270
- .with(headers: { 'X-Custom-Header' => 'foo' })
331
+ .with(headers: { 'x-custom-header' => 'foo' })
271
332
  ).to have_been_made.once
272
333
  end
273
334
 
@@ -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) { {} }
@@ -54,7 +54,7 @@ RSpec.describe Flipper::Adapters::Memoizable do
54
54
  it 'memoizes feature' do
55
55
  feature = flipper[:stats]
56
56
  result = subject.get(feature)
57
- expect(cache[described_class.key_for(feature.key)]).to be(result)
57
+ expect(cache["feature/#{feature.key}"]).to be(result)
58
58
  end
59
59
  end
60
60
 
@@ -83,8 +83,8 @@ RSpec.describe Flipper::Adapters::Memoizable do
83
83
  features = names.map { |name| flipper[name] }
84
84
  results = subject.get_multi(features)
85
85
  features.each do |feature|
86
- expect(cache[described_class.key_for(feature.key)]).not_to be(nil)
87
- 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])
88
88
  end
89
89
  end
90
90
  end
@@ -115,10 +115,10 @@ RSpec.describe Flipper::Adapters::Memoizable do
115
115
  features = names.map { |name| flipper[name].tap(&:enable) }
116
116
  results = subject.get_all
117
117
  features.each do |feature|
118
- expect(cache[described_class.key_for(feature.key)]).not_to be(nil)
119
- 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])
120
120
  end
121
- 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)
122
122
  end
123
123
 
124
124
  it 'only calls get_all once for memoized adapter' do
@@ -188,9 +188,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
188
188
  it 'unmemoizes feature' do
189
189
  feature = flipper[:stats]
190
190
  gate = feature.gate(:boolean)
191
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
191
+ cache["feature/#{feature.key}"] = { some: 'thing' }
192
192
  subject.enable(feature, gate, Flipper::Types::Boolean.new)
193
- expect(cache[described_class.key_for(feature.key)]).to be_nil
193
+ expect(cache["feature/#{feature.key}"]).to be_nil
194
194
  end
195
195
  end
196
196
 
@@ -218,9 +218,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
218
218
  it 'unmemoizes feature' do
219
219
  feature = flipper[:stats]
220
220
  gate = feature.gate(:boolean)
221
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
221
+ cache["feature/#{feature.key}"] = { some: 'thing' }
222
222
  subject.disable(feature, gate, Flipper::Types::Boolean.new)
223
- expect(cache[described_class.key_for(feature.key)]).to be_nil
223
+ expect(cache["feature/#{feature.key}"]).to be_nil
224
224
  end
225
225
  end
226
226
 
@@ -332,9 +332,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
332
332
 
333
333
  it 'unmemoizes the feature' do
334
334
  feature = flipper[:stats]
335
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
335
+ cache["feature/#{feature.key}"] = { some: 'thing' }
336
336
  subject.remove(feature)
337
- expect(cache[described_class.key_for(feature.key)]).to be_nil
337
+ expect(cache["feature/#{feature.key}"]).to be_nil
338
338
  end
339
339
  end
340
340
 
@@ -357,9 +357,9 @@ RSpec.describe Flipper::Adapters::Memoizable do
357
357
 
358
358
  it 'unmemoizes feature' do
359
359
  feature = flipper[:stats]
360
- cache[described_class.key_for(feature.key)] = { some: 'thing' }
360
+ cache["feature/#{feature.key}"] = { some: 'thing' }
361
361
  subject.clear(feature)
362
- expect(cache[described_class.key_for(feature.key)]).to be_nil
362
+ expect(cache["feature/#{feature.key}"]).to be_nil
363
363
  end
364
364
  end
365
365
 
@@ -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
@@ -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
47
  adapter.enable(feature, boolean_gate, Flipper::Types::Boolean.new)
46
48
  adapter.enable(feature, group_gate, flipper.group(:admins))
47
49
  adapter.enable(feature, actor_gate, Flipper::Types::Actor.new(actor22))
48
50
  adapter.enable(feature, actors_gate, Flipper::Types::PercentageOfActors.new(25))
49
51
  adapter.enable(feature, time_gate, Flipper::Types::PercentageOfTime.new(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')
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
@@ -0,0 +1,64 @@
1
+ RSpec.describe Flipper::Adapters::Strict do
2
+ let(:flipper) { Flipper.new(subject) }
3
+ let(:feature) { flipper[:unknown] }
4
+
5
+ it_should_behave_like 'a flipper adapter' do
6
+ subject { described_class.new(Flipper::Adapters::Memory.new, :noop) }
7
+ end
8
+
9
+ [true, :raise].each do |handler|
10
+ context "handler = #{handler}" do
11
+ subject { described_class.new(Flipper::Adapters::Memory.new, handler) }
12
+
13
+ context "#get" do
14
+ it "raises an error for unknown feature" do
15
+ expect { subject.get(feature) }.to raise_error(Flipper::Adapters::Strict::NotFound)
16
+ end
17
+ end
18
+
19
+ context "#get_multi" do
20
+ it "raises an error for unknown feature" do
21
+ expect { subject.get_multi([feature]) }.to raise_error(Flipper::Adapters::Strict::NotFound)
22
+ end
23
+ end
24
+ end
25
+ end
26
+
27
+ context "handler = :warn" do
28
+ subject { described_class.new(Flipper::Adapters::Memory.new, :warn) }
29
+
30
+ context "#get" do
31
+ it "raises an error for unknown feature" do
32
+ expect(capture_output { subject.get(feature) }).to match(/Could not find feature "unknown"/)
33
+ end
34
+ end
35
+
36
+ context "#get_multi" do
37
+ it "raises an error for unknown feature" do
38
+ expect(capture_output { subject.get_multi([feature]) }).to match(/Could not find feature "unknown"/)
39
+ end
40
+ end
41
+ end
42
+
43
+ context "handler = Block" do
44
+ let(:unknown_features) { [] }
45
+ subject do
46
+ described_class.new(Flipper::Adapters::Memory.new) { |feature| unknown_features << feature.key}
47
+ end
48
+
49
+
50
+ context "#get" do
51
+ it "raises an error for unknown feature" do
52
+ subject.get(feature)
53
+ expect(unknown_features).to eq(["unknown"])
54
+ end
55
+ end
56
+
57
+ context "#get_multi" do
58
+ it "raises an error for unknown feature" do
59
+ subject.get_multi([flipper[:foo], flipper[:bar]])
60
+ expect(unknown_features).to eq(["foo", "bar"])
61
+ end
62
+ end
63
+ end
64
+ end
@@ -8,6 +8,8 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
8
8
  end
9
9
  let(:flipper) { Flipper.new(adapter) }
10
10
  let(:feature) { flipper[:search] }
11
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
12
+ let(:age_expression) { Flipper.property(:age).gte(21) }
11
13
 
12
14
  context "when remote disabled" do
13
15
  let(:remote) { Flipper::GateValues.new({}) }
@@ -63,6 +65,7 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
63
65
  boolean: nil,
64
66
  actors: Set["1"],
65
67
  groups: Set["staff"],
68
+ expression: plan_expression.value,
66
69
  percentage_of_time: 10,
67
70
  percentage_of_actors: 15,
68
71
  }
@@ -74,10 +77,34 @@ RSpec.describe Flipper::Adapters::Sync::FeatureSynchronizer do
74
77
  expect(local_gate_values_hash.fetch(:boolean)).to be(nil)
75
78
  expect(local_gate_values_hash.fetch(:actors)).to eq(Set["1"])
76
79
  expect(local_gate_values_hash.fetch(:groups)).to eq(Set["staff"])
80
+ expect(local_gate_values_hash.fetch(:expression)).to eq(plan_expression.value)
77
81
  expect(local_gate_values_hash.fetch(:percentage_of_time)).to eq("10")
78
82
  expect(local_gate_values_hash.fetch(:percentage_of_actors)).to eq("15")
79
83
  end
80
84
 
85
+ it "updates expression when remote is updated" do
86
+ any_expression = Flipper.any(plan_expression, age_expression)
87
+ remote = Flipper::GateValues.new(expression: any_expression.value)
88
+ feature.enable_expression(age_expression)
89
+ adapter.reset
90
+
91
+ described_class.new(feature, feature.gate_values, remote).call
92
+
93
+ expect(feature.expression_value).to eq(any_expression.value)
94
+ expect_only_enable
95
+ end
96
+
97
+ it "does nothing to expression if in sync" do
98
+ remote = Flipper::GateValues.new(expression: plan_expression.value)
99
+ feature.enable_expression(plan_expression)
100
+ adapter.reset
101
+
102
+ described_class.new(feature, feature.gate_values, remote).call
103
+
104
+ expect(feature.expression_value).to eq(plan_expression.value)
105
+ expect_no_enable_or_disable
106
+ end
107
+
81
108
  it "adds remotely added actors" do
82
109
  remote = Flipper::GateValues.new(actors: Set["1", "2"])
83
110
  feature.enable_actor(Flipper::Actor.new("1"))