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
@@ -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,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'
@@ -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
@@ -196,6 +196,73 @@ RSpec.describe Flipper::Middleware::Memoizer do
196
196
  end
197
197
  end
198
198
 
199
+ context 'with preload block' do
200
+ let(:app) do
201
+ app = lambda do |_env|
202
+ flipper[:stats].enabled?
203
+ flipper[:stats].enabled?
204
+ flipper[:shiny].enabled?
205
+ flipper[:shiny].enabled?
206
+ [200, {}, []]
207
+ end
208
+
209
+ described_class.new(app, preload: ->(request) {
210
+ case request.path
211
+ when "/true"
212
+ true
213
+ when "/specific"
214
+ [:stats]
215
+ else
216
+ false
217
+ end
218
+ })
219
+ end
220
+
221
+ include_examples 'flipper middleware'
222
+
223
+ it 'eagerly caches known features for duration of request if block returns true' do
224
+ flipper[:stats].enable
225
+ flipper[:shiny].enable
226
+
227
+ # clear the log of operations
228
+ adapter.reset
229
+
230
+ get '/true', {}, 'flipper' => flipper
231
+
232
+ expect(adapter.operations.size).to be(1)
233
+ expect(adapter.count(:get_all)).to be(1)
234
+ expect(adapter.count(:get)).to be(0)
235
+ end
236
+
237
+ it 'does not eagerly cache known features if block returns false' do
238
+ flipper[:stats].enable
239
+ flipper[:shiny].enable
240
+
241
+ # clear the log of operations
242
+ adapter.reset
243
+
244
+ get '/false', {}, 'flipper' => flipper
245
+
246
+ expect(adapter.operations.size).to be(2)
247
+ expect(adapter.count(:get_all)).to be(0)
248
+ expect(adapter.count(:get)).to be(2)
249
+ end
250
+
251
+ it 'eagerly caches specified features for duration of request if block returns array of specified features' do
252
+ flipper[:stats].enable
253
+ flipper[:shiny].enable
254
+
255
+ # clear the log of operations
256
+ adapter.reset
257
+
258
+ get '/specific', {}, 'flipper' => flipper
259
+
260
+ expect(adapter.operations.size).to be(2)
261
+ expect(adapter.count(:get_multi)).to be(1)
262
+ expect(adapter.count(:get)).to be(1)
263
+ end
264
+ end
265
+
199
266
  context 'with multiple instances' do
200
267
  let(:app) do
201
268
  # ensure scoped for builder block, annoying...
@@ -0,0 +1,13 @@
1
+ require 'flipper/serializers/gzip'
2
+
3
+ RSpec.describe Flipper::Serializers::Gzip do
4
+ it "serializes and deserializes" do
5
+ serialized = described_class.serialize("my data")
6
+ expect(described_class.deserialize(serialized)).to eq("my data")
7
+ end
8
+
9
+ it "doesn't fail with nil" do
10
+ expect(described_class.serialize(nil)).to be(nil)
11
+ expect(described_class.deserialize(nil)).to be(nil)
12
+ end
13
+ end
@@ -0,0 +1,13 @@
1
+ require 'flipper/serializers/json'
2
+
3
+ RSpec.describe Flipper::Serializers::Json do
4
+ it "serializes and deserializes" do
5
+ serialized = described_class.serialize("my data")
6
+ expect(described_class.deserialize(serialized)).to eq("my data")
7
+ end
8
+
9
+ it "doesn't fail with nil" do
10
+ expect(described_class.serialize(nil)).to be(nil)
11
+ expect(described_class.deserialize(nil)).to be(nil)
12
+ end
13
+ end
@@ -56,7 +56,7 @@ RSpec.describe Flipper::Typecast do
56
56
  nil => 0,
57
57
  '' => 0,
58
58
  0 => 0,
59
- 0.0 => 0,
59
+ 0.0 => 0.0,
60
60
  1 => 1,
61
61
  1.1 => 1.1,
62
62
  '0.01' => 0.01,
@@ -65,9 +65,9 @@ RSpec.describe Flipper::Typecast do
65
65
  '99' => 99,
