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
@@ -4,56 +4,134 @@ require 'flipper/engine'
4
4
  RSpec.describe Flipper::Engine do
5
5
  let(:application) do
6
6
  Class.new(Rails::Application) do
7
+ config.load_defaults Rails::VERSION::STRING.to_f
7
8
  config.eager_load = false
8
9
  config.logger = ActiveSupport::Logger.new($stdout)
9
- end
10
+ config.active_support.remove_deprecated_time_with_zone_name = false
11
+ end.instance
10
12
  end
11
13
 
12
14
  before do
15
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
13
16
  Rails.application = nil
14
17
  ActiveSupport::Dependencies.autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
15
18
  ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
16
19
  end
17
20
 
21
+ # Reset Rails.env around each example
22
+ around do |example|
23
+ begin
24
+ env = Rails.env.to_s
25
+ example.run
26
+ ensure
27
+ Rails.env = env
28
+ end
29
+ end
30
+
18
31
  let(:config) { application.config.flipper }
19
32
 
20
- subject { application.initialize! }
33
+ subject { SpecHelpers.silence { application.initialize! } }
21
34
 
22
- context 'cloudless' do
23
- it 'can set env_key from ENV' do
24
- with_env 'FLIPPER_ENV_KEY' => 'flopper' do
35
+ shared_examples 'config.strict' do
36
+ let(:adapter) { Flipper.adapter.adapter }
37
+
38
+ it 'can set strict=true from ENV' do
39
+ ENV['FLIPPER_STRICT'] = 'true'
40
+ subject
41
+ expect(config.strict).to eq(:raise)
42
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
43
+ end
44
+
45
+ it 'can set strict=warn from ENV' do
46
+ ENV['FLIPPER_STRICT'] = 'warn'
47
+ subject
48
+ expect(config.strict).to eq(:warn)
49
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
50
+ expect(adapter.handler).to be(:warn)
51
+ end
52
+
53
+ it 'can set strict=false from ENV' do
54
+ ENV['FLIPPER_STRICT'] = 'false'
55
+ subject
56
+ expect(config.strict).to eq(false)
57
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::Strict)
58
+ end
59
+
60
+ [true, :raise, :warn].each do |value|
61
+ it "can set strict=#{value.inspect} in initializer" do
62
+ initializer { config.strict = value }
25
63
  subject
26
- expect(config.env_key).to eq('flopper')
64
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
65
+ expect(adapter.handler).to be(value)
27
66
  end
28
67
  end
29
68
 
30
- it 'can set memoize from ENV' do
31
- with_env 'FLIPPER_MEMOIZE' => 'false' do
69
+ it "can set strict=false in initializer" do
70
+ initializer { config.strict = false }
71
+ subject
72
+ expect(config.strict).to eq(false)
73
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::Strict)
74
+ end
75
+
76
+ it "defaults to strict=:warn in RAILS_ENV=development" do
77
+ Rails.env = "development"
78
+ subject
79
+ expect(config.strict).to eq(:warn)
80
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
81
+ end
82
+
83
+ %w(production test).each do |env|
84
+ it "defaults to strict=warn in RAILS_ENV=#{env}" do
85
+ Rails.env = env
86
+ expect(Rails.env).to eq(env)
32
87
  subject
33
- expect(config.memoize).to eq(false)
88
+ expect(config.strict).to eq(false)
89
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::Strict)
34
90
  end
35
91
  end
36
92
 
93
+ it "defaults to strict=warn in RAILS_ENV=development" do
94
+ Rails.env = "development"
95
+ expect(Rails.env).to eq("development")
96
+ subject
97
+ expect(config.strict).to eq(:warn)
98
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
99
+ expect(adapter.handler).to be(:warn)
100
+ end
101
+ end
102
+
103
+ context 'cloudless' do
104
+ it_behaves_like 'config.strict'
105
+
106
+ it 'can set env_key from ENV' do
107
+ ENV['FLIPPER_ENV_KEY'] = 'flopper'
108
+ subject
109
+ expect(config.env_key).to eq('flopper')
110
+ end
111
+
112
+ it 'can set memoize from ENV' do
113
+ ENV['FLIPPER_MEMOIZE'] = 'false'
114
+ subject
115
+ expect(config.memoize).to eq(false)
116
+ end
117
+
37
118
  it 'can set preload from ENV' do
