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.
@@ -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