66
66
  '99.9' => 99.9,
67
67
  }.each do |value, expected|
68
- context "#to_percentage for #{value.inspect}" do
68
+ context "#to_number for #{value.inspect}" do
69
69
  it "returns #{expected}" do
70
- expect(described_class.to_percentage(value)).to be(expected)
70
+ expect(described_class.to_number(value)).to be(expected)
71
71
  end
72
72
  end
73
73
  end
@@ -99,14 +99,14 @@ RSpec.describe Flipper::Typecast do
99
99
 
100
100
  it 'raises argument error for bad integer percentage' do
101
101
  expect do
102
- described_class.to_percentage(['asdf'])
103
- end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a percentage))
102
+ described_class.to_number(['asdf'])
103
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a number))
104
104
  end
105
105
 
106
106
  it 'raises argument error for bad float percentage' do
107
107
  expect do
108
- described_class.to_percentage(['asdf.0'])
109
- end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a percentage))
108
+ described_class.to_number(['asdf.0'])
109
+ end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a number))
110
110
  end
111
111
 
112
112
  it 'raises argument error for set value that cannot be converted to a set' do
@@ -127,6 +127,30 @@ RSpec.describe Flipper::Typecast do
127
127
  expect(result["search"]).not_to be(hash["search"])
128
128
  end
129
129
 
130
+ it "converts does not convert expressions" do
131
+ hash = {
132
+ "search" => {
133
+ boolean: nil,
134
+ expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
135
+ groups: ['a', 'b'],
136
+ actors: ['User;1'],
137
+ percentage_of_actors: nil,
138
+ percentage_of_time: nil,
139
+ },
140
+ }
141
+ result = described_class.features_hash(hash)
142
+ expect(result).to eq({
143
+ "search" => {
144
+ boolean: nil,
145
+ expression: {"Equal"=>[{"Property"=>["plan"]}, "basic"]},
146
+ groups: Set['a', 'b'],
147
+ actors: Set['User;1'],
148
+ percentage_of_actors: nil,
149
+ percentage_of_time: nil,
150
+ },
151
+ })
152
+ end
153
+
130
154
  it "converts gate value arrays to sets" do
131
155
  hash = {
132
156
  "search" => {
@@ -193,4 +217,16 @@ RSpec.describe Flipper::Typecast do
193
217
  })
194
218
  end
195
219
  end
220
+
221
+ it "converts to and from json" do
222
+ source = {"foo" => "bar"}
223
+ output = described_class.to_json(source)
224
+ expect(described_class.from_json(output)).to eq(source)
225
+ end
226
+
227
+ it "converts to and from gzip" do
228
+ source = "foo bar"
229
+ output = described_class.to_gzip(source)
230
+ expect(described_class.from_gzip(output)).to eq(source)
231
+ end
196
232
  end
@@ -11,12 +11,19 @@ RSpec.describe Flipper::Types::Actor do
11
11
  attr_reader :flipper_id
12
12
 
13
13
  def initialize(flipper_id)
14
- @flipper_id = flipper_id
14
+ @flipper_id = flipper_id.to_s
15
15
  end
16
16
 
17
17
  def admin?
18
18
  true
19
19
  end
20
+
21
+ def flipper_properties
22
+ {
23
+ "flipper_id" => flipper_id,
24
+ "admin" => admin?,
25
+ }
26
+ end
20
27
  end
21
28
  end
22
29
 
@@ -87,6 +94,15 @@ RSpec.describe Flipper::Types::Actor do
87
94
  expect(actor.admin?).to eq(true)
88
95
  end
89
96
 
97
+ it 'proxies flipper_properties to actor' do
98
+ actor = actor_class.new(10)
99
+ actor = described_class.new(actor)
100
+ expect(actor.flipper_properties).to eq({
101
+ "flipper_id" => "10",
102
+ "admin" => true,
103
+ })
104
+ end
105
+
90
106
  it 'exposes actor' do
91
107
  actor = actor_class.new(10)
92
108
  actor_type_instance = described_class.new(actor)
