flip 0.0.1.alpha
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +4 -0
- data/Gemfile +2 -0
- data/README.md +112 -0
- data/Rakefile +10 -0
- data/flip.gemspec +27 -0
- data/lib/flip.rb +24 -0
- data/lib/flip/abstract_strategy.rb +26 -0
- data/lib/flip/controller_filters.rb +21 -0
- data/lib/flip/cookie_strategy.rb +56 -0
- data/lib/flip/database_strategy.rb +40 -0
- data/lib/flip/declarable.rb +20 -0
- data/lib/flip/declaration_strategy.rb +20 -0
- data/lib/flip/definition.rb +21 -0
- data/lib/flip/facade.rb +18 -0
- data/lib/flip/feature_set.rb +57 -0
- data/lib/flip/version.rb +3 -0
- data/spec/abstract_strategy_spec.rb +11 -0
- data/spec/controller_filters_spec.rb +27 -0
- data/spec/cookie_strategy_spec.rb +105 -0
- data/spec/database_strategy_spec.rb +66 -0
- data/spec/declarable_spec.rb +31 -0
- data/spec/declaration_strategy_spec.rb +33 -0
- data/spec/definition_spec.rb +19 -0
- data/spec/feature_set_spec.rb +64 -0
- data/spec/flip_spec.rb +33 -0
- data/spec/spec_helper.rb +1 -0
- metadata +128 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
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…*
|
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
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
|
data/lib/flip/facade.rb
ADDED
@@ -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
|
data/lib/flip/version.rb
ADDED
@@ -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
|
data/spec/spec_helper.rb
ADDED
@@ -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
|