flipper 1.0.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 (180) 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 +50 -8
  5. data/CLAUDE.md +74 -0
  6. data/Changelog.md +1 -584
  7. data/Gemfile +15 -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/backoff_policy.rb +13 -0
  14. data/examples/cloud/cloud_setup.rb +16 -0
  15. data/examples/cloud/forked.rb +7 -2
  16. data/examples/cloud/threaded.rb +15 -18
  17. data/examples/expressions.rb +213 -0
  18. data/examples/strict.rb +18 -0
  19. data/exe/flipper +5 -0
  20. data/flipper.gemspec +6 -3
  21. data/lib/flipper/actor.rb +6 -3
  22. data/lib/flipper/adapter.rb +10 -0
  23. data/lib/flipper/adapter_builder.rb +44 -0
  24. data/lib/flipper/adapters/actor_limit.rb +28 -0
  25. data/lib/flipper/adapters/cache_base.rb +143 -0
  26. data/lib/flipper/adapters/dual_write.rb +1 -3
  27. data/lib/flipper/adapters/failover.rb +0 -4
  28. data/lib/flipper/adapters/failsafe.rb +0 -4
  29. data/lib/flipper/adapters/http/client.rb +40 -12
  30. data/lib/flipper/adapters/http/error.rb +2 -2
  31. data/lib/flipper/adapters/http.rb +19 -14
  32. data/lib/flipper/adapters/instrumented.rb +0 -4
  33. data/lib/flipper/adapters/memoizable.rb +14 -19
  34. data/lib/flipper/adapters/memory.rb +4 -6
  35. data/lib/flipper/adapters/operation_logger.rb +18 -92
  36. data/lib/flipper/adapters/poll.rb +16 -3
  37. data/lib/flipper/adapters/pstore.rb +17 -11
  38. data/lib/flipper/adapters/read_only.rb +8 -41
  39. data/lib/flipper/adapters/strict.rb +45 -0
  40. data/lib/flipper/adapters/sync/feature_synchronizer.rb +10 -1
  41. data/lib/flipper/adapters/sync.rb +0 -4
  42. data/lib/flipper/adapters/wrapper.rb +54 -0
  43. data/lib/flipper/cli.rb +263 -0
  44. data/lib/flipper/cloud/configuration.rb +131 -54
  45. data/lib/flipper/cloud/middleware.rb +5 -5
  46. data/lib/flipper/cloud/telemetry/backoff_policy.rb +96 -0
  47. data/lib/flipper/cloud/telemetry/instrumenter.rb +22 -0
  48. data/lib/flipper/cloud/telemetry/metric.rb +39 -0
  49. data/lib/flipper/cloud/telemetry/metric_storage.rb +30 -0
  50. data/lib/flipper/cloud/telemetry/submitter.rb +100 -0
  51. data/lib/flipper/cloud/telemetry.rb +191 -0
  52. data/lib/flipper/cloud.rb +1 -1
  53. data/lib/flipper/configuration.rb +25 -4
  54. data/lib/flipper/dsl.rb +51 -0
  55. data/lib/flipper/engine.rb +42 -3
  56. data/lib/flipper/export.rb +0 -2
  57. data/lib/flipper/exporters/json/export.rb +1 -1
  58. data/lib/flipper/exporters/json/v1.rb +1 -1
  59. data/lib/flipper/expression/builder.rb +73 -0
  60. data/lib/flipper/expression/constant.rb +25 -0
  61. data/lib/flipper/expression.rb +71 -0
  62. data/lib/flipper/expressions/all.rb +9 -0
  63. data/lib/flipper/expressions/any.rb +9 -0
  64. data/lib/flipper/expressions/boolean.rb +9 -0
  65. data/lib/flipper/expressions/comparable.rb +13 -0
  66. data/lib/flipper/expressions/duration.rb +28 -0
  67. data/lib/flipper/expressions/equal.rb +9 -0
  68. data/lib/flipper/expressions/greater_than.rb +9 -0
  69. data/lib/flipper/expressions/greater_than_or_equal_to.rb +9 -0
  70. data/lib/flipper/expressions/less_than.rb +9 -0
  71. data/lib/flipper/expressions/less_than_or_equal_to.rb +9 -0
  72. data/lib/flipper/expressions/not_equal.rb +9 -0
  73. data/lib/flipper/expressions/now.rb +9 -0
  74. data/lib/flipper/expressions/number.rb +9 -0
  75. data/lib/flipper/expressions/percentage.rb +9 -0
  76. data/lib/flipper/expressions/percentage_of_actors.rb +12 -0
  77. data/lib/flipper/expressions/property.rb +9 -0
  78. data/lib/flipper/expressions/random.rb +9 -0
  79. data/lib/flipper/expressions/string.rb +9 -0
  80. data/lib/flipper/expressions/time.rb +9 -0
  81. data/lib/flipper/feature.rb +63 -1
  82. data/lib/flipper/gate.rb +2 -1
  83. data/lib/flipper/gate_values.rb +5 -2
  84. data/lib/flipper/gates/expression.rb +75 -0
  85. data/lib/flipper/instrumentation/log_subscriber.rb +13 -5
  86. data/lib/flipper/instrumentation/statsd.rb +4 -2
  87. data/lib/flipper/instrumentation/statsd_subscriber.rb +2 -4
  88. data/lib/flipper/instrumentation/subscriber.rb +0 -4
  89. data/lib/flipper/metadata.rb +4 -1
  90. data/lib/flipper/middleware/memoizer.rb +29 -13
  91. data/lib/flipper/model/active_record.rb +23 -0
  92. data/lib/flipper/poller.rb +9 -8
  93. data/lib/flipper/serializers/gzip.rb +22 -0
  94. data/lib/flipper/serializers/json.rb +17 -0
  95. data/lib/flipper/spec/shared_adapter_specs.rb +46 -27
  96. data/lib/flipper/test/shared_adapter_test.rb +41 -22
  97. data/lib/flipper/test_help.rb +43 -0
  98. data/lib/flipper/typecast.rb +37 -9
  99. data/lib/flipper/types/percentage.rb +1 -1
  100. data/lib/flipper/version.rb +11 -1
  101. data/lib/flipper.rb +41 -2
  102. data/lib/generators/flipper/setup_generator.rb +68 -0
  103. data/lib/generators/flipper/templates/initializer.rb +45 -0
  104. data/lib/generators/flipper/templates/update/migrations/01_create_flipper_tables.rb.erb +22 -0
  105. data/lib/generators/flipper/templates/update/migrations/02_change_flipper_gates_value_to_text.rb.erb +18 -0
  106. data/lib/generators/flipper/update_generator.rb +35 -0
  107. data/package-lock.json +41 -0
  108. data/package.json +10 -0
  109. data/spec/fixtures/environment.rb +1 -0
  110. data/spec/flipper/adapter_builder_spec.rb +72 -0
  111. data/spec/flipper/adapter_spec.rb +1 -0
  112. data/spec/flipper/adapters/actor_limit_spec.rb +20 -0
  113. data/spec/flipper/adapters/http/client_spec.rb +61 -0
  114. data/spec/flipper/adapters/http_spec.rb +135 -74
  115. data/spec/flipper/adapters/memoizable_spec.rb +15 -15
  116. data/spec/flipper/adapters/poll_spec.rb +41 -0
  117. data/spec/flipper/adapters/read_only_spec.rb +26 -11
  118. data/spec/flipper/adapters/strict_spec.rb +64 -0
  119. data/spec/flipper/adapters/sync/feature_synchronizer_spec.rb +27 -0
  120. data/spec/flipper/cli_spec.rb +166 -0
  121. data/spec/flipper/cloud/configuration_spec.rb +39 -57
  122. data/spec/flipper/cloud/dsl_spec.rb +6 -6
  123. data/spec/flipper/cloud/middleware_spec.rb +8 -8
  124. data/spec/flipper/cloud/telemetry/backoff_policy_spec.rb +107 -0
  125. data/spec/flipper/cloud/telemetry/metric_spec.rb +87 -0
  126. data/spec/flipper/cloud/telemetry/metric_storage_spec.rb +58 -0
  127. data/spec/flipper/cloud/telemetry/submitter_spec.rb +145 -0
  128. data/spec/flipper/cloud/telemetry_spec.rb +208 -0
  129. data/spec/flipper/cloud_spec.rb +31 -25
  130. data/spec/flipper/configuration_spec.rb +17 -0
  131. data/spec/flipper/dsl_spec.rb +39 -3
  132. data/spec/flipper/engine_spec.rb +226 -42
  133. data/spec/flipper/exporters/json/v1_spec.rb +3 -3
  134. data/spec/flipper/expression/builder_spec.rb +248 -0
  135. data/spec/flipper/expression_spec.rb +188 -0
  136. data/spec/flipper/expressions/all_spec.rb +15 -0
  137. data/spec/flipper/expressions/any_spec.rb +15 -0
  138. data/spec/flipper/expressions/boolean_spec.rb +15 -0
  139. data/spec/flipper/expressions/duration_spec.rb +43 -0
  140. data/spec/flipper/expressions/equal_spec.rb +24 -0
  141. data/spec/flipper/expressions/greater_than_or_equal_to_spec.rb +28 -0
  142. data/spec/flipper/expressions/greater_than_spec.rb +28 -0
  143. data/spec/flipper/expressions/less_than_or_equal_to_spec.rb +28 -0
  144. data/spec/flipper/expressions/less_than_spec.rb +32 -0
  145. data/spec/flipper/expressions/not_equal_spec.rb +15 -0
  146. data/spec/flipper/expressions/now_spec.rb +11 -0
  147. data/spec/flipper/expressions/number_spec.rb +21 -0
  148. data/spec/flipper/expressions/percentage_of_actors_spec.rb +20 -0
  149. data/spec/flipper/expressions/percentage_spec.rb +15 -0
  150. data/spec/flipper/expressions/property_spec.rb +13 -0
  151. data/spec/flipper/expressions/random_spec.rb +9 -0
  152. data/spec/flipper/expressions/string_spec.rb +11 -0
  153. data/spec/flipper/expressions/time_spec.rb +13 -0
  154. data/spec/flipper/feature_spec.rb +380 -10
  155. data/spec/flipper/gate_values_spec.rb +2 -2
  156. data/spec/flipper/gates/expression_spec.rb +108 -0
  157. data/spec/flipper/identifier_spec.rb +4 -5
  158. data/spec/flipper/instrumentation/log_subscriber_spec.rb +10 -2
  159. data/spec/flipper/instrumentation/statsd_subscriber_spec.rb +16 -2
  160. data/spec/flipper/middleware/memoizer_spec.rb +79 -10
  161. data/spec/flipper/model/active_record_spec.rb +72 -0
  162. data/spec/flipper/serializers/gzip_spec.rb +13 -0
  163. data/spec/flipper/serializers/json_spec.rb +13 -0
  164. data/spec/flipper/typecast_spec.rb +43 -7
  165. data/spec/flipper/types/actor_spec.rb +18 -1
  166. data/spec/flipper_integration_spec.rb +102 -4
  167. data/spec/flipper_spec.rb +91 -3
  168. data/spec/spec_helper.rb +17 -5
  169. data/spec/support/actor_names.yml +1 -0
  170. data/spec/support/fail_on_output.rb +8 -0
  171. data/spec/support/fake_backoff_policy.rb +15 -0
  172. data/spec/support/spec_helpers.rb +34 -8
  173. data/test/adapters/actor_limit_test.rb +20 -0
  174. data/test_rails/generators/flipper/setup_generator_test.rb +69 -0
  175. data/test_rails/generators/flipper/update_generator_test.rb +96 -0
  176. data/test_rails/helper.rb +22 -2
  177. data/test_rails/system/test_help_test.rb +52 -0
  178. metadata +145 -29
  179. data/lib/flipper/cloud/instrumenter.rb +0 -48
  180. data/spec/support/climate_control.rb +0 -7
