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.
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