flipper 0.11.0.beta6 → 0.11.0.beta7

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.
@@ -22,8 +22,8 @@ module Flipper
22
22
  @boolean = Typecast.to_boolean(adapter_values[:boolean])
23
23
  @actors = Typecast.to_set(adapter_values[:actors])
24
24
  @groups = Typecast.to_set(adapter_values[:groups])
25
- @percentage_of_actors = Typecast.to_integer(adapter_values[:percentage_of_actors])
26
- @percentage_of_time = Typecast.to_integer(adapter_values[:percentage_of_time])
25
+ @percentage_of_actors = Typecast.to_percentage(adapter_values[:percentage_of_actors])
26
+ @percentage_of_time = Typecast.to_percentage(adapter_values[:percentage_of_time])
27
27
  end
28
28
 
29
29
  def [](key)
@@ -28,12 +28,8 @@ module Flipper
28
28
  false
29
29
  else
30
30
  value.any? do |name|
31
- begin
32
- group = Flipper.group(name)
33
- group.match?(context.thing, context)
34
- rescue GroupNotRegistered
35
- false
36
- end
31
+ group = Flipper.group(name)
32
+ group.match?(context.thing, context)
37
33
  end
38
34
  end
39
35
  end
@@ -30,7 +30,9 @@ module Flipper
30
30
  if Types::Actor.wrappable?(context.thing)
31
31
  actor = Types::Actor.wrap(context.thing)
32
32
  id = "#{context.feature_name}#{actor.value}"
33
- Zlib.crc32(id) % 100 < percentage
33
+ # this is to support up to 3 decimal places in percentages
34
+ scaling_factor = 1_000
35
+ Zlib.crc32(id) % (100 * scaling_factor) < percentage * scaling_factor
34
36
  else
35
37
  false
36
38
  end
@@ -26,6 +26,30 @@ module Flipper
26
26
  end
27
27
  end
28
28
 
29
+ # Internal: Convert value to a float.
30
+ #
31
+ # Returns a Float representation of the value.
32
+ # Raises ArgumentError if conversion is not possible.
33
+ def self.to_float(value)
34
+ if value.respond_to?(:to_f)
35
+ value.to_f
36
+ else
37
+ raise ArgumentError, "#{value.inspect} cannot be converted to a float"
38
+ end
39
+ end
40
+
41
+ # Internal: Convert value to a percentage.
42
+ #
43
+ # Returns a Integer or Float representation of the value.
44
+ # Raises ArgumentError if conversion is not possible.
45
+ def self.to_percentage(value)
46
+ if value.to_s.include?('.'.freeze)
47
+ to_float(value)
48
+ else
49
+ to_integer(value)
50
+ end
51
+ end
52
+
29
53
  # Internal: Convert value to a set.
30
54
  #
31
55
  # Returns a Set representation of the value.
@@ -2,7 +2,7 @@ module Flipper
2
2
  module Types
3
3
  class Percentage < Type
4
4
  def initialize(value)
5
- value = Typecast.to_integer(value)
5
+ value = Typecast.to_percentage(value)
6
6
 
7
7
  if value < 0 || value > 100
8
8
  raise ArgumentError,
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = '0.11.0.beta6'.freeze
2
+ VERSION = '0.11.0.beta7'.freeze
3
3
  end
@@ -0,0 +1,16 @@
1
+ require 'helper'
2
+ require 'flipper/configuration'
3
+
4
+ RSpec.describe Flipper::Configuration do
5
+ describe '#default' do
6
+ it 'raises if default not configured' do
7
+ expect { subject.default }.to raise_error(Flipper::DefaultNotSet)
8
+ end
9
+
10
+ it 'can be set default' do
11
+ instance = Flipper.new(Flipper::Adapters::Memory.new)
12
+ subject.default { instance }
13
+ expect(subject.default).to be(instance)
14
+ end
15
+ end
16
+ end
@@ -274,6 +274,15 @@ RSpec.describe Flipper::DSL do
274
274
  subject.disable_percentage_of_time(:stats)
275
275
  expect(subject[:stats].percentage_of_time_value).to be(0)
276
276
  end
277
+
278
+ it 'can enable/disable float values' do
279
+ expect(subject[:stats].percentage_of_time_value).to be(0)
280
+ subject.enable_percentage_of_time(:stats, 0.01)
281
+ expect(subject[:stats].percentage_of_time_value).to be(0.01)
282
+
283
+ subject.disable_percentage_of_time(:stats)
284
+ expect(subject[:stats].percentage_of_time_value).to be(0)
285
+ end
277
286
  end