@@ -76,6 +76,26 @@ RSpec.describe Flipper::Feature do
76
76
  expect(subject.enabled?(actors)).to be(false)
77
77
  end
78
78
  end
79
+
80
+ context "for an object that implements .nil? == true" do
81
+ let(:actor) { Flipper::Actor.new("User;1") }
82
+
83
+ before do
84
+ def actor.nil?
85
+ true
86
+ end
87
+ end
88
+
89
+ it 'returns true if feature is enabled' do
90
+ subject.enable
91
+ expect(subject.enabled?(actor)).to be(true)
92
+ end
93
+
94
+ it 'returns false if feature is disabled' do
95
+ subject.disable
96
+ expect(subject.enabled?(actor)).to be(false)
97
+ end
98
+ end
79
99
  end
80
100
 
81
101
  describe '#to_s' do
@@ -109,7 +129,7 @@ RSpec.describe Flipper::Feature do
109
129
  instance.gates.each do |gate|
110
130
  expect(gate).to be_a(Flipper::Gate)
111
131
  end
112
- expect(instance.gates.size).to be(5)
132
+ expect(instance.gates.size).to be(6)
113
133
  end
114
134
  end
115
135
 
@@ -195,7 +215,7 @@ RSpec.describe Flipper::Feature do
195
215
 
