flipper 0.9.2 → 0.10.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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
@@ -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] = gate(:actor).wrap(thing) unless thing.nil?
90
-
91
- open_gate = gates.detect { |gate|
92
- gate.open?(thing, values[gate.key], feature_name: @name)
93
- }
94
-
95
- if open_gate.nil?
96
- false
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
@@ -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?(thing, value, options = {})
26
- if thing.nil?
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?(thing, value, options = {})
27
- value
26
+ def open?(context)
27
+ context.values[key]
28
28
  end
29
29
 
30
30
  def wrap(thing)
@@ -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?(thing, value, options = {})
26
- if thing.nil?
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?(thing, value, options = {})
28
- if Types::Actor.wrappable?(thing)
29
- actor = Types::Actor.wrap(thing)
30
- feature_name = options.fetch(:feature_name)
31
- key = "#{feature_name}#{actor.value}"
32
- Zlib.crc32(key) % 100 < value
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?(thing, value, options = {})
25
+ def open?(context)
26
+ value = context.values[key]
26
27
  rand < (value / 100.0)
27
28
  end
28
29
 
@@ -1,6 +1,6 @@
1
1
  # Requires the following methods:
2
2
  # * subject - The instance of the adapter
3
- shared_examples_for 'a flipper adapter' do
3
+ RSpec.shared_examples_for 'a flipper adapter' do
4
4
  let(:actor_class) { Struct.new(:flipper_id) }
5
5
 
6
6
  let(:flipper) { Flipper.new(subject) }
@@ -11,11 +11,22 @@ module Flipper
11
11
  def initialize(name, &block)
12
12
  @name = name.to_sym
13
13
  @value = @name
14
- @block = block
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?(*args)
18
- @block.call(*args)
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
@@ -1,3 +1,3 @@
1
1
  module Flipper
2
- VERSION = "0.9.2".freeze
2
+ VERSION = "0.10.0".freeze
3
3
  end
@@ -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.wrap(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?(Object.new, true, feature_name: feature_name)).to eq(true)
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?(Object.new, false, feature_name: feature_name)).to eq(false)
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?(thing, Set[:newbs, :staff], feature_name: feature_name)).to eq(true)
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?(Object.new, Set[:stinkers], feature_name: feature_name)
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? actor, percentage_as_integer, feature_name: :name_one }
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? actor, percentage_as_integer, feature_name: :name_two }
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