flipper 1.0.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 (140) hide show
  1. checksums.yaml +4 -4
  2. data/.github/FUNDING.yml +1 -0
  3. data/.github/workflows/ci.yml +7 -3
  4. data/.github/workflows/examples.yml +27 -5
  5. data/Changelog.md +42 -0
  6. data/Gemfile +4 -4
  7. data/README.md +13 -11
  8. data/benchmark/typecast_ips.rb +8 -0
  9. data/docs/images/flipper_cloud.png +0 -0
  10. data/examples/cloud/backoff_policy.rb +13 -0
  11. data/examples/cloud/cloud_setup.rb +16 -0
  12. data/examples/cloud/forked.rb +7 -2
  13. data/examples/cloud/threaded.rb +15 -18
  14. data/examples/expressions.rb +213 -0
  15. data/examples/strict.rb +18 -0
  16. data/flipper.gemspec +1 -2
  17. data/lib/flipper/actor.rb +6 -3
  18. data/lib/flipper/adapter.rb +10 -0
  19. data/lib/flipper/adapter_builder.rb +44 -0
  20. data/lib/flipper/adapters/dual_write.rb +1 -3
  21. data/lib/flipper/adapters/failover.rb +0 -4
  22. data/lib/flipper/adapters/failsafe.rb +0 -4
  23. data/lib/flipper/adapters/http/client.rb +26 -7
  24. data/lib/flipper/adapters/http/error.rb +1 -1
  25. data/lib/flipper/adapters/http.rb +18 -13
  26. data/lib/flipper/adapters/instrumented.rb +0 -4
  27. data/lib/flipper/adapters/memoizable.rb +14 -19
  28. data/lib/flipper/adapters/memory.rb +4 -6
  29. data/lib/flipper/adapters/operation_logger.rb +0 -4
  30. data/lib/flipper/adapters/poll.rb +1 -3
  31. data/lib/flipper/adapters/pstore.rb +17 -11
  32. data/lib/flipper/adapters/read_only.rb +4 -4
  33. data/lib/flipper/adapters/strict.rb +47 -0
  34. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  35. data/lib/flipper/adapters/sync.rb +0 -4
  36. data/lib/flipper/cloud/configuration.rb +121 -52
  37. data/lib/flipper/cloud/telemetry/backoff_policy.rb +93 -0
  38. data/lib/flipper/cloud/telemetry/instrumenter.rb +26 -0
  39. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  40. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  41. data/lib/flipper/cloud/telemetry/submitter.rb +98 -0
  42. data/lib/flipper/cloud/telemetry.rb +183 -0
  43. data/lib/flipper/configuration.rb +25 -4
  44. data/lib/flipper/dsl.rb +51 -0
  45. data/lib/flipper/engine.rb +28 -3
  46. data/lib/flipper/exporters/json/export.rb +1 -1
  47. data/lib/flipper/exporters/json/v1.rb +1 -1
  48. data/lib/flipper/expression/builder.rb +73 -0
  49. data/lib/flipper/expression/constant.rb +25 -0
  50. data/lib/flipper/expression.rb +71 -0
  51. data/lib/flipper/expressions/all.rb +11 -0
  52. data/lib/flipper/expressions/any.rb +9 -0
  53. data/lib/flipper/expressions/boolean.rb +9 -0
  54. data/lib/flipper/expressions/comparable.rb +13 -0
  55. data/lib/flipper/expressions/duration.rb +28 -0
  56. data/lib/flipper/expressions/equal.rb +9 -0
  57. data/lib/flipper/expressions/greater_than.rb +9 -0
  58. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  59. data/lib/flipper/expressions/less_than.rb +9 -0
  60. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  61. data/lib/flipper/expressions/not_equal.rb +9 -0
  62. data/lib/flipper/expressions/now.rb +9 -0
  63. data/lib/flipper/expressions/number.rb +9 -0
  64. data/lib/flipper/expressions/percentage.rb +9 -0
  65. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  66. data/lib/flipper/expressions/property.rb +9 -0
  67. data/lib/flipper/expressions/random.rb +9 -0
  68. data/lib/flipper/expressions/string.rb +9 -0
  69. data/lib/flipper/expressions/time.rb +9 -0
  70. data/lib/flipper/feature.rb +55 -0
  71. data/lib/flipper/gate.rb +1 -0
  72. data/lib/flipper/gate_values.rb +5 -2
  73. data/lib/flipper/gates/expression.rb +75 -0
  74. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  75. data/lib/flipper/middleware/memoizer.rb +29 -13
  76. data/lib/flipper/poller.rb +1 -1
  77. data/lib/flipper/serializers/gzip.rb +24 -0
  78. data/lib/flipper/serializers/json.rb +19 -0
  79. data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
  80. data/lib/flipper/test/shared_adapter_test.rb +24 -5
  81. data/lib/flipper/typecast.rb +34 -6
  82. data/lib/flipper/types/percentage.rb +1 -1
  83. data/lib/flipper/version.rb +1 -1
  84. data/lib/flipper.rb +38 -1
  85. data/spec/flipper/adapter_builder_spec.rb +73 -0
  86. data/spec/flipper/adapter_spec.rb +1 -0
  87. data/spec/flipper/adapters/http_spec.rb +39 -5
  88. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  89. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  90. data/spec/flipper/adapters/strict_spec.rb +62 -0
  91. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  92. data/spec/flipper/cloud/configuration_spec.rb +6 -23
  93. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  94. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  95. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  96. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  97. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  98. data/spec/flipper/cloud_spec.rb +12 -12
  99. data/spec/flipper/configuration_spec.rb +17 -0
  100. data/spec/flipper/dsl_spec.rb +39 -0
  101. data/spec/flipper/engine_spec.rb +108 -7
  102. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  103. data/spec/flipper/expression/builder_spec.rb +248 -0
  104. data/spec/flipper/expression_spec.rb +188 -0
  105. data/spec/flipper/expressions/all_spec.rb +15 -0
  106. data/spec/flipper/expressions/any_spec.rb +15 -0
  107. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  108. data/spec/flipper/expressions/duration_spec.rb +43 -0
  109. data/spec/flipper/expressions/equal_spec.rb +24 -0
  110. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  111. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  112. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  113. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  114. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  115. data/spec/flipper/expressions/now_spec.rb +11 -0
  116. data/spec/flipper/expressions/number_spec.rb +21 -0
  117. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  118. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  119. data/spec/flipper/expressions/property_spec.rb +13 -0
  120. data/spec/flipper/expressions/random_spec.rb +9 -0
  121. data/spec/flipper/expressions/string_spec.rb +11 -0
  122. data/spec/flipper/expressions/time_spec.rb +13 -0
  123. data/spec/flipper/feature_spec.rb +360 -1
  124. data/spec/flipper/gate_values_spec.rb +2 -2
  125. data/spec/flipper/gates/expression_spec.rb +108 -0
  126. data/spec/flipper/identifier_spec.rb +4 -5
  127. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
  128. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  129. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  130. data/spec/flipper/serializers/json_spec.rb +13 -0
  131. data/spec/flipper/typecast_spec.rb +43 -7
  132. data/spec/flipper/types/actor_spec.rb +18 -1
  133. data/spec/flipper_integration_spec.rb +102 -4
  134. data/spec/flipper_spec.rb +89 -1
  135. data/spec/spec_helper.rb +5 -0
  136. data/spec/support/actor_names.yml +1 -0
  137. data/spec/support/fake_backoff_policy.rb +15 -0
  138. data/spec/support/spec_helpers.rb +11 -3
  139. metadata +104 -18
  140. data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -6,7 +6,7 @@ RSpec.describe Flipper::Engine do