196
216
  it 'is recorded for enable' do
197
217
  actor = Flipper::Types::Actor.new(Flipper::Actor.new('1'))
198
- gate = subject.gate_for(actor)
218
+ subject.gate_for(actor)
199
219
 
200
220
  subject.enable(actor)
201
221
 
@@ -210,7 +230,7 @@ RSpec.describe Flipper::Feature do
210
230
 
211
231
  it 'always instruments flipper type instance for enable' do
212
232
  actor = Flipper::Actor.new('1')
213
- gate = subject.gate_for(actor)
233
+ subject.gate_for(actor)
214
234
 
215
235
  subject.enable(actor)
216
236
 
@@ -221,7 +241,6 @@ RSpec.describe Flipper::Feature do
221
241
 
222
242
  it 'is recorded for disable' do
223
243
  thing = Flipper::Types::Boolean.new
224
- gate = subject.gate_for(thing)
225
244
 
226
245
  subject.disable(thing)
227
246
 
@@ -266,7 +285,6 @@ RSpec.describe Flipper::Feature do
266
285
 
267
286
  it 'always instruments flipper type instance for disable' do
268
287
  actor = Flipper::Actor.new('1')
269
- gate = subject.gate_for(actor)
270
288
 
271
289
  subject.disable(actor)
272
290
 
