flipper 0.26.0 → 1.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (199) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +19 -13
  4. data/.github/workflows/examples.yml +32 -15
  5. data/Changelog.md +294 -154
  6. data/Gemfile +15 -10
  7. data/README.md +13 -11
  8. data/benchmark/enabled_ips.rb +10 -0
  9. data/benchmark/enabled_multiple_actors_ips.rb +20 -0
  10. data/benchmark/enabled_profile.rb +20 -0
  11. data/benchmark/instrumentation_ips.rb +21 -0
  12. data/benchmark/typecast_ips.rb +27 -0
  13. data/docs/images/flipper_cloud.png +0 -0
  14. data/examples/api/basic.ru +3 -4
  15. data/examples/api/custom_memoized.ru +3 -4
  16. data/examples/api/memoized.ru +3 -4
  17. data/examples/cloud/app.ru +12 -0
  18. data/examples/cloud/backoff_policy.rb +13 -0
  19. data/examples/cloud/basic.rb +22 -0
  20. data/examples/cloud/cloud_setup.rb +20 -0
  21. data/examples/cloud/forked.rb +36 -0
  22. data/examples/cloud/import.rb +17 -0
  23. data/examples/cloud/threaded.rb +33 -0
  24. data/examples/dsl.rb +1 -15
  25. data/examples/enabled_for_actor.rb +4 -2
  26. data/examples/expressions.rb +213 -0
  27. data/examples/mirroring.rb +59 -0
  28. data/examples/strict.rb +18 -0
  29. data/flipper-cloud.gemspec +19 -0
  30. data/flipper.gemspec +3 -5
  31. data/lib/flipper/actor.rb +6 -3
  32. data/lib/flipper/adapter.rb +33 -7
  33. data/lib/flipper/adapter_builder.rb +44 -0
  34. data/lib/flipper/adapters/dual_write.rb +1 -3
  35. data/lib/flipper/adapters/failover.rb +0 -4
  36. data/lib/flipper/adapters/failsafe.rb +0 -4
  37. data/lib/flipper/adapters/http/client.rb +26 -7
  38. data/lib/flipper/adapters/http/error.rb +1 -1
  39. data/lib/flipper/adapters/http.rb +29 -16
  40. data/lib/flipper/adapters/instrumented.rb +25 -6
  41. data/lib/flipper/adapters/memoizable.rb +33 -21
  42. data/lib/flipper/adapters/memory.rb +81 -46
  43. data/lib/flipper/adapters/operation_logger.rb +16 -7
  44. data/lib/flipper/adapters/poll/poller.rb +2 -125
  45. data/lib/flipper/adapters/poll.rb +5 -3
  46. data/lib/flipper/adapters/pstore.rb +17 -11
  47. data/lib/flipper/adapters/read_only.rb +4 -4
  48. data/lib/flipper/adapters/strict.rb +47 -0
  49. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  50. data/lib/flipper/adapters/sync.rb +0 -4
  51. data/lib/flipper/cloud/configuration.rb +258 -0
  52. data/lib/flipper/cloud/dsl.rb +27 -0
  53. data/lib/flipper/cloud/message_verifier.rb +95 -0
  54. data/lib/flipper/cloud/middleware.rb +63 -0
  55. data/lib/flipper/cloud/routes.rb +14 -0
  56. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  57. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  58. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  59. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  60. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  61. data/lib/flipper/cloud/telemetry.rb +183 -0
  62. data/lib/flipper/cloud.rb +53 -0
  63. data/lib/flipper/configuration.rb +25 -4
  64. data/lib/flipper/dsl.rb +46 -45
  65. data/lib/flipper/engine.rb +88 -0
  66. data/lib/flipper/errors.rb +3 -3
  67. data/lib/flipper/export.rb +26 -0
  68. data/lib/flipper/exporter.rb +17 -0
  69. data/lib/flipper/exporters/json/export.rb +32 -0
  70. data/lib/flipper/exporters/json/v1.rb +33 -0
  71. data/lib/flipper/expression/builder.rb +73 -0
  72. data/lib/flipper/expression/constant.rb +25 -0
  73. data/lib/flipper/expression.rb +71 -0
  74. data/lib/flipper/expressions/all.rb +11 -0
  75. data/lib/flipper/expressions/any.rb +9 -0
  76. data/lib/flipper/expressions/boolean.rb +9 -0
  77. data/lib/flipper/expressions/comparable.rb +13 -0
  78. data/lib/flipper/expressions/duration.rb +28 -0
  79. data/lib/flipper/expressions/equal.rb +9 -0
  80. data/lib/flipper/expressions/greater_than.rb +9 -0
  81. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  82. data/lib/flipper/expressions/less_than.rb +9 -0
  83. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  84. data/lib/flipper/expressions/not_equal.rb +9 -0
  85. data/lib/flipper/expressions/now.rb +9 -0
  86. data/lib/flipper/expressions/number.rb +9 -0
  87. data/lib/flipper/expressions/percentage.rb +9 -0
  88. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  89. data/lib/flipper/expressions/property.rb +9 -0
  90. data/lib/flipper/expressions/random.rb +9 -0
  91. data/lib/flipper/expressions/string.rb +9 -0
  92. data/lib/flipper/expressions/time.rb +9 -0
  93. data/lib/flipper/feature.rb +87 -26
  94. data/lib/flipper/feature_check_context.rb +10 -6
  95. data/lib/flipper/gate.rb +13 -11
  96. data/lib/flipper/gate_values.rb +5 -18
  97. data/lib/flipper/gates/actor.rb +10 -17
  98. data/lib/flipper/gates/boolean.rb +1 -1
  99. data/lib/flipper/gates/expression.rb +75 -0
  100. data/lib/flipper/gates/group.rb +5 -7
  101. data/lib/flipper/gates/percentage_of_actors.rb +10 -13
  102. data/lib/flipper/gates/percentage_of_time.rb +1 -2
  103. data/lib/flipper/identifier.rb +2 -2
  104. data/lib/flipper/instrumentation/log_subscriber.rb +24 -5
  105. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  106. data/lib/flipper/instrumentation/subscriber.rb +8 -1
  107. data/lib/flipper/metadata.rb +5 -1
  108. data/lib/flipper/middleware/memoizer.rb +30 -14
  109. data/lib/flipper/poller.rb +117 -0
  110. data/lib/flipper/serializers/gzip.rb +24 -0
  111. data/lib/flipper/serializers/json.rb +19 -0
  112. data/lib/flipper/spec/shared_adapter_specs.rb +95 -54
  113. data/lib/flipper/test/shared_adapter_test.rb +91 -48
  114. data/lib/flipper/typecast.rb +56 -15
  115. data/lib/flipper/types/actor.rb +13 -13
  116. data/lib/flipper/types/group.rb +4 -4
  117. data/lib/flipper/types/percentage.rb +1 -1
  118. data/lib/flipper/version.rb +1 -1
  119. data/lib/flipper.rb +47 -10
  120. data/spec/fixtures/flipper_pstore_1679087600.json +46 -0
  121. data/spec/flipper/adapter_builder_spec.rb +73 -0
  122. data/spec/flipper/adapter_spec.rb +30 -2
  123. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  124. data/spec/flipper/adapters/http_spec.rb +64 -8
  125. data/spec/flipper/adapters/instrumented_spec.rb +29 -11
  126. data/spec/flipper/adapters/memoizable_spec.rb +51 -31
  127. data/spec/flipper/adapters/memory_spec.rb +14 -3
  128. data/spec/flipper/adapters/operation_logger_spec.rb +31 -12
  129. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  130. data/spec/flipper/adapters/strict_spec.rb +62 -0
  131. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  132. data/spec/flipper/cloud/configuration_spec.rb +252 -0
  133. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  134. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  135. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  136. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  137. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  138. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  139. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  140. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  141. data/spec/flipper/cloud_spec.rb +180 -0
  142. data/spec/flipper/configuration_spec.rb +17 -0
  143. data/spec/flipper/dsl_spec.rb +54 -73
  144. data/spec/flipper/engine_spec.rb +291 -0
  145. data/spec/flipper/export_spec.rb +13 -0
  146. data/spec/flipper/exporter_spec.rb +16 -0
  147. data/spec/flipper/exporters/json/export_spec.rb +60 -0
  148. data/spec/flipper/exporters/json/v1_spec.rb +33 -0
  149. data/spec/flipper/expression/builder_spec.rb +248 -0
  150. data/spec/flipper/expression_spec.rb +188 -0
  151. data/spec/flipper/expressions/all_spec.rb +15 -0
  152. data/spec/flipper/expressions/any_spec.rb +15 -0
  153. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  154. data/spec/flipper/expressions/duration_spec.rb +43 -0
  155. data/spec/flipper/expressions/equal_spec.rb +24 -0
  156. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  157. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  159. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  160. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  161. data/spec/flipper/expressions/now_spec.rb +11 -0
  162. data/spec/flipper/expressions/number_spec.rb +21 -0
  163. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  164. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  165. data/spec/flipper/expressions/property_spec.rb +13 -0
  166. data/spec/flipper/expressions/random_spec.rb +9 -0
  167. data/spec/flipper/expressions/string_spec.rb +11 -0
  168. data/spec/flipper/expressions/time_spec.rb +13 -0
  169. data/spec/flipper/feature_check_context_spec.rb +17 -17
  170. data/spec/flipper/feature_spec.rb +436 -33
  171. data/spec/flipper/gate_values_spec.rb +2 -33
  172. data/spec/flipper/gates/boolean_spec.rb +1 -1
  173. data/spec/flipper/gates/expression_spec.rb +108 -0
  174. data/spec/flipper/gates/group_spec.rb +2 -3
  175. data/spec/flipper/gates/percentage_of_actors_spec.rb +61 -5
  176. data/spec/flipper/gates/percentage_of_time_spec.rb +2 -2
  177. data/spec/flipper/identifier_spec.rb +4 -5
  178. data/spec/flipper/instrumentation/log_subscriber_spec.rb +15 -5
  179. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +25 -1
  180. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  181. data/spec/flipper/poller_spec.rb +47 -0
  182. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  183. data/spec/flipper/serializers/json_spec.rb +13 -0
  184. data/spec/flipper/typecast_spec.rb +121 -6
  185. data/spec/flipper/types/actor_spec.rb +63 -46
  186. data/spec/flipper/types/group_spec.rb +2 -2
  187. data/spec/flipper_integration_spec.rb +168 -58
  188. data/spec/flipper_spec.rb +92 -28
  189. data/spec/spec_helper.rb +6 -13
  190. data/spec/support/actor_names.yml +1 -0
  191. data/spec/support/climate_control.rb +7 -0
  192. data/spec/support/fake_backoff_policy.rb +15 -0
  193. data/spec/support/skippable.rb +18 -0
  194. data/spec/support/spec_helpers.rb +11 -3
  195. metadata +166 -13
  196. data/.github/workflows/release.yml +0 -44
  197. data/.tool-versions +0 -1
  198. data/lib/flipper/railtie.rb +0 -47
  199. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,291 @@
