flipper 1.0.0 → 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (142) 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 +326 -272
  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 +27 -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/model/active_record.rb +23 -0
  77. data/lib/flipper/poller.rb +1 -1
  78. data/lib/flipper/serializers/gzip.rb +24 -0
  79. data/lib/flipper/serializers/json.rb +19 -0
  80. data/lib/flipper/spec/shared_adapter_specs.rb +29 -11
  81. data/lib/flipper/test/shared_adapter_test.rb +24 -5
  82. data/lib/flipper/typecast.rb +34 -6
  83. data/lib/flipper/types/percentage.rb +1 -1
  84. data/lib/flipper/version.rb +1 -1
  85. data/lib/flipper.rb +38 -1
  86. data/spec/flipper/adapter_builder_spec.rb +73 -0
  87. data/spec/flipper/adapter_spec.rb +1 -0
  88. data/spec/flipper/adapters/http_spec.rb +39 -5
  89. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  90. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  91. data/spec/flipper/adapters/strict_spec.rb +62 -0
  92. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  93. data/spec/flipper/cloud/configuration_spec.rb +6 -23
  94. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +108 -0
  95. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  96. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  97. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  98. data/spec/flipper/cloud/telemetry_spec.rb +156 -0
  99. data/spec/flipper/cloud_spec.rb +12 -12
  100. data/spec/flipper/configuration_spec.rb +17 -0
  101. data/spec/flipper/dsl_spec.rb +39 -0
  102. data/spec/flipper/engine_spec.rb +108 -7
  103. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  104. data/spec/flipper/expression/builder_spec.rb +248 -0
  105. data/spec/flipper/expression_spec.rb +188 -0
  106. data/spec/flipper/expressions/all_spec.rb +15 -0
  107. data/spec/flipper/expressions/any_spec.rb +15 -0
  108. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  109. data/spec/flipper/expressions/duration_spec.rb +43 -0
  110. data/spec/flipper/expressions/equal_spec.rb +24 -0
  111. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  112. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  113. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  114. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  115. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  116. data/spec/flipper/expressions/now_spec.rb +11 -0
  117. data/spec/flipper/expressions/number_spec.rb +21 -0
  118. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  119. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  120. data/spec/flipper/expressions/property_spec.rb +13 -0
  121. data/spec/flipper/expressions/random_spec.rb +9 -0
  122. data/spec/flipper/expressions/string_spec.rb +11 -0
  123. data/spec/flipper/expressions/time_spec.rb +13 -0
  124. data/spec/flipper/feature_spec.rb +360 -1
  125. data/spec/flipper/gate_values_spec.rb +2 -2
  126. data/spec/flipper/gates/expression_spec.rb +108 -0
  127. data/spec/flipper/identifier_spec.rb +4 -5
  128. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +15 -1
  129. data/spec/flipper/middleware/memoizer_spec.rb +67 -0
  130. data/spec/flipper/model/active_record_spec.rb +61 -0
  131. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  132. data/spec/flipper/serializers/json_spec.rb +13 -0
  133. data/spec/flipper/typecast_spec.rb +43 -7
  134. data/spec/flipper/types/actor_spec.rb +18 -1
  135. data/spec/flipper_integration_spec.rb +102 -4
  136. data/spec/flipper_spec.rb +89 -1
  137. data/spec/spec_helper.rb +5 -0
  138. data/spec/support/actor_names.yml +1 -0
  139. data/spec/support/fake_backoff_policy.rb +15 -0
  140. data/spec/support/spec_helpers.rb +11 -3
  141. metadata +107 -18
  142. data/lib/flipper/cloud/instrumenter.rb +0 -48