@@ -693,6 +711,361 @@ RSpec.describe Flipper::Feature do
693
711
  end
694
712
  end
695
713
 
714
+ describe '#expression' do
715
+ it "returns nil if feature has no expression" do
716
+ expect(subject.expression).to be(nil)
717
+ end
718
+
719
+ it "returns expression if feature has expression" do
720
+ expression = Flipper.property(:plan).eq("basic")
721
+ subject.enable_expression expression
722
+ expect(subject.expression).to eq(expression)
723
+ end
724
+ end
725
+
726
+ describe '#enable_expression/disable_expression' do
727
+ context "with expression instance" do
728
+ it "updates gate values to equal expression or clears expression" do
729
+ expression = Flipper.property(:plan).eq("basic")
730
+ expect(subject.gate_values.expression).to be(nil)
731
+ subject.enable_expression(expression)
732
+ expect(subject.gate_values.expression).to eq(expression.value)
733
+ subject.disable_expression
734
+ expect(subject.gate_values.expression).to be(nil)
735
+ end
736
+ end
737
+
738
+ context "with Hash" do
739
+ it "updates gate values to equal expression or clears expression" do
740
+ expression = Flipper.property(:plan).eq("basic")
741
+ expect(subject.gate_values.expression).to be(nil)
742
+ subject.enable_expression(expression.value)
743
+ expect(subject.gate_values.expression).to eq(expression.value)
744
+ subject.disable_expression
745
+ expect(subject.gate_values.expression).to be(nil)
746
+ end
747
+ end
748
+ end
749
+
750
+ describe "#add_expression" do
751
+ context "when nothing enabled" do
752
+ context "with Expression instance" do
753
+ it "sets expression to Expression" do
754
+ expression = Flipper.property(:plan).eq("basic")
755
+ subject.add_expression(expression)
756
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
757
+ expect(subject.expression).to eq(expression)
758
+ end
759
+ end
760
+
761
+ context "with Any instance" do
762
+ it "sets expression to Any" do
763
+ expression = Flipper.any(Flipper.property(:plan).eq("basic"))
764
+ subject.add_expression(expression)
765
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
766
+ expect(subject.expression).to eq(expression)
767
+ end
768
+ end
769
+
770
+ context "with All instance" do
771
+ it "sets expression to All" do
772
+ expression = Flipper.all(Flipper.property(:plan).eq("basic"))
773
+ subject.add_expression(expression)
774
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
775
+ expect(subject.expression).to eq(expression)
776
+ end
777
+ end
778
+ end
779
+
780
+ context "when Expression enabled" do
781
+ let(:expression) { Flipper.property(:plan).eq("basic") }
782
+
783
+ before do
784
+ subject.enable_expression expression
785
+ end
786
+
787
+ context "with Expression instance" do
788
+ it "changes expression to Any and adds new Expression" do
789
+ new_expression = Flipper.property(:age).gte(21)
790
+ subject.add_expression(new_expression)
791
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
792
+ expect(subject.expression.args).to include(expression)
793
+ expect(subject.expression.args).to include(new_expression)
794
+ end
795
+ end
796
+
797
+ context "with Any instance" do
798
+ it "changes expression to Any and adds new Any" do
799
+ new_expression = Flipper.any(Flipper.property(:age).eq(21))
800
+ subject.add_expression new_expression
801
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
802
+ expect(subject.expression.args).to include(expression)
803
+ expect(subject.expression.args).to include(new_expression)
804
+ end
805
+ end
806
+
807
+ context "with All instance" do
808
+ it "changes expression to Any and adds new All" do
809
+ new_expression = Flipper.all(Flipper.property(:plan).eq("basic"))
810
+ subject.add_expression new_expression
811
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
812
+ expect(subject.expression.args).to include(expression)
813
+ expect(subject.expression.args).to include(new_expression)
814
+ end
815
+ end
816
+ end
817
+
818
+ context "when Any enabled" do
819
+ let(:condition) { Flipper.property(:plan).eq("basic") }
820
+ let(:expression) { Flipper.any(condition) }
821
+
822
+ before do
823
+ subject.enable_expression expression
824
+ end
825
+
826
+ context "with Expression instance" do
827
+ it "adds Expression to Any" do
828
+ new_expression = Flipper.property(:age).gte(21)
829
+ subject.add_expression(new_expression)
830
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
831
+ expect(subject.expression.args).to include(condition)
832
+ expect(subject.expression.args).to include(new_expression)
833
+ end
834
+ end
835
+
836
+ context "with Any instance" do
837
+ it "adds Any to Any" do
838
+ new_expression = Flipper.any(Flipper.property(:age).gte(21))
839
+ subject.add_expression(new_expression)
840
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
841
+ expect(subject.expression.args).to include(condition)
842
+ expect(subject.expression.args).to include(new_expression)
843
+ end
844
+ end
845
+
846
+ context "with All instance" do
847
+ it "adds All to Any" do
848
+ new_expression = Flipper.all(Flipper.property(:age).gte(21))
849
+ subject.add_expression(new_expression)
850
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
851
+ expect(subject.expression.args).to include(condition)
852
+ expect(subject.expression.args).to include(new_expression)
853
+ end
854
+ end
855
+ end
856
+
857
+ context "when All enabled" do
858
+ let(:condition) { Flipper.property(:plan).eq("basic") }
859
+ let(:expression) { Flipper.all(condition) }
860
+
861
+ before do
862
+ subject.enable_expression expression
863
+ end
864
+
865
+ context "with Expression instance" do
866
+ it "adds Expression to All" do
867
+ new_expression = Flipper.property(:age).gte(21)
868
+ subject.add_expression(new_expression)
869
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
870
+ expect(subject.expression.args).to include(condition)
871
+ expect(subject.expression.args).to include(new_expression)
872
+ end
873
+ end
874
+
875
+ context "with Any instance" do
876
+ it "adds Any to All" do
877
+ new_expression = Flipper.any(Flipper.property(:age).gte(21))
878
+ subject.add_expression(new_expression)
879
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
880
+ expect(subject.expression.args).to include(condition)
881
+ expect(subject.expression.args).to include(new_expression)
882
+ end
883
+ end
884
+
885
+ context "with All instance" do
886
+ it "adds All to All" do
887
+ new_expression = Flipper.all(Flipper.property(:age).gte(21))
888
+ subject.add_expression(new_expression)
889
+ expect(subject.expression).to be_instance_of(Flipper::Expression)
890
+ expect(subject.expression.args).to include(condition)
891
+ expect(subject.expression.args).to include(new_expression)
892
+ end
893
+ end
894
+ end
895
+ end
896
+
897
+ describe '#remove_expression' do
898
+ context "when nothing enabled" do
899
+ context "with Expression instance" do
900
+ it "does nothing" do
901
+ expression = Flipper.property(:plan).eq("basic")
902
+ subject.remove_expression(expression)
903
+ expect(subject.expression).to be(nil)
904
+ end
905
+ end
906
+
907
+ context "with Any instance" do
908
+ it "does nothing" do
909
+ expression = Flipper.any(Flipper.property(:plan).eq("basic"))
910
+ subject.remove_expression expression
911
+ expect(subject.expression).to be(nil)
912
+ end
913
+ end
914
+
915
+ context "with All instance" do
916
+ it "does nothing" do
917
+ expression = Flipper.all(Flipper.property(:plan).eq("basic"))
918
+ subject.remove_expression expression
919
+ expect(subject.expression).to be(nil)
920
+ end
921
+ end
922
+ end
923
+
924
+ context "when Expression enabled" do
925
+ let(:expression) { Flipper.property(:plan).eq("basic") }
926
+
927
+ before do
928
+ subject.enable_expression expression
929
+ end
930
+
931
+ context "with Expression instance" do
932
+ it "changes expression to Any and removes Expression if it matches" do
933
+ new_expression = Flipper.property(:plan).eq("basic")
934
+ subject.remove_expression new_expression
935
+ expect(subject.expression).to eq(Flipper.any)
936
+ end
937
+
938
+ it "changes expression to Any if Expression doesn't match" do
939
+ new_expression = Flipper.property(:plan).eq("premium")
940
+ subject.remove_expression new_expression
941
+ expect(subject.expression).to eq(Flipper.any(expression))
942
+ end
943
+ end
944
+
945
+ context "with Any instance" do
946
+ it "changes expression to Any and does nothing" do
947
+ new_expression = Flipper.any(Flipper.property(:plan).eq("basic"))
948
+ subject.remove_expression new_expression
949
+ expect(subject.expression).to eq(Flipper.any(expression))
950
+ end
951
+ end
952
+
953
+ context "with All instance" do
954
+ it "changes expression to Any and does nothing" do
955
+ new_expression = Flipper.all(Flipper.property(:plan).eq("basic"))
956
+ subject.remove_expression new_expression
957
+ expect(subject.expression).to eq(Flipper.any(expression))
958
+ end
959
+ end
960
+ end
961
+
962
+ context "when Any enabled" do
963
+ let(:condition) { Flipper.property(:plan).eq("basic") }
964
+ let(:expression) { Flipper.any condition }
965
+
966
+ before do
967
+ subject.enable_expression expression
968
+ end
969
+
970
+ context "with Expression instance" do
971
+ it "removes Expression if it matches" do
972
+ subject.remove_expression condition
973
+ expect(subject.expression).to eq(Flipper.any)
974
+ end
975
+
976
+ it "does nothing if Expression does not match" do
977
+ subject.remove_expression Flipper.property(:plan).eq("premium")
978
+ expect(subject.expression).to eq(expression)
979
+ end
980
+ end
981
+
982
+ context "with Any instance" do
983
+ it "removes Any if it matches" do
984
+ new_expression = Flipper.any(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 Any does not match" do
992
+ new_expression = Flipper.any(Flipper.property(:plan).eq("premium"))
993
+ subject.remove_expression new_expression
994
+ expect(subject.expression).to eq(expression)
995
+ end
996
+ end
997
+
998
+ context "with All instance" do
999
+ it "removes All if it matches" do
1000
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
1001
+ subject.add_expression new_expression
1002
+ expect(subject.expression.args.size).to be(2)
1003
+ subject.remove_expression new_expression
1004
+ expect(subject.expression).to eq(expression)
1005
+ end
1006
+
1007
+ it "does nothing if All does not match" do
1008
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
1009
+ subject.remove_expression new_expression
1010
+ expect(subject.expression).to eq(expression)
1011
+ end
1012
+ end
1013
+ end
1014
+
1015
+ context "when All enabled" do
1016
+ let(:condition) { Flipper.property(:plan).eq("basic") }
1017
+ let(:expression) { Flipper.all condition }
1018
+
1019
+ before do
1020
+ subject.enable_expression expression
1021
+ end
1022
+
1023
+ context "with Expression instance" do
1024
+ it "removes Expression if it matches" do
1025
+ subject.remove_expression condition
1026
+ expect(subject.expression).to eq(Flipper.all)
1027
+ end
1028
+
1029
+ it "does nothing if Expression does not match" do
1030
+ subject.remove_expression Flipper.property(:plan).eq("premium")
1031
+ expect(subject.expression).to eq(expression)
1032
+ end
1033
+ end
1034
+
1035
+ context "with Any instance" do
1036
+ it "removes Any if it matches" do
1037
+ new_expression = Flipper.any(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 Any does not match" do
1045
+ new_expression = Flipper.any(Flipper.property(:plan).eq("premium"))
1046
+ subject.remove_expression new_expression
1047
+ expect(subject.expression).to eq(expression)
1048
+ end
1049
+ end
1050
+
1051
+ context "with All instance" do
1052
+ it "removes All if it matches" do
1053
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
1054
+ subject.add_expression new_expression
1055
+ expect(subject.expression.args.size).to be(2)
1056
+ subject.remove_expression new_expression
1057
+ expect(subject.expression).to eq(expression)
1058
+ end
1059
+
1060
+ it "does nothing if All does not match" do
1061
+ new_expression = Flipper.all(Flipper.property(:plan).eq("premium"))
1062
+ subject.remove_expression new_expression
1063
+ expect(subject.expression).to eq(expression)
1064
+ end
1065
+ end
1066
+ end
1067
+ end
1068
+
696
1069
  describe '#enable_actor/disable_actor' do
697
1070
  context 'with object that responds to flipper_id' do
698
1071
  it 'updates the gate values to include the actor' do
@@ -721,8 +1094,6 @@ RSpec.describe Flipper::Feature do
721
1094
  describe '#enable_group/disable_group' do
722
1095
  context 'with symbol group name' do
723
1096
  it 'updates the gate values to include the group' do
724
- actor = Flipper::Actor.new(5)
725
- group = Flipper.register(:five_only) { |actor| actor.flipper_id == 5 }
726
1097
  expect(subject.gate_values.groups).to be_empty
727
1098
  subject.enable_group(:five_only)
728
1099
  expect(subject.gate_values.groups).to eq(Set['five_only'])
@@ -733,8 +1104,6 @@ RSpec.describe Flipper::Feature do
733
1104
 
734
1105
  context 'with string group name' do
735
1106
  it 'updates the gate values to include the group' do
736
- actor = Flipper::Actor.new(5)
737
- group = Flipper.register(:five_only) { |actor| actor.flipper_id == 5 }
738
1107
  expect(subject.gate_values.groups).to be_empty
739
1108
  subject.enable_group('five_only')
740
1109
  expect(subject.gate_values.groups).to eq(Set['five_only'])
@@ -745,7 +1114,6 @@ RSpec.describe Flipper::Feature do
745
1114
 
746
1115
  context 'with group instance' do
747
1116
  it 'updates the gate values for the group' do
748
- actor = Flipper::Actor.new(5)
749
1117
  group = Flipper.register(:five_only) { |actor| actor.flipper_id == 5 }
750
1118
  expect(subject.gate_values.groups).to be_empty
751
1119
  subject.enable_group(group)
@@ -845,12 +1213,14 @@ RSpec.describe Flipper::Feature do
845
1213
  :actor,
846
1214
  :boolean,
847
1215
  :group,
1216
+ :expression,
848
1217
  ])
