Skip to content

Commit

Permalink
Update feature definition with location.
Browse files Browse the repository at this point in the history
  • Loading branch information
rolftimmermans committed Oct 20, 2017
1 parent 5181576 commit a6e639c
Show file tree
Hide file tree
Showing 7 changed files with 111 additions and 34 deletions.
5 changes: 5 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,11 @@ end
This file is automatically reloaded in development mode. No need to restart
your server after making changes.

Feature definitions support these options:
* `:default` – The feature's default value. This is the value of the feature if no strategy configures an explicit value. Defaults to `false`.
* `:description` – An optional description of the feature. Displayed on the dashboard if present.
* `:title` – An optional title of the feature. This defaults to a humanized version of the feature name. Displayed on the dashboard.

## Strategies

The following strategies are provided:
Expand Down
8 changes: 4 additions & 4 deletions app/controllers/flipflop/strategies_controller.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ class StrategiesController < ApplicationController
include ActionController::RequestForgeryProtection

def update
strategy.switch!(feature_key, enable?)
FeatureSet.current.switch!(feature_key, strategy_key, enable?)
redirect_to(features_url)
end

def destroy
strategy.clear!(feature_key)
FeatureSet.current.clear!(feature_key, strategy_key)
redirect_to(features_url)
end

Expand All @@ -27,8 +27,8 @@ def feature_key
params[:feature_id].to_sym
end

def strategy
FeatureSet.current.strategy(params[:id])
def strategy_key
params[:id]
end
end
end
9 changes: 7 additions & 2 deletions lib/flipflop/feature_definition.rb
Original file line number Diff line number Diff line change
@@ -1,14 +1,19 @@
module Flipflop
class FeatureDefinition
attr_reader :key, :name, :title, :description, :default, :group
attr_reader :key, :name, :title, :description, :default, :group, :location

def initialize(key, **options)
@key = key
@name = @key.to_s.freeze
@title = @name.humanize.freeze
@title = options.delete(:title).freeze || @name.humanize.freeze
@description = options.delete(:description).freeze
@default = !!options.delete(:default) || false
@group = options.delete(:group).freeze
@location = caller_locations(3, 1).first.freeze

if options.any?
raise FeatureError.new(name, "has unknown option #{options.keys.map(&:inspect) * ', '}")
end
end
end
end
51 changes: 37 additions & 14 deletions lib/flipflop/feature_set.rb
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
module Flipflop
class FeatureError < StandardError
def initialize(feature, error)
super("Feature '#{feature}' #{error}.")
def initialize(key, error)
super("Feature '#{key}' #{error}.")
end
end

class StrategyError < StandardError
def initialize(strategy, error)
super("Strategy '#{strategy}' #{error}.")
def initialize(key, error)
super("Strategy '#{key}' #{error}.")
end
end

class Callback < StandardError
def initialize(key, error)
super("Callback '#{key}' #{error}.")
end
end

Expand Down Expand Up @@ -70,34 +76,51 @@ def use(strategy)
end
end

def enabled?(feature)
FeatureCache.current.fetch(feature) do
def enabled?(feature_key)
FeatureCache.current.fetch(feature_key) do
feature = feature(feature_key)

result = @strategies.each_value.inject(nil) do |status, strategy|
break status unless status.nil?
strategy.enabled?(feature)
strategy.enabled?(feature_key)
end
result.nil? ? feature(feature).default : result

result.nil? ? feature.default : result
end
end

def feature(feature)
@features.fetch(feature) do
raise FeatureError.new(feature, "unknown")
def feature(feature_key)
@features.fetch(feature_key) do
raise FeatureError.new(feature_key, "unknown")
end
end

def features
@features.values
end

def strategy(strategy)
@strategies.fetch(strategy) do
raise StrategyError.new(strategy, "unknown")
def strategy(strategy_key)
@strategies.fetch(strategy_key) do
raise StrategyError.new(strategy_key, "unknown")
end
end

def strategies
@strategies.values
end

def switch!(feature_key, strategy_key, value)
strategy = strategy(strategy_key)
feature = feature(feature_key)

strategy.switch!(feature_key, value)
end

def clear!(feature_key, strategy_key)
strategy = strategy(strategy_key)
feature = feature(feature_key)

strategy.clear!(feature_key)
end
end
end
29 changes: 15 additions & 14 deletions lib/generators/flipflop/install/install_generator.rb
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,27 @@ def invoke_generators
end

def configure_dashboard
comment = <<-RUBY
# Replace with a lambda or method name defined in ApplicationController
# to implement access control for the Flipflop dashboard.
RUBY

forbidden = <<-RUBY
config.flipflop.dashboard_access_filter = -> { head :forbidden }
RUBY

allowed = <<-RUBY
config.flipflop.dashboard_access_filter = nil
RUBY
app = tmpl("-> { head :forbidden }")
env_dev_test = tmpl("nil")

environment(indent(comment + forbidden + "\n", 4).lstrip)
environment(indent(comment + allowed + "\n", 2).lstrip, env: [:development, :test])
environment(indent(app + "\n", 4).lstrip)
environment(indent(env_dev_test + "\n", 2).lstrip, env: [:development, :test])
end

private

def tmpl(access_filter)
return <<-RUBY
# Before filter for Flipflop dashboard. Replace with a lambda or method name
# defined in ApplicationController to implement access control.
config.flipflop.dashboard_access_filter = #{access_filter}
RUBY
end

def indent(content, multiplier = 2)
# Don't fix indentation if Rails already does this (5.2+).
return content if respond_to?(:optimize_indentation, true)

spaces = " " * multiplier
content.each_line.map {|line| line.blank? ? line : "#{spaces}#{line}" }.join
end
Expand Down
17 changes: 17 additions & 0 deletions test/integration/app_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,23 @@
@app
end

describe "configuration" do
it "should be added to dev" do
assert_match /^ config\.flipflop\.dashboard_access_filter = nil$/,
File.read("config/environments/development.rb")
end

it "should be added to test" do
assert_match /^ config\.flipflop\.dashboard_access_filter = nil$/,
File.read("config/environments/test.rb")
end

it "should be added to app" do
assert_match /^ config\.flipflop\.dashboard_access_filter = -> \{ head :forbidden \}$/,
File.read("config/application.rb")
end
end

describe "middleware" do
it "should include cache middleware" do
middlewares = Rails.application.middleware.map(&:klass)
Expand Down
26 changes: 26 additions & 0 deletions test/unit/feature_definition_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@
it "should have no group" do
assert_nil subject.group
end

it "should have location" do
# Because we have no indirection via FeatureSet, the location is minitest.
assert_equal "instance_eval", subject.location.label
end
end

describe "with options" do
Expand Down Expand Up @@ -63,5 +68,26 @@
it "should have specified group" do
assert_equal :my_group, subject.group.key
end

it "should have location" do
# Because we have no indirection via FeatureSet, the location is minitest.
assert_equal "instance_eval", subject.location.label
end
end

describe "with unknown options" do
subject do
Flipflop::FeatureDefinition.new(:my_key,
unknown: "one",
other: "two",
)
end

it "should raise error with message" do
error = assert_raises Flipflop::FeatureError do
subject
end
assert_equal "Feature 'my_key' has unknown option :unknown, :other.", error.message
end
end
end

0 comments on commit a6e639c

Please sign in to comment.