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
@@ -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