6
6
  Class.new(Rails::Application) do
7
7
  config.eager_load = false
8
8
  config.logger = ActiveSupport::Logger.new($stdout)
9
- end
9
+ end.instance
10
10
  end
11
11
 
12
12
  before do
@@ -15,11 +15,70 @@ RSpec.describe Flipper::Engine do
15
15
  ActiveSupport::Dependencies.autoload_once_paths = ActiveSupport::Dependencies.autoload_once_paths.dup
16
16
  end
17
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
+
18
28
  let(:config) { application.config.flipper }
19
29
 
20
30
  subject { application.initialize! }
21
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
+
22
79
  context 'cloudless' do
80
+ it_behaves_like 'config.strict'
81
+
23
82
  it 'can set env_key from ENV' do
24
83
  with_env 'FLIPPER_ENV_KEY' => 'flopper' do
25
84
  subject
@@ -94,12 +153,6 @@ RSpec.describe Flipper::Engine do
94
153
  if: nil
95
154
  })
96
155
  end
97
-
98
- it "defines #flipper_id on AR::Base" do
99
- subject
100
- require 'active_record'
101
- expect(ActiveRecord::Base.ancestors).to include(Flipper::Identifier)
102
- end
103
156
  end
104
157
 
105
158
  context 'with cloud' do
@@ -112,6 +165,14 @@ RSpec.describe Flipper::Engine do
112
165
  # App for Rack::Test
113
166
  let(:app) { application.routes }
114
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
+
115
176
  it "initializes cloud configuration" do
116
177
  stub_request(:get, /flippercloud\.io/).to_return(status: 200, body: "{}")
117
178
 
@@ -181,6 +242,46 @@ RSpec.describe Flipper::Engine do
181
242
  end
