flip 0.0.1.alpha
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
|