@@ -0,0 +1,24 @@
1
+ RSpec.describe Flipper::Expressions::Equal do
2
+ describe "#call" do
3
+ it "returns true when equal" do
4
+ expect(described_class.call("basic", "basic")).to be(true)
5
+ end
6
+
7
+ it "returns false when not equal" do
8
+ expect(described_class.call("basic", "plus")).to be(false)
9
+ end
10
+
11
+ it "returns false when value evaluates to nil" do
12
+ expect(described_class.call(nil, 1)).to be(false)
13
+ expect(described_class.call(1, nil)).to be(false)
14
+ end
15
+
16
+ it "raises ArgumentError with no arguments" do
17
+ expect { described_class.call }.to raise_error(ArgumentError)
18
+ end
19
+
20
+ it "raises ArgumentError with one argument" do
21
+ expect { described_class.call(10) }.to raise_error(ArgumentError)
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,28 @@
1
+ RSpec.describe Flipper::Expressions::GreaterThanOrEqualTo do
2
+ describe "#call" do
3
+ it "returns true when equal" do
4
+ expect(described_class.call(2, 2)).to be(true)
5
+ end
6
+
7
+ it "returns true when greater" do
8
+ expect(described_class.call(2, 1)).to be(true)
9
+ end
10
+
11
+ it "returns false when less" do
12
+ expect(described_class.call(1, 2)).to be(false)
13
+ end
14
+
15
+ it "returns false when value evaluates to nil" do
16
+ expect(described_class.call(nil, 1)).to be(false)
17
+ expect(described_class.call(1, nil)).to be(false)
18
+ end
19
+
20
+ it "raises ArgumentError with no arguments" do
21
+ expect { described_class.call }.to raise_error(ArgumentError)
22
+ end
23
+
24
+ it "raises ArgumentError with one argument" do
25
+ expect { described_class.call(10) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ RSpec.describe Flipper::Expressions::GreaterThan do
2
+ describe "#call" do
3
+ it "returns false when equal" do
4
+ expect(described_class.call(2, 2)).to be(false)
5
+ end
6
+
7
+ it "returns true when greater" do
8
+ expect(described_class.call(2, 1)).to be(true)
9
+ end
10
+
11
+ it "returns false when less" do
12
+ expect(described_class.call(1, 2)).to be(false)
13
+ end
14
+
15
+ it "returns false when value evaluates to nil" do
16
+ expect(described_class.call(nil, 1)).to be(false)
17
+ expect(described_class.call(1, nil)).to be(false)
18
+ end
19
+
20
+ it "raises ArgumentError with no arguments" do
21
+ expect { described_class.call }.to raise_error(ArgumentError)
22
+ end
23
+
24
+ it "raises ArgumentError with one argument" do
25
+ expect { described_class.call(10) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,28 @@
1
+ RSpec.describe Flipper::Expressions::LessThanOrEqualTo do
2
+ describe "#call" do
3
+ it "returns true when equal" do
4
+ expect(described_class.call(2, 2)).to be(true)
5
+ end
6
+
7
+ it "returns true when less" do
8
+ expect(described_class.call(1, 2)).to be(true)
9
+ end
10
+
11
+ it "returns false when greater" do
12
+ expect(described_class.call(2, 1)).to be(false)
13
+ end
14
+
15
+ it "returns false when value evaluates to nil" do
16
+ expect(described_class.call(nil, 1)).to be(false)
17
+ expect(described_class.call(1, nil)).to be(false)
18
+ end
19
+
20
+ it "raises ArgumentError with no arguments" do
21
+ expect { described_class.call }.to raise_error(ArgumentError)
22
+ end
23
+
24
+ it "raises ArgumentError with one argument" do
25
+ expect { described_class.call(10) }.to raise_error(ArgumentError)
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,32 @@
1
+ RSpec.describe Flipper::Expressions::LessThan do
2
+ describe "#call" do
3
+ it "returns false when equal" do
4
+ expect(described_class.call(2, 2)).to be(false)
5
+ end
6
+
7
+ it "returns true when less" do
8
+ expect(described_class.call(1, 2)).to be(true)
9
+ end
10
+
11
+ it "returns true when less with args that need evaluation" do
12
+ expect(described_class.call(1, 2)).to be(true)
13
+ end
14
+
15
+ it "returns false when greater" do
16
+ expect(described_class.call(2, 1)).to be(false)
17
+ end
18
+
19
+ it "returns false when value evaluates to nil" do
20
+ expect(described_class.call(nil, 1)).to be(false)
21
+ expect(described_class.call(1, nil)).to be(false)
22
+ end
23
+
24
+ it "raises ArgumentError with no arguments" do
25
+ expect { described_class.call }.to raise_error(ArgumentError)
26
+ end
27
+
28
+ it "raises ArgumentError with one argument" do
29
+ expect { described_class.call(10) }.to raise_error(ArgumentError)
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.describe Flipper::Expressions::NotEqual do
2
+ describe "#call" do
3
+ it "returns true when not equal" do
4
+ expect(described_class.call("basic", "plus")).to be(true)
5
+ end
6
+
7
+ it "returns false when equal" do
8
+ expect(described_class.call("basic", "basic")).to be(false)
9
+ end
10
+
11
+ it "raises ArgumentError for more arguments" do
12
+ expect { described_class.call(20, 10, 20).evaluate }.to raise_error(ArgumentError)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,11 @@
1
+ RSpec.describe Flipper::Expressions::Now do
2
+ describe "#call" do
3
+ it "returns current time" do
4
+ expect(described_class.call).to be_within(2).of(Time.now.utc)
5
+ end
6
+
7
+ it "defaults to UTC" do
8
+ expect(described_class.call.zone).to eq("UTC")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,21 @@
1
+ RSpec.describe Flipper::Expressions::Number do
2
+ describe "#call" do
3
+ it "returns Integer for Integer" do
4
+ expect(described_class.call(10)).to be(10)
5
+ end
6
+
7
+ it "returns Float for Float" do
8
+ expect(described_class.call(10.1)).to be(10.1)
9
+ expect(described_class.call(10.0)).to be(10.0)
10
+ end
11
+
12
+ it "returns Integer for String" do
13
+ expect(described_class.call('10')).to be(10)
14
+ end
15
+
16
+ it "returns Float for String" do
17
+ expect(described_class.call('10.0')).to be(10.0)
18
+ expect(described_class.call('10.1')).to be(10.1)
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,20 @@
1
+ RSpec.describe Flipper::Expressions::PercentageOfActors do
2
+ describe "#call" do
3
+ it "returns true when string in percentage enabled" do
4
+ expect(described_class.call("User;1", 42)).to be(true)
5
+ end
6
+
7
+ it "returns true when string in fractional percentage enabled" do
8
+ expect(described_class.call("User;1", 41.687)).to be(true)
9
+ end
10
+
11
+ it "returns false when string in percentage enabled" do
12
+ expect(described_class.call("User;1", 0)).to be(false)
13
+ end
14
+
15
+ it "changes value based on feature_name so not all actors get all features first" do
16
+ expect(described_class.call("User;1", 70, context: {feature_name: "a"})).to be(true)
17
+ expect(described_class.call("User;1", 70, context: {feature_name: "b"})).to be(false)
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,15 @@
1
+ RSpec.describe Flipper::Expressions::Percentage do
2
+ describe "#call" do
3
+ it "returns numeric" do
4
+ expect(described_class.call(10)).to be(10.0)
5
+ end
6
+
7
+ it "returns 0 if less than 0" do
8
+ expect(described_class.call(-1)).to be(0)
9
+ end
10
+
11
+ it "returns 100 if greater than 100" do
12
+ expect(described_class.call(101)).to be(100)
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,13 @@
1
+ RSpec.describe Flipper::Expressions::Property do
2
+ describe "#call" do
3
+ it "returns value for property key" do
4
+ context = { properties: { "flipper_id" => "User;1" } }
5
+ expect(described_class.call("flipper_id", context: context)).to eq("User;1")
6
+ end
7
+
8
+ it "returns nil if key not found in properties" do
9
+ context = { properties: { } }
10
+ expect(described_class.call("flipper_id", context: context)).to be(nil)
11
+ end
12
+ end
13
+ end
@@ -0,0 +1,9 @@
1
+ RSpec.describe Flipper::Expressions::Random do
2
+ describe "#call" do
3
+ it "returns random number based on max" do
4
+ 100.times do
5
+ expect(described_class.call(10)).to be_between(0, 10)
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,11 @@
1
+ RSpec.describe Flipper::Expressions::String do
2
+ describe "#call" do
3
+ it "returns String for Numeric" do
4
+ expect(described_class.call(10)).to eq("10")
5
+ end
6
+
7
+ it "returns String" do
8
+ expect(described_class.call("test")).to eq("test")
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,13 @@
1
+ RSpec.describe Flipper::Expressions::Time do
2
+ let(:time) { Time.now.round }
3
+
4
+ describe "#call" do
5
+ it "returns time for #to_s format" do
6
+ expect(described_class.call(time.to_s)).to eq(time)
7
+ end
8
+
9
+ it "returns time for #iso8601 format" do
10
+ expect(described_class.call(time.iso8601)).to eq(time)
11
+ end
12
+ end
13
+ end
@@ -109,7 +109,7 @@ RSpec.describe Flipper::Feature do
109
109
  instance.gates.each do |gate|
110
110
  expect(gate).to be_a(Flipper::Gate)
111
111
  end
112
- expect(instance.gates.size).to be(5)
112
+ expect(instance.gates.size).to be(6)
113
113
  end
114
114
  end
115
115
 
@@ -693,6 +693,363 @@ RSpec.describe Flipper::Feature do
693
693
  end
694
694
  end
695
695
 
696
+ describe '#expression' do
697
+ it "returns nil if feature has no expression" do
698
+ expect(subject.expression).to be(nil)
699
+ end
700
+
701
+ it "returns expression if feature has expression" do
702
+ expression = Flipper.property(:plan).eq("basic")
703
+ subject.enable_expression expression
704
+ expect(subject.expression).to eq(expression)
705
+ end
706
+ end
707
+
708
+ describe '#enable_expression/disable_expression' do
709
+ context "with expression instance" do
710
+ it "updates gate values to equal expression or clears expression" do
711
+ expression = Flipper.property(:plan).eq("basic")
712
+ other_expression = Flipper.property(:age).gte(21)
713
+ expect(subject.gate_values.expression).to be(nil)
714
+ subject.enable_expression(expression)
715
+ expect(subject.gate_values.expression).to eq(expression.value)
716
+ subject.disable_expression
717
+ expect(subject.gate_values.expression).to be(nil)
718
+ end
719
+ end
720
+
721
+ context "with Hash" do
722
+ it "updates gate values to equal expression or clears expression" do
723
+ expression = Flipper.property(:plan).eq("basic")
724
+ other_expression = Flipper.property(:age).gte(21)
725
+ expect(subject.gate_values.expression).to be(nil)
726
+ subject.enable_expression(expression.value)
727
+ expect(subject.gate_values.expression).to eq(expression.value)
728
+ subject.disable_expression
729
+ expect(subject.gate_values.expression).to be(nil)
730
+ end
731
+ end
732
+ end
733
+
734
+ describe "#add_expression" do
735
+ context "when nothing enabled" do
736
+ context "with Expression instance" do
737
+ it "sets expression to Expression" do
738
+ expression = Flipper.property(:plan).eq("basic")
739
+ subject.add_expression(expression)
740
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
741
+ expect(subject.expression).to eq(expression)
742
+ end
743
+ end
744
+
745
+ context "with Any instance" do
746
+ it "sets expression to Any" do
747
+ expression = Flipper.any(Flipper.property(:plan).eq("basic"))
748
+ subject.add_expression(expression)
749
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
750
+ expect(subject.expression).to eq(expression)
751
+ end
752
+ end
753
+
754
+ context "with All instance" do
755
+ it "sets expression to All" do
756
+ expression = Flipper.all(Flipper.property(:plan).eq("basic"))
757
+ subject.add_expression(expression)
758
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
759
+ expect(subject.expression).to eq(expression)
760
+ end
761
+ end
762
+ end
763
+
764
+ context "when Expression enabled" do
765
+ let(:expression) { Flipper.property(:plan).eq("basic") }
766
+
767
+ before do
768
+ subject.enable_expression expression
769
+ end
770
+
771
+ context "with Expression instance" do
772
+ it "changes expression to Any and adds new Expression" do
773
+ new_expression = Flipper.property(:age).gte(21)
774
+ subject.add_expression(new_expression)
775
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
776
+ expect(subject.expression.args).to include(expression)
777
+ expect(subject.expression.args).to include(new_expression)
778
+ end
779
+ end
780
+
781
+ context "with Any instance" do
782
+ it "changes expression to Any and adds new Any" do
783
+ new_expression = Flipper.any(Flipper.property(:age).eq(21))
784
+ subject.add_expression new_expression
785
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
786
+ expect(subject.expression.args).to include(expression)
787
+ expect(subject.expression.args).to include(new_expression)
788
+ end
789
+ end
790
+
791
+ context "with All instance" do
792
+ it "changes expression to Any and adds new All" do
793
+ new_expression = Flipper.all(Flipper.property(:plan).eq("basic"))
794
+ subject.add_expression new_expression
795
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
796
+ expect(subject.expression.args).to include(expression)
797
+ expect(subject.expression.args).to include(new_expression)
798
+ end
799
+ end
800
+ end
801
+
802
+ context "when Any enabled" do
803
+ let(:condition) { Flipper.property(:plan).eq("basic") }
804
+ let(:expression) { Flipper.any(condition) }
805
+
806
+ before do
807
+ subject.enable_expression expression
808
+ end
809
+
810
+ context "with Expression instance" do
811
+ it "adds Expression to Any" do
812
+ new_expression = Flipper.property(:age).gte(21)
813
+ subject.add_expression(new_expression)
814
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
815
+ expect(subject.expression.args).to include(condition)
816
+ expect(subject.expression.args).to include(new_expression)
817
+ end
818
+ end
819
+
820
+ context "with Any instance" do
821
+ it "adds Any to Any" do
822
+ new_expression = Flipper.any(Flipper.property(:age).gte(21))
823
+ subject.add_expression(new_expression)
824
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
825
+ expect(subject.expression.args).to include(condition)
826
+ expect(subject.expression.args).to include(new_expression)
827
+ end
828
+ end
829
+
830
+ context "with All instance" do
831
+ it "adds All to Any" do
832
+ new_expression = Flipper.all(Flipper.property(:age).gte(21))
833
+ subject.add_expression(new_expression)
834
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
835
+ expect(subject.expression.args).to include(condition)
836
+ expect(subject.expression.args).to include(new_expression)
837
+ end
838
+ end
839
+ end
840
+
841
+ context "when All enabled" do
842
+ let(:condition) { Flipper.property(:plan).eq("basic") }
843
+ let(:expression) { Flipper.all(condition) }
844
+
845
+ before do
846
+ subject.enable_expression expression
847
+ end
848
+
849
+ context "with Expression instance" do
850
+ it "adds Expression to All" do
851
+ new_expression = Flipper.property(:age).gte(21)
852
+ subject.add_expression(new_expression)
853
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
854
+ expect(subject.expression.args).to include(condition)
855
+ expect(subject.expression.args).to include(new_expression)
856
+ end
857
+ end
858
+
859
+ context "with Any instance" do
860
+ it "adds Any to All" do
861
+ new_expression = Flipper.any(Flipper.property(:age).gte(21))
862
+ subject.add_expression(new_expression)
863
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
864
+ expect(subject.expression.args).to include(condition)
865
+ expect(subject.expression.args).to include(new_expression)
866
+ end
867
+ end
868
+
869
+ context "with All instance" do
870
+ it "adds All to All" do
871
+ new_expression = Flipper.all(Flipper.property(:age).gte(21))
872
+ subject.add_expression(new_expression)
873
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
874
+ expect(subject.expression.args).to include(condition)
875
+ expect(subject.expression.args).to include(new_expression)
876
+ end
877
+ end
878
+ end
879
+ end
880
+
881
+ describe '#remove_expression' do
882
+ context "when nothing enabled" do
883
+ context "with Expression instance" do
884
+ it "does nothing" do
885
+ expression = Flipper.property(:plan).eq("basic")
886
+ subject.remove_expression(expression)
887
+ expect(subject.expression).to be(nil)
888
+ end
889
+ end
890
+
891
+ context "with Any instance" do
892
+ it "does nothing" do
893
+ expression = Flipper.any(Flipper.property(:plan).eq("basic"))
894
+ subject.remove_expression expression
895
+ expect(subject.expression).to be(nil)
896
+ end
897
+ end
898
+
899
+ context "with All instance" do
900
+ it "does nothing" do
901
+ expression = Flipper.all(Flipper.property(:plan).eq("basic"))
902
+ subject.remove_expression expression
903
+ expect(subject.expression).to be(nil)
904
+ end
905
+ end
906
+ end
907
+
908
+ context "when Expression enabled" do
909
+ let(:expression) { Flipper.property(:plan).eq("basic") }
910
+
911
+ before do
912
+ subject.enable_expression expression
913
+ end
914
+
915
+ context "with Expression instance" do
916
+ it "changes expression to Any and removes Expression if it matches" do
917
+ new_expression = Flipper.property(:plan).eq("basic")
918
+ subject.remove_expression new_expression
919
+ expect(subject.expression).to eq(Flipper.any)
920
+ end
921
+
922
+ it "changes expression to Any if Expression doesn't match" do
923
+ new_expression = Flipper.property(:plan).eq("premium")
924
+ subject.remove_expression new_expression
925
+ expect(subject.expression).to eq(Flipper.any(expression))
926
+ end
927
+ end
928
+
929
+ context "with Any instance" do
930
+ it "changes expression to Any and does nothing" do
931
+ new_expression = Flipper.any(Flipper.property(:plan).eq("basic"))
932
+ subject.remove_expression new_expression
933
+ expect(subject.expression).to eq(Flipper.any(expression))
934
+ end
935
+ end
936
+
937
+ context "with All instance" do
938
+ it "changes expression to Any and does nothing" do
939
+ new_expression = Flipper.all(Flipper.property(:plan).eq("basic"))
940
+ subject.remove_expression new_expression
941
+ expect(subject.expression).to eq(Flipper.any(expression))
942
+ end
943
+ end
944
+ end
945
+
946
+ context "when Any enabled" do
947
+ let(:condition) { Flipper.property(:plan).eq("basic") }
948
+ let(:expression) { Flipper.any condition }
949
+
950
+ before do
951
+ subject.enable_expression expression
952
+ end
953
+
954
+ context "with Expression instance" do
955
+ it "removes Expression if it matches" do
956
+ subject.remove_expression condition
957
+ expect(subject.expression).to eq(Flipper.any)
958
+ end
959
+
960
+ it "does nothing if Expression does not match" do
961
+ subject.remove_expression Flipper.property(:plan).eq("premium")
962
+ expect(subject.expression).to eq(expression)
963
+ end
964
+ end
965
+
966
+ context "with Any instance" do
967
+ it "removes Any if it matches" do
968
+ new_expression = Flipper.any(Flipper.property(:plan).eq("premium"))
969
+ subject.add_expression new_expression
970
+ expect(subject.expression.args.size).to be(2)
971
+ subject.remove_expression new_expression
972
+ expect(subject.expression).to eq(expression)
973
+ end
974
+
975
+ it "does nothing if Any does not match" do
976
+ new_expression = Flipper.any(Flipper.property(:plan).eq("premium"))
977
+ subject.remove_expression new_expression
978
+ expect(subject.expression).to eq(expression)
979
+ end
980
+ end
981
+
982
+ context "with All instance" do
983
+ it "removes All if it matches" do
984
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
985
+ subject.add_expression new_expression
986
+ expect(subject.expression.args.size).to be(2)
987
+ subject.remove_expression new_expression
988
+ expect(subject.expression).to eq(expression)
989
+ end
990
+
991
+ it "does nothing if All does not match" do
992
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
993
+ subject.remove_expression new_expression
994
+ expect(subject.expression).to eq(expression)
995
+ end
996
+ end
997
+ end
998
+
999
+ context "when All enabled" do
1000
+ let(:condition) { Flipper.property(:plan).eq("basic") }
1001
+ let(:expression) { Flipper.all condition }
1002
+
1003
+ before do
1004
+ subject.enable_expression expression
1005
+ end
1006
+
1007
+ context "with Expression instance" do
1008
+ it "removes Expression if it matches" do
1009
+ subject.remove_expression condition
1010
+ expect(subject.expression).to eq(Flipper.all)
1011
+ end
1012
+
1013
+ it "does nothing if Expression does not match" do
1014
+ subject.remove_expression Flipper.property(:plan).eq("premium")
1015
+ expect(subject.expression).to eq(expression)
1016
+ end
1017
+ end
1018
+
1019
+ context "with Any instance" do
1020
+ it "removes Any if it matches" do
1021
+ new_expression = Flipper.any(Flipper.property(:plan).eq("premium"))
1022
+ subject.add_expression new_expression
1023
+ expect(subject.expression.args.size).to be(2)
1024
+ subject.remove_expression new_expression
1025
+ expect(subject.expression).to eq(expression)
1026
+ end
1027
+
1028
+ it "does nothing if Any does not match" do
1029
+ new_expression = Flipper.any(Flipper.property(:plan).eq("premium"))
1030
+ subject.remove_expression new_expression
1031
+ expect(subject.expression).to eq(expression)
1032
+ end
1033
+ end
1034
+
1035
+ context "with All instance" do
1036
+ it "removes All if it matches" do
1037
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
1038
+ subject.add_expression new_expression
1039
+ expect(subject.expression.args.size).to be(2)
1040
+ subject.remove_expression new_expression
1041
+ expect(subject.expression).to eq(expression)
1042
+ end
1043
+
1044
+ it "does nothing if All does not match" do
1045
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
1046
+ subject.remove_expression new_expression
1047
+ expect(subject.expression).to eq(expression)
1048
+ end
1049
+ end
1050
+ end
1051
+ end
1052
+
696
1053
  describe '#enable_actor/disable_actor' do
697
1054
  context 'with object that responds to flipper_id' do
698
1055
  it 'updates the gate values to include the actor' do
@@ -845,12 +1202,14 @@ RSpec.describe Flipper::Feature do
845
1202
  :actor,
846
1203
  :boolean,
847
1204
  :group,
1205
+ :expression,
848
1206
  ])
849
1207
 
850
1208
  expect(subject.disabled_gate_names.to_set).to eq(Set[
851
1209
  :actor,
852
1210
  :boolean,
853
1211
  :group,
1212
+ :expression,
854
1213
  ])
855
1214
  end
856
1215
  end