182
243
  end
183
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
+
184
285
  # Add app initializer in the same order as config/initializers/*
185
286
  def initializer(&block)
186
287
  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
@@ -0,0 +1,188 @@
1
+ require 'flipper/expression'
2
+
3
+ RSpec.describe Flipper::Expression do
4
+ describe "#build" do
5
+ it "can build Equal" do
6
+ expression = described_class.build({
7
+ "Equal" => [
8
+ "basic",
9
+ "basic",
10
+ ]
11
+ })
12
+
13
+ expect(expression).to be_instance_of(Flipper::Expression)
14
+ expect(expression.function).to be(Flipper::Expressions::Equal)
15
+ expect(expression.args).to eq([
16
+ Flipper.constant("basic"),
17
+ Flipper.constant("basic"),
18
+ ])
19
+ end
20
+
21
+ it "can build GreaterThanOrEqualTo" do
22
+ expression = described_class.build({
23
+ "GreaterThanOrEqualTo" => [
24
+ 2,
25
+ 1,
26
+ ]
27
+ })
28
+
29
+ expect(expression).to be_instance_of(Flipper::Expression)
30
+ expect(expression.function).to be(Flipper::Expressions::GreaterThanOrEqualTo)
31
+ expect(expression.args).to eq([
32
+ Flipper.constant(2),
33
+ Flipper.constant(1),
34
+ ])
35
+ end
36
+
37
+ it "can build GreaterThan" do
38
+ expression = described_class.build({
39
+ "GreaterThan" => [
40
+ 2,
41
+ 1,
42
+ ]
43
+ })
44
+
45
+ expect(expression).to be_instance_of(Flipper::Expression)
46
+ expect(expression.function).to be(Flipper::Expressions::GreaterThan)
47
+ expect(expression.args).to eq([
48
+ Flipper.constant(2),
49
+ Flipper.constant(1),
50
+ ])
51
+ end
52
+
53
+ it "can build LessThanOrEqualTo" do
54
+ expression = described_class.build({
55
+ "LessThanOrEqualTo" => [
56
+ 2,
57
+ 1,
58
+ ]
59
+ })
60
+
61
+ expect(expression).to be_instance_of(Flipper::Expression)
62
+ expect(expression.function).to be(Flipper::Expressions::LessThanOrEqualTo)
63
+ expect(expression.args).to eq([
64
+ Flipper.constant(2),
65
+ Flipper.constant(1),
66
+ ])
67
+ end
68
+
69
+ it "can build LessThan" do
70
+ expression = described_class.build({
71
+ "LessThan" => [2, 1]
72
+ })
73
+
74
+ expect(expression).to be_instance_of(Flipper::Expression)
75
+ expect(expression.function).to be(Flipper::Expressions::LessThan)
76
+ expect(expression.args).to eq([
77
+ Flipper.constant(2),
78
+ Flipper.constant(1),
79
+ ])
80
+ end
81
+
82
+ it "can build NotEqual" do
83
+ expression = described_class.build({
84
+ "NotEqual" => [
85
+ "basic",
86
+ "plus",
87
+ ]
88
+ })
89
+
90
+ expect(expression).to be_instance_of(Flipper::Expression)
91
+ expect(expression.function).to be(Flipper::Expressions::NotEqual)
92
+ expect(expression.args).to eq([
93
+ Flipper.constant("basic"),
94
+ Flipper.constant("plus"),
95
+ ])
96
+ end
97
+
98
+ it "can build Number" do
99
+ expression = described_class.build(1)
100
+
101
+ expect(expression).to be_instance_of(Flipper::Expression::Constant)
102
+ expect(expression.value).to eq(1)
103
+ end
104
+
105
+ it "can build Percentage" do
106
+ expression = described_class.build({
107
+ "Percentage" => [1]
108
+ })
109
+
110
+ expect(expression).to be_instance_of(Flipper::Expression)
111
+ expect(expression.function).to be(Flipper::Expressions::Percentage)
112
+ expect(expression.args).to eq([Flipper.constant(1)])
113
+ end
114
+
115
+ it "can build PercentageOfActors" do
116
+ expression = described_class.build({
117
+ "PercentageOfActors" => [
118
+ "User;1",
119
+ 40,
120
+ ]
121
+ })
122
+
123
+ expect(expression).to be_instance_of(Flipper::Expression)
124
+ expect(expression.function).to be(Flipper::Expressions::PercentageOfActors)
125
+ expect(expression.args).to eq([
126
+ Flipper.constant("User;1"),
127
+ Flipper.constant(40),
128
+ ])
129
+ end
130
+
131
+ it "can build String" do
132
+ expression = described_class.build("basic")
133
+
134
+ expect(expression).to be_instance_of(Flipper::Expression::Constant)
135
+ expect(expression.value).to eq("basic")
136
+ end
137
+
138
+ it "can build Property" do
139
+ expression = described_class.build({
140
+ "Property" => ["flipper_id"]
141
+ })
142
+
143
+ expect(expression).to be_instance_of(Flipper::Expression)
144
+ expect(expression.function).to be(Flipper::Expressions::Property)
145
+ expect(expression.args).to eq([Flipper.constant("flipper_id")])
146
+ end
147
+ end
148
+
149
+ describe "#eql?" do
150
+ it "returns true for same class and args" do
151
+ expression = described_class.build("foo")
152
+ other = described_class.build("foo")
153
+ expect(expression.eql?(other)).to be(true)
154
+ end
155
+
156
+ it "returns false for different class" do
157
+ expression = described_class.build("foo")
158
+ other = Object.new
159
+ expect(expression.eql?(other)).to be(false)
160
+ end
161
+
162
+ it "returns false for different args" do
163
+ expression = described_class.build("foo")
164
+ other = described_class.build("bar")
165
+ expect(expression.eql?(other)).to be(false)
166
+ end
167
+ end
168
+
169
+ describe "#==" do
170
+ it "returns true for same class and args" do
171
+ expression = described_class.build("foo")
172
+ other = described_class.build("foo")
173
+ expect(expression == other).to be(true)
174
+ end
175
+
176
+ it "returns false for different class" do
177
+ expression = described_class.build("foo")
178
+ other = Object.new
179
+ expect(expression == other).to be(false)
180
+ end
181
+
182
+ it "returns false for different args" do
183
+ expression = described_class.build("foo")
184
+ other = described_class.build("bar")
185
+ expect(expression == other).to be(false)
186
+ end
187
+ end
188
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.describe Flipper::Expressions::All do
2
+ describe "#call" do
3
+ it "returns true if all args evaluate as true" do
4
+ expect(described_class.call(true, true)).to be(true)
5
+ end
6
+
7
+ it "returns false if any args evaluate as false" do
8
+ expect(described_class.call(false, true)).to be(false)
9
+ end
10
+
11
+ it "returns true with empty args" do
12
+ expect(described_class.call).to be(true)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.describe Flipper::Expressions::Any do
2
+ describe "#call" do
3
+ it "returns true if any args evaluate as true" do
4
+ expect(described_class.call(true, false)).to be(true)
5
+ end
6
+
7
+ it "returns false if all args evaluate as false" do
8
+ expect(described_class.call(false, false)).to be(false)
9
+ end
10
+
11
+ it "returns false with empty args" do
12
+ expect(described_class.call).to be(false)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.describe Flipper::Expressions::Boolean do
2
+ describe "#call" do
3
+ [true, 'true', 1, '1'].each do |value|
4
+ it "returns a true for #{value.inspect}" do
5
+ expect(described_class.call(value)).to be(true)
6
+ end
7
+ end
8
+
9
+ [false, 'false', 0, '0', nil].each do |value|
10
+ it "returns a true for #{value.inspect}" do
11
+ expect(described_class.call(value)).to be(false)
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,43 @@
1
+ RSpec.describe Flipper::Expressions::Duration do
2
+ describe "#call" do
3
+ it "raises error with invalid value" do
4
+ expect { described_class.call(false, 'minute') }.to raise_error(ArgumentError)
5
+ end
6
+
7
+ it "raises error with invalid unit" do
8
+ expect { described_class.call(4, 'score') }.to raise_error(ArgumentError)
9
+ end
10
+
11
+ it 'defaults unit to seconds' do
12
+ expect(described_class.call(10)).to eq(10)
13
+ end
14
+
15
+ it "evaluates seconds" do
16
+ expect(described_class.call(10, 'seconds')).to eq(10)
17
+ end
18
+
19
+ it "evaluates minutes" do
20
+ expect(described_class.call(2, 'minutes')).to eq(120)
21
+ end
22
+
23
+ it "evaluates hours" do
24
+ expect(described_class.call(2, 'hours')).to eq(7200)
25
+ end
26
+
27
+ it "evaluates days" do
28
+ expect(described_class.call(2, 'days')).to eq(172_800)
29
+ end
30
+
31
+ it "evaluates weeks" do
32
+ expect(described_class.call(2, 'weeks')).to eq(1_209_600)
33
+ end
34
+
35
+ it "evaluates months" do
36
+ expect(described_class.call(2, 'months')).to eq(5_259_492)
37
+ end
38
+
39
+ it "evaluates years" do
40
+ expect(described_class.call(2, 'years')).to eq(63_113_904)
41
+ end
42
+ end
43
+ end