38
- with_env 'FLIPPER_PRELOAD' => 'false' do
39
- subject
40
- expect(config.preload).to eq(false)
41
- end
119
+ ENV['FLIPPER_PRELOAD'] = 'false'
120
+ subject
121
+ expect(config.preload).to eq(false)
42
122
  end
43
123
 
44
124
  it 'can set instrumenter from ENV' do
45
125
  stub_const('My::Cool::Instrumenter', Class.new)
46
- with_env 'FLIPPER_INSTRUMENTER' => 'My::Cool::Instrumenter' do
47
- subject
48
- expect(config.instrumenter).to eq(My::Cool::Instrumenter)
49
- end
126
+ ENV['FLIPPER_INSTRUMENTER'] = 'My::Cool::Instrumenter'
127
+ subject
128
+ expect(config.instrumenter).to eq(My::Cool::Instrumenter)
50
129
  end
51
130
 
52
131
  it 'can set log from ENV' do
53
- with_env 'FLIPPER_LOG' => 'false' do
54
- subject
55
- expect(config.log).to eq(false)
56
- end
132
+ ENV['FLIPPER_LOG'] = 'false'
133
+ subject
134
+ expect(config.log).to eq(false)
57
135
  end
58
136
 
59
137
  it 'sets defaults' do
@@ -95,37 +173,77 @@ RSpec.describe Flipper::Engine do
95
173
  })
96
174
  end
97
175
 
98
- it "defines #flipper_id on AR::Base" do
99
- subject
100
- require 'active_record'
101
- expect(ActiveRecord::Base.ancestors).to include(Flipper::Identifier)
176
+ context "test_help" do
177
+ it "is loaded if RAILS_ENV=test" do
178
+ Rails.env = "test"
179
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
180
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help")
181
+ subject
182
+ expect(config.test_help).to eq(true)
183
+ end
184
+
185
+ it "is loaded if FLIPPER_TEST_HELP=true" do
186
+ ENV["FLIPPER_TEST_HELP"] = "true"
187
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
188
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help")
189
+ subject
190
+ expect(config.test_help).to eq(true)
191
+ end
192
+
193
+ it "is loaded if config.flipper.test_help = true" do
194
+ initializer { config.test_help = true }
195
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
196
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help")
197
+ subject
198
+ end
199
+
200
+ it "is not loaded if FLIPPER_TEST_HELP=false" do
201
+ ENV["FLIPPER_TEST_HELP"] = "false"
202
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
203
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help").never
204
+ subject
205
+ end
206
+
207
+ it "is not loaded if config.flipper.test_help = false" do
208
+ Rails.env = "true"
209
+ initializer { config.test_help = false }
210
+ allow(Flipper::Engine.instance).to receive(:require).and_call_original
211
+ expect(Flipper::Engine.instance).to receive(:require).with("flipper/test_help").never
212
+ subject
213
+ end
102
214
  end
103
215
  end
104
216
 
105
217
  context 'with cloud' do
106
- around do |example|
107
- with_env "FLIPPER_CLOUD_TOKEN" => "test-token" do
108
- example.run
109
- end
218
+ before do
219
+ ENV["FLIPPER_CLOUD_TOKEN"] = "test-token"
110
220
  end
111
221
 
112
222
  # App for Rack::Test
113
223
  let(:app) { application.routes }
114
224
 
225
+ it_behaves_like 'config.strict' do
226
+ let(:adapter) do
227
+ memoizable = Flipper.adapter
228
+ dual_write = memoizable.adapter
229
+ poll = dual_write.local
230
+ poll.adapter
231
+ end
232
+ end
233
+
115
234
  it "initializes cloud configuration" do