849
1218
 
850
1219
  expect(subject.disabled_gate_names.to_set).to eq(Set[
851
1220
  :actor,
852
1221
  :boolean,
853
1222
  :group,
1223
+ :expression,
854
1224
  ])
855
1225
  end
856
1226
  end
@@ -80,13 +80,13 @@ RSpec.describe Flipper::GateValues do
80
80
  it 'raises argument error for percentage of time value that cannot be converted to an integer' do
81
81
  expect do
82
82
  described_class.new(percentage_of_time: ['asdf'])
83
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
83
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
84
84
  end
85
85
 
86
86
  it 'raises argument error for percentage of actors value that cannot be converted to an int' do
87
87
  expect do
88
88
  described_class.new(percentage_of_actors: ['asdf'])
89
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
89
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
90
90
  end
91
91
 
92
92
  it 'raises argument error for actors value that cannot be converted to a set' do
@@ -0,0 +1,108 @@
1
+ RSpec.describe Flipper::Gates::Expression do
2
+ let(:feature_name) { :search }
3
+
4
+ subject do
5
+ described_class.new
6
+ end
7
+
8
+ def context(expression, properties: {})
9
+ Flipper::FeatureCheckContext.new(
10
+ feature_name: feature_name,
11
+ values: Flipper::GateValues.new(expression: expression),
12
+ actors: [Flipper::Types::Actor.new(Flipper::Actor.new(1, properties))]
13
+ )
14
+ end
15
+
16
+ describe '#enabled?' do
17
+ context 'for nil value' do
18
+ it 'returns false' do
19
+ expect(subject.enabled?(nil)).to eq(false)
20
+ end
21
+ end
22
+
23
+ context 'for empty value' do
24
+ it 'returns false' do
25
+ expect(subject.enabled?({})).to eq(false)
26
+ end
27
+ end
28
+
29
+ context "for not empty value" do
30
+ it 'returns true' do
31
+ expect(subject.enabled?({"Boolean" => [true]})).to eq(true)
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#open?' do
37
+ context 'for expression that evaluates to true' do
38
+ it 'returns true' do
39
+ expression = Flipper.boolean(true).eq(true)
40
+ expect(subject.open?(context(expression.value))).to be(true)
41
+ end
42
+ end
43
+
44
+ context 'for expression that evaluates to false' do
45
+ it 'returns false' do
46
+ expression = Flipper.boolean(true).eq(false)
47
+ expect(subject.open?(context(expression.value))).to be(false)
48
+ end
49
+ end
50
+
51
+ context 'for properties that have string keys' do
52
+ it 'returns true when expression evalutes to true' do
53
+ expression = Flipper.property(:type).eq("User")
54
+ context = context(expression.value, properties: {"type" => "User"})
55
+ expect(subject.open?(context)).to be(true)
56
+ end
57
+
58
+ it 'returns false when expression evaluates to false' do
59
+ expression = Flipper.property(:type).eq("User")
60
+ context = context(expression.value, properties: {"type" => "Org"})
61
+ expect(subject.open?(context)).to be(false)
62
+ end
63
+ end
64
+
65
+ context 'for properties that have symbol keys' do
66
+ it 'returns true when expression evalutes to true' do
67
+ expression = Flipper.property(:type).eq("User")
68
+ context = context(expression.value, properties: {type: "User"})
69
+ expect(subject.open?(context)).to be(true)
70
+ end
71
+
72
+ it 'returns false when expression evaluates to false' do
73
+ expression = Flipper.property(:type).eq("User")
74
+ context = context(expression.value, properties: {type: "Org"})
75
+ expect(subject.open?(context)).to be(false)
76
+ end
77
+ end
78
+ end
79
+
80
+ describe '#protects?' do
81
+ it 'returns true for Flipper::Expression' do
82
+ expression = Flipper.number(20).eq(20)
83
+ expect(subject.protects?(expression)).to be(true)
84
+ end
85
+
86
+ it 'returns true for Hash' do
87
+ expression = Flipper.number(20).eq(20)
88
+ expect(subject.protects?(expression.value)).to be(true)
89
+ end
90
+
91
+ it 'returns false for other things' do
92
+ expect(subject.protects?(false)).to be(false)
93
+ end
94
+ end
95
+
96
+ describe '#wrap' do
97
+ it 'returns self for Flipper::Expression' do
98
+ expression = Flipper.number(20).eq(20)
99
+ expect(subject.wrap(expression)).to be(expression)
100
+ end
101
+
102
+ it 'returns Flipper::Expression for Hash' do
103
+ expression = Flipper.number(20).eq(20)
104
+ expect(subject.wrap(expression.value)).to be_instance_of(Flipper::Expression)
105
+ expect(subject.wrap(expression.value)).to eq(expression)
106
+ end
107
+ end
108
+ end
@@ -2,12 +2,11 @@ require 'flipper/identifier'
2
2
 