278
287
 
279
288
  describe '#enable_percentage_of_actors/disable_percentage_of_actors' do
@@ -285,6 +294,15 @@ RSpec.describe Flipper::DSL do
285
294
  subject.disable_percentage_of_actors(:stats)
286
295
  expect(subject[:stats].percentage_of_actors_value).to be(0)
287
296
  end
297
+
298
+ it 'can enable/disable float values' do
299
+ expect(subject[:stats].percentage_of_actors_value).to be(0)
300
+ subject.enable_percentage_of_actors(:stats, 0.01)
301
+ expect(subject[:stats].percentage_of_actors_value).to be(0.01)
302
+
303
+ subject.disable_percentage_of_actors(:stats)
304
+ expect(subject[:stats].percentage_of_actors_value).to be(0)
305
+ end
288
306
  end
289
307
 
290
308
  describe '#add' do
@@ -7,10 +7,10 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
7
7
  described_class.new
8
8
  end
9
9
 
10
- def context(integer, feature = feature_name, thing = nil)
10
+ def context(percentage_of_actors_value, feature = feature_name, thing = nil)
11
11
  Flipper::FeatureCheckContext.new(
12
12
  feature_name: feature,
13
- values: Flipper::GateValues.new(percentage_of_actors: integer),
13
+ values: Flipper::GateValues.new(percentage_of_actors: percentage_of_actors_value),
14
14
  thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
15
15
  )
16
16
  end
@@ -19,20 +19,18 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
19
19
  context 'when compared against two features' do
20
20
  let(:percentage) { 0.05 }
21
21
  let(:percentage_as_integer) { percentage * 100 }
22
- let(:number_of_actors) { 100 }
22
+ let(:number_of_actors) { 10_000 }
23
23
 
24
24
  let(:actors) do
25
25
  (1..number_of_actors).map { |n| Flipper::Actor.new(n) }
26
26
  end
27
27
 
28
28
  let(:feature_one_enabled_actors) do
29
- gate = described_class.new
30
- actors.select { |actor| gate.open? context(percentage_as_integer, :name_one, actor) }
29
+ actors.select { |actor| subject.open? context(percentage_as_integer, :name_one, actor) }
31
30
  end
32
31
 
33
32
  let(:feature_two_enabled_actors) do
34
- gate = described_class.new
35
- actors.select { |actor| gate.open? context(percentage_as_integer, :name_two, actor) }
33
+ actors.select { |actor| subject.open? context(percentage_as_integer, :name_two, actor) }
36
34
  end
37
35
 
38
36
  it 'does not enable both features for same set of actors' do
@@ -51,5 +49,29 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
51
49
  end
52
50
  end
53
51
  end
52
+
53
+ context 'for fractional percentage' do
54
+ let(:decimal) { 0.001 }
55
+ let(:percentage) { decimal * 100 }
56
+ let(:number_of_actors) { 10_000 }
57
+
58
+ let(:actors) do
59
+ (1..number_of_actors).map { |n| Flipper::Actor.new(n) }
60
+ end
61
+
62
+ subject { described_class.new }
63
+
64
+ it 'enables feature for accurate number of actors' do
65
+ margin_of_error = 0.02 * number_of_actors
66
+ expected_open_count = number_of_actors * decimal
67
+
68
+ open_count = actors.select do |actor|
69
+ context = context(percentage, :feature, actor)
70
+ subject.open?(context)
71
+ end.size
72
+
73
+ expect(open_count).to be_within(margin_of_error).of(expected_open_count)
74
+ end
75
+ end
54
76
  end
55
77
  end
@@ -6,4 +6,34 @@ RSpec.describe Flipper::Gates::PercentageOfTime do
6
6
  subject do
7
7
  described_class.new
8
8
  end
9
+
10
+ def context(percentage_of_time_value, feature = feature_name, thing = nil)
11
+ Flipper::FeatureCheckContext.new(
12
+ feature_name: feature,
13
+ values: Flipper::GateValues.new(percentage_of_time: percentage_of_time_value),
14
+ thing: thing || Flipper::Types::Actor.new(Flipper::Actor.new(1))
15
+ )
16
+ end
17
+
18
+ describe '#open?' do
19
+ context 'for fractional percentage' do
20
+ let(:decimal) { 0.001 }
21
+ let(:percentage) { decimal * 100 }
22
+ let(:number_of_invocations) { 10_000 }
23
+
24
+ subject { described_class.new }
25
+
26
+ it 'enables feature for accurate percentage of time' do
27
+ margin_of_error = 0.02 * number_of_invocations
28
+ expected_open_count = number_of_invocations * decimal
29
+
30
+ open_count = (1..number_of_invocations).select do |_actor|
31
+ context = context(percentage, :feature, Flipper::Actor.new("1"))
32
+ subject.open?(context)
33
+ end.size
34
+
35
+ expect(open_count).to be_within(margin_of_error).of(expected_open_count)
36
+ end
37
+ end
38
+ end
9
39
  end