@@ -104,6 +120,7 @@ RSpec.describe Flipper::Types::Actor do
104
120
  actor = actor_class.new(10)
105
121
  actor_type_instance = described_class.new(actor)
106
122
  expect(actor_type_instance.respond_to?(:admin?)).to eq(true)
123
+ expect(actor_type_instance.respond_to?(:flipper_properties)).to eq(true)
107
124
  end
108
125
 
109
126
  it 'returns false if does not respond to method and actor does not respond to method' do
@@ -8,17 +8,24 @@ RSpec.describe Flipper do
8
8
  let(:dev_group) { flipper.group(:devs) }
9
9
 
10
10
  let(:admin_actor) do
11
- double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false
11
+ double 'Non Flipper Thing', flipper_id: 1, admin?: true, dev?: false, flipper_properties: {"admin" => true, "dev" => false}
12
12
  end
13
13
  let(:dev_actor) do
14
- double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true
14
+ double 'Non Flipper Thing', flipper_id: 10, admin?: false, dev?: true, flipper_properties: {"admin" => false, "dev" => true}
15
15
  end
16
16
 
17
17
  let(:admin_truthy_actor) do
18
- double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false
18
+ double 'Non Flipper Thing', flipper_id: 1, admin?: 'true-ish', dev?: false, flipper_properties: {"admin" => "true-ish", "dev" => false}
19
19
  end
20
20
  let(:admin_falsey_actor) do
21
- double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false
21
+ double 'Non Flipper Thing', flipper_id: 1, admin?: nil, dev?: false, flipper_properties: {"admin" => nil, "dev" => false}
22
+ end
23
+
24
+ let(:basic_plan_actor) do
25
+ double 'Non Flipper Thing', flipper_id: 1, flipper_properties: {"plan" => "basic"}
26
+ end
27
+ let(:premium_plan_actor) do
28
+ double 'Non Flipper Thing', flipper_id: 10, flipper_properties: {"plan" => "premium"}
22
29
  end
23
30
 
24
31
  let(:pitt) { Flipper::Actor.new(1) }
@@ -70,10 +77,12 @@ RSpec.describe Flipper do
70
77
 
71
78
  it 'enables feature for flipper actor in group' do
72
79
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_actor))).to eq(true)
80
+ expect(feature.enabled?(admin_actor)).to eq(true)
73
81
  end
74
82
 
75
83
  it 'does not enable for flipper actor not in group' do
76
84
  expect(feature.enabled?(Flipper::Types::Actor.new(dev_actor))).to eq(false)
85
+ expect(feature.enabled?(dev_actor)).to eq(false)
77
86
  end
78
87
 
79
88
  it 'does not enable feature for all' do
@@ -257,10 +266,12 @@ RSpec.describe Flipper do
257
266
 
258
267
  it 'disables feature for flipper actor in group' do
259
268
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_actor))).to eq(false)
269
+ expect(feature.enabled?(admin_actor)).to eq(false)
260
270
  end
261
271
 
262
272
  it 'does not disable feature for flipper actor in other groups' do
263
273
  expect(feature.enabled?(Flipper::Types::Actor.new(dev_actor))).to eq(true)
274
+ expect(feature.enabled?(dev_actor)).to eq(true)
264
275
  end
265
276
 
266
277
  it 'adds feature to set of features' do
@@ -379,6 +390,7 @@ RSpec.describe Flipper do
379
390
 
380
391
  it 'returns true for truthy block values' do
381
392
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_truthy_actor))).to eq(true)
393
+ expect(feature.enabled?(admin_truthy_actor)).to eq(true)
382
394
  end
383
395
 
384
396
  it 'returns true if any actor is in enabled group' do
@@ -394,6 +406,7 @@ RSpec.describe Flipper do
394
406
 
395
407
  it 'returns false for falsey block values' do
396
408
  expect(feature.enabled?(Flipper::Types::Actor.new(admin_falsey_actor))).to eq(false)
409
+ expect(feature.enabled?(admin_falsey_actor)).to eq(false)
397
410
  end
398
411
  end
