Skip to content

Commit

Permalink
Merge pull request #158 from jnunemaker/context
Browse files Browse the repository at this point in the history
Feature Check Context
  • Loading branch information
greatuserongithub authored Sep 24, 2016
2 parents 7bde3e5 + ac2cb1f commit cb7ba8f
Show file tree
Hide file tree
Showing 15 changed files with 307 additions and 40 deletions.
90 changes: 90 additions & 0 deletions examples/group_dynamic_lookup.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
require File.expand_path('../example_setup', __FILE__)

require 'flipper'
require 'flipper/adapters/memory'

adapter = Flipper::Adapters::Memory.new
flipper = Flipper.new(adapter)
stats = flipper[:stats]

# Register group
Flipper.register(:enabled_team_member) do |actor, context|
combos = context.actors_value.map { |flipper_id| flipper_id.split(":", 2) }
team_names = combos.select { |class_name, id| class_name == "Team" }.map { |class_name, id| id }
teams = team_names.map { |name| Team.find(name) }
teams.any? { |team| team.member?(actor) }
end

# Some class that represents actor that will be trying to do something
class User
attr_reader :id

def initialize(id)
@id = id
end

def flipper_id
"User:#{@id}"
end
end

class Team
attr_reader :name

def self.all
@all ||= {}
end

def self.find(name)
all.fetch(name.to_s)
end

def initialize(name, members)
@name = name.to_s
@members = members
self.class.all[@name] = self
end

def id
@name
end

def member?(actor)
@members.map(&:id).include?(actor.id)
end

def flipper_id
"Team:#{@name}"
end
end

jnunemaker = User.new("jnunemaker")
jbarnette = User.new("jbarnette")
aroben = User.new("aroben")

core_app = Team.new(:core_app, [jbarnette, jnunemaker])
feature_flags = Team.new(:feature_flags, [aroben, jnunemaker])

stats.enable_actor jbarnette

actors = [jbarnette, jnunemaker, aroben]

actors.each do |actor|
if stats.enabled?(actor)
puts "stats are enabled for #{actor.id}"
else
puts "stats are NOT enabled for #{actor.id}"
end
end

puts "enabling team_actor group"
stats.enable_actor core_app
stats.enable_group :enabled_team_member

actors.each do |actor|
if stats.enabled?(actor)
puts "stats are enabled for #{actor.id}"
else
puts "stats are NOT enabled for #{actor.id}"
end
end
22 changes: 12 additions & 10 deletions lib/flipper/feature.rb
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
require 'flipper/errors'
require 'flipper/type'
require 'flipper/gate'
require 'flipper/feature_check_context'
require 'flipper/gate_values'
require 'flipper/instrumenters/noop'

Expand Down Expand Up @@ -85,18 +86,19 @@ def remove
def enabled?(thing = nil)
instrument(:enabled?) { |payload|
values = gate_values

payload[:thing] = gate(:actor).wrap(thing) unless thing.nil?

open_gate = gates.detect { |gate|
gate.open?(thing, values[gate.key], feature_name: @name)
}

if open_gate.nil?
false
else
thing = gate(:actor).wrap(thing) unless thing.nil?
payload[:thing] = thing
context = FeatureCheckContext.new(
feature_name: @name,
values: values,
thing: thing,
)

if open_gate = gates.detect { |gate| gate.open?(context) }
payload[:gate_name] = open_gate.name
true
else
false
end
}
end
Expand Down
44 changes: 44 additions & 0 deletions lib/flipper/feature_check_context.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
module Flipper
class FeatureCheckContext
# Public: The name of the feature.
attr_reader :feature_name

# Public: The GateValues instance that keeps track of the values for the
# gates for the feature.
attr_reader :values

# Public: The thing we want to know if a feature is enabled for.
attr_reader :thing

def initialize(options = {})
@feature_name = options.fetch(:feature_name)
@values = options.fetch(:values)
@thing = options.fetch(:thing)
end

# Public: Convenience method for groups value like Feature has.
def groups_value
values.groups
end

# Public: Convenience method for actors value value like Feature has.
def actors_value
values.actors
end

# Public: Convenience method for boolean value value like Feature has.
def boolean_value
values.boolean
end

# Public: Convenience method for percentage of actors value like Feature has.
def percentage_of_actors_value
values.percentage_of_actors
end

# Public: Convenience method for percentage of time value like Feature has.
def percentage_of_time_value
values.percentage_of_time
end
end
end
9 changes: 5 additions & 4 deletions lib/flipper/gates/actor.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,13 @@ def enabled?(value)
# Internal: Checks if the gate is open for a thing.
#
# Returns true if gate open for thing, false if not.
def open?(thing, value, options = {})
if thing.nil?
def open?(context)
value = context.values[key]
if context.thing.nil?
false
else
if protects?(thing)
actor = wrap(thing)
if protects?(context.thing)
actor = wrap(context.thing)
enabled_actor_ids = value
enabled_actor_ids.include?(actor.value)
else
Expand Down
4 changes: 2 additions & 2 deletions lib/flipper/gates/boolean.rb
Original file line number Diff line number Diff line change
Expand Up @@ -23,8 +23,8 @@ def enabled?(value)
#
# Returns true if explicitly set to true, false if explicitly set to false
# or nil if not explicitly set.
def open?(thing, value, options = {})
value
def open?(context)
context.values[key]
end