116
235
  stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
117
236
 
118
- application.initialize!
237
+ silence { application.initialize! }
119
238
 
120
239
  expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
121
- expect(Flipper.instance.instrumenter).to be(ActiveSupport::Notifications)
240
+ expect(Flipper.instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
241
+ expect(Flipper.instance.instrumenter.instrumenter).to be(ActiveSupport::Notifications)
122
242
  end
123
243
 
124
244
  context "with CLOUD_SYNC_SECRET" do
125
- around do |example|
126
- with_env "FLIPPER_CLOUD_SYNC_SECRET" => "test-secret" do
127
- example.run
128
- end
245
+ before do
246
+ ENV["FLIPPER_CLOUD_SYNC_SECRET"] = "test-secret"
129
247
  end
130
248
 
131
249
  let(:request_body) do
@@ -146,10 +264,10 @@ RSpec.describe Flipper::Engine do
146
264
  }
147
265
 
148
266
  it "configures webhook app" do
149
- application.initialize!
267
+ silence { application.initialize! }
150
268
 
151
269
  stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").with({
152
- headers: { "Flipper-Cloud-Token" => ENV["FLIPPER_CLOUD_TOKEN"] },
270
+ headers: { "flipper-cloud-token" => ENV["FLIPPER_CLOUD_TOKEN"] },
153
271
  }).to_return(status: 200, body: JSON.generate({ features: {} }), headers: {})
154
272
 