@@ -36,6 +36,43 @@ RSpec.describe Flipper::Typecast do
36
36
  end
37
37
  end
38
38
 
39
+ {
40
+ nil => 0.0,
41
+ '' => 0.0,
42
+ 0 => 0.0,
43
+ 1 => 1.0,
44
+ 1.1 => 1.1,
45
+ '0.01' => 0.01,
46
+ '1' => 1.0,
47
+ '99' => 99.0,
48
+ }.each do |value, expected|
49
+ context "#to_float for #{value.inspect}" do
50
+ it "returns #{expected}" do
51
+ expect(described_class.to_float(value)).to be(expected)
52
+ end
53
+ end
54
+ end
55
+
56
+ {
57
+ nil => 0,
58
+ '' => 0,
59
+ 0 => 0,
60
+ 0.0 => 0.0,
61
+ 1 => 1,
62
+ 1.1 => 1.1,
63
+ '0.01' => 0.01,
64
+ '1' => 1,
65
+ '1.1' => 1.1,
66
+ '99' => 99,
67
+ '99.9' => 99.9,
68
+ }.each do |value, expected|
69
+ context "#to_percentage for #{value.inspect}" do
70
+ it "returns #{expected}" do
71
+ expect(described_class.to_percentage(value)).to be(expected)
72
+ end
73
+ end
74
+ end
75
+
39
76
  {
40
77
  nil => Set.new,
41
78
  '' => Set.new,
@@ -55,6 +92,24 @@ RSpec.describe Flipper::Typecast do
55
92
  end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
56
93
  end
57
94
 
95
+ it 'raises argument error for float value that cannot be converted to an float' do
96
+ expect do
97
+ described_class.to_float(['asdf'])
98
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to a float))
99
+ end
100
+
101
+ it 'raises argument error for bad integer percentage' do
102
+ expect do
103
+ described_class.to_percentage(['asdf'])
104
+ end.to raise_error(ArgumentError, %(["asdf"] cannot be converted to an integer))
105
+ end
106
+
107
+ it 'raises argument error for bad float percentage' do
108
+ expect do
109
+ described_class.to_percentage(['asdf.0'])
110
+ end.to raise_error(ArgumentError, %(["asdf.0"] cannot be converted to a float))
111
+ end
112
+
58
113
  it 'raises argument error for set value that cannot be converted to a set' do
59
114
  expect do
60
115
  described_class.to_set('asdf')
data/spec/flipper_spec.rb CHANGED
@@ -8,28 +8,173 @@ RSpec.describe Flipper do
8
8
  end
9
9
  end
10
10
 
11
- describe '.group_exists' do
12
- it 'returns true if the group is already created' do
13
- group = described_class.register('admins', &:admin?)
14
- expect(described_class.group_exists?(:admins)).to eq(true)
11
+ describe '.configure' do
12
+ it 'yield configuration instance' do
13
+ described_class.configure do |config|
14
+ expect(config).to be_instance_of(Flipper::Configuration)
15
+ end
15
16
  end
17
+ end
16
18
 
17
- it 'returns false when the group is not yet registered' do
18
- expect(described_class.group_exists?(:non_existing)).to eq(false)
19
+ describe '.configuration' do
20
+ it 'returns configuration instance' do
21
+ expect(described_class.configuration).to be_instance_of(Flipper::Configuration)
19
22
  end
20
23
  end
21
24
 
22
- describe '.groups_registry' do
23
- it 'returns a registry instance' do
24
- expect(described_class.groups_registry).to be_instance_of(Flipper::Registry)
25
+ describe '.configuration=' do
26
+ it "sets configuration instance" do
27
+ configuration = Flipper::Configuration.new
28
+ described_class.configuration = configuration
29
+ expect(described_class.configuration).to be(configuration)
25
30
  end
26
31
  end
27
32
 