3
3
  RSpec.describe Flipper::Identifier do
4
4
  describe '#flipper_id' do
5
- class User < Struct.new(:id)
6
- include Flipper::Identifier
7
- end
8
-
9
5
  it 'uses class name and id' do
10
- expect(User.new(5).flipper_id).to eq('User;5')
6
+ class BlahBlah < Struct.new(:id)
7
+ include Flipper::Identifier
8
+ end
9
+ expect(BlahBlah.new(5).flipper_id).to eq('BlahBlah;5')
11
10
  end
12
11
  end
13
12
  end
@@ -1,6 +1,7 @@
1
1
  require 'logger'
2
- require 'flipper/adapters/instrumented'
2
+ require 'active_support/core_ext/object/blank'
3
3
  require 'flipper/instrumentation/log_subscriber'
4
+ require 'flipper/adapters/instrumented'
4
5
 
5
6
  begin
6
7
  require 'active_support/isolated_execution_state'
@@ -8,6 +9,9 @@ rescue LoadError
8
9
  # ActiveSupport::IsolatedExecutionState is only available in Rails 5.2+
9
10
  end
10
11
 
12
+ # Don't log in other tests, we'll manually re-attach when this one starts
13
+ Flipper::Instrumentation::LogSubscriber.detach
14
+
11
15
  RSpec.describe Flipper::Instrumentation::LogSubscriber do