def wrap(thing)
Expand Down
7 changes: 4 additions & 3 deletions lib/flipper/gates/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,14 +22,15 @@ def enabled?(value)
# Internal: Checks if the gate is open for a thing.
#
# Returns true if gate open for thing, false if not.
def open?(thing, value, options = {})
if thing.nil?
def open?(context)
value = context.values[key]
if context.thing.nil?
false
else
value.any? { |name|
begin
group = Flipper.group(name)
group.match?(thing)
group.match?(context.thing, context)
rescue GroupNotRegistered
false
end
Expand Down
13 changes: 7 additions & 6 deletions lib/flipper/gates/percentage_of_actors.rb
Original file line number Diff line number Diff line change
Expand Up @@ -24,12 +24,13 @@ def enabled?(value)
# Internal: Checks if the gate is open for a thing.
#
# Returns true if gate open for thing, false if not.
def open?(thing, value, options = {})
if Types::Actor.wrappable?(thing)
actor = Types::Actor.wrap(thing)
feature_name = options.fetch(:feature_name)
key = "#{feature_name}#{actor.value}"
Zlib.crc32(key) % 100 < value
def open?(context)
percentage = context.values[key]

if Types::Actor.wrappable?(context.thing)
actor = Types::Actor.wrap(context.thing)
key = "#{context.feature_name}#{actor.value}"
Zlib.crc32(key) % 100 < percentage
else
false
end
Expand Down
3 changes: 2 additions & 1 deletion lib/flipper/gates/percentage_of_time.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,8 @@ def enabled?(value)
# Internal: Checks if the gate is open for a thing.
#
# Returns true if gate open for thing, false if not.
def open?(thing, value, options = {})
def open?(context)
value = context.values[key]
rand < (value / 100.0)
end

Expand Down
17 changes: 14 additions & 3 deletions lib/flipper/types/group.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,22 @@ def self.wrap(group_or_name)
def initialize(name, &block)
@name = name.to_sym
@value = @name
@block = block

if block_given?
@block = block
@single_argument = @block.arity == 1
else
@block = lambda { |thing, context| false }
@single_argument = false
end
end

def match?(*args)
@block.call(*args)
def match?(thing, context)
if @single_argument
@block.call(thing)
else
@block.call(thing, context)
end
end
end
end
Expand Down
67 changes: 67 additions & 0 deletions spec/flipper/feature_check_context_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
require 'helper'

RSpec.describe Flipper::FeatureCheckContext do
let(:feature_name) { :new_profiles }
let(:values) { Flipper::GateValues.new({}) }
let(:thing) { Struct.new(:flipper_id).new("5") }
let(:options) {
{
feature_name: feature_name,
values: values,
thing: thing,
}
}

it "initializes just fine" do
instance = described_class.new(options)
expect(instance.feature_name).to eq(feature_name)
expect(instance.values).to eq(values)
expect(instance.thing).to eq(thing)
end

it "requires feature_name" do
options.delete(:feature_name)
expect {
described_class.new(options)
}.to raise_error(KeyError)
end

it "requires values" do
options.delete(:values)
expect {
described_class.new(options)
}.to raise_error(KeyError)
end

it "requires thing" do
options.delete(:thing)
expect {
described_class.new(options)
}.to raise_error(KeyError)
end

it "knows actors_value" do
instance = described_class.new(options.merge(values: Flipper::GateValues.new({actors: Set["User:1"]})))
expect(instance.actors_value).to eq(Set["User:1"])
end

it "knows groups_value" do
instance = described_class.new(options.merge(values: Flipper::GateValues.new({groups: Set["admins"]})))
expect(instance.groups_value).to eq(Set["admins"])
end

it "knows boolean_value" do
instance = described_class.new(options.merge(values: Flipper::GateValues.new({boolean: true})))
expect(instance.boolean_value).to eq(true)
end

it "knows percentage_of_actors_value" do
instance = described_class.new(options.merge(values: Flipper::GateValues.new({percentage_of_actors: 14})))
expect(instance.percentage_of_actors_value).to eq(14)
end

it "knows percentage_of_time_value" do
instance = described_class.new(options.merge(values: Flipper::GateValues.new({percentage_of_time: 41})))
expect(instance.percentage_of_time_value).to eq(41)
end
end
2 changes: 1 addition & 1 deletion spec/flipper/feature_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -606,7 +606,7 @@
context "with actor instance" do
it "updates the gate values to include the actor" do
actor = Struct.new(:flipper_id).new(5)
instance = Flipper::Types::Actor.wrap(actor)
instance = Flipper::Types::Actor.new(actor)
expect(subject.gate_values.actors).to be_empty
subject.enable_actor(instance)
expect(subject.gate_values.actors).to eq(Set["5"])
Expand Down
12 changes: 10 additions & 2 deletions spec/flipper/gates/boolean_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,14 @@
described_class.new
}

def context(bool)
Flipper::FeatureCheckContext.new(
feature_name: feature_name,
values: Flipper::GateValues.new({boolean: bool}),
thing: Flipper::Types::Actor.new(Struct.new(:flipper_id).new(1)),
)
end

describe "#enabled?" do
context "for true value" do
it "returns true" do
Expand All @@ -24,13 +32,13 @@
describe "#open?" do
context "for true value" do
it "returns true" do
expect(subject.open?(Object.new, true, feature_name: feature_name)).to eq(true)
expect(subject.open?(context(true))).to be(true)
end
end

context "for false value" do
it "returns false" do
expect(subject.open?(Object.new, false, feature_name: feature_name)).to eq(false)
expect(subject.open?(context(false))).to be(false)
end
end
end
Expand Down
Loading

0 comments on commit cb7ba8f

Please sign in to comment.