flipper 0.9.2 → 0.10.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.
- checksums.yaml +4 -4
- data/Changelog.md +12 -0
- data/README.md +8 -0
- data/docs/Adapters.md +1 -1
- data/docs/api/README.md +775 -0
- data/examples/group_dynamic_lookup.rb +90 -0
- data/lib/flipper/feature.rb +12 -10
- data/lib/flipper/feature_check_context.rb +44 -0
- data/lib/flipper/gates/actor.rb +5 -4
- data/lib/flipper/gates/boolean.rb +2 -2
- data/lib/flipper/gates/group.rb +4 -3
- data/lib/flipper/gates/percentage_of_actors.rb +7 -6
- data/lib/flipper/gates/percentage_of_time.rb +2 -1
- data/lib/flipper/spec/shared_adapter_specs.rb +1 -1
- data/lib/flipper/types/group.rb +14 -3
- data/lib/flipper/version.rb +1 -1
- data/spec/flipper/feature_check_context_spec.rb +67 -0
- data/spec/flipper/feature_spec.rb +1 -1
- data/spec/flipper/gates/boolean_spec.rb +10 -2
- data/spec/flipper/gates/group_spec.rb +10 -2
- data/spec/flipper/gates/percentage_of_actors_spec.rb +10 -2
- data/spec/flipper/middleware/memoizer_spec.rb +1 -1
- data/spec/flipper/types/group_spec.rb +29 -4
- data/spec/helper.rb +4 -2
- metadata +7 -2
@@ -0,0 +1,90 @@
|
|
1
|
+
require File.expand_path('../example_setup', __FILE__)
|
2
|
+
|
3
|
+
require 'flipper'
|
4
|
+
require 'flipper/adapters/memory'
|
5
|
+
|
6
|
+
adapter = Flipper::Adapters::Memory.new
|
7
|
+
flipper = Flipper.new(adapter)
|
8
|
+
stats = flipper[:stats]
|
9
|
+
|
10
|
+
# Register group
|
11
|
+
Flipper.register(:enabled_team_member) do |actor, context|
|
12
|
+
combos = context.actors_value.map { |flipper_id| flipper_id.split(":", 2) }
|
13
|
+
team_names = combos.select { |class_name, id| class_name == "Team" }.map { |class_name, id| id }
|
14
|
+
teams = team_names.map { |name| Team.find(name) }
|
15
|
+
teams.any? { |team| team.member?(actor) }
|
16
|
+
end
|
17
|
+
|
18
|
+
# Some class that represents actor that will be trying to do something
|
19
|
+
class User
|
20
|
+
attr_reader :id
|
21
|
+
|
22
|
+
def initialize(id)
|
23
|
+
@id = id
|
24
|
+
end
|
25
|
+
|
26
|
+
def flipper_id
|
27
|
+
"User:#{@id}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
class Team
|
32
|
+
attr_reader :name
|
33
|
+
|
34
|
+
def self.all
|
35
|
+
@all ||= {}
|
36
|
+
end
|
37
|
+
|
38
|
+
def self.find(name)
|
39
|
+
all.fetch(name.to_s)
|
40
|
+
end
|
41
|
+
|
42
|
+
def initialize(name, members)
|
43
|
+
@name = name.to_s
|
44
|
+
@members = members
|
45
|
+
self.class.all[@name] = self
|
46
|
+
end
|
47
|
+
|
48
|
+
def id
|
49
|
+
@name
|
50
|
+
end
|
51
|
+
|
52
|
+
def member?(actor)
|
53
|
+
@members.map(&:id).include?(actor.id)
|
54
|
+
end
|
55
|
+
|
56
|
+
def flipper_id
|
57
|
+
"Team:#{@name}"
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
jnunemaker = User.new("jnunemaker")
|
62
|
+
jbarnette = User.new("jbarnette")
|
63
|
+
aroben = User.new("aroben")
|
64
|
+
|
65
|
+
core_app = Team.new(:core_app, [jbarnette, jnunemaker])
|
66
|
+
feature_flags = Team.new(:feature_flags, [aroben, jnunemaker])
|
67
|
+
|
68
|
+
stats.enable_actor jbarnette
|
69
|
+
|
70
|
+
actors = [jbarnette, jnunemaker, aroben]
|
71
|
+
|
72
|
+
actors.each do |actor|
|
73
|
+
if stats.enabled?(actor)
|
74
|
+
puts "stats are enabled for #{actor.id}"
|
75
|
+
else
|
76
|
+
puts "stats are NOT enabled for #{actor.id}"
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
puts "enabling team_actor group"
|
81
|
+
stats.enable_actor core_app
|
82
|
+
stats.enable_group :enabled_team_member
|
83
|
+
|
84
|
+
actors.each do |actor|
|
85
|
+
if stats.enabled?(actor)
|
86
|
+
puts "stats are enabled for #{actor.id}"
|
87
|
+
else
|
88
|
+
puts "stats are NOT enabled for #{actor.id}"
|
89
|
+
end
|
90
|
+
end
|
data/lib/flipper/feature.rb
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
require 'flipper/errors'
|
2
2
|
require 'flipper/type'
|
3
3
|
require 'flipper/gate'
|
4
|
+
require 'flipper/feature_check_context'
|
4
5
|
require 'flipper/gate_values'
|
5
6
|
require 'flipper/instrumenters/noop'
|
6
7
|
|
@@ -85,18 +86,19 @@ module Flipper
|
|
85
86
|
def enabled?(thing = nil)
|
86
87
|
instrument(:enabled?) { |payload|
|
87
88
|
values = gate_values
|
88
|
-
|
89
|
-
payload[:thing] =
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
else
|
89
|
+
thing = gate(:actor).wrap(thing) unless thing.nil?
|
90
|
+
payload[:thing] = thing
|
91
|
+
context = FeatureCheckContext.new(
|
92
|
+
feature_name: @name,
|
93
|
+
values: values,
|
94
|
+
thing: thing,
|
95
|
+
)
|
96
|
+
|
97
|
+
if open_gate = gates.detect { |gate| gate.open?(context) }
|
98
98
|
payload[:gate_name] = open_gate.name
|
99
99
|
true
|
100
|
+
else
|
101
|
+
false
|
100
102
|
end
|
101
103
|
}
|
102
104
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module Flipper
|
2
|
+
class FeatureCheckContext
|
3
|
+
# Public: The name of the feature.
|
4
|
+
attr_reader :feature_name
|
5
|
+
|
6
|
+
# Public: The GateValues instance that keeps track of the values for the
|
7
|
+
# gates for the feature.
|
8
|
+
attr_reader :values
|
9
|
+
|
10
|
+
# Public: The thing we want to know if a feature is enabled for.
|
11
|
+
attr_reader :thing
|
12
|
+
|
13
|
+
def initialize(options = {})
|
14
|
+
@feature_name = options.fetch(:feature_name)
|
15
|
+
@values = options.fetch(:values)
|
16
|
+
@thing = options.fetch(:thing)
|
17
|
+
end
|
18
|
+
|
19
|
+
# Public: Convenience method for groups value like Feature has.
|
20
|
+
def groups_value
|
21
|
+
values.groups
|
22
|
+
end
|
23
|
+
|
24
|
+
# Public: Convenience method for actors value value like Feature has.
|
25
|
+
def actors_value
|
26
|
+
values.actors
|
27
|
+
end
|
28
|
+
|
29
|
+
# Public: Convenience method for boolean value value like Feature has.
|
30
|
+
def boolean_value
|
31
|
+
values.boolean
|
32
|
+
end
|
33
|
+
|
34
|
+
# Public: Convenience method for percentage of actors value like Feature has.
|
35
|
+
def percentage_of_actors_value
|
36
|
+
values.percentage_of_actors
|
37
|
+
end
|
38
|
+
|
39
|
+
# Public: Convenience method for percentage of time value like Feature has.
|
40
|
+
def percentage_of_time_value
|
41
|
+
values.percentage_of_time
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
data/lib/flipper/gates/actor.rb
CHANGED
@@ -22,12 +22,13 @@ module Flipper
|
|
22
22
|
# Internal: Checks if the gate is open for a thing.
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
|
-
def open?(
|
26
|
-
|
25
|
+
def open?(context)
|
26
|
+
value = context.values[key]
|
27
|
+
if context.thing.nil?
|
27
28
|
false
|
28
29
|
else
|
29
|
-
if protects?(thing)
|
30
|
-
actor = wrap(thing)
|
30
|
+
if protects?(context.thing)
|
31
|
+
actor = wrap(context.thing)
|
31
32
|
enabled_actor_ids = value
|
32
33
|
enabled_actor_ids.include?(actor.value)
|
33
34
|
else
|
@@ -23,8 +23,8 @@ module Flipper
|
|
23
23
|
#
|
24
24
|
# Returns true if explicitly set to true, false if explicitly set to false
|
25
25
|
# or nil if not explicitly set.
|
26
|
-
def open?(
|
27
|
-
|
26
|
+
def open?(context)
|
27
|
+
context.values[key]
|
28
28
|
end
|
29
29
|
|
30
30
|
def wrap(thing)
|
data/lib/flipper/gates/group.rb
CHANGED
@@ -22,14 +22,15 @@ module Flipper
|
|
22
22
|
# Internal: Checks if the gate is open for a thing.
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
|
-
def open?(
|
26
|
-
|
25
|
+
def open?(context)
|
26
|
+
value = context.values[key]
|
27
|
+
if context.thing.nil?
|
27
28
|
false
|
28
29
|
else
|
29
30
|
value.any? { |name|
|
30
31
|
begin
|
31
32
|
group = Flipper.group(name)
|
32
|
-
group.match?(thing)
|
33
|
+
group.match?(context.thing, context)
|
33
34
|
rescue GroupNotRegistered
|
34
35
|
false
|
35
36
|
end
|
@@ -24,12 +24,13 @@ module Flipper
|
|
24
24
|
# Internal: Checks if the gate is open for a thing.
|
25
25
|
#
|
26
26
|
# Returns true if gate open for thing, false if not.
|
27
|
-
def open?(
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
27
|
+
def open?(context)
|
28
|
+
percentage = context.values[key]
|
29
|
+
|
30
|
+
if Types::Actor.wrappable?(context.thing)
|
31
|
+
actor = Types::Actor.wrap(context.thing)
|
32
|
+
key = "#{context.feature_name}#{actor.value}"
|
33
|
+
Zlib.crc32(key) % 100 < percentage
|
33
34
|
else
|
34
35
|
false
|
35
36
|
end
|
@@ -22,7 +22,8 @@ module Flipper
|
|
22
22
|
# Internal: Checks if the gate is open for a thing.
|
23
23
|
#
|
24
24
|
# Returns true if gate open for thing, false if not.
|
25
|
-
def open?(
|
25
|
+
def open?(context)
|
26
|
+
value = context.values[key]
|
26
27
|
rand < (value / 100.0)
|
27
28
|
end
|
28
29
|
|
data/lib/flipper/types/group.rb
CHANGED
@@ -11,11 +11,22 @@ module Flipper
|
|
11
11
|
def initialize(name, &block)
|
12
12
|
@name = name.to_sym
|
13
13
|
@value = @name
|
14
|
-
|
14
|
+
|
15
|
+
if block_given?
|
16
|
+
@block = block
|
17
|
+
@single_argument = @block.arity == 1
|
18
|
+
else
|
19
|
+
@block = lambda { |thing, context| false }
|
20
|
+
@single_argument = false
|
21
|
+
end
|
15
22
|
end
|
16
23
|
|
17
|
-
def match?(
|
18
|
-
@
|
24
|
+
def match?(thing, context)
|
25
|
+
if @single_argument
|
26
|
+
@block.call(thing)
|
27
|
+
else
|
28
|
+
@block.call(thing, context)
|
29
|
+
end
|
19
30
|
end
|
20
31
|
end
|
21
32
|
end
|
data/lib/flipper/version.rb
CHANGED
@@ -0,0 +1,67 @@
|
|
1
|
+
require 'helper'
|
2
|
+
|
3
|
+
RSpec.describe Flipper::FeatureCheckContext do
|
4
|
+
let(:feature_name) { :new_profiles }
|
5
|
+
let(:values) { Flipper::GateValues.new({}) }
|
6
|
+
let(:thing) { Struct.new(:flipper_id).new("5") }
|
7
|
+
let(:options) {
|
8
|
+
{
|
9
|
+
feature_name: feature_name,
|
10
|
+
values: values,
|
11
|
+
thing: thing,
|
12
|
+
}
|
13
|
+
}
|
14
|
+
|
15
|
+
it "initializes just fine" do
|
16
|
+
instance = described_class.new(options)
|
17
|
+
expect(instance.feature_name).to eq(feature_name)
|
18
|
+
expect(instance.values).to eq(values)
|
19
|
+
expect(instance.thing).to eq(thing)
|
20
|
+
end
|
21
|
+
|
22
|
+
it "requires feature_name" do
|
23
|
+
options.delete(:feature_name)
|
24
|
+
expect {
|
25
|
+
described_class.new(options)
|
26
|
+
}.to raise_error(KeyError)
|
27
|
+
end
|
28
|
+
|
29
|
+
it "requires values" do
|
30
|
+
options.delete(:values)
|
31
|
+
expect {
|
32
|
+
described_class.new(options)
|
33
|
+
}.to raise_error(KeyError)
|
34
|
+
end
|
35
|
+
|
36
|
+
it "requires thing" do
|
37
|
+
options.delete(:thing)
|
38
|
+
expect {
|
39
|
+
described_class.new(options)
|
40
|
+
}.to raise_error(KeyError)
|
41
|
+
end
|
42
|
+
|
43
|
+
it "knows actors_value" do
|
44
|
+
instance = described_class.new(options.merge(values: Flipper::GateValues.new({actors: Set["User:1"]})))
|
45
|
+
expect(instance.actors_value).to eq(Set["User:1"])
|
46
|
+
end
|
47
|
+
|
48
|
+
it "knows groups_value" do
|
49
|
+
instance = described_class.new(options.merge(values: Flipper::GateValues.new({groups: Set["admins"]})))
|
50
|
+
expect(instance.groups_value).to eq(Set["admins"])
|
51
|
+
end
|
52
|
+
|
53
|
+
it "knows boolean_value" do
|
54
|
+
instance = described_class.new(options.merge(values: Flipper::GateValues.new({boolean: true})))
|
55
|
+
expect(instance.boolean_value).to eq(true)
|
56
|
+
end
|
57
|
+
|
58
|
+
it "knows percentage_of_actors_value" do
|
59
|
+
instance = described_class.new(options.merge(values: Flipper::GateValues.new({percentage_of_actors: 14})))
|
60
|
+
expect(instance.percentage_of_actors_value).to eq(14)
|
61
|
+
end
|
62
|
+
|
63
|
+
it "knows percentage_of_time_value" do
|
64
|
+
instance = described_class.new(options.merge(values: Flipper::GateValues.new({percentage_of_time: 41})))
|
65
|
+
expect(instance.percentage_of_time_value).to eq(41)
|
66
|
+
end
|
67
|
+
end
|
@@ -606,7 +606,7 @@ RSpec.describe Flipper::Feature do
|
|
606
606
|
context "with actor instance" do
|
607
607
|
it "updates the gate values to include the actor" do
|
608
608
|
actor = Struct.new(:flipper_id).new(5)
|
609
|
-
instance = Flipper::Types::Actor.
|
609
|
+
instance = Flipper::Types::Actor.new(actor)
|
610
610
|
expect(subject.gate_values.actors).to be_empty
|
611
611
|
subject.enable_actor(instance)
|
612
612
|
expect(subject.gate_values.actors).to eq(Set["5"])
|
@@ -7,6 +7,14 @@ RSpec.describe Flipper::Gates::Boolean do
|
|
7
7
|
described_class.new
|
8
8
|
}
|
9
9
|
|
10
|
+
def context(bool)
|
11
|
+
Flipper::FeatureCheckContext.new(
|
12
|
+
feature_name: feature_name,
|
13
|
+
values: Flipper::GateValues.new({boolean: bool}),
|
14
|
+
thing: Flipper::Types::Actor.new(Struct.new(:flipper_id).new(1)),
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
10
18
|
describe "#enabled?" do
|
11
19
|
context "for true value" do
|
12
20
|
it "returns true" do
|
@@ -24,13 +32,13 @@ RSpec.describe Flipper::Gates::Boolean do
|
|
24
32
|
describe "#open?" do
|
25
33
|
context "for true value" do
|
26
34
|
it "returns true" do
|
27
|
-
expect(subject.open?(
|
35
|
+
expect(subject.open?(context(true))).to be(true)
|
28
36
|
end
|
29
37
|
end
|
30
38
|
|
31
39
|
context "for false value" do
|
32
40
|
it "returns false" do
|
33
|
-
expect(subject.open?(
|
41
|
+
expect(subject.open?(context(false))).to be(false)
|
34
42
|
end
|
35
43
|
end
|
36
44
|
end
|
@@ -7,6 +7,14 @@ RSpec.describe Flipper::Gates::Group do
|
|
7
7
|
described_class.new
|
8
8
|
}
|
9
9
|
|
10
|
+
def context(set)
|
11
|
+
Flipper::FeatureCheckContext.new(
|
12
|
+
feature_name: feature_name,
|
13
|
+
values: Flipper::GateValues.new({groups: set}),
|
14
|
+
thing: Flipper::Types::Actor.new(Struct.new(:flipper_id).new("5")),
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
10
18
|
describe "#open?" do
|
11
19
|
context "with a group in adapter, but not registered" do
|
12
20
|
before do
|
@@ -15,7 +23,7 @@ RSpec.describe Flipper::Gates::Group do
|
|
15
23
|
|
16
24
|
it "ignores group" do
|
17
25
|
thing = Struct.new(:flipper_id).new('5')
|
18
|
-
expect(subject.open?(
|
26
|
+
expect(subject.open?(context(Set[:newbs, :staff]))).to be(true)
|
19
27
|
end
|
20
28
|
end
|
21
29
|
|
@@ -26,7 +34,7 @@ RSpec.describe Flipper::Gates::Group do
|
|
26
34
|
|
27
35
|
it "raises error" do
|
28
36
|
expect {
|
29
|
-
subject.open?(
|
37
|
+
subject.open?(context(Set[:stinkers]))
|
30
38
|
}.to raise_error(NoMethodError)
|
31
39
|
end
|
32
40
|
end
|
@@ -7,6 +7,14 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
7
7
|
described_class.new
|
8
8
|
}
|
9
9
|
|
10
|
+
def context(integer, feature = feature_name, thing = nil)
|
11
|
+
Flipper::FeatureCheckContext.new(
|
12
|
+
feature_name: feature,
|
13
|
+
values: Flipper::GateValues.new({percentage_of_actors: integer}),
|
14
|
+
thing: thing || Flipper::Types::Actor.new(Struct.new(:flipper_id).new(1)),
|
15
|
+
)
|
16
|
+
end
|
17
|
+
|
10
18
|
describe "#open?" do
|
11
19
|
context "when compared against two features" do
|
12
20
|
let(:percentage) { 0.05 }
|
@@ -19,12 +27,12 @@ RSpec.describe Flipper::Gates::PercentageOfActors do
|
|
19
27
|
|
20
28
|
let(:feature_one_enabled_actors) do
|
21
29
|
gate = described_class.new
|
22
|
-
actors.select { |actor| gate.open?
|
30
|
+
actors.select { |actor| gate.open? context(percentage_as_integer, :name_one, actor) }
|
23
31
|
end
|
24
32
|
|
25
33
|
let(:feature_two_enabled_actors) do
|
26
34
|
gate = described_class.new
|
27
|
-
actors.select { |actor| gate.open?
|
35
|
+
actors.select { |actor| gate.open? context(percentage_as_integer, :name_two, actor) }
|
28
36
|
end
|
29
37
|
|
30
38
|
it "does not enable both features for same set of actors" do
|