flip2 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +2 -0
  6. data/README.md +180 -0
  7. data/Rakefile +11 -0
  8. data/TODO +3 -0
  9. data/app/assets/stylesheets/flip.css +70 -0
  10. data/app/controllers/flip/features_controller.rb +47 -0
  11. data/app/controllers/flip/strategies_controller.rb +31 -0
  12. data/app/helpers/flip_helper.rb +9 -0
  13. data/app/views/flip/features/index.html.erb +62 -0
  14. data/config/routes.rb +14 -0
  15. data/flip.gemspec +27 -0
  16. data/lib/flip/abstract_strategy.rb +26 -0
  17. data/lib/flip/cacheable.rb +25 -0
  18. data/lib/flip/controller_filters.rb +25 -0
  19. data/lib/flip/cookie_strategy.rb +67 -0
  20. data/lib/flip/database_strategy.rb +46 -0
  21. data/lib/flip/declarable.rb +24 -0
  22. data/lib/flip/declaration_strategy.rb +20 -0
  23. data/lib/flip/definition.rb +21 -0
  24. data/lib/flip/engine.rb +9 -0
  25. data/lib/flip/facade.rb +18 -0
  26. data/lib/flip/feature_set.rb +57 -0
  27. data/lib/flip/forbidden.rb +7 -0
  28. data/lib/flip/version.rb +3 -0
  29. data/lib/flip2.rb +28 -0
  30. data/lib/generators/flip/install/install_generator.rb +9 -0
  31. data/lib/generators/flip/migration/USAGE +5 -0
  32. data/lib/generators/flip/migration/migration_generator.rb +22 -0
  33. data/lib/generators/flip/migration/templates/create_features.rb +11 -0
  34. data/lib/generators/flip/model/USAGE +8 -0
  35. data/lib/generators/flip/model/model_generator.rb +8 -0
  36. data/lib/generators/flip/model/templates/feature.rb +15 -0
  37. data/lib/generators/flip/routes/USAGE +7 -0
  38. data/lib/generators/flip/routes/routes_generator.rb +7 -0
  39. data/lib/generators/flip/views/USAGE +8 -0
  40. data/lib/generators/flip/views/templates/index.html.erb +54 -0
  41. data/lib/generators/flip/views/views_generator.rb +8 -0
  42. data/spec/abstract_strategy_spec.rb +11 -0
  43. data/spec/cacheable_spec.rb +49 -0
  44. data/spec/controller_filters_spec.rb +27 -0
  45. data/spec/cookie_strategy_spec.rb +112 -0
  46. data/spec/database_strategy_spec.rb +110 -0
  47. data/spec/declarable_spec.rb +32 -0
  48. data/spec/declaration_strategy_spec.rb +39 -0
  49. data/spec/definition_spec.rb +19 -0
  50. data/spec/feature_set_spec.rb +67 -0
  51. data/spec/flip_spec.rb +33 -0
  52. data/spec/spec_helper.rb +2 -0
  53. metadata +172 -0
@@ -0,0 +1,9 @@
1
+ class Flip::InstallGenerator < Rails::Generators::Base
2
+
3
+ def invoke_generators
4
+ %w{ model migration routes }.each do |name|
5
+ generate "flip:#{name}"
6
+ end
7
+ end
8
+
9
+ end
@@ -0,0 +1,5 @@
1
+ Description:
2
+ Generates migration to create the features table for the Feature model.
3
+
4
+ Example:
5
+ rails generate flip:migration
@@ -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,8 @@
1
+ Description:
2
+ Generates the Feature database model class for Flip.
3
+
4
+ Example:
5
+ rails generate flip:model
6
+
7
+ This will create:
8
+ app/models/feature.rb (class Feature < ActiveRecord::Base)
@@ -0,0 +1,8 @@
1
+ class Flip::ModelGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('../templates', __FILE__)
3
+
4
+ def copy_feature_model_file
5
+ copy_file "feature.rb", "app/models/feature.rb"
6
+ end
7
+
8
+ 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,7 @@
1
+ Description:
2
+ Add routes for the Flip control page.
3
+
4
+ Example:
5
+ rails generate flip:routes
6
+
7
+ This will add routes to app/routes.rb
@@ -0,0 +1,7 @@
1
+ class Flip::RoutesGenerator < Rails::Generators::Base
2
+
3
+ def add_route
4
+ route %{mount Flip::Engine => "/flip"}
5
+ end
6
+
7
+ end
@@ -0,0 +1,8 @@
1
+ Description:
2
+ Generates flip/features/index view template.
3
+
4
+ Example:
5
+ rails generate flip:views
6
+
7
+ This will create:
8
+ app/views/flip/features/index.html.erb
@@ -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,8 @@
1
+ class Flip::ViewsGenerator < Rails::Generators::Base
2
+ source_root File.expand_path('../templates', __FILE__)
3
+
4
+ def copy_view_index_file
5
+ copy_file "index.html.erb", "app/views/flip/features/index.html.erb"
6
+ end
7
+
8
+ 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,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