flipper 0.9.2 → 0.10.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|