28
- describe '.groups_registry=' do
29
- it 'sets groups_registry registry' do
30
- registry = Flipper::Registry.new
31
- described_class.groups_registry = registry
32
- expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
33
+ describe '.instance' do
34
+ it 'returns DSL instance using result of default invocation' do
35
+ instance = described_class.new(Flipper::Adapters::Memory.new)
36
+ described_class.configure do |config|
37
+ config.default { instance }
38
+ end
39
+ expect(described_class.instance).to be(instance)
40
+ expect(described_class.instance).to be(described_class.instance) # memoized
41
+ end
42
+ end
43
+
44
+ describe "delegation to instance" do
45
+ let(:group) { Flipper::Types::Group.new(:admins) }
46
+ let(:actor) { Flipper::Actor.new("1") }
47
+
48
+ before do
49
+ described_class.configure do |config|
50
+ config.default { described_class.new(Flipper::Adapters::Memory.new) }
51
+ end
52
+ end
53
+
54
+ it 'delegates enabled? to instance' do
55
+ expect(described_class.enabled?(:search)).to eq(described_class.instance.enabled?(:search))
56
+ described_class.instance.enable(:search)
57
+ expect(described_class.enabled?(:search)).to eq(described_class.instance.enabled?(:search))
58
+ end
59
+
60
+ it 'delegates enable to instance' do
61
+ described_class.enable(:search)
62
+ expect(described_class.instance.enabled?(:search)).to be(true)
63
+ end
64
+
65
+ it 'delegates disable to instance' do
66
+ described_class.disable(:search)
67
+ expect(described_class.instance.enabled?(:search)).to be(false)
68
+ end
69
+
70
+ it 'delegates bool to instance' do
71
+ expect(described_class.bool).to eq(described_class.instance.bool)
72
+ end
73
+
74
+ it 'delegates boolean to instance' do
75
+ expect(described_class.boolean).to eq(described_class.instance.boolean)
76
+ end
77
+
78
+ it 'delegates enable_actor to instance' do
79
+ described_class.enable_actor(:search, actor)
80
+ expect(described_class.instance.enabled?(:search, actor)).to be(true)
81
+ end
82
+
83
+ it 'delegates disable_actor to instance' do
84
+ described_class.disable_actor(:search, actor)
85
+ expect(described_class.instance.enabled?(:search, actor)).to be(false)
86
+ end
87
+
88
+ it 'delegates actor to instance' do
89
+ expect(described_class.actor(actor)).to eq(described_class.instance.actor(actor))
90
+ end
91
+
92
+ it 'delegates enable_group to instance' do
93
+ described_class.enable_group(:search, group)
94
+ expect(described_class.instance[:search].enabled_groups).to include(group)
95
+ end
96
+
97
+ it 'delegates disable_group to instance' do
98
+ described_class.disable_group(:search, group)
99
+ expect(described_class.instance[:search].enabled_groups).not_to include(group)
100
+ end
101
+
102
+ it 'delegates enable_percentage_of_actors to instance' do
103
+ described_class.enable_percentage_of_actors(:search, 5)
104
+ expect(described_class.instance[:search].percentage_of_actors_value).to be(5)
105
+ end
106
+
107
+ it 'delegates disable_percentage_of_actors to instance' do
108
+ described_class.disable_percentage_of_actors(:search)
109
+ expect(described_class.instance[:search].percentage_of_actors_value).to be(0)
110
+ end
111
+
112
+ it 'delegates actors to instance' do
113
+ expect(described_class.actors(5)).to eq(described_class.instance.actors(5))
114
+ end
115
+
116
+ it 'delegates percentage_of_actors to instance' do
117
+ expected = described_class.instance.percentage_of_actors(5)
118
+ expect(described_class.percentage_of_actors(5)).to eq(expected)
119
+ end
120
+
121
+ it 'delegates enable_percentage_of_time to instance' do
122
+ described_class.enable_percentage_of_time(:search, 5)
123
+ expect(described_class.instance[:search].percentage_of_time_value).to be(5)
124
+ end
125
+
126
+ it 'delegates disable_percentage_of_time to instance' do
127
+ described_class.disable_percentage_of_time(:search)
128
+ expect(described_class.instance[:search].percentage_of_time_value).to be(0)
129
+ end
130
+
131
+ it 'delegates time to instance' do
132
+ expect(described_class.time(56)).to eq(described_class.instance.time(56))
133
+ end
134
+
135
+ it 'delegates percentage_of_time to instance' do
136
+ expected = described_class.instance.percentage_of_time(56)
137
+ expect(described_class.percentage_of_time(56)).to eq(expected)
138
+ end
139
+
140
+ it 'delegates features to instance' do
141
+ described_class.instance.add(:search)
142
+ expect(described_class.features).to eq(described_class.instance.features)
143
+ expect(described_class.features).to include(described_class.instance[:search])
144
+ end
145
+
146
+ it 'delegates feature to instance' do
147
+ expect(described_class.feature(:search)).to eq(described_class.instance.feature(:search))
148
+ end
149
+
150
+ it 'delegates [] to instance' do
151
+ expect(described_class[:search]).to eq(described_class.instance[:search])
152
+ end
153
+
154
+ it 'delegates preload to instance' do
155
+ described_class.instance.enable(:search)
156
+ expect(described_class.preload([:search])).to eq(described_class.instance.preload([:search]))
157
+ end
158
+
159
+ it 'delegates preload_all to instance' do
160
+ described_class.instance.enable(:search)
161
+ described_class.instance.enable(:stats)
162
+ expect(described_class.preload_all).to eq(described_class.instance.preload_all)
163
+ end
164
+
165
+ it 'delegates add to instance' do
166
+ expect(described_class.add(:search)).to eq(described_class.instance.add(:search))
167
+ end
168
+
169
+ it 'delegates remove to instance' do
170
+ expect(described_class.remove(:search)).to eq(described_class.instance.remove(:search))
171
+ end
172
+
173
+ it 'delegates import to instance' do
174
+ other = described_class.new(Flipper::Adapters::Memory.new)
175
+ other.enable(:search)
176
+ described_class.import(other)
177
+ expect(described_class.enabled?(:search)).to be(true)
33
178
  end
