diff --git a/README.md b/README.md index 887c57a..9f98b41 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ There are three layers of strategies per feature: * default * database, to flip features site-wide for all users -* cookie, to flip features just for you (or someone else) +* cookie/session, uses cookies or session to flip features just for you (or someone else) There is also a configurable system-wide default - !Rails.env.production?` works nicely. @@ -30,10 +30,10 @@ Install # Gemfile gem "flip" - + # Generate the model and migration > rails g flip:install - + # Run the migration > rake db:migrate @@ -49,7 +49,7 @@ class Feature < ActiveRecord::Base include Flip::Declarable # The recommended Flip strategy stack. - strategy Flip::CookieStrategy + strategy Flip::CookieStrategy # alternatively can use strategy Flip::CookieStrategy for session-based strategy Flip::DatabaseStrategy strategy Flip::DefaultStrategy default false diff --git a/lib/flip.rb b/lib/flip.rb index b91eb86..516a668 100644 --- a/lib/flip.rb +++ b/lib/flip.rb @@ -18,6 +18,7 @@ facade feature_set forbidden + session_strategy }.each { |name| require "flip/#{name}" } require "flip/engine" if defined?(Rails) diff --git a/lib/flip/declarable.rb b/lib/flip/declarable.rb index 8a5331c..4f26e18 100644 --- a/lib/flip/declarable.rb +++ b/lib/flip/declarable.rb @@ -13,6 +13,7 @@ def feature(key, options = {}) # Adds a strategy for determining feature status. def strategy(strategy) FeatureSet.instance.add_strategy strategy + ActionController::Base.send(:include, "#{strategy}::Loader".constantize) if [Flip::SessionStrategy, Flip::CookieStrategy].include? strategy end # The default response, boolean or a Proc to be called. diff --git a/lib/flip/engine.rb b/lib/flip/engine.rb index 655d165..b36e830 100644 --- a/lib/flip/engine.rb +++ b/lib/flip/engine.rb @@ -1,9 +1,4 @@ module Flip class Engine < ::Rails::Engine - - initializer "flip.blarg" do - ActionController::Base.send(:include, Flip::CookieStrategy::Loader) - end - end end diff --git a/lib/flip/session_strategy.rb b/lib/flip/session_strategy.rb new file mode 100644 index 0000000..9594c93 --- /dev/null +++ b/lib/flip/session_strategy.rb @@ -0,0 +1,64 @@ +# Uses session to determine feature state. +module Flip + class SessionStrategy < AbstractStrategy + + def description + "Uses session cookie to apply only to your session." + end + + def knows? definition + session.key? session_name(definition) + end + + def on? definition + feature = session[session_name(definition)] + feature_value = feature.is_a?(Hash) ? feature['value'] : feature + feature_value === 'true' + end + + def switchable? + true + end + + def switch! key, on + session[session_name(key)] = on ? "true" : "false" + end + + def delete! key + session.delete session_name(key) + end + + def self.session= session + @session = session + end + + def session_name(definition) + definition = definition.key unless definition.is_a? Symbol + "flip_#{definition}" + end + + private + + def session + result = self.class.instance_variable_get(:@session) || {} + end + + # Include in ApplicationController to push cookies into CookieStrategy. + # Users before_filter and after_filter rather than around_filter to + # avoid pointlessly adding to stack depth. + module Loader + extend ActiveSupport::Concern + included do + before_filter :flip_session_strategy_before + after_filter :flip_session_strategy_after + end + def flip_session_strategy_before + SessionStrategy.session = session + end + def flip_session_strategy_after + SessionStrategy.session = nil + end + end + + end +end diff --git a/spec/session_strategy_spec.rb b/spec/session_strategy_spec.rb new file mode 100644 index 0000000..5c90a6f --- /dev/null +++ b/spec/session_strategy_spec.rb @@ -0,0 +1,110 @@ +require "spec_helper" + +class ControllerWithoutSessionStrategy; end +class ControllerWithSessionStrategy + def self.before_filter(_); end + def self.after_filter(_); end + def session; []; end + include Flip::SessionStrategy::Loader +end + +describe Flip::SessionStrategy do + + let(:session) do + { strategy.session_name(:one) => "true", + strategy.session_name(:two) => "false" } + end + let(:strategy) do + Flip::SessionStrategy.new.tap do |s| + s.stub(:session) { session } + end + end + + its(:description) { should be_present } + it { should be_switchable } + describe "session interrogration" do + context "enabled feature" do + specify "#knows? is true" do + strategy.knows?(:one).should be true + end + specify "#on? is true" do + strategy.on?(:one).should be true + end + end + context "disabled feature" do + specify "#knows? is true" do + strategy.knows?(:two).should be true + end + specify "#on? is false" do + strategy.on?(:two).should be false + end + end + context "feature with no session present" do + specify "#knows? is false" do + strategy.knows?(:three).should be false + end + specify "#on? is false" do + strategy.on?(:three).should be false + end + end + end + + describe "session manipulation" do + it "can switch known features on" do + strategy.switch! :one, true + strategy.on?(:one).should be true + end + it "can switch unknown features on" do + strategy.switch! :three, true + strategy.on?(:three).should be true + end + it "can switch features off" do + strategy.switch! :two, false + strategy.on?(:two).should be false + end + it "can delete knowledge of a feature" do + strategy.delete! :one + strategy.on?(:one).should be false + strategy.knows?(:one).should be false + end + end + +end + +describe Flip::SessionStrategy::Loader do + + it "adds filters when included in controller" do + ControllerWithoutSessionStrategy.tap do |klass| + klass.should_receive(:before_filter).with(:flip_session_strategy_before) + klass.should_receive(:after_filter).with(:flip_session_strategy_after) + klass.send :include, Flip::SessionStrategy::Loader + end + end + + describe "filter methods" do + let(:strategy) { Flip::SessionStrategy.new } + let(:controller) { ControllerWithSessionStrategy.new } + describe "#flip_session_strategy_before" do + it "passes controller session to SessionStrategy" do + controller.should_receive(:session).and_return(strategy.session_name(:test) => "true") + expect { + controller.flip_session_strategy_before + }.to change { + [ strategy.knows?(:test), strategy.on?(:test) ] + }.from([false, false]).to([true, true]) + end + end + describe "#flip_session_strategy_after" do + before do + Flip::SessionStrategy.session = { strategy.session_name(:test) => "true" } + end + it "passes controller session to SessionStrategy" do + expect { + controller.flip_session_strategy_after + }.to change { + [ strategy.knows?(:test), strategy.on?(:test) ] + }.from([true, true]).to([false, false]) + end + end + end +end