399
412
 
@@ -549,4 +562,89 @@ RSpec.describe Flipper do
549
562
  expect(feature.enabled?(dev_actor)).to eq(false)
550
563
  end
551
564
  end
565
+
566
+ context "for expression" do
567
+ it "works" do
568
+ feature.enable Flipper.property(:plan).eq("basic")
569
+
570
+ expect(feature.enabled?).to be(false)
571
+ expect(feature.enabled?(basic_plan_actor)).to be(true)
572
+ expect(feature.enabled?(premium_plan_actor)).to be(false)
573
+ expect(feature.enabled?(admin_actor)).to be(false)
574
+ end
575
+
576
+ it "works for true expression with no actor" do
577
+ feature.enable Flipper.boolean(true)
578
+ expect(feature.enabled?).to be(true)
579
+ end
580
+
581
+ it "works for multiple actors" do
582
+ feature.enable Flipper.property(:plan).eq("basic")
583
+
584
+ expect(feature.enabled?(basic_plan_actor, premium_plan_actor)).to be(true)
585
+ expect(feature.enabled?(premium_plan_actor, basic_plan_actor)).to be(true)
586
+ expect(feature.enabled?(premium_plan_actor, admin_actor)).to be(false)
587
+ end
588
+ end
589
+
590
+ context "for Any" do
591
+ it "works" do
592
+ expression = Flipper.any(
593
+ Flipper.property(:plan).eq("basic"),
594
+ Flipper.property(:plan).eq("plus"),
595
+ )
596
+ feature.enable expression
597
+
598
+ expect(feature.enabled?(basic_plan_actor)).to be(true)
599
+ expect(feature.enabled?(premium_plan_actor)).to be(false)
600
+ end
601
+ end
602
+
603
+ context "for All" do
604
+ it "works" do
605
+ true_actor = Flipper::Actor.new("User;1", {
606
+ "plan" => "basic",
607
+ "age" => 21,
608
+ })
609
+ false_actor = Flipper::Actor.new("User;1", {
610
+ "plan" => "basic",
611
+ "age" => 20,
612
+ })
613
+ expression = Flipper.all(
614
+ Flipper.property(:plan).eq("basic"),
615
+ Flipper.property(:age).eq(21)
616
+ )
617
+ feature.enable expression
618
+
619
+ expect(feature.enabled?(true_actor)).to be(true)
620
+ expect(feature.enabled?(false_actor)).to be(false)
621
+ end
622
+
623
+ it "works when nested" do
624
+ admin_actor = Flipper::Actor.new("User;1", {
625
+ "admin" => true,
626
+ })
627
+ true_actor = Flipper::Actor.new("User;1", {
628
+ "plan" => "basic",
629
+ "age" => 21,
630
+ })
631
+ false_actor = Flipper::Actor.new("User;1", {
632
+ "plan" => "basic",
633
+ "age" => 20,
634
+ })
635
+ expression = Flipper.any(
636
+ Flipper.property(:admin).eq(true),
637
+ Flipper.all(
638
+ Flipper.property(:plan).eq("basic"),
639
+ Flipper.property(:age).eq(21)
640
+ )
641
+ )
642
+
643
+ feature.enable expression
644
+
645
+ expect(feature.enabled?(admin_actor)).to be(true)
646
+ expect(feature.enabled?(true_actor)).to be(true)
647
+ expect(feature.enabled?(false_actor)).to be(false)
648
+ end
649
+ end
552
650
  end
data/spec/flipper_spec.rb CHANGED
@@ -64,7 +64,12 @@ RSpec.describe Flipper do
64
64
 
65
65
  describe "delegation to instance" do
66
66
  let(:group) { Flipper::Types::Group.new(:admins) }
67
- let(:actor) { Flipper::Actor.new("1") }
67
+ let(:actor) {
68
+ Flipper::Actor.new("1", {
69
+ "plan" => "basic",
70
+ })
71
+ }
72
+ let(:expression) { Flipper.property(:plan).eq("basic") }
68
73
 
69
74
  before do
70
75
  described_class.configure do |config|