34
179
  end
35
180
 
@@ -57,6 +202,28 @@ RSpec.describe Flipper do
57
202
  end
58
203
  end
59
204
 
205
+ describe '.groups' do
206
+ it 'returns array of group instances' do
207
+ admins = described_class.register(:admins, &:admin?)
208
+ preview_features = described_class.register(:preview_features, &:preview_features?)
209
+ expect(described_class.groups).to eq(Set[
210
+ admins,
211
+ preview_features,
212
+ ])
213
+ end
214
+ end
215
+
216
+ describe '.group_names' do
217
+ it 'returns array of group names' do
218
+ described_class.register(:admins, &:admin?)
219
+ described_class.register(:preview_features, &:preview_features?)
220
+ expect(described_class.group_names).to eq(Set[
221
+ :admins,
222
+ :preview_features,
223
+ ])
224
+ end
225
+ end
226
+
60
227
  describe '.unregister_groups' do
61
228
  it 'clear group registry' do
62
229
  expect(described_class.groups_registry).to receive(:clear)
@@ -64,6 +231,17 @@ RSpec.describe Flipper do
64
231
  end
65
232
  end
66
233
 
234
+ describe '.group_exists' do
235
+ it 'returns true if the group is already created' do
236
+ group = described_class.register('admins', &:admin?)
237
+ expect(described_class.group_exists?(:admins)).to eq(true)
238
+ end
239
+
240
+ it 'returns false when the group is not yet registered' do
241
+ expect(described_class.group_exists?(:non_existing)).to eq(false)
242
+ end
243
+ end
244
+
67
245
  describe '.group' do
68
246
  context 'for registered group' do
69
247
  before do
@@ -95,25 +273,17 @@ RSpec.describe Flipper do
95
273
  end
96
274
  end
97
275
 
98
- describe '.groups' do
99
- it 'returns array of group instances' do
100
- admins = described_class.register(:admins, &:admin?)
101
- preview_features = described_class.register(:preview_features, &:preview_features?)
102
- expect(described_class.groups).to eq(Set[
103
- admins,
104
- preview_features,
105
- ])
276
+ describe '.groups_registry' do
277
+ it 'returns a registry instance' do
278
+ expect(described_class.groups_registry).to be_instance_of(Flipper::Registry)
106
279
  end
107
280
  end
108
281
 
109
- describe '.group_names' do
110
- it 'returns array of group names' do
111
- described_class.register(:admins, &:admin?)
112
- described_class.register(:preview_features, &:preview_features?)
113
- expect(described_class.group_names).to eq(Set[
114
- :admins,
115
- :preview_features,
116
- ])
282
+ describe '.groups_registry=' do
283
+ it 'sets groups_registry registry' do
284
+ registry = Flipper::Registry.new
285
+ described_class.groups_registry = registry
286
+ expect(described_class.instance_variable_get('@groups_registry')).to eq(registry)
117
287
  end
118
288
  end
119
289
  end