flipper 0.28.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 (195) 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 +52 -10
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -526
  7. data/Gemfile +17 -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/app.ru +12 -0
  14. data/examples/cloud/backoff_policy.rb +13 -0
  15. data/examples/cloud/basic.rb +22 -0
  16. data/examples/cloud/cloud_setup.rb +20 -0
  17. data/examples/cloud/forked.rb +36 -0
  18. data/examples/cloud/import.rb +17 -0
  19. data/examples/cloud/threaded.rb +33 -0
  20. data/examples/dsl.rb +0 -14
  21. data/examples/expressions.rb +213 -0
  22. data/examples/mirroring.rb +59 -0
  23. data/examples/strict.rb +18 -0
  24. data/exe/flipper +5 -0
  25. data/flipper-cloud.gemspec +19 -0
  26. data/flipper.gemspec +8 -4
  27. data/lib/flipper/actor.rb +6 -3
  28. data/lib/flipper/adapter.rb +10 -0
  29. data/lib/flipper/adapter_builder.rb +44 -0
  30. data/lib/flipper/adapters/actor_limit.rb +28 -0
  31. data/lib/flipper/adapters/cache_base.rb +143 -0
  32. data/lib/flipper/adapters/dual_write.rb +1 -3
  33. data/lib/flipper/adapters/failover.rb +0 -4
  34. data/lib/flipper/adapters/failsafe.rb +0 -4
  35. data/lib/flipper/adapters/http/client.rb +40 -12
  36. data/lib/flipper/adapters/http/error.rb +2 -2
  37. data/lib/flipper/adapters/http.rb +19 -14
  38. data/lib/flipper/adapters/instrumented.rb +0 -4
  39. data/lib/flipper/adapters/memoizable.rb +14 -19
  40. data/lib/flipper/adapters/memory.rb +37 -19
  41. data/lib/flipper/adapters/operation_logger.rb +18 -92
  42. data/lib/flipper/adapters/poll.rb +16 -3
  43. data/lib/flipper/adapters/pstore.rb +17 -11
  44. data/lib/flipper/adapters/read_only.rb +8 -41
  45. data/lib/flipper/adapters/strict.rb +45 -0
  46. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  47. data/lib/flipper/adapters/sync.rb +0 -4
  48. data/lib/flipper/adapters/wrapper.rb +54 -0
  49. data/lib/flipper/cli.rb +263 -0
  50. data/lib/flipper/cloud/configuration.rb +266 -0
  51. data/lib/flipper/cloud/dsl.rb +27 -0
  52. data/lib/flipper/cloud/message_verifier.rb +95 -0
  53. data/lib/flipper/cloud/middleware.rb +63 -0
  54. data/lib/flipper/cloud/routes.rb +14 -0
  55. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  56. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  57. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  58. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  59. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  60. data/lib/flipper/cloud/telemetry.rb +191 -0
  61. data/lib/flipper/cloud.rb +53 -0
  62. data/lib/flipper/configuration.rb +25 -4
  63. data/lib/flipper/dsl.rb +47 -42
  64. data/lib/flipper/engine.rb +102 -0
  65. data/lib/flipper/export.rb +0 -2
  66. data/lib/flipper/exporters/json/export.rb +1 -1
  67. data/lib/flipper/exporters/json/v1.rb +1 -1
  68. data/lib/flipper/expression/builder.rb +73 -0
  69. data/lib/flipper/expression/constant.rb +25 -0
  70. data/lib/flipper/expression.rb +71 -0
  71. data/lib/flipper/expressions/all.rb +9 -0
  72. data/lib/flipper/expressions/any.rb +9 -0
  73. data/lib/flipper/expressions/boolean.rb +9 -0
  74. data/lib/flipper/expressions/comparable.rb +13 -0
  75. data/lib/flipper/expressions/duration.rb +28 -0
  76. data/lib/flipper/expressions/equal.rb +9 -0
  77. data/lib/flipper/expressions/greater_than.rb +9 -0
  78. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  79. data/lib/flipper/expressions/less_than.rb +9 -0
  80. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  81. data/lib/flipper/expressions/not_equal.rb +9 -0
  82. data/lib/flipper/expressions/now.rb +9 -0
  83. data/lib/flipper/expressions/number.rb +9 -0
  84. data/lib/flipper/expressions/percentage.rb +9 -0
  85. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  86. data/lib/flipper/expressions/property.rb +9 -0
  87. data/lib/flipper/expressions/random.rb +9 -0
  88. data/lib/flipper/expressions/string.rb +9 -0
  89. data/lib/flipper/expressions/time.rb +9 -0
  90. data/lib/flipper/feature.rb +63 -1
  91. data/lib/flipper/gate.rb +2 -1
  92. data/lib/flipper/gate_values.rb +5 -2
  93. data/lib/flipper/gates/expression.rb +75 -0
  94. data/lib/flipper/instrumentation/log_subscriber.rb +28 -5
  95. data/lib/flipper/instrumentation/statsd.rb +4 -2
  96. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  97. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  98. data/lib/flipper/metadata.rb +8 -1
  99. data/lib/flipper/middleware/memoizer.rb +30 -14
  100. data/lib/flipper/model/active_record.rb +23 -0
  101. data/lib/flipper/poller.rb +10 -9
  102. data/lib/flipper/serializers/gzip.rb +22 -0
  103. data/lib/flipper/serializers/json.rb +17 -0
  104. data/lib/flipper/spec/shared_adapter_specs.rb +82 -63
  105. data/lib/flipper/test/shared_adapter_test.rb +77 -58
  106. data/lib/flipper/test_help.rb +43 -0
  107. data/lib/flipper/typecast.rb +37 -9
  108. data/lib/flipper/types/percentage.rb +1 -1
  109. data/lib/flipper/version.rb +11 -1
  110. data/lib/flipper.rb +44 -7
  111. data/lib/generators/flipper/setup_generator.rb +68 -0
  112. data/lib/generators/flipper/templates/initializer.rb +45 -0
  113. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  114. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  115. data/lib/generators/flipper/update_generator.rb +35 -0
  116. data/package-lock.json +41 -0
  117. data/package.json +10 -0
  118. data/spec/fixtures/environment.rb +1 -0
  119. data/spec/flipper/adapter_builder_spec.rb +72 -0
  120. data/spec/flipper/adapter_spec.rb +1 -0
  121. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  122. data/spec/flipper/adapters/dual_write_spec.rb +2 -2
  123. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  124. data/spec/flipper/adapters/http_spec.rb +135 -74
  125. data/spec/flipper/adapters/instrumented_spec.rb +1 -1
  126. data/spec/flipper/adapters/memoizable_spec.rb +21 -21
  127. data/spec/flipper/adapters/memory_spec.rb +11 -2
  128. data/spec/flipper/adapters/operation_logger_spec.rb +2 -2
  129. data/spec/flipper/adapters/poll_spec.rb +41 -0
  130. data/spec/flipper/adapters/read_only_spec.rb +32 -17
  131. data/spec/flipper/adapters/strict_spec.rb +64 -0
  132. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  133. data/spec/flipper/cli_spec.rb +166 -0
  134. data/spec/flipper/cloud/configuration_spec.rb +251 -0
  135. data/spec/flipper/cloud/dsl_spec.rb +82 -0
  136. data/spec/flipper/cloud/message_verifier_spec.rb +104 -0
  137. data/spec/flipper/cloud/middleware_spec.rb +289 -0
  138. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  139. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  140. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  141. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  142. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  143. data/spec/flipper/cloud_spec.rb +186 -0
  144. data/spec/flipper/configuration_spec.rb +17 -0
  145. data/spec/flipper/dsl_spec.rb +34 -73
  146. data/spec/flipper/engine_spec.rb +374 -0
  147. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  148. data/spec/flipper/expression/builder_spec.rb +248 -0
  149. data/spec/flipper/expression_spec.rb +188 -0
  150. data/spec/flipper/expressions/all_spec.rb +15 -0
  151. data/spec/flipper/expressions/any_spec.rb +15 -0
  152. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  153. data/spec/flipper/expressions/duration_spec.rb +43 -0
  154. data/spec/flipper/expressions/equal_spec.rb +24 -0
  155. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  156. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  157. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  158. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  159. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  160. data/spec/flipper/expressions/now_spec.rb +11 -0
  161. data/spec/flipper/expressions/number_spec.rb +21 -0
  162. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  163. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  164. data/spec/flipper/expressions/property_spec.rb +13 -0
  165. data/spec/flipper/expressions/random_spec.rb +9 -0
  166. data/spec/flipper/expressions/string_spec.rb +11 -0
  167. data/spec/flipper/expressions/time_spec.rb +13 -0
  168. data/spec/flipper/feature_spec.rb +380 -10
  169. data/spec/flipper/gate_values_spec.rb +2 -2
  170. data/spec/flipper/gates/expression_spec.rb +108 -0
  171. data/spec/flipper/identifier_spec.rb +4 -5
  172. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  173. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  174. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  175. data/spec/flipper/model/active_record_spec.rb +72 -0
  176. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  177. data/spec/flipper/serializers/json_spec.rb +13 -0
  178. data/spec/flipper/typecast_spec.rb +43 -7
  179. data/spec/flipper/types/actor_spec.rb +18 -1
  180. data/spec/flipper_integration_spec.rb +114 -16
  181. data/spec/flipper_spec.rb +87 -29
  182. data/spec/spec_helper.rb +17 -17
  183. data/spec/support/actor_names.yml +1 -0
  184. data/spec/support/fail_on_output.rb +8 -0
  185. data/spec/support/fake_backoff_policy.rb +15 -0
  186. data/spec/support/spec_helpers.rb +34 -8
  187. data/test/adapters/actor_limit_test.rb +20 -0
  188. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  189. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  190. data/test_rails/helper.rb +22 -2
  191. data/test_rails/system/test_help_test.rb +52 -0
  192. metadata +179 -19
  193. data/.tool-versions +0 -1
  194. data/lib/flipper/railtie.rb +0 -47
  195. data/spec/flipper/railtie_spec.rb +0 -109