@@ -88,6 +93,37 @@ RSpec.describe Flipper do
88
93
  expect(described_class.instance.enabled?(:search)).to be(false)
89
94
  end
90
95
 
96
+ it 'delegates expression to instance' do
97
+ expect(described_class.expression(:search)).to be(nil)
98
+
99
+ expression = Flipper.property(:plan).eq("basic")
100
+ Flipper.instance.enable_expression :search, expression
101
+
102
+ expect(described_class.expression(:search)).to eq(expression)
103
+ end
104
+
105
+ it 'delegates enable_expression to instance' do
106
+ described_class.enable_expression(:search, expression)
107
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
108
+ end
109
+
110
+ it 'delegates disable_expression to instance' do
111
+ described_class.disable_expression(:search)
112
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
113
+ end
114
+
115
+ it 'delegates add_expression to instance' do
116
+ described_class.add_expression(:search, expression)
117
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
118
+ end
119
+
120
+ it 'delegates remove_expression to instance' do
121
+ described_class.enable_expression(:search, Flipper.any(expression))
122
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
123
+ described_class.remove_expression(:search, expression)
124
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
125
+ end
126
+
91
127
  it 'delegates enable_actor to instance' do
92
128
  described_class.enable_actor(:search, actor)
93
129
  expect(described_class.instance.enabled?(:search, actor)).to be(true)
@@ -192,6 +228,10 @@ RSpec.describe Flipper do
192
228
  expect(described_class.memoizing?).to eq(described_class.adapter.memoizing?)
193
229
  end
194
230
 
231
+ it 'delegates read_only? to instance' do
232
+ expect(described_class.read_only?).to eq(described_class.adapter.read_only?)
233
+ end
234
+
195
235
  it 'delegates sync stuff to instance and does nothing' do
196
236
  expect(described_class.sync).to be(nil)
197
237
  expect(described_class.sync_secret).to be(nil)
@@ -326,4 +366,52 @@ RSpec.describe Flipper do
326
366
  expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
327
367
  end
328
368
  end
369
+
370
+ describe ".constant" do
371
+ it "returns Flipper::Expression::Constant instance" do
372
+ expect(described_class.constant(false)).to eq(Flipper::Expression::Constant.new(false))
373
+ expect(described_class.constant("string")).to eq(Flipper::Expression::Constant.new("string"))
374
+ end
375
+ end
376
+
377
+ describe ".property" do
378
+ it "returns Flipper::Expressions::Property expression" do
379
+ expect(Flipper.property("name")).to eq(Flipper::Expression.build(Property: "name"))
380
+ end
381
+ end
382
+
383
+ describe ".boolean" do
384
+ it "returns Flipper::Expressions::Boolean expression" do
385
+ expect(described_class.boolean(true)).to eq(Flipper::Expression.build(Boolean: true))
386
+ expect(described_class.boolean(false)).to eq(Flipper::Expression.build(Boolean: false))
387
+ end
388
+ end
389
+
390
+ describe ".random" do
391
+ it "returns Flipper::Expressions::Random expression" do
392
+ expect(Flipper.random(100)).to eq(Flipper::Expression.build(Random: 100))
393
+ end
394
+ end
395
+
396
+ describe ".any" do
397
+ let(:age_expression) { Flipper.property(:age).gte(21) }
398
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
399
+
400
+ it "returns Flipper::Expressions::Any instance" do
401
+ expect(Flipper.any(age_expression, plan_expression)).to eq(
402
+ Flipper::Expression.build({Any: [age_expression, plan_expression]})
403
+ )
404
+ end
405
+ end
406
+
407
+ describe ".all" do
408
+ let(:age_expression) { Flipper.property(:age).gte(21) }
409
+ let(:plan_expression) { Flipper.property(:plan).eq("basic") }
410
+
411
+ it "returns Flipper::Expressions::All instance" do
412
+ expect(Flipper.all(age_expression, plan_expression)).to eq(
413
+ Flipper::Expression.build({All: [age_expression, plan_expression]})
414
+ )
415
+ end
416
+ end
329
417
  end