flip2 1.1.1
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.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +2 -0
- data/README.md +180 -0
- data/Rakefile +11 -0
- data/TODO +3 -0
- data/app/assets/stylesheets/flip.css +70 -0
- data/app/controllers/flip/features_controller.rb +47 -0
- data/app/controllers/flip/strategies_controller.rb +31 -0
- data/app/helpers/flip_helper.rb +9 -0
- data/app/views/flip/features/index.html.erb +62 -0
- data/config/routes.rb +14 -0
- data/flip.gemspec +27 -0
- data/lib/flip/abstract_strategy.rb +26 -0
- data/lib/flip/cacheable.rb +25 -0
- data/lib/flip/controller_filters.rb +25 -0
- data/lib/flip/cookie_strategy.rb +67 -0
- data/lib/flip/database_strategy.rb +46 -0
- data/lib/flip/declarable.rb +24 -0
- data/lib/flip/declaration_strategy.rb +20 -0
- data/lib/flip/definition.rb +21 -0
- data/lib/flip/engine.rb +9 -0
- data/lib/flip/facade.rb +18 -0
- data/lib/flip/feature_set.rb +57 -0
- data/lib/flip/forbidden.rb +7 -0
- data/lib/flip/version.rb +3 -0
- data/lib/flip2.rb +28 -0
- data/lib/generators/flip/install/install_generator.rb +9 -0
- data/lib/generators/flip/migration/USAGE +5 -0
- data/lib/generators/flip/migration/migration_generator.rb +22 -0
- data/lib/generators/flip/migration/templates/create_features.rb +11 -0
- data/lib/generators/flip/model/USAGE +8 -0
- data/lib/generators/flip/model/model_generator.rb +8 -0
- data/lib/generators/flip/model/templates/feature.rb +15 -0
- data/lib/generators/flip/routes/USAGE +7 -0
- data/lib/generators/flip/routes/routes_generator.rb +7 -0
- data/lib/generators/flip/views/USAGE +8 -0
- data/lib/generators/flip/views/templates/index.html.erb +54 -0
- data/lib/generators/flip/views/views_generator.rb +8 -0
- data/spec/abstract_strategy_spec.rb +11 -0
- data/spec/cacheable_spec.rb +49 -0
- data/spec/controller_filters_spec.rb +27 -0
- data/spec/cookie_strategy_spec.rb +112 -0
- data/spec/database_strategy_spec.rb +110 -0
- data/spec/declarable_spec.rb +32 -0
- data/spec/declaration_strategy_spec.rb +39 -0
- data/spec/definition_spec.rb +19 -0
- data/spec/feature_set_spec.rb +67 -0
- data/spec/flip_spec.rb +33 -0
- data/spec/spec_helper.rb +2 -0
- metadata +172 -0
@@ -0,0 +1,22 @@
|
|
1
|
+
require "rails/generators/migration"
|
2
|
+
|
3
|
+
class Flip::MigrationGenerator < Rails::Generators::Base
|
4
|
+
include Rails::Generators::Migration
|
5
|
+
|
6
|
+
source_root File.expand_path("../templates", __FILE__)
|
7
|
+
|
8
|
+
def create_migration_file
|
9
|
+
migration_template "create_features.rb", "db/migrate/create_features.rb"
|
10
|
+
end
|
11
|
+
|
12
|
+
# Stubbed in railties/lib/rails/generators/migration.rb
|
13
|
+
#
|
14
|
+
# This implementation a simplified version of:
|
15
|
+
# activerecord/lib/rails/generators/active_record/migration.rb
|
16
|
+
#
|
17
|
+
# See: http://www.ruby-forum.com/topic/203205
|
18
|
+
def self.next_migration_number(dirname)
|
19
|
+
Time.now.utc.strftime("%Y%m%d%H%M%S")
|
20
|
+
end
|
21
|
+
|
22
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# Created by the flip gem, see https://github.com/pda/flip
|
2
|
+
class CreateFeatures < ActiveRecord::Migration
|
3
|
+
def change
|
4
|
+
create_table :features do |t|
|
5
|
+
t.string :key, null: false
|
6
|
+
t.boolean :enabled, null: false, default: false
|
7
|
+
|
8
|
+
t.timestamps null: false
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
class Feature < ActiveRecord::Base
|
2
|
+
extend Flip::Declarable
|
3
|
+
|
4
|
+
strategy Flip::CookieStrategy
|
5
|
+
strategy Flip::DatabaseStrategy
|
6
|
+
strategy Flip::DeclarationStrategy
|
7
|
+
default false
|
8
|
+
|
9
|
+
# Declare your features here, e.g:
|
10
|
+
#
|
11
|
+
# feature :world_domination,
|
12
|
+
# default: true,
|
13
|
+
# description: "Take over the world."
|
14
|
+
|
15
|
+
end
|
@@ -0,0 +1,54 @@
|
|
1
|
+
<div class="flip">
|
2
|
+
<h1>Feature Flippers</h1>
|
3
|
+
<table>
|
4
|
+
<thead>
|
5
|
+
<th class="name">Feature Name</th>
|
6
|
+
<th class="description">Description</th>
|
7
|
+
<th class="status">Status</th>
|
8
|
+
<% @p.strategies.each do |strategy| %>
|
9
|
+
<th>
|
10
|
+
<%= strategy.name %>
|
11
|
+
<span class="description"><%= strategy.description %></span>
|
12
|
+
</th>
|
13
|
+
<% end %>
|
14
|
+
<th>
|
15
|
+
Default
|
16
|
+
<span class="description">The system default when no strategies match.</span>
|
17
|
+
</th>
|
18
|
+
</thead>
|
19
|
+
<tbody>
|
20
|
+
<% @p.definitions.each do |definition| %>
|
21
|
+
<tr>
|
22
|
+
<td class="name"><%= definition.name %></td>
|
23
|
+
<td class="description"><%= definition.description %></td>
|
24
|
+
<%= content_tag :td, class: @p.status(definition) do %>
|
25
|
+
<%= @p.status definition %>
|
26
|
+
<% end %>
|
27
|
+
<% @p.strategies.each do |strategy| %>
|
28
|
+
<%= content_tag :td, class: @p.strategy_status(strategy, definition) || "pass" do %>
|
29
|
+
<%= @p.strategy_status strategy, definition %>
|
30
|
+
<% if strategy.switchable? %>
|
31
|
+
<%= form_tag(@p.switch_url(strategy, definition), method: :put) do %>
|
32
|
+
<% unless @p.strategy_status(strategy, definition) == "on" %>
|
33
|
+
<%= submit_tag "Switch On" %>
|
34
|
+
<% end %>
|
35
|
+
<% unless @p.strategy_status(strategy, definition) == "off" %>
|
36
|
+
<%= submit_tag "Switch Off" %>
|
37
|
+
<% end %>
|
38
|
+
<% end %>
|
39
|
+
<% unless @p.strategy_status(strategy, definition).blank? %>
|
40
|
+
<%= form_tag(@p.switch_url(strategy, definition), method: :delete) do %>
|
41
|
+
<%= submit_tag "Delete" %>
|
42
|
+
<% end %>
|
43
|
+
<% end %>
|
44
|
+
<% end %>
|
45
|
+
<% end %>
|
46
|
+
<% end %>
|
47
|
+
<%= content_tag :td, class: @p.default_status(definition) do %>
|
48
|
+
<%= @p.default_status definition %>
|
49
|
+
<% end %>
|
50
|
+
</tr>
|
51
|
+
<% end %>
|
52
|
+
</tbody>
|
53
|
+
</table>
|
54
|
+
</div>
|
@@ -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,49 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Flip::Cacheable do
|
4
|
+
|
5
|
+
subject(:model_class) do
|
6
|
+
class Sample
|
7
|
+
attr_accessor :key
|
8
|
+
end
|
9
|
+
|
10
|
+
Class.new do
|
11
|
+
extend Flip::Declarable
|
12
|
+
extend Flip::Cacheable
|
13
|
+
|
14
|
+
strategy Flip::DeclarationStrategy
|
15
|
+
default false
|
16
|
+
|
17
|
+
feature :one
|
18
|
+
feature :two, description: "Second one."
|
19
|
+
feature :three, default: true
|
20
|
+
|
21
|
+
def self.all
|
22
|
+
list = []
|
23
|
+
i = 65
|
24
|
+
3.times do
|
25
|
+
list << Sample.new
|
26
|
+
list.last.key = i.chr()
|
27
|
+
i += 1
|
28
|
+
end
|
29
|
+
list
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
describe "with feature cache" do
|
35
|
+
context "initial context" do
|
36
|
+
it { should respond_to(:use_feature_cache) }
|
37
|
+
it { should respond_to(:start_feature_cache) }
|
38
|
+
it { should respond_to(:feature_cache) }
|
39
|
+
specify { model_class.use_feature_cache.should be_nil }
|
40
|
+
end
|
41
|
+
|
42
|
+
context "after a cache clear" do
|
43
|
+
before { model_class.start_feature_cache }
|
44
|
+
specify { model_class.use_feature_cache.should eq true }
|
45
|
+
specify { model_class.feature_cache.size == 3}
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
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_action without options" do
|
12
|
+
ControllerWithFlipFilters.tap do |klass|
|
13
|
+
klass.should_receive(:before_action).with({})
|
14
|
+
klass.send(:require_feature, :testable)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
it "adds before_action with options" do
|
19
|
+
ControllerWithFlipFilters.tap do |klass|
|
20
|
+
klass.should_receive(:before_action).with({ only: [ :show ] })
|
21
|
+
klass.send(:require_feature, :testable, only: [ :show ])
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
end
|
26
|
+
|
27
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
class ControllerWithoutCookieStrategy; end
|
4
|
+
class ControllerWithCookieStrategy
|
5
|
+
def self.before_action(_); end
|
6
|
+
def self.after_action(_); end
|
7
|
+
def cookies; []; end
|
8
|
+
include Flip::CookieStrategy::Loader
|
9
|
+
end
|
10
|
+
|
11
|
+
describe Flip::CookieStrategy do
|
12
|
+
|
13
|
+
let(:cookies) do
|
14
|
+
{ strategy.cookie_name(:one) => "true",
|
15
|
+
strategy.cookie_name(:two) => "false" }
|
16
|
+
end
|
17
|
+
let(:strategy) do
|
18
|
+
Flip::CookieStrategy.new.tap do |s|
|
19
|
+
s.stub(:cookies) { cookies }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
its(:description) { should be_present }
|
24
|
+
it { should be_switchable }
|
25
|
+
|
26
|
+
describe "cookie interrogration" do
|
27
|
+
context "enabled feature" do
|
28
|
+
specify "#knows? is true" do
|
29
|
+
strategy.knows?(:one).should be true
|
30
|
+
end
|
31
|
+
specify "#on? is true" do
|
32
|
+
strategy.on?(:one).should be true
|
33
|
+
end
|
34
|
+
end
|
35
|
+
context "disabled feature" do
|
36
|
+
specify "#knows? is true" do
|
37
|
+
strategy.knows?(:two).should be true
|
38
|
+
end
|
39
|
+
specify "#on? is false" do
|
40
|
+
strategy.on?(:two).should be false
|
41
|
+
end
|
42
|
+
end
|
43
|
+
context "feature with no cookie present" do
|
44
|
+
specify "#knows? is false" do
|
45
|
+
strategy.knows?(:three).should be false
|
46
|
+
end
|
47
|
+
specify "#on? is false" do
|
48
|
+
strategy.on?(:three).should be false
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
describe "cookie manipulation" do
|
54
|
+
it "can switch known features on" do
|
55
|
+
strategy.switch! :one, true
|
56
|
+
strategy.on?(:one).should be true
|
57
|
+
end
|
58
|
+
it "can switch unknown features on" do
|
59
|
+
strategy.switch! :three, true
|
60
|
+
strategy.on?(:three).should be true
|
61
|
+
end
|
62
|
+
it "can switch features off" do
|
63
|
+
strategy.switch! :two, false
|
64
|
+
strategy.on?(:two).should be false
|
65
|
+
end
|
66
|
+
it "can delete knowledge of a feature" do
|
67
|
+
strategy.delete! :one
|
68
|
+
strategy.on?(:one).should be false
|
69
|
+
strategy.knows?(:one).should be false
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
end
|
74
|
+
|
75
|
+
describe Flip::CookieStrategy::Loader do
|
76
|
+
|
77
|
+
it "adds filters when included in controller" do
|
78
|
+
ControllerWithoutCookieStrategy.tap do |klass|
|
79
|
+
klass.should_receive(:before_action).with(:flip_cookie_strategy_before)
|
80
|
+
klass.should_receive(:after_action).with(:flip_cookie_strategy_after)
|
81
|
+
klass.send :include, Flip::CookieStrategy::Loader
|
82
|
+
end
|
83
|
+
end
|
84
|
+
|
85
|
+
describe "filter methods" do
|
86
|
+
let(:strategy) { Flip::CookieStrategy.new }
|
87
|
+
let(:controller) { ControllerWithCookieStrategy.new }
|
88
|
+
describe "#flip_cookie_strategy_before" do
|
89
|
+
it "passes controller cookies to CookieStrategy" do
|
90
|
+
controller.should_receive(:cookies).and_return(strategy.cookie_name(:test) => "true")
|
91
|
+
expect {
|
92
|
+
controller.flip_cookie_strategy_before
|
93
|
+
}.to change {
|
94
|
+
[ strategy.knows?(:test), strategy.on?(:test) ]
|
95
|
+
}.from([false, false]).to([true, true])
|
96
|
+
end
|
97
|
+
end
|
98
|
+
describe "#flip_cookie_strategy_after" do
|
99
|
+
before do
|
100
|
+
Flip::CookieStrategy.cookies = { strategy.cookie_name(:test) => "true" }
|
101
|
+
end
|
102
|
+
it "passes controller cookies to CookieStrategy" do
|
103
|
+
expect {
|
104
|
+
controller.flip_cookie_strategy_after
|
105
|
+
}.to change {
|
106
|
+
[ strategy.knows?(:test), strategy.on?(:test) ]
|
107
|
+
}.from([true, true]).to([false, false])
|
108
|
+
end
|
109
|
+
end
|
110
|
+
end
|
111
|
+
|
112
|
+
end
|
@@ -0,0 +1,110 @@
|
|
1
|
+
require "spec_helper"
|
2
|
+
|
3
|
+
describe Flip::DatabaseStrategy do
|
4
|
+
|
5
|
+
let(:definition) { double("definition", key: "one") }
|
6
|
+
let(:strategy) { Flip::DatabaseStrategy.new(model_klass) }
|
7
|
+
let(:model_klass) do
|
8
|
+
class Sample
|
9
|
+
attr_accessor :key
|
10
|
+
|
11
|
+
def enabled?
|
12
|
+
true
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
Class.new do
|
17
|
+
extend Flip::Cacheable
|
18
|
+
extend Flip::Declarable
|
19
|
+
feature :one
|
20
|
+
feature :two, description: "Second one."
|
21
|
+
feature :three, default: true
|
22
|
+
|
23
|
+
def self.all
|
24
|
+
list = []
|
25
|
+
keys = ['one', 'two', 'three']
|
26
|
+
3.times do |i|
|
27
|
+
list << Sample.new
|
28
|
+
list.last.key = keys[i]
|
29
|
+
end
|
30
|
+
list
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
let(:enabled_record) { model_klass.new.tap { |m| m.stub(:enabled?) { true } } }
|
36
|
+
let(:disabled_record) { model_klass.new.tap { |m| m.stub(:enabled?) { false } } }
|
37
|
+
|
38
|
+
subject { strategy }
|
39
|
+
|
40
|
+
its(:switchable?) { should be true }
|
41
|
+
its(:description) { should be_present }
|
42
|
+
|
43
|
+
let(:db_result) { [] }
|
44
|
+
before do
|
45
|
+
allow(model_klass).to(receive(:where).with(key: "one").and_return(db_result))
|
46
|
+
end
|
47
|
+
|
48
|
+
describe "#knows?" do
|
49
|
+
context "for unknown key" do
|
50
|
+
it "returns true" do
|
51
|
+
expect(strategy.knows?(definition)).to eq(false)
|
52
|
+
end
|
53
|
+
end
|
54
|
+
context "for known key" do
|
55
|
+
let(:db_result) { [disabled_record] }
|
56
|
+
it "returns false" do
|
57
|
+
expect(strategy.knows?(definition)).to eq(true)
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
|
62
|
+
describe "#on? with feature cache" do
|
63
|
+
before { model_klass.start_feature_cache }
|
64
|
+
context "for an enabled record" do
|
65
|
+
let(:db_result) { [enabled_record] }
|
66
|
+
it "returns true" do
|
67
|
+
expect(strategy.on?(definition)).to eq(true)
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
describe "#on?" do
|
73
|
+
context "for an enabled record" do
|
74
|
+
let(:db_result) { [enabled_record] }
|
75
|
+
it "returns true" do
|
76
|
+
expect(strategy.on?(definition)).to eq(true)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
context "for a disabled record" do
|
80
|
+
let(:db_result) { [disabled_record] }
|
81
|
+
it "returns true" do
|
82
|
+
expect(strategy.on?(definition)).to eq(false)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
describe "#switch!" do
|
88
|
+
it "can switch a feature on" do
|
89
|
+
expect(db_result).to receive(:first_or_initialize).and_return(disabled_record)
|
90
|
+
expect(disabled_record).to receive(:enabled=).with(true)
|
91
|
+
expect(disabled_record).to receive(:save!)
|
92
|
+
strategy.switch! :one, true
|
93
|
+
end
|
94
|
+
it "can switch a feature off" do
|
95
|
+
expect(db_result).to receive(:first_or_initialize).and_return(enabled_record)
|
96
|
+
expect(enabled_record).to receive(:enabled=).with(false)
|
97
|
+
expect(enabled_record).to receive(:save!)
|
98
|
+
strategy.switch! :one, false
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
describe "#delete!" do
|
103
|
+
let(:db_result) { [enabled_record] }
|
104
|
+
it "can delete a feature record" do
|
105
|
+
enabled_record.should_receive(:try).with(:destroy)
|
106
|
+
strategy.delete! :one
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
end
|