@@ -0,0 +1,208 @@
1
+ require 'flipper/cloud/telemetry'
2
+ require 'flipper/cloud/configuration'
3
+
4
+ RSpec.describe Flipper::Cloud::Telemetry do
5
+ before do
6
+ # Stub polling for features.
7
+ stub_request(:get, "https://www.flippercloud.io/adapter/features?exclude_gate_names=true").
8
+ to_return(status: 200, body: "{}")
9
+ end
10
+
11
+ it "phones home and does not update telemetry interval if missing" do
12
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
13
+ to_return(status: 200, body: "{}")
14
+
15
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
16
+
17
+ # Record some telemetry and stop the threads so we submit a response.
18
+ telemetry = described_class.new(cloud_configuration)
19
+ telemetry.record(Flipper::Feature::InstrumentationName, {
20
+ operation: :enabled?,
21
+ feature_name: :foo,
22
+ result: true,
23
+ })
24
+ telemetry.stop
25
+
26
+ expect(telemetry.interval).to eq(60)
27
+ expect(telemetry.timer.execution_interval).to eq(60)
28
+ expect(stub).to have_been_requested.at_least_once
29
+ end
30
+
31
+ it "phones home and updates telemetry interval if present" do
32
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
33
+ to_return(status: 200, body: "{}", headers: {"telemetry-interval" => "120"})
34
+
35
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
36
+
37
+ # Record some telemetry and stop the threads so we submit a response.
38
+ telemetry = described_class.new(cloud_configuration)
39
+ telemetry.record(Flipper::Feature::InstrumentationName, {
40
+ operation: :enabled?,
41
+ feature_name: :foo,
42
+ result: true,
43
+ })
44
+ telemetry.stop
45
+
46
+ expect(telemetry.interval).to eq(120)
47
+ expect(telemetry.timer.execution_interval).to eq(120)
48
+ expect(stub).to have_been_requested.at_least_once
49
+ end
50
+
51
+ it "phones home and requests shutdown if telemetry-shutdown header is true" do
52
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
53
+ to_return(status: 404, body: "{}", headers: {"telemetry-shutdown" => "true"})
54
+
55
+ output = StringIO.new
56
+ cloud_configuration = Flipper::Cloud::Configuration.new(
57
+ token: "test",
58
+ logger: Logger.new(output),
59
+ logging_enabled: true,
60
+ )
61
+
62
+ # Record some telemetry and stop the threads so we submit a response.
63
+ telemetry = described_class.new(cloud_configuration)
64
+ telemetry.record(Flipper::Feature::InstrumentationName, {
65
+ operation: :enabled?,
66
+ feature_name: :foo,
67
+ result: true,
68
+ })
69
+ telemetry.stop
70
+ expect(stub).to have_been_requested.at_least_once
71
+ expect(output.string).to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
72
+ end
73
+
74
+ it "phones home and does not shutdown if telemetry shutdown header is missing" do
75
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
76
+ to_return(status: 404, body: "{}", headers: {})
77
+
78
+ output = StringIO.new
79
+ cloud_configuration = Flipper::Cloud::Configuration.new(
80
+ token: "test",
81
+ logger: Logger.new(output),
82
+ logging_enabled: true,
83
+ )
84
+
85
+ # Record some telemetry and stop the threads so we submit a response.
86
+ telemetry = described_class.new(cloud_configuration)
87
+ telemetry.record(Flipper::Feature::InstrumentationName, {
88
+ operation: :enabled?,
89
+ feature_name: :foo,
90
+ result: true,
91
+ })
92
+ telemetry.stop
93
+ expect(stub).to have_been_requested.at_least_once
94
+ expect(output.string).not_to match(/action=telemetry_shutdown message=The server has requested that telemetry be shut down./)
95
+ end
96
+
97
+ it "can update telemetry interval from error" do
98
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
99
+ to_return(status: 500, body: "{}", headers: {"telemetry-interval" => "120"})
100
+
101
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
102
+ telemetry = described_class.new(cloud_configuration)
103
+
104
+ # Override the submitter to use back off policy that doesn't actually
105
+ # sleep. If we don't then the stop below kills the working thread and the
106
+ # interval is never updated.
107
+ telemetry.submitter = ->(drained) {
108
+ Flipper::Cloud::Telemetry::Submitter.new(
109
+ cloud_configuration,
110
+ backoff_policy: FakeBackoffPolicy.new
111
+ ).call(drained)
112
+ }
113
+
114
+ # Record some telemetry and stop the threads so we submit a response.
115
+ telemetry.record(Flipper::Feature::InstrumentationName, {
116
+ operation: :enabled?,
117
+ feature_name: :foo,
118
+ result: true,
119
+ })
120
+ telemetry.stop
121
+
122
+ # Check the conig interval and the timer interval.
123
+ expect(telemetry.interval).to eq(120)
124
+ expect(telemetry.timer.execution_interval).to eq(120)
125
+ expect(stub).to have_been_requested.times(5)
126
+ end
127
+
128
+ it "doesn't try to update telemetry interval from error if not response error" do
129
+ stub = stub_request(:post, "https://www.flippercloud.io/adapter/telemetry").
130
+ to_raise(Net::OpenTimeout)
131
+
132
+ cloud_configuration = Flipper::Cloud::Configuration.new(token: "test")
133
+ telemetry = described_class.new(cloud_configuration)
134
+
135
+ # Override the submitter to use back off policy that doesn't actually
136
+ # sleep. If we don't then the stop below kills the working thread and the
137
+ # interval is never updated.
138
+ telemetry.submitter = ->(drained) {
139
+ Flipper::Cloud::Telemetry::Submitter.new(
140
+ cloud_configuration,
141
+ backoff_policy: FakeBackoffPolicy.new
142
+ ).call(drained)
143
+ }
144
+
145
+ # Record some telemetry and stop the threads so we submit a response.
146
+ telemetry.record(Flipper::Feature::InstrumentationName, {
147
+ operation: :enabled?,
148
+ feature_name: :foo,
149
+ result: true,
150
+ })
151
+ telemetry.stop
152
+
153
+ expect(telemetry.interval).to eq(60)
154
+ expect(telemetry.timer.execution_interval).to eq(60)
155
+ expect(stub).to have_been_requested.times(5)
156
+ end
157
+
158
+ describe '#record' do
159
+ it "increments in metric storage" do
160
+ begin
161
+ config = Flipper::Cloud::Configuration.new(token: "test")
162
+ telemetry = described_class.new(config)
163
+ telemetry.record(Flipper::Feature::InstrumentationName, {
164
+ operation: :enabled?,
165
+ feature_name: :foo,
166
+ result: true,
167
+ })
168
+ telemetry.record(Flipper::Feature::InstrumentationName, {
169
+ operation: :enabled?,
170
+ feature_name: :foo,
171
+ result: true,
172
+ })
173
+ telemetry.record(Flipper::Feature::InstrumentationName, {
174
+ operation: :enabled?,
175
+ feature_name: :bar,
176
+ result: true,
177
+ })
178
+ telemetry.record(Flipper::Feature::InstrumentationName, {
179
+ operation: :enabled?,
180
+ feature_name: :baz,
181
+ result: true,
182
+ })
183
+ telemetry.record(Flipper::Feature::InstrumentationName, {
184
+ operation: :enabled?,
185
+ feature_name: :foo,
186
+ result: false,
187
+ })
188
+
189
+ drained = telemetry.metric_storage.drain
190
+ metrics_by_key = drained.keys.group_by(&:key)
191
+
192
+ foo_true, foo_false = metrics_by_key["foo"].partition { |metric| metric.result }
193
+ foo_true_sum = foo_true.map { |metric| drained[metric] }.sum
194
+ expect(foo_true_sum).to be(2)
195
+ foo_false_sum = foo_false.map { |metric| drained[metric] }.sum
196
+ expect(foo_false_sum).to be(1)
197
+
198
+ bar_true_sum = metrics_by_key["bar"].map { |metric| drained[metric] }.sum
199
+ expect(bar_true_sum).to be(1)
200
+
201
+ baz_true_sum = metrics_by_key["baz"].map { |metric| drained[metric] }.sum
202
+ expect(baz_true_sum).to be(1)
203
+ ensure
204
+ telemetry.stop
205
+ end
206
+ end
207
+ end
208
+ end
@@ -0,0 +1,186 @@
1
+ require 'flipper/cloud'
2
+ require 'flipper/adapters/instrumented'
3
+ require 'flipper/instrumenters/memory'
4
+
5
+ RSpec.describe Flipper::Cloud do
6
+ before do
7
+ stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
8
+ end
9
+
10
+ context "initialize with token" do
11
+ let(:token) { 'asdf' }
12
+
13
+ before do
14
+ @instance = described_class.new(token: token)
15
+ end
16
+
17
+ it 'returns Flipper::DSL instance' do
18
+ expect(@instance).to be_instance_of(Flipper::Cloud::DSL)
19
+ end
20
+
21
+ it 'can read the cloud configuration' do
22
+ expect(@instance.cloud_configuration).to be_instance_of(Flipper::Cloud::Configuration)
23
+ end
24
+
25
+ it 'configures the correct adapter' do
26
+ # pardon the nesting...
27
+ memoized_adapter = @instance.adapter
28
+ dual_write_adapter = memoized_adapter.adapter
29
+ expect(dual_write_adapter).to be_instance_of(Flipper::Adapters::DualWrite)
30
+ poll_adapter = dual_write_adapter.local
31
+ expect(poll_adapter).to be_instance_of(Flipper::Adapters::Poll)
32
+
33
+ http_adapter = dual_write_adapter.remote
34
+ client = http_adapter.client
35
+ expect(client.uri.scheme).to eq('https')
36
+ expect(client.uri.host).to eq('www.flippercloud.io')
37
+ expect(client.uri.path).to eq('/adapter')
38
+ expect(client.headers["flipper-cloud-token"]).to eq(token)
39
+ expect(@instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
40
+ expect(@instance.instrumenter.instrumenter).to be(Flipper::Instrumenters::Noop)
41
+ end
42
+ end
43
+
44
+ context 'initialize with token and options' do
45
+ it 'sets correct url' do
46
+ stub_request(:any, %r{fakeflipper.com}).to_return(status: 200)
47
+ instance = described_class.new(token: 'asdf', url: 'https://www.fakeflipper.com/sadpanda')
48
+ # pardon the nesting...
49
+ memoized = instance.adapter
50
+ dual_write = memoized.adapter
51
+ remote = dual_write.remote
52
+ uri = remote.client.uri
53
+ expect(uri.scheme).to eq('https')
54
+ expect(uri.host).to eq('www.fakeflipper.com')
55
+ expect(uri.path).to eq('/sadpanda')
56
+ end
57
+ end
58
+
59
+ it 'can initialize with no token explicitly provided' do
60
+ ENV['FLIPPER_CLOUD_TOKEN'] = 'asdf'
61
+ expect(described_class.new).to be_instance_of(Flipper::Cloud::DSL)
62
+ end
63
+
64
+ it 'can set instrumenter' do
65
+ instrumenter = Flipper::Instrumenters::Memory.new
66
+ instance = described_class.new(token: 'asdf', instrumenter: instrumenter)
67
+ expect(instance.instrumenter).to be_a(Flipper::Cloud::Telemetry::Instrumenter)
68
+ expect(instance.instrumenter.instrumenter).to be(instrumenter)
69
+ end
70
+
71
+ it 'allows wrapping adapter with another adapter like the instrumenter' do
72
+ instance = described_class.new(token: 'asdf') do |config|
73
+ config.adapter do |adapter|
74
+ Flipper::Adapters::Instrumented.new(adapter)
75
+ end
76
+ end
77
+ # instance.adapter is memoizable adapter instance
78
+ expect(instance.adapter.adapter).to be_instance_of(Flipper::Adapters::Instrumented)
79
+ end
80
+
81
+ it 'can set debug_output' do
82
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
83
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
84
+ .with(hash_including(debug_output: STDOUT)).at_least(:once).and_return(instance)
85
+ described_class.new(token: 'asdf', debug_output: STDOUT)
86
+ end
87
+
88
+ it 'can set read_timeout' do
89
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
90
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
91
+ .with(hash_including(read_timeout: 1)).at_least(:once).and_return(instance)
92
+ described_class.new(token: 'asdf', read_timeout: 1)
93
+ end
94
+
95
+ it 'can set open_timeout' do
96
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
97
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
98
+ .with(hash_including(open_timeout: 1)).at_least(:once).and_return(instance)
99
+ described_class.new(token: 'asdf', open_timeout: 1)
100
+ end
101
+
102
+ if RUBY_VERSION >= '2.6.0'
103
+ it 'can set write_timeout' do
104
+ instance = Flipper::Adapters::Http::Client.new(token: 'asdf', url: 'https://www.flippercloud.io/adapter')
105
+ expect(Flipper::Adapters::Http::Client).to receive(:new)
106
+ .with(hash_including(open_timeout: 1)).at_least(:once).and_return(instance)
107
+ described_class.new(token: 'asdf', open_timeout: 1)
108
+ end
109
+ end
110
+
111
+ it 'can import' do
112
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
113
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_return(status: 200, body: "{}", headers: {})
114
+
115
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
116
+
117
+ flipper.enable(:test)
118
+ flipper.enable(:search)
119
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
120
+ flipper.enable_percentage_of_time(:logging, 5)
121
+
122
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
123
+
124
+ get_all = {
125
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
126
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
127
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
128
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
129
+ }
130
+
131
+ expect(flipper.adapter.get_all).to eq(get_all)
132
+ cloud_flipper.import(flipper)
133
+ expect(flipper.adapter.get_all).to eq(get_all)
134
+ expect(cloud_flipper.adapter.get_all).to eq(get_all)
135
+ end
136
+
137
+ it 'raises error for failure while importing' do
138
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
139
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_return(status: 500, body: "{}")
140
+
141
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
142
+
143
+ flipper.enable(:test)
144
+ flipper.enable(:search)
145
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
146
+ flipper.enable_percentage_of_time(:logging, 5)
147
+
148
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
149
+
150
+ get_all = {
151
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
152
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
153
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
154
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
155
+ }
156
+
157
+ expect(flipper.adapter.get_all).to eq(get_all)
158
+ expect { cloud_flipper.import(flipper) }.to raise_error(Flipper::Adapters::Http::Error)
159
+ expect(flipper.adapter.get_all).to eq(get_all)
160
+ end
161
+
162
+ it 'raises error for timeout while importing' do
163
+ stub_request(:post, /www\.flippercloud\.io\/adapter\/features.*/).
164
+ with(headers: {'flipper-cloud-token'=>'asdf'}).to_timeout
165
+
166
+ flipper = Flipper.new(Flipper::Adapters::Memory.new)
167
+
168
+ flipper.enable(:test)
169
+ flipper.enable(:search)
170
+ flipper.enable_actor(:stats, Flipper::Actor.new("jnunemaker"))
171
+ flipper.enable_percentage_of_time(:logging, 5)
172
+
173
+ cloud_flipper = Flipper::Cloud.new(token: "asdf")
174
+
175
+ get_all = {
176
+ "logging" => {actors: Set.new, boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: "5"},
177
+ "search" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
178
+ "stats" => {actors: Set["jnunemaker"], boolean: nil, groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
179
+ "test" => {actors: Set.new, boolean: "true", groups: Set.new, expression: nil, percentage_of_actors: nil, percentage_of_time: nil},
180
+ }
181
+
182
+ expect(flipper.adapter.get_all).to eq(get_all)
183
+ expect { cloud_flipper.import(flipper) }.to raise_error(Net::OpenTimeout)
184
+ expect(flipper.adapter.get_all).to eq(get_all)
185
+ end
186
+ end
@@ -30,4 +30,21 @@ RSpec.describe Flipper::Configuration do
30
30
  expect(subject.default).to be(instance)
31
31
  end
32
32
  end
33
+
34
+ describe '#statsd' do
35
+ let(:statsd) { double(Statsd) }
36
+
37
+ after do
38
+ Flipper::Instrumentation::StatsdSubscriber.client = nil
39
+ end
40
+
41
+ it 'returns nil by default' do
42
+ expect(subject.statsd).to be_nil
43
+ end
44
+
45
+ it 'can be set' do
46
+ subject.statsd = statsd
47
+ expect(subject.statsd).to be(statsd)
48
+ end
49
+ end
33
50
  end
@@ -123,18 +123,6 @@ RSpec.describe Flipper::DSL do
123
123
  end
124
124
  end
125
125
 
126
- describe '#boolean' do
127
- it_should_behave_like 'a DSL boolean method' do
128
- let(:method_name) { :boolean }
129
- end
130
- end
131
-
132
- describe '#bool' do
133
- it_should_behave_like 'a DSL boolean method' do
134
- let(:method_name) { :bool }
135
- end
136
- end
137
-
138
126
  describe '#group' do
139
127
  context 'for registered group' do
140
128
  before do
@@ -148,66 +136,15 @@ RSpec.describe Flipper::DSL do
148
136
  end
149
137
  end
150
138
 
151
- describe '#actor' do
152
- context 'for an actor' do
153
- it 'returns actor instance' do
154
- actor = Flipper::Actor.new(33)
155
- flipper_actor = subject.actor(actor)
156
- expect(flipper_actor).to be_instance_of(Flipper::Types::Actor)
157
- expect(flipper_actor.value).to eq('33')
158
- end
139
+ describe '#expression' do
140
+ it "returns nil if feature has no expression" do
141
+ expect(subject.expression(:stats)).to be(nil)
159
142
  end
160
143
 
161
- context 'for nil' do
162
- it 'raises argument error' do
163
- expect do
164
- subject.actor(nil)
165
- end.to raise_error(ArgumentError)
166
- end
167
- end
168
-
169
- context 'for something that is not actor wrappable' do
170
- it 'raises argument error' do
171
- expect do
172
- subject.actor(Object.new)
173
- end.to raise_error(ArgumentError)
174
- end
175
- end
176
- end
177
-
178
- describe '#time' do
179
- before do
180
- @result = subject.time(5)
181
- end
182
-
183
- it 'returns percentage of time' do
184
- expect(@result).to be_instance_of(Flipper::Types::PercentageOfTime)
185
- end
186
-
187
- it 'sets value' do
188
- expect(@result.value).to eq(5)
189
- end
190
-
191
- it 'is aliased to percentage_of_time' do
192
- expect(@result).to eq(subject.percentage_of_time(@result.value))
193
- end
194
- end
195
-
196
- describe '#actors' do
197
- before do
198
- @result = subject.actors(17)
199
- end
200
-
201
- it 'returns percentage of actors' do
202
- expect(@result).to be_instance_of(Flipper::Types::PercentageOfActors)
203
- end
204
-
205
- it 'sets value' do
206
- expect(@result.value).to eq(17)
207
- end
208
-
209
- it 'is aliased to percentage_of_actors' do
210
- expect(@result).to eq(subject.percentage_of_actors(@result.value))
144
+ it "returns expression if feature has expression" do
145
+ expression = Flipper.property(:plan).eq("basic")
146
+ subject[:stats].enable_expression expression
147
+ expect(subject.expression(:stats)).to eq(expression)
211
148
  end
212
149
  end
213
150
 
@@ -246,6 +183,33 @@ RSpec.describe Flipper::DSL do
246
183
  end
247
184
  end
248
185
 
186
+ describe '#enable_expression/disable_expression' do
187
+ it 'enables and disables the feature for the expression' do
188
+ expression = Flipper.property(:plan).eq("basic")
189
+
190
+ expect(subject[:stats].expression).to be(nil)
191
+ subject.enable_expression(:stats, expression)
192
+ expect(subject[:stats].expression).to eq(expression)
193
+
194
+ subject.disable_expression(:stats)
195
+ expect(subject[:stats].expression).to be(nil)
196
+ end
197
+ end
198
+
199
+ describe '#add_expression/remove_expression' do
200
+ it 'enables and disables the feature for the expression' do
201
+ expression = Flipper.property(:plan).eq("basic")
202
+ any_expression = Flipper.any(expression)
203
+
204
+ expect(subject[:stats].expression).to be(nil)
205
+ subject.add_expression(:stats, any_expression)
206
+ expect(subject[:stats].expression).to eq(any_expression)
207
+
208
+ subject.remove_expression(:stats, expression)
209
+ expect(subject[:stats].expression).to eq(Flipper.any)
210
+ end
211
+ end
212
+
249
213
  describe '#enable_actor/disable_actor' do
250
214
  it 'enables and disables the feature for actor' do
251
215
  actor = Flipper::Actor.new(5)
@@ -261,9 +225,6 @@ RSpec.describe Flipper::DSL do
261
225
 
262
226
  describe '#enable_group/disable_group' do
263
227
  it 'enables and disables the feature for group' do
264
- actor = Flipper::Actor.new(5)
265
- group = Flipper.register(:fives) { |actor| actor.flipper_id == 5 }
266
-
267
228
  expect(subject[:stats].groups_value).to be_empty
268
229
  subject.enable_group(:stats, :fives)
269
230
  expect(subject[:stats].groups_value).to eq(Set['fives'])