155
273
  post "/_flipper", request_body, { "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value }
@@ -161,7 +279,7 @@ RSpec.describe Flipper::Engine do
161
279
 
162
280
  context "without CLOUD_SYNC_SECRET" do
163
281
  it "does not configure webhook app" do
164
- application.initialize!
282
+ silence { application.initialize! }
165
283
 
166
284
  post "/_flipper"
167
285
  expect(last_response.status).to eq(404)
@@ -170,10 +288,9 @@ RSpec.describe Flipper::Engine do
170
288
 
171
289
  context "without FLIPPER_CLOUD_TOKEN" do
172
290
  it "gracefully skips configuring webhook app" do
173
- with_env "FLIPPER_CLOUD_TOKEN" => nil do
174
- application.initialize!
175
- expect(Flipper.instance).to be_a(Flipper::DSL)
176
- end
291
+ ENV["FLIPPER_CLOUD_TOKEN"] = nil
292
+ silence { application.initialize! }
293
+ expect(Flipper.instance).to be_a(Flipper::DSL)
177
294
 
178
295
  post "/_flipper"
179
296
  expect(last_response.status).to eq(404)
@@ -181,6 +298,73 @@ RSpec.describe Flipper::Engine do
181
298
  end
182
299
  end
183
300
 
301
+ context 'with cloud secrets in Rails.credentials' do
302
+ around do |example|
303
+ # Create temporary directory for Rails.root to write credentials to
304
+ # Once Rails 5.2 support is dropped, this can all be replaced with
305
+ # `config.credentials.content_path = Tempfile.new.path`
306
+ Dir.mktmpdir do |dir|
307
+ Dir.chdir(dir) do
308
+ Dir.mkdir("#{dir}/config")
309
+
310
+ example.run
311
+ end
312
+ end
313
+ end
314
+
315
+ before do
316
+ # Set master key which is needed to write credentials
317
+ ENV["RAILS_MASTER_KEY"] = "a" * 32
318
+
319
+ application.credentials.write(YAML.dump({
320
+ flipper: {
321
+ cloud_token: "credentials-token",
322
+ cloud_sync_secret: "credentials-secret",
323
+ }
324
+ }))
325
+ end
326
+
327
+ it "enables cloud" do
328
+ silence { application.initialize! }
329
+ expect(ENV["FLIPPER_CLOUD_TOKEN"]).to eq("credentials-token")
330
+ expect(ENV["FLIPPER_CLOUD_SYNC_SECRET"]).to eq("credentials-secret")
331
+ expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
332
+ end
333
+ end
334
+
335
+ it "includes model methods" do
336
+ subject
337
+ require 'active_record'
338
+ expect(ActiveRecord::Base.ancestors).to include(Flipper::Model::ActiveRecord)
339
+ end
340
+
341
+ describe "config.actor_limit" do
342
+ let(:adapter) do
343
+ silence { application.initialize! }
344
+ Flipper.adapter.adapter.adapter
345
+ end
346
+
347
+ it "defaults to 100" do
348
+ expect(adapter).to be_instance_of(Flipper::Adapters::ActorLimit)
349
+ expect(adapter.limit).to eq(100)
350
+ end
351
+
352
+ it "can be set from FLIPPER_ACTOR_LIMIT env" do
353
+ ENV["FLIPPER_ACTOR_LIMIT"] = "500"
354
+ expect(adapter.limit).to eq(500)
355
+ end
356
+
357
+ it "can be set from an initializer" do
358
+ initializer { config.actor_limit = 99 }
359
+ expect(adapter.limit).to eq(99)
360
+ end
361
+
362
+ it "can be disabled from an initializer" do
363
+ initializer { config.actor_limit = false }
364
+ expect(adapter).not_to be_instance_of(Flipper::Adapters::ActorLimit)
365
+ end
366
+ end
367
+
184
368
  # Add app initializer in the same order as config/initializers/*
185
369
  def initializer(&block)
186
370
  application.initializer 'spec', before: :load_config_initializers do
@@ -25,9 +25,9 @@ RSpec.describe Flipper::Exporters::Json::V1 do
25
25
  export = subject.call(adapter)
26
26
 
27
27
  expect(export.features).to eq({
28
- "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
29
- "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
30
- "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
28
+ "google_analytics" => {actors: Set.new, boolean: nil, expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
29
+ "plausible" => {actors: Set.new, boolean: "true", expression: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
30
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, expression: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
31
31
  })
32
32
  end
33
33
  end
@@ -0,0 +1,248 @@
1
+ RSpec.describe Flipper::Expression::Builder do
2
+ def build(object)
3
+ Flipper::Expression.build(object)
4
+ end
5
+
6
+ describe "#add" do
7
+ it "converts to Any and adds new expressions" do
8
+ expression = build("something")
9
+ first = Flipper.boolean(true).eq(true)
10
+ second = Flipper.boolean(false).eq(false)
11
+ new_expression = expression.add(first, second)
12
+ expect(new_expression).to eq(build({ Any: ["something", first, second] }))
13
+ end
14
+ end
15
+
16
+ describe "#remove" do
17
+ it "converts to Any and removes any expressions that match" do
18
+ expression = build("something")
19
+ first = Flipper.boolean(true).eq(true)
20
+ second = Flipper.boolean(false).eq(false)
21
+ new_expression = expression.remove(build("something"), first, second)
22
+ expect(new_expression).to eq(build(Any: []))
23
+ end
24
+ end
25
+
26
+ it "can convert to Any" do
27
+ expression = build("something")
28
+ converted = expression.any
29
+ expect(converted).to be_instance_of(Flipper::Expression)
30
+ expect(converted.function).to be(Flipper::Expressions::Any)
31
+ expect(converted.args).to eq([expression])
32
+ end
33
+
34
+ it "can convert to All" do
35
+ expression = build("something")
36
+ converted = expression.all
37
+ expect(converted).to eq(build(All: ["something"]))
38
+ end
39
+
40
+ context "Any" do
41
+ describe "#any" do
42
+ it "returns self" do
43
+ expression = build(Any: [
44
+ Flipper.boolean(true),
45
+ Flipper.string("yep").eq("yep"),
46
+ ])
47
+ expect(expression.any).to be(expression)
48
+ end
49
+ end
50
+
51
+ describe "#add" do
52
+ it "returns new instance with expression added" do
53
+ expression = Flipper.boolean(true)
54
+ other = Flipper.string("yep").eq("yep")
55
+
56
+ result = expression.add(other)
57
+ expect(result.args).to eq([
58
+ Flipper.boolean(true),
59
+ Flipper.string("yep").eq("yep"),
60
+ ])
61
+ end
62
+
63
+ it "returns new instance with many expressions added" do
64
+ expression = Flipper.boolean(true)
65
+ second = Flipper.string("yep").eq("yep")
66
+ third = Flipper.number(1).lte(20)
67
+
68
+ result = expression.add(second, third)
69
+ expect(result.args).to eq([
70
+ Flipper.boolean(true),
71
+ Flipper.string("yep").eq("yep"),
72
+ Flipper.number(1).lte(20),
73
+ ])
74
+ end
75
+
76
+ it "returns new instance with array of expressions added" do
77
+ expression = Flipper.boolean(true)
78
+ second = Flipper.string("yep").eq("yep")
79
+ third = Flipper.number(1).lte(20)
80
+
81
+ result = expression.add([second, third])
82
+ expect(result.args).to eq([
83
+ Flipper.boolean(true),
84
+ Flipper.string("yep").eq("yep"),
85
+ Flipper.number(1).lte(20),
86
+ ])
87
+ end
88
+ end
89
+
90
+ describe "#remove" do
91
+ it "returns new instance with expression removed" do
92
+ first = Flipper.boolean(true)
93
+ second = Flipper.string("yep").eq("yep")
94
+ third = Flipper.number(1).lte(20)
95
+ expression = Flipper.any([first, second, third])
96
+
97
+ result = expression.remove(second)
98
+ expect(expression.args).to eq([first, second, third])
99
+ expect(result.args).to eq([first, third])
100
+ end
101
+
102
+ it "returns new instance with many expressions removed" do
103
+ first = Flipper.boolean(true)
104
+ second = Flipper.string("yep").eq("yep")
105
+ third = Flipper.number(1).lte(20)
106
+ expression = Flipper.any([first, second, third])
107
+
108
+ result = expression.remove(second, third)
109
+ expect(expression.args).to eq([first, second, third])
110
+ expect(result.args).to eq([first])
111
+ end
112
+
113
+ it "returns new instance with array of expressions removed" do
114
+ first = Flipper.boolean(true)
115
+ second = Flipper.string("yep").eq("yep")
116
+ third = Flipper.number(1).lte(20)
117
+ expression = Flipper.any([first, second, third])
118
+
119
+ result = expression.remove([second, third])
120
+ expect(expression.args).to eq([first, second, third])
121
+ expect(result.args).to eq([first])
122
+ end
123
+ end
124
+ end
125
+
126
+ [
127
+ [2, 3, "equal", "eq", :Equal],
128
+ [2, 3, "not_equal", "neq", :NotEqual],
129
+ [2, 3, "greater_than", "gt", :GreaterThan],
130
+ [2, 3, "greater_than_or_equal_to", "gte", :GreaterThanOrEqualTo],
131
+ [2, 3, "greater_than_or_equal_to", "greater_than_or_equal", :GreaterThanOrEqualTo],
132
+ [2, 3, "less_than", "lt", :LessThan],
133
+ [2, 3, "less_than_or_equal_to", "lte", :LessThanOrEqualTo],
134
+ [2, 3, "less_than_or_equal_to", "less_than_or_equal", :LessThanOrEqualTo],
135
+ ].each do |(left, right, method_name, shortcut_name, function)|
136
+ it "can convert to #{function}" do
137
+ expression = build(left)
138
+ other = build(right)
139
+ converted = expression.send(method_name, other)
140
+ expect(converted).to eq(build({ function => [ left, right] }))
141
+ end
142
+
143
+ it "can convert to #{function} using #{shortcut_name}" do
144
+ expression = build(left)
145
+ other = build(right)
146
+ converted = expression.send(shortcut_name, other)
147
+ expect(converted).to eq(build({ function => [ left, right] }))
148
+ end
149
+
150
+ it "builds args into expressions when converting to #{function}" do
151
+ expression = build(left)
152
+ other = Flipper.property(:age)
153
+ converted = expression.send(method_name, other.value)
154
+ expect(converted).to eq(build({ function => [ left, other.value] }))
155
+ end
156
+ end
157
+
158
+ it "can convert to PercentageOfActors" do
159
+ expression = Flipper.constant("User;1").percentage_of_actors(40)
160
+ expect(expression).to eq(build({ PercentageOfActors: [ "User;1", 40 ] }))
161
+ end
162
+
163
+ context "All" do
164
+ describe "#all" do
165
+ it "returns self" do
166
+ expression = Flipper.all([
167
+ Flipper.boolean(true),
168
+ Flipper.string("yep").eq("yep"),
169
+ ])
170
+ expect(expression.all).to be(expression)
171
+ end
172
+ end
173
+
174
+ describe "#add" do
175
+ it "returns new instance with expression added" do
176
+ expression = Flipper.all([Flipper.boolean(true)])
177
+ other = Flipper.string("yep").eq("yep")
178
+
179
+ result = expression.add(other)
180
+ expect(result.args).to eq([
181
+ Flipper.boolean(true),
182
+ Flipper.string("yep").eq("yep"),
183
+ ])
184
+ end
185
+
186
+ it "returns new instance with many expressions added" do
187
+ expression = Flipper.all([Flipper.boolean(true)])
188
+ second = Flipper.string("yep").eq("yep")
189
+ third = Flipper.number(1).lte(20)
190
+
191
+ result = expression.add(second, third)
192
+ expect(result.args).to eq([
193
+ Flipper.boolean(true),
194
+ Flipper.string("yep").eq("yep"),
195
+ Flipper.number(1).lte(20),
196
+ ])
197
+ end
198
+
199
+ it "returns new instance with array of expressions added" do
200
+ expression = Flipper.all([Flipper.boolean(true)])
201
+ second = Flipper.string("yep").eq("yep")
202
+ third = Flipper.number(1).lte(20)
203
+
204
+ result = expression.add([second, third])
205
+ expect(result.args).to eq([
206
+ Flipper.boolean(true),
207
+ Flipper.string("yep").eq("yep"),
208
+ Flipper.number(1).lte(20),
209
+ ])
210
+ end
211
+ end
212
+
213
+ describe "#remove" do
214
+ it "returns new instance with expression removed" do
215
+ first = Flipper.boolean(true)
216
+ second = Flipper.string("yep").eq("yep")
217
+ third = Flipper.number(1).lte(20)
218
+ expression = Flipper.all([first, second, third])
219
+
220
+ result = expression.remove(second)
221
+ expect(expression.args).to eq([first, second, third])
222
+ expect(result.args).to eq([first, third])
223
+ end
224
+
225
+ it "returns new instance with many expressions removed" do
226
+ first = Flipper.boolean(true)
227
+ second = Flipper.string("yep").eq("yep")
228
+ third = Flipper.number(1).lte(20)
229
+ expression = Flipper.all([first, second, third])
230
+
231
+ result = expression.remove(second, third)
232
+ expect(expression.args).to eq([first, second, third])
233
+ expect(result.args).to eq([first])
234
+ end
235
+
236
+ it "returns new instance with array of expressions removed" do
237
+ first = Flipper.boolean(true)
238
+ second = Flipper.string("yep").eq("yep")
239
+ third = Flipper.number(1).lte(20)
240
+ expression = Flipper.all([first, second, third])
241
+
242
+ result = expression.remove([second, third])
243
+ expect(expression.args).to eq([first, second, third])
244
+ expect(result.args).to eq([first])
245
+ end
246
+ end
247
+ end
248
+ end