flip 0.0.1.alpha

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source :rubygems
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,112 @@
1
+ Flip — flip your features
2
+ ================
3
+
4
+ [Learnable](https://learnable.com) uses feature flippers ([so does Flickr](http://code.flickr.com/blog/2009/12/02/flipping-out/)) as a tool to help achieve [continuous deployment](http://timothyfitz.wordpress.com/2009/02/10/continuous-deployment-at-imvu-doing-the-impossible-fifty-times-a-day/).
5
+
6
+ **Flip** gives us a declarative, layered mechanism to enable and disable features. There's a configurable system-wide default (`default: !Rails.env.production?` works nicely), plus three layers of strategies to determine status per-feature:
7
+
8
+ * The declared default, e.g. `feature :world_domination, default: true`,
9
+ * A database-backed strategy, for flipping features site-wide for all users.
10
+ * A cookie-backed strategy, for privately previewing features in your own browser only.
11
+
12
+ (Hint: that last one is a a killer feature..)
13
+
14
+ Install
15
+ -------
16
+
17
+ Note: the alpha version number indicates Flip is currently being extracted from its host application. **The process described here is currently fictional.** But it does have a happy ending.
18
+
19
+ **Rails 3.0 and 3.1+**
20
+
21
+ # Gemfile
22
+ gem "flip"
23
+
24
+ # Generate the model and migration
25
+ > rails g flip:install
26
+
27
+ # Run the migration
28
+ > rake db:migrate
29
+
30
+ # They lived happily ever after.
31
+
32
+
33
+ Declaring Features
34
+ ------------------
35
+
36
+ # This is the model class generated by rails g flip:install
37
+ class Feature < ActiveRecord::Base
38
+ include Flip::Declarable
39
+
40
+ # The recommended Flip strategy stack.
41
+ strategy Flip::CookieStrategy
42
+ strategy Flip::DatabaseStrategy
43
+ strategy Flip::DefaultStrategy
44
+ default false
45
+
46
+ # A basic feature declaration.
47
+ feature :shiny_things
48
+
49
+ # Override the system-wide default.
50
+ feature :world_domination, default: true
51
+
52
+ # Enabled half the time..? Sure, we can do that.
53
+ feature :flakey,
54
+ default: proc { rand(2).zero? }
55
+
56
+ # Provide a description, normally derived from the feature name.
57
+ feature :something,
58
+ default: true,
59
+ description: "Ability to purchase enrollments in courses",
60
+
61
+ end
62
+
63
+
64
+ Checking Features
65
+ -----------------
66
+
67
+ Feature status can be checked by any code using `on?` or using the dynamic predicate methods:
68
+
69
+ Flip.on? :world_domination # true
70
+ Flip.world_domination? # true
71
+
72
+ Flip.on? :shiny_things # false
73
+ Flip.shiny_things? # false
74
+
75
+ Within view and controller methods, the `FlipHelper` module provides a `feature?(key)` method:
76
+
77
+ <div>
78
+ <% if feature? :world_domination %>
79
+ <%= link_to "Dominate World", world_dominations_path %>
80
+ <% end %>
81
+ </div>
82
+
83
+
84
+ Feature Flipping Controllers
85
+ ----------------------------
86
+
87
+ The `Flip::ControllerFilters` module is mixed into the base `ApplicationController` class. The following controller will respond with 404 Page Not Found to all but the `index` action unless the :new_stuff feature is enabled:
88
+
89
+ class SampleController < ApplicationController
90
+
91
+ require_feature :something, :except => :index
92
+
93
+ def show
94
+ end
95
+
96
+ def index
97
+ end
98
+
99
+ end
100
+
101
+ Note that conditionally declared routes require a server restart to notice changes to feature flags, so they're not a good idea; database/cookie feature flipping will be ignored.
102
+
103
+
104
+ Command Center
105
+ --------------
106
+
107
+ A dashboard allows you to view the current state of the feature set, and flip any switchable strategies (database, cookie). *Screenshot coming&hellip;*
108
+
109
+
110
+ ----
111
+ Copyright © 2011 Paul Annesley and Learnable Pty Ltd, [MIT Licence](http://www.opensource.org/licenses/mit-license.php).
112
+
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ desc "Run all tests"
5
+ task :default => :spec
6
+
7
+ desc "Run specs"
8
+ task :spec do
9
+ system 'bundle exec rspec --color --format documentation spec/*_spec.rb'
10
+ end
data/flip.gemspec ADDED
@@ -0,0 +1,27 @@
1
+ # -*- encoding: utf-8 -*-
2
+ $:.push File.expand_path("../lib", __FILE__)
3
+ require "flip/version"
4
+
5
+ Gem::Specification.new do |s|
6
+ s.name = "flip"
7
+ s.version = Flip::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Paul Annesley"]
10
+ s.email = ["paul@annesley.cc"]
11
+ s.homepage = "https://github.com/pda/flip"
12
+ s.summary = %q{A feature flipper for Rails web applications.}
13
+ s.description = %q{Declarative API for specifying features, switchable in declaration, database and cookies.}
14
+
15
+ s.rubyforge_project = "flip"
16
+
17
+ s.files = `git ls-files`.split("\n")
18
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
20
+ s.require_paths = ["lib"]
21
+
22
+ s.add_dependency("activesupport", "~> 3.0")
23
+ s.add_dependency("i18n")
24
+
25
+ s.add_development_dependency("rspec", "~> 2.5")
26
+ s.add_development_dependency("rake")
27
+ end
data/lib/flip.rb ADDED
@@ -0,0 +1,24 @@
1
+ # ActiveSupport dependencies.
2
+ %w{
3
+ concern
4
+ inflector
5
+ core_ext/hash/reverse_merge
6
+ core_ext/object/blank
7
+ }.each { |name| require "active_support/#{name}" }
8
+
9
+ # Flip files.
10
+ %w{
11
+ abstract_strategy
12
+ controller_filters
13
+ cookie_strategy
14
+ database_strategy
15
+ declarable
16
+ declaration_strategy
17
+ definition
18
+ facade
19
+ feature_set
20
+ }.each { |name| require "flip/#{name}" }
21
+
22
+ module Flip
23
+ extend Facade
24
+ end
@@ -0,0 +1,26 @@
1
+ module Flip
2
+ class AbstractStrategy
3
+
4
+ def name
5
+ self.class.name.split("::").last.gsub(/Strategy$/, "").underscore
6
+ end
7
+
8
+ def description; ""; end
9
+
10
+ # Whether the strategy knows the on/off state of the switch.
11
+ def knows? definition; raise; end
12
+
13
+ # Given the state is known, whether it is on or off.
14
+ def on? definition; raise; end
15
+
16
+ # Whether the feature can be switched on and off at runtime.
17
+ # If true, the strategy must also respond to switch! and delete!
18
+ def switchable?
19
+ false
20
+ end
21
+
22
+ def switch! key, on; raise; end
23
+ def delete! key; raise; end
24
+
25
+ end
26
+ end
@@ -0,0 +1,21 @@
1
+ module Flip
2
+ module ControllerFilters
3
+
4
+ extend ActiveSupport::Concern
5
+
6
+ module ClassMethods
7
+
8
+ def require_feature key, options = {}
9
+ before_filter options do
10
+ flip_feature_disabled key unless Flip.on? key
11
+ end
12
+ end
13
+
14
+ end
15
+
16
+ def flip_feature_disabled key
17
+ # TODO: handle this with a 404
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,56 @@
1
+ # Uses cookie to determine feature state.
2
+ module Flip
3
+ class CookieStrategy < AbstractStrategy
4
+
5
+ def description
6
+ "Uses cookies to apply only to your session."
7
+ end
8
+
9
+ def knows? definition
10
+ cookies.key? cookie_name(definition)
11
+ end
12
+
13
+ def on? definition
14
+ cookies[cookie_name(definition)] === "true"
15
+ end
16
+
17
+ def switchable?
18
+ true
19
+ end
20
+
21
+ def switch! key, on
22
+ cookies[cookie_name(key)] = on ? "true" : "false"
23
+ end
24
+
25
+ def delete! key
26
+ cookies.delete cookie_name(key)
27
+ end
28
+
29
+ def self.cookies= cookies
30
+ @@cookies = cookies
31
+ end
32
+
33
+ def cookie_name(definition)
34
+ definition = definition.key unless definition.is_a? Symbol
35
+ "flip_#{definition}"
36
+ end
37
+
38
+ private
39
+
40
+ def cookies
41
+ @@cookies || raise("Cookies not loaded")
42
+ end
43
+
44
+ # Include in ApplicationController to push cookies into CookieStrategy.
45
+ module Loader
46
+ extend ActiveSupport::Concern
47
+ included { around_filter :cookie_feature_strategy }
48
+ def cookie_feature_strategy
49
+ CookieStrategy.cookies = cookies
50
+ yield
51
+ CookieStrategy.cookies = nil
52
+ end
53
+ end
54
+
55
+ end
56
+ end
@@ -0,0 +1,40 @@
1
+ # Database backed system-wide
2
+ module Flip
3
+ class DatabaseStrategy < AbstractStrategy
4
+
5
+ def initialize(model_klass)
6
+ @klass = model_klass
7
+ end
8
+
9
+ def description
10
+ "Database backed, applies to all users."
11
+ end
12
+
13
+ def knows? definition
14
+ !!feature(definition)
15
+ end
16
+
17
+ def on? definition
18
+ feature(definition).on?
19
+ end
20
+
21
+ def switchable?
22
+ true
23
+ end
24
+
25
+ def switch! key, on
26
+ @klass.find_or_initialize_by_key(key).update_attributes! on: on
27
+ end
28
+
29
+ def delete! key
30
+ @klass.find_by_key(key).try(:destroy)
31
+ end
32
+
33
+ private
34
+
35
+ def feature(definition)
36
+ @klass.find_by_key definition.key
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,20 @@
1
+ module Flip
2
+ module Declarable
3
+
4
+ # Adds a new feature definition, creates predicate method.
5
+ def feature(key, options = {})
6
+ FeatureSet.instance << Flip::Definition.new(key, options)
7
+ end
8
+
9
+ # Adds a strategy for determining feature status.
10
+ def strategy(strategy)
11
+ FeatureSet.instance.add_strategy strategy
12
+ end
13
+
14
+ # The default response, boolean or a Proc to be called.
15
+ def default(default)
16
+ FeatureSet.instance.default = default
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,20 @@
1
+ # Uses :default option passed to feature declaration.
2
+ # May be boolean or a Proc to be passed the definition.
3
+ module Flip
4
+ class DeclarationStrategy < AbstractStrategy
5
+
6
+ def description
7
+ "The default status declared with the feature."
8
+ end
9
+
10
+ def knows? definition
11
+ definition.options.key? :default
12
+ end
13
+
14
+ def on? definition
15
+ default = definition.options[:default]
16
+ default.is_a?(Proc) ? default.call(definition) : default
17
+ end
18
+
19
+ end
20
+ end
@@ -0,0 +1,21 @@
1
+ module Flip
2
+ class Definition
3
+
4
+ attr_accessor :key
5
+ attr_accessor :options
6
+
7
+ def initialize(key, options = {})
8
+ @key = key
9
+ @options = options.reverse_merge \
10
+ description: key.to_s.humanize + "."
11
+ end
12
+
13
+ alias :name :key
14
+ alias :to_s :key
15
+
16
+ def description
17
+ options[:description]
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,18 @@
1
+ module Flip
2
+ module Facade
3
+
4
+ def on?(feature)
5
+ FeatureSet.instance.on? feature
6
+ end
7
+
8
+ def reset
9
+ FeatureSet.reset
10
+ end
11
+
12
+ def method_missing(method, *parameters)
13
+ super unless method =~ %r{^(.*)\?$}
14
+ FeatureSet.instance.on? $1.to_sym
15
+ end
16
+
17
+ end
18
+ end
@@ -0,0 +1,57 @@
1
+ module Flip
2
+ class FeatureSet
3
+
4
+ def self.instance
5
+ @instance ||= self.new
6
+ end
7
+
8
+ def self.reset
9
+ remove_instance_variable :@instance
10
+ end
11
+
12
+ # Sets the default for definitions which fall through the strategies.
13
+ # Accepts boolean or a Proc to be called.
14
+ attr_writer :default
15
+
16
+ def initialize
17
+ @definitions = Hash.new { |_, k| raise "No feature declared with key #{k.inspect}" }
18
+ @strategies = Hash.new { |_, k| raise "No strategy named #{k}" }
19
+ @default = false
20
+ end
21
+
22
+ # Whether the given feature is switched on.
23
+ def on? key
24
+ d = @definitions[key]
25
+ @strategies.each_value { |s| return s.on?(d) if s.knows?(d) }
26
+ default_for d
27
+ end
28
+
29
+ # Adds a feature definition to the set.
30
+ def << definition
31
+ @definitions[definition.key] = definition
32
+ end
33
+
34
+ # Adds a strategy for determing feature status.
35
+ def add_strategy(strategy)
36
+ strategy = strategy.new if strategy.is_a? Class
37
+ @strategies[strategy.name] = strategy
38
+ end
39
+
40
+ def strategy(klass)
41
+ @strategies[klass]
42
+ end
43
+
44
+ def default_for(definition)
45
+ @default.is_a?(Proc) ? @default.call(definition) : @default
46
+ end
47
+
48
+ def definitions
49
+ @definitions.values
50
+ end
51
+
52
+ def strategies
53
+ @strategies.values
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,3 @@
1
+ module Flip
2
+ VERSION = "0.0.1.alpha"
3
+ end
@@ -0,0 +1,11 @@
1
+ require "spec_helper"
2
+
3
+ # Perhaps this is silly, but it provides some
4
+ # coverage to an important base class.
5
+ describe Flip::AbstractStrategy do
6
+
7
+ its(:name) { should == "abstract" }
8
+ its(:description) { should == "" }
9
+ it { should_not be_switchable }
10
+
11
+ end
@@ -0,0 +1,27 @@
1
+ require "spec_helper"
2
+
3
+ class ControllerWithFlipFilters
4
+ include Flip::ControllerFilters
5
+ end
6
+
7
+ describe ControllerWithFlipFilters do
8
+
9
+ describe ".require_feature" do
10
+
11
+ it "adds before_filter without options" do
12
+ ControllerWithFlipFilters.tap do |klass|
13
+ klass.should_receive(:before_filter).with({})
14
+ klass.send(:require_feature, :testable)
15
+ end
16
+ end
17
+
18
+ it "adds before_filter with options" do
19
+ ControllerWithFlipFilters.tap do |klass|
20
+ klass.should_receive(:before_filter).with({ only: [ :show ] })
21
+ klass.send(:require_feature, :testable, only: [ :show ])
22
+ end
23
+ end
24
+
25
+ end
26
+
27
+ end
@@ -0,0 +1,105 @@
1
+ require "spec_helper"
2
+
3
+ class ControllerWithoutCookieStrategy; end
4
+ class ControllerWithCookieStrategy
5
+ def self.around_filter(_); end
6
+ def cookies; []; end
7
+ include Flip::CookieStrategy::Loader
8
+ end
9
+
10
+ describe Flip::CookieStrategy do
11
+
12
+ let(:cookies) do
13
+ { strategy.cookie_name(:one) => "true",
14
+ strategy.cookie_name(:two) => "false" }
15
+ end
16
+ let(:strategy) do
17
+ Flip::CookieStrategy.new.tap do |s|
18
+ s.stub(:cookies) { cookies }
19
+ end
20
+ end
21
+
22
+ its(:description) { should be_present }
23
+ it { should be_switchable }
24
+
25
+ describe "cookie interrogration" do
26
+ context "enabled feature" do
27
+ specify "#knows? is true" do
28
+ strategy.knows?(:one).should be_true
29
+ end
30
+ specify "#on? is true" do
31
+ strategy.on?(:one).should be_true
32
+ end
33
+ end
34
+ context "disabled feature" do
35
+ specify "#knows? is true" do
36
+ strategy.knows?(:two).should be_true
37
+ end
38
+ specify "#on? is false" do
39
+ strategy.on?(:two).should be_false
40
+ end
41
+ end
42
+ context "feature with no cookie present" do
43
+ specify "#knows? is false" do
44
+ strategy.knows?(:three).should be_false
45
+ end
46
+ specify "#on? is false" do
47
+ strategy.on?(:three).should be_false
48
+ end
49
+ end
50
+ end
51
+
52
+ describe "cookie manipulation" do
53
+ it "can switch known features on" do
54
+ strategy.switch! :one, true
55
+ strategy.on?(:one).should be_true
56
+ end
57
+ it "can switch unknown features on" do
58
+ strategy.switch! :three, true
59
+ strategy.on?(:three).should be_true
60
+ end
61
+ it "can switch features off" do
62
+ strategy.switch! :two, false
63
+ strategy.on?(:two).should be_false
64
+ end
65
+ it "can delete knowledge of a feature" do
66
+ strategy.delete! :one
67
+ strategy.on?(:one).should be_false
68
+ strategy.knows?(:one).should be_false
69
+ end
70
+ end
71
+
72
+ end
73
+
74
+ describe Flip::CookieStrategy::Loader do
75
+
76
+ it "adds around_filter when included in controller" do
77
+ ControllerWithoutCookieStrategy.tap do |klass|
78
+ klass.should_receive(:around_filter).with(:cookie_feature_strategy)
79
+ klass.send :include, Flip::CookieStrategy::Loader
80
+ end
81
+ end
82
+
83
+ describe "#cookie_feature_strategy as around_filter" do
84
+
85
+ let(:strategy) { Flip::CookieStrategy.new }
86
+ let(:controller) { ControllerWithCookieStrategy.new }
87
+
88
+ it "yields to block" do
89
+ run = false
90
+ ControllerWithCookieStrategy.new.cookie_feature_strategy { run = true }
91
+ run.should be_true
92
+ end
93
+
94
+ it "passes controller cookies to Flip::CookieStrategy" do
95
+ controller.should_receive(:cookies).and_return(strategy.cookie_name(:test) => "true")
96
+ results = []
97
+ controller.cookie_feature_strategy {
98
+ results << strategy.on?(:test)
99
+ results << strategy.on?(:different)
100
+ }
101
+ results.should == [ true, false ]
102
+ end
103
+
104
+ end
105
+ end
@@ -0,0 +1,66 @@
1
+ require "spec_helper"
2
+
3
+ describe Flip::DatabaseStrategy do
4
+
5
+ let(:definition) { double("definition").tap{ |d| d.stub(:key) { :one } } }
6
+ let(:strategy) { Flip::DatabaseStrategy.new(model_klass) }
7
+ let(:model_klass) do
8
+ Class.new do
9
+ extend Flip::Declarable
10
+ feature :one
11
+ feature :two, description: "Second one."
12
+ feature :three, default: true
13
+ end
14
+ end
15
+ let(:enabled_record) { model_klass.new.tap { |m| m.stub(:on?) { true } } }
16
+ let(:disabled_record) { model_klass.new.tap { |m| m.stub(:on?) { false } } }
17
+
18
+ subject { strategy }
19
+
20
+ its(:switchable?) { should be_true }
21
+ its(:description) { should be_present }
22
+
23
+ describe "#knows?" do
24
+ it "does not know features that cannot be found" do
25
+ model_klass.stub(:find_by_key) { nil }
26
+ strategy.knows?(definition).should be_false
27
+ end
28
+ it "knows features that can be found" do
29
+ model_klass.stub(:find_by_key) { disabled_record }
30
+ strategy.knows?(definition).should be_true
31
+ end
32
+ end
33
+
34
+ describe "#on?" do
35
+ it "is true for an enabled record from the database" do
36
+ model_klass.stub(:find_by_key) { enabled_record }
37
+ strategy.on?(definition).should be_true
38
+ end
39
+ it "is false for a disabled record from the database" do
40
+ model_klass.stub(:find_by_key) { disabled_record }
41
+ strategy.on?(definition).should be_false
42
+ end
43
+ end
44
+
45
+ describe "#switch!" do
46
+ it "can switch a feature on" do
47
+ model_klass.should_receive(:find_or_initialize_by_key).with(:one).and_return(disabled_record)
48
+ disabled_record.should_receive(:update_attributes!).with(on: true)
49
+ strategy.switch! :one, true
50
+ end
51
+ it "can switch a feature off" do
52
+ model_klass.should_receive(:find_or_initialize_by_key).with(:one).and_return(enabled_record)
53
+ enabled_record.should_receive(:update_attributes!).with(on: false)
54
+ strategy.switch! :one, false
55
+ end
56
+ end
57
+
58
+ describe "#delete!" do
59
+ it "can delete a feature record" do
60
+ model_klass.should_receive(:find_by_key).with(:one).and_return(enabled_record)
61
+ enabled_record.should_receive(:try).with(:destroy)
62
+ strategy.delete! :one
63
+ end
64
+ end
65
+
66
+ end
@@ -0,0 +1,31 @@
1
+ require "spec_helper"
2
+
3
+ class TestableFlipModel
4
+ extend Flip::Declarable
5
+
6
+ strategy Flip::DeclarationStrategy
7
+ default false
8
+
9
+ feature :one
10
+ feature :two, description: "Second one."
11
+ feature :three, default: true
12
+ end
13
+
14
+ describe Flip::Declarable do
15
+
16
+ let(:model_class) { TestableFlipModel }
17
+ subject { Flip::FeatureSet.instance }
18
+
19
+ describe "the .on? class method" do
20
+ context "with default set to false" do
21
+ it { should_not be_on(:one) }
22
+ it { should be_on(:three) }
23
+ end
24
+ context "with default set to true" do
25
+ before(:all) { model_class.send(:default, true) }
26
+ it { should be_on(:one) }
27
+ it { should be_on(:three) }
28
+ end
29
+ end
30
+
31
+ end
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe Flip::DeclarationStrategy do
4
+
5
+ def definition(default)
6
+ Flip::Definition.new :feature, default: default
7
+ end
8
+
9
+ describe "#knows?" do
10
+ specify "definition without default should be false" do
11
+ subject.knows?(Flip::Definition.new :feature).should be_false
12
+ end
13
+ specify " should be true" do
14
+ subject.knows?(definition(true)).should be_true
15
+ end
16
+ end
17
+
18
+ describe "#on? for Flip::Definition with default of" do
19
+ specify "true" do
20
+ subject.on?(definition(true)).should be_true
21
+ end
22
+ specify "false" do
23
+ subject.on?(definition(false)).should be_false
24
+ end
25
+ specify "proc returning true" do
26
+ subject.on?(definition(proc { true })).should be_true
27
+ end
28
+ specify "proc returning false" do
29
+ subject.on?(definition(proc { false })).should be_false
30
+ end
31
+ end
32
+
33
+ end
@@ -0,0 +1,19 @@
1
+ require "spec_helper"
2
+
3
+ describe Flip::Definition do
4
+
5
+ subject { Flip::Definition.new :the_key, description: "The description" }
6
+
7
+ [:key, :name, :to_s].each do |method|
8
+ its(method) { should == :the_key }
9
+ end
10
+
11
+ its(:description) { should == "The description" }
12
+ its(:options) { should == { description: "The description" } }
13
+
14
+ context "without description specified" do
15
+ subject { Flip::Definition.new :the_key }
16
+ its(:description) { should == "The key." }
17
+ end
18
+
19
+ end
@@ -0,0 +1,64 @@
1
+ require "spec_helper"
2
+
3
+ class NullStrategy < Flip::AbstractStrategy
4
+ def knows?(d); false; end
5
+ end
6
+
7
+ class TrueStrategy < Flip::AbstractStrategy
8
+ def knows?(d); true; end
9
+ def on?(d); true; end
10
+ end
11
+
12
+ describe Flip::FeatureSet do
13
+
14
+ let :feature_set_with_null_strategy do
15
+ Flip::FeatureSet.new.tap do |s|
16
+ s << Flip::Definition.new(:feature)
17
+ s.add_strategy NullStrategy
18
+ end
19
+ end
20
+
21
+ let :feature_set_with_null_then_true_strategies do
22
+ feature_set_with_null_strategy.tap do |s|
23
+ s.add_strategy TrueStrategy
24
+ end
25
+ end
26
+
27
+ describe ".instance" do
28
+ it "returns a singleton instance" do
29
+ Flip::FeatureSet.instance.should equal(Flip::FeatureSet.instance)
30
+ end
31
+ it "can be reset" do
32
+ instance_before_reset = Flip::FeatureSet.instance
33
+ Flip::FeatureSet.reset
34
+ Flip::FeatureSet.instance.should_not equal(instance_before_reset)
35
+ end
36
+ end
37
+
38
+ describe "#default= and #on? with null strategy" do
39
+ subject { feature_set_with_null_strategy }
40
+ it "defaults to false" do
41
+ subject.on?(:feature).should be_false
42
+ end
43
+ it "can default to true" do
44
+ subject.default = true
45
+ subject.on?(:feature).should be_true
46
+ end
47
+ it "accepts a proc returning true" do
48
+ subject.default = proc { true }
49
+ subject.on?(:feature).should be_true
50
+ end
51
+ it "accepts a proc returning false" do
52
+ subject.default = proc { false }
53
+ subject.on?(:feature).should be_false
54
+ end
55
+ end
56
+
57
+ describe "feature set with null strategy then always-true strategy" do
58
+ subject { feature_set_with_null_then_true_strategies }
59
+ it "returns true due to second strategy" do
60
+ subject.on?(:feature).should be_true
61
+ end
62
+ end
63
+
64
+ end
data/spec/flip_spec.rb ADDED
@@ -0,0 +1,33 @@
1
+ require "spec_helper"
2
+
3
+ describe Flip do
4
+
5
+ before(:all) do
6
+ Class.new do
7
+ extend Flip::Declarable
8
+ strategy Flip::DeclarationStrategy
9
+ default false
10
+ feature :one, default: true
11
+ feature :two, default: false
12
+ end
13
+ end
14
+
15
+ after(:all) do
16
+ Flip.reset
17
+ end
18
+
19
+ describe ".on?" do
20
+ it "returns true for enabled features" do
21
+ Flip.on?(:one).should be_true
22
+ end
23
+ it "returns false for disabled features" do
24
+ Flip.on?(:two).should be_false
25
+ end
26
+ end
27
+
28
+ describe "dynamic predicate methods" do
29
+ its(:one?) { should be_true }
30
+ its(:two?) { should be_false }
31
+ end
32
+
33
+ end
@@ -0,0 +1 @@
1
+ require "flip"
metadata ADDED
@@ -0,0 +1,128 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: flip
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.0.1.alpha
5
+ prerelease: 6
6
+ platform: ruby
7
+ authors:
8
+ - Paul Annesley
9
+ autorequire:
10
+ bindir: bin
11
+ cert_chain: []
12
+ date: 2011-05-24 00:00:00.000000000 +10:00
13
+ default_executable:
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: activesupport
17
+ requirement: &2153418240 !ruby/object:Gem::Requirement
18
+ none: false
19
+ requirements:
20
+ - - ~>
21
+ - !ruby/object:Gem::Version
22
+ version: '3.0'
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: *2153418240
26
+ - !ruby/object:Gem::Dependency
27
+ name: i18n
28
+ requirement: &2153417820 !ruby/object:Gem::Requirement
29
+ none: false
30
+ requirements:
31
+ - - ! '>='
32
+ - !ruby/object:Gem::Version
33
+ version: '0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: *2153417820
37
+ - !ruby/object:Gem::Dependency
38
+ name: rspec
39
+ requirement: &2153417280 !ruby/object:Gem::Requirement
40
+ none: false
41
+ requirements:
42
+ - - ~>
43
+ - !ruby/object:Gem::Version
44
+ version: '2.5'
45
+ type: :development
46
+ prerelease: false
47
+ version_requirements: *2153417280
48
+ - !ruby/object:Gem::Dependency
49
+ name: rake
50
+ requirement: &2153416860 !ruby/object:Gem::Requirement
51
+ none: false
52
+ requirements:
53
+ - - ! '>='
54
+ - !ruby/object:Gem::Version
55
+ version: '0'
56
+ type: :development
57
+ prerelease: false
58
+ version_requirements: *2153416860
59
+ description: Declarative API for specifying features, switchable in declaration, database
60
+ and cookies.
61
+ email:
62
+ - paul@annesley.cc
63
+ executables: []
64
+ extensions: []
65
+ extra_rdoc_files: []
66
+ files:
67
+ - .gitignore
68
+ - Gemfile
69
+ - README.md
70
+ - Rakefile
71
+ - flip.gemspec
72
+ - lib/flip.rb
73
+ - lib/flip/abstract_strategy.rb
74
+ - lib/flip/controller_filters.rb
75
+ - lib/flip/cookie_strategy.rb
76
+ - lib/flip/database_strategy.rb
77
+ - lib/flip/declarable.rb
78
+ - lib/flip/declaration_strategy.rb
79
+ - lib/flip/definition.rb
80
+ - lib/flip/facade.rb
81
+ - lib/flip/feature_set.rb
82
+ - lib/flip/version.rb
83
+ - spec/abstract_strategy_spec.rb
84
+ - spec/controller_filters_spec.rb
85
+ - spec/cookie_strategy_spec.rb
86
+ - spec/database_strategy_spec.rb
87
+ - spec/declarable_spec.rb
88
+ - spec/declaration_strategy_spec.rb
89
+ - spec/definition_spec.rb
90
+ - spec/feature_set_spec.rb
91
+ - spec/flip_spec.rb
92
+ - spec/spec_helper.rb
93
+ has_rdoc: true
94
+ homepage: https://github.com/pda/flip
95
+ licenses: []
96
+ post_install_message:
97
+ rdoc_options: []
98
+ require_paths:
99
+ - lib
100
+ required_ruby_version: !ruby/object:Gem::Requirement
101
+ none: false
102
+ requirements:
103
+ - - ! '>='
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ required_rubygems_version: !ruby/object:Gem::Requirement
107
+ none: false
108
+ requirements:
109
+ - - ! '>'
110
+ - !ruby/object:Gem::Version
111
+ version: 1.3.1
112
+ requirements: []
113
+ rubyforge_project: flip
114
+ rubygems_version: 1.5.2
115
+ signing_key:
116
+ specification_version: 3
117
+ summary: A feature flipper for Rails web applications.
118
+ test_files:
119
+ - spec/abstract_strategy_spec.rb
120
+ - spec/controller_filters_spec.rb
121
+ - spec/cookie_strategy_spec.rb
122
+ - spec/database_strategy_spec.rb
123
+ - spec/declarable_spec.rb
124
+ - spec/declaration_strategy_spec.rb
125
+ - spec/definition_spec.rb
126
+ - spec/feature_set_spec.rb
127
+ - spec/flip_spec.rb
128
+ - spec/spec_helper.rb