1
+ require 'rails'
2
+ require 'flipper/engine'
3
+
4
+ RSpec.describe Flipper::Engine do
5
+ let(:application) do
6
+ Class.new(Rails::Application) do
7
+ config.eager_load = false
8
+ config.logger = ActiveSupport::Logger.new($stdout)
9
+ end.instance
10
+ end
11
+
12
+ before do
13
+ Rails.application = nil
14
+ ActiveSupport::Dependencies.autoload_paths = ActiveSupport::Dependencies.autoload_paths.dup
15
+ ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
16
+ end
17
+
18
+ # Reset Rails.env around each example
19
+ around do |example|
20
+ begin
21
+ env = Rails.env.to_s
22
+ example.run
23
+ ensure
24
+ Rails.env = env
25
+ end
26
+ end
27
+
28
+ let(:config) { application.config.flipper }
29
+
30
+ subject { application.initialize! }
31
+
32
+ shared_examples 'config.strict' do
33
+ let(:adapter) { Flipper.adapter.adapter }
34
+
35
+ it 'can set strict=true from ENV' do
36
+ with_env 'FLIPPER_STRICT' => 'true' do
37
+ subject
38
+ expect(config.strict).to eq(:raise)
39
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
40
+ end
41
+ end
42
+
43
+ it 'can set strict=warn from ENV' do
44
+ with_env 'FLIPPER_STRICT' => 'warn' do
45
+ subject
46
+ expect(config.strict).to eq(:warn)
47
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
48
+ expect(adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
49
+ end
50
+ end
51
+
52
+ it 'can set strict=false from ENV' do
53
+ with_env 'FLIPPER_STRICT' => 'false' do
54
+ subject
55
+ expect(config.strict).to eq(false)
56
+ expect(adapter).to be_instance_of(Flipper::Adapters::Memory)
57
+ end
58
+ end
59
+
60
+ it "defaults to strict=false in RAILS_ENV=production" do
61
+ Rails.env = "production"
62
+ subject
63
+ expect(config.strict).to eq(false)
64
+ expect(adapter).to be_instance_of(Flipper::Adapters::Memory)
65
+ end
66
+
67
+ %w(development test).each do |env|
68
+ it "defaults to strict=warn in RAILS_ENV=#{env}" do
69
+ Rails.env = env
70
+ expect(Rails.env).to eq(env)
71
+ subject
72
+ expect(config.strict).to eq(:warn)
73
+ expect(adapter).to be_instance_of(Flipper::Adapters::Strict)
74
+ expect(adapter.handler).to be(Flipper::Adapters::Strict::HANDLERS.fetch(:warn))
75
+ end
76
+ end
77
+ end
78
+
79
+ context 'cloudless' do
80
+ it_behaves_like 'config.strict'
81
+
82
+ it 'can set env_key from ENV' do
83
+ with_env 'FLIPPER_ENV_KEY' => 'flopper' do
84
+ subject
85
+ expect(config.env_key).to eq('flopper')
86
+ end
87
+ end
88
+
89
+ it 'can set memoize from ENV' do
90
+ with_env 'FLIPPER_MEMOIZE' => 'false' do
91
+ subject
92
+ expect(config.memoize).to eq(false)
93
+ end
94
+ end
95
+
96
+ it 'can set preload from ENV' do
97
+ with_env 'FLIPPER_PRELOAD' => 'false' do
98
+ subject
99
+ expect(config.preload).to eq(false)
100
+ end
101
+ end
102
+
103
+ it 'can set instrumenter from ENV' do
104
+ stub_const('My::Cool::Instrumenter', Class.new)
105
+ with_env 'FLIPPER_INSTRUMENTER' => 'My::Cool::Instrumenter' do
106
+ subject
107
+ expect(config.instrumenter).to eq(My::Cool::Instrumenter)
108
+ end
109
+ end
110
+
111
+ it 'can set log from ENV' do
112
+ with_env 'FLIPPER_LOG' => 'false' do
113
+ subject
114
+ expect(config.log).to eq(false)
115
+ end
116
+ end
117
+
118
+ it 'sets defaults' do
119
+ subject # initialize
120
+ expect(config.env_key).to eq("flipper")
121
+ expect(config.memoize).to be(true)
122
+ expect(config.preload).to be(true)
123
+ end
124
+
125
+ it "configures instrumentor on default instance" do
126
+ subject # initialize
127
+ expect(Flipper.instance.instrumenter).to eq(ActiveSupport::Notifications)
128
+ end
129
+
130
+ it 'uses Memoizer middleware if config.memoize = true' do
131
+ initializer { config.memoize = true }
132
+ expect(subject.middleware).to include(Flipper::Middleware::Memoizer)
133
+ end
134
+
135
+ it 'does not use Memoizer middleware if config.memoize = false' do
136
+ initializer { config.memoize = false }
137
+ expect(subject.middleware).not_to include(Flipper::Middleware::Memoizer)
138
+ end
139
+
140
+ it 'passes config to memoizer' do
141
+ initializer do
142
+ config.update(
143
+ env_key: 'my_flipper',
144
+ preload: [:stats, :search]
145
+ )
146
+ end
147
+
148
+ expect(subject.middleware).to include(Flipper::Middleware::Memoizer)
149
+ middleware = subject.middleware.detect { |m| m.klass == Flipper::Middleware::Memoizer }
150
+ expect(middleware.args[0]).to eq({
151
+ env_key: config.env_key,
152
+ preload: config.preload,
153
+ if: nil
154
+ })
155
+ end
156
+ end
157
+
158
+ context 'with cloud' do
159
+ around do |example|
160
+ with_env "FLIPPER_CLOUD_TOKEN" => "test-token" do
161
+ example.run
162
+ end
163
+ end
164
+
165
+ # App for Rack::Test
166
+ let(:app) { application.routes }
167
+
168
+ it_behaves_like 'config.strict' do
169
+ let(:adapter) do
170
+ dual_write = Flipper.adapter.adapter
171
+ poll = dual_write.local
172
+ poll.adapter
173
+ end
174
+ end
175
+
176
+ it "initializes cloud configuration" do
177
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
178
+
179
+ application.initialize!
180
+
181
+ expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
182
+ expect(Flipper.instance.instrumenter).to be(ActiveSupport::Notifications)
183
+ end
184
+
185
+ context "with CLOUD_SYNC_SECRET" do
186
+ around do |example|
187
+ with_env "FLIPPER_CLOUD_SYNC_SECRET" => "test-secret" do
188
+ example.run
189
+ end
190
+ end
191
+
192
+ let(:request_body) do
193
+ JSON.generate({
194
+ "environment_id" => 1,
195
+ "webhook_id" => 1,
196
+ "delivery_id" => SecureRandom.uuid,
197
+ "action" => "sync",
198
+ })
199
+ end
200
+ let(:timestamp) { Time.now }
201
+
202
+ let(:signature) {
203
+ Flipper::Cloud::MessageVerifier.new(secret: ENV["FLIPPER_CLOUD_SYNC_SECRET"]).generate(request_body, timestamp)
204
+ }
205
+ let(:signature_header_value) {
206
+ Flipper::Cloud::MessageVerifier.new(secret: "").header(signature, timestamp)
207
+ }
208
+
209
+ it "configures webhook app" do
210
+ application.initialize!
211
+
212
+ stub = stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").with({
213
+ headers: { "Flipper-Cloud-Token" => ENV["FLIPPER_CLOUD_TOKEN"] },
214
+ }).to_return(status: 200, body: JSON.generate({ features: {} }), headers: {})
215
+
216
+ post "/_flipper", request_body, { "HTTP_FLIPPER_CLOUD_SIGNATURE" => signature_header_value }
217
+
218
+ expect(last_response.status).to eq(200)
219
+ expect(stub).to have_been_requested
220
+ end
221
+ end
222
+
223
+ context "without CLOUD_SYNC_SECRET" do
224
+ it "does not configure webhook app" do
225
+ application.initialize!
226
+
227
+ post "/_flipper"
228
+ expect(last_response.status).to eq(404)
229
+ end
230
+ end
231
+
232
+ context "without FLIPPER_CLOUD_TOKEN" do
233
+ it "gracefully skips configuring webhook app" do
234
+ with_env "FLIPPER_CLOUD_TOKEN" => nil do
235
+ application.initialize!
236
+ expect(Flipper.instance).to be_a(Flipper::DSL)
237
+ end
238
+
239
+ post "/_flipper"
240
+ expect(last_response.status).to eq(404)
241
+ end
242
+ end
243
+ end
244
+
245
+ context 'with cloud secrets in Rails.credentials' do
246
+ around do |example|
247
+ # Create temporary directory for Rails.root to write credentials to
248
+ # Once Rails 5.2 support is dropped, this can all be replaced with
249
+ # `config.credentials.content_path = Tempfile.new.path`
250
+ Dir.mktmpdir do |dir|
251
+ Dir.chdir(dir) do
252
+ Dir.mkdir("#{dir}/config")
253
+
254
+ example.run
255
+ end
256
+ end
257
+ end
258
+
259
+ before do
260
+ # Set master key which is needed to write credentials
261
+ ENV["RAILS_MASTER_KEY"] = "a" * 32
262
+
263
+ application.credentials.write(YAML.dump({
264
+ flipper: {
265
+ cloud_token: "credentials-token",
266
+ cloud_sync_secret: "credentials-secret",
267
+ }
268
+ }))
269
+ end
270
+
271
+ it "enables cloud" do
272
+ application.initialize!
273
+ expect(ENV["FLIPPER_CLOUD_TOKEN"]).to eq("credentials-token")
274
+ expect(ENV["FLIPPER_CLOUD_SYNC_SECRET"]).to eq("credentials-secret")
275
+ expect(Flipper.instance).to be_a(Flipper::Cloud::DSL)
276
+ end
277
+ end
278
+
279
+ it "includes model methods" do
280
+ subject
281
+ require 'active_record'
282
+ expect(ActiveRecord::Base.ancestors).to include(Flipper::Model::ActiveRecord)
283
+ end
284
+
285
+ # Add app initializer in the same order as config/initializers/*
286
+ def initializer(&block)
287
+ application.initializer 'spec', before: :load_config_initializers do
288
+ block.call
289
+ end
290
+ end
291
+ end
@@ -0,0 +1,13 @@
1
+ RSpec.describe Flipper::Export do
2
+ it "can initialize" do
3
+ export = described_class.new(contents: "{}", format: :json, version: 1)
4
+ expect(export.contents).to eq("{}")
5
+ expect(export.format).to eq(:json)
6
+ expect(export.version).to eq(1)
7
+ end
8
+
9
+ it "raises not implemented for features" do
10
+ export = described_class.new(contents: "{}", format: :json, version: 1)
11
+ expect { export.features }.to raise_error(NotImplementedError)
12
+ end
13
+ end
@@ -0,0 +1,16 @@
1
+ RSpec.describe Flipper::Exporter do
2
+ describe ".build" do
3
+ it "builds instance of exporter" do
4
+ exporter = described_class.build(format: :json, version: 1)
5
+ expect(exporter).to be_instance_of(Flipper::Exporters::Json::V1)
6
+ end
7
+
8
+ it "raises if format not found" do
9
+ expect { described_class.build(format: :nope, version: 1) }.to raise_error(KeyError)
10
+ end
11
+
12
+ it "raises if version not found" do
13
+ expect { described_class.build(format: :json, version: 0) }.to raise_error(KeyError)
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,60 @@
1
+ require 'flipper/exporters/json/v1'
2
+
3
+ RSpec.describe Flipper::Exporters::Json::Export do
4
+ let(:contents) {
5
+ <<~JSON
6
+ {
7
+ "version":1,
8
+ "features":{
9
+ "search":{"boolean":null,"groups":["admins","employees"],"actors":["User;1","User;100"],"percentage_of_actors":"10","percentage_of_time":"15"},
10
+ "plausible":{"boolean":"true","groups":[],"actors":[],"percentage_of_actors":null,"percentage_of_time":null},
11
+ "google_analytics":{"boolean":null,"groups":[],"actors":[],"percentage_of_actors":null,"percentage_of_time":null}
12
+ }
13
+ }
14
+ JSON
15
+ }
16
+
17
+ it "can initialize" do
18
+ export = described_class.new(contents: contents)
19
+ expect(export.format).to eq(:json)
20
+ expect(export.version).to be(1)
21
+ end
22
+
23
+ it "can initialize with version" do
24
+ export = described_class.new(contents: contents, version: 1)
25
+ expect(export.version).to be(1)
26
+ end
27
+
28
+ it "can build features from contents" do
29
+ export = Flipper::Exporters::Json::Export.new(contents: contents)
30
+ expect(export.features).to eq({
31
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
32
+ "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
33
+ "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
34
+ })
35
+ end
36
+
37
+ it "can build an adapter from features" do
38
+ export = Flipper::Exporters::Json::Export.new(contents: contents)
39
+ expect(export.adapter).to be_instance_of(Flipper::Adapters::Memory)
40
+ expect(export.adapter.get_all).to eq({
41
+ "plausible" => {actors: Set.new, boolean: "true", groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
42
+ "search" => {actors: Set["User;1", "User;100"], boolean: nil, groups: Set["admins", "employees"], percentage_of_actors: "10", percentage_of_time: "15"},
43
+ "google_analytics" => {actors: Set.new, boolean: nil, groups: Set.new, percentage_of_actors: nil, percentage_of_time: nil},
44
+ })
45
+ end
46
+
47
+ it "raises for invalid json" do
48
+ export = described_class.new(contents: "bad contents")
49
+ expect {
50
+ export.features
51
+ }.to raise_error(Flipper::Exporters::Json::JsonError)
52
+ end
53
+
54
+ it "raises for missing features key" do
55
+ export = described_class.new(contents: "{}")
56
+ expect {
57
+ export.features
58
+ }.to raise_error(Flipper::Exporters::Json::InvalidError)
59
+ end
60
+ end
@@ -0,0 +1,33 @@
1
+ require 'flipper/exporters/json/v1'
2
+
3
+ RSpec.describe Flipper::Exporters::Json::V1 do
4
+ subject { described_class.new }
5
+
6
+ it "has a version number" do
7
+ adapter = Flipper::Adapters::Memory.new
8
+ export = subject.call(adapter)
9
+ data = JSON.parse(export.contents)
10
+ expect(data["version"]).to eq(1)
11
+ end
12
+
13
+ it "exports features and gates" do
14
+ adapter = Flipper::Adapters::Memory.new
15
+ flipper = Flipper.new(adapter)
16
+ flipper.enable_percentage_of_actors :search, 10
17
+ flipper.enable_percentage_of_time :search, 15
18
+ flipper.enable_actor :search, Flipper::Actor.new('User;1')
19
+ flipper.enable_actor :search, Flipper::Actor.new('User;100')
20
+ flipper.enable_group :search, :admins
21
+ flipper.enable_group :search, :employees
22
+ flipper.enable :plausible
23
+ flipper.disable :google_analytics
24
+
25
+ export = subject.call(adapter)
26
+
27
+ expect(export.features).to eq({
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
+ })
32
+ end
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