12
16
  let(:adapter) do
13
17
  memory = Flipper::Adapters::Memory.new
@@ -32,8 +36,12 @@ RSpec.describe Flipper::Instrumentation::LogSubscriber do
32
36
  described_class.logger = nil
33
37
  end
34
38
 
39
+ before(:all) do
40
+ described_class.attach
41
+ end
42
+
35
43
  after(:all) do
36
- ActiveSupport::Notifications.unsubscribe("flipper")
44
+ described_class.detach
37
45
  end
38
46
 
39
47
  let(:log) { @io.string }
@@ -1,6 +1,5 @@
1
1
  require 'flipper/adapters/instrumented'
2
2
  require 'flipper/instrumentation/statsd'
3
- require 'statsd'
4
3
 
5
4
  begin
6
5
  require 'active_support/isolated_execution_state'
@@ -19,7 +18,7 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
19
18
  Flipper.new(adapter, instrumenter: ActiveSupport::Notifications)
20
19
  end
21
20
 
22
- let(:user) { user = Flipper::Actor.new('1') }
21
+ let(:user) { Flipper::Actor.new('1') }
23
22
 
24
23
  before do
25
24
  described_class.client = statsd_client
@@ -78,4 +77,19 @@ RSpec.describe Flipper::Instrumentation::StatsdSubscriber do
78
77
  flipper[:stats].disable(user)
79
78
  assert_timer 'flipper.adapter.memory.disable'
80
79
  end
80
+
81
+ context 'when client is nil' do
82
+ before do
83
+ described_class.client = nil
84
+ end
85
+
86
+ it 'does not raise error' do
87
+ expect { flipper[:stats].enable(user) }.not_to raise_error
88
+ end
89
+
90
+ it 'does not update metrics' do
91
+ flipper[:stats].enable(user)
92
+ expect(socket.buffer).to be_empty
93
+ end
94
+ end
81
95
  end