flip_fork 0.1.0

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 (47) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.travis.yml +7 -0
  4. data/Gemfile +2 -0
  5. data/README.md +161 -0
  6. data/Rakefile +10 -0
  7. data/TODO +3 -0
  8. data/app/assets/stylesheets/flip.css +70 -0
  9. data/app/controllers/flip/features_controller.rb +47 -0
  10. data/app/controllers/flip/strategies_controller.rb +31 -0
  11. data/app/helpers/flip_helper.rb +9 -0
  12. data/app/views/flip/features/index.html.erb +62 -0
  13. data/config/routes.rb +14 -0
  14. data/flip.gemspec +25 -0
  15. data/lib/flip/abstract_strategy.rb +26 -0
  16. data/lib/flip/controller_filters.rb +21 -0
  17. data/lib/flip/cookie_strategy.rb +62 -0
  18. data/lib/flip/database_strategy.rb +40 -0
  19. data/lib/flip/declarable.rb +24 -0
  20. data/lib/flip/declaration_strategy.rb +20 -0
  21. data/lib/flip/definition.rb +21 -0
  22. data/lib/flip/engine.rb +9 -0
  23. data/lib/flip/facade.rb +18 -0
  24. data/lib/flip/feature_set.rb +57 -0
  25. data/lib/flip/forbidden.rb +7 -0
  26. data/lib/flip/version.rb +3 -0
  27. data/lib/flip.rb +27 -0
  28. data/lib/generators/flip/install/install_generator.rb +9 -0
  29. data/lib/generators/flip/migration/USAGE +5 -0
  30. data/lib/generators/flip/migration/migration_generator.rb +22 -0
  31. data/lib/generators/flip/migration/templates/create_features.rb +10 -0
  32. data/lib/generators/flip/model/USAGE +8 -0
  33. data/lib/generators/flip/model/model_generator.rb +8 -0
  34. data/lib/generators/flip/model/templates/feature.rb +15 -0
  35. data/lib/generators/flip/routes/USAGE +7 -0
  36. data/lib/generators/flip/routes/routes_generator.rb +7 -0
  37. data/spec/abstract_strategy_spec.rb +11 -0
  38. data/spec/controller_filters_spec.rb +27 -0
  39. data/spec/cookie_strategy_spec.rb +112 -0
  40. data/spec/database_strategy_spec.rb +66 -0
  41. data/spec/declarable_spec.rb +32 -0
  42. data/spec/declaration_strategy_spec.rb +39 -0
  43. data/spec/definition_spec.rb +19 -0
  44. data/spec/feature_set_spec.rb +67 -0
  45. data/spec/flip_spec.rb +33 -0
  46. data/spec/spec_helper.rb +1 -0
  47. metadata +156 -0
@@ -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
+ @instance = nil
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
@@ -0,0 +1,7 @@
1
+ module Flip
2
+ class Forbidden < StandardError
3
+ def initialize(key)
4
+ super("requires :#{key} feature")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Flip
2
+ VERSION = "0.1.0"
3
+ end
data/lib/flip.rb ADDED
@@ -0,0 +1,27 @@
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
+ forbidden
21
+ }.each { |name| require "flip/#{name}" }
22
+
23
+ require "flip/engine" if defined?(Rails)
24
+
25
+ module Flip
26
+ extend Facade
27
+ end
@@ -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/active_record/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,10 @@
1
+ class CreateFeatures < ActiveRecord::Migration
2
+ def change
3
+ create_table :features do |t|
4
+ t.string :key, null: false
5
+ t.boolean :enabled, null: false, default: false
6
+
7
+ t.timestamps null: false
8
+ end
9
+ end
10
+ 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,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,112 @@
1
+ require "spec_helper"
2
+
3
+ class ControllerWithoutCookieStrategy; end
4
+ class ControllerWithCookieStrategy
5
+ def self.before_filter(_); end
6
+ def self.after_filter(_); 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_filter).with(:flip_cookie_strategy_before)
80
+ klass.should_receive(:after_filter).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,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(:enabled?) { true } } }
16
+ let(:disabled_record) { model_klass.new.tap { |m| m.stub(:enabled?) { 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(enabled: 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(enabled: 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,32 @@
1
+ require "spec_helper"
2
+
3
+ describe Flip::Declarable do
4
+
5
+ let!(:model_class) do
6
+ Class.new do
7
+ extend Flip::Declarable
8
+
9
+ strategy Flip::DeclarationStrategy
10
+ default false
11
+
12
+ feature :one
13
+ feature :two, description: "Second one."
14
+ feature :three, default: true
15
+ end
16
+ end
17
+
18
+ subject { Flip::FeatureSet.instance }
19
+
20
+ describe "the .on? class method" do
21
+ context "with default set to false" do
22
+ it { should_not be_on(:one) }
23
+ it { should be_on(:three) }
24
+ end
25
+ context "with default set to true" do
26
+ before(:all) { model_class.send(:default, true) }
27
+ it { should be_on(:one) }
28
+ it { should be_on(:three) }
29
+ end
30
+ end
31
+
32
+ end
@@ -0,0 +1,39 @@
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
+ it "does not know definition with no default specified" do
11
+ subject.knows?(Flip::Definition.new :feature).should be_false
12
+ end
13
+ it "does not know definition with default of nil" do
14
+ subject.knows?(definition(nil)).should be_false
15
+ end
16
+ it "knows definition with default set to true" do
17
+ subject.knows?(definition(true)).should be_true
18
+ end
19
+ it "knows definition with default set to false" do
20
+ subject.knows?(definition(false)).should be_true
21
+ end
22
+ end
23
+
24
+ describe "#on? for Flip::Definition" do
25
+ subject { Flip::DeclarationStrategy.new.on? definition(default) }
26
+ [
27
+ { default: true, result: true },
28
+ { default: false, result: false },
29
+ { default: proc { true }, result: true, name: "proc returning true" },
30
+ { default: proc { false }, result: false, name: "proc returning false" },
31
+ ].each do |parameters|
32
+ context "with default of #{parameters[:name] || parameters[:default]}" do
33
+ let(:default) { parameters[:default] }
34
+ it { should == parameters[:result] }
35
+ end
36
+ end
37
+ end
38
+
39
+ 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,67 @@
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
+ it "can be reset multiple times without error" do
37
+ 2.times { Flip::FeatureSet.reset }
38
+ end
39
+ end
40
+
41
+ describe "#default= and #on? with null strategy" do
42
+ subject { feature_set_with_null_strategy }
43
+ it "defaults to false" do
44
+ subject.on?(:feature).should be_false
45
+ end
46
+ it "can default to true" do
47
+ subject.default = true
48
+ subject.on?(:feature).should be_true
49
+ end
50
+ it "accepts a proc returning true" do
51
+ subject.default = proc { true }
52
+ subject.on?(:feature).should be_true
53
+ end
54
+ it "accepts a proc returning false" do
55
+ subject.default = proc { false }
56
+ subject.on?(:feature).should be_false
57
+ end
58
+ end
59
+
60
+ describe "feature set with null strategy then always-true strategy" do
61
+ subject { feature_set_with_null_then_true_strategies }
62
+ it "returns true due to second strategy" do
63
+ subject.on?(:feature).should be_true
64
+ end
65
+ end
66
+
67
+ 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
@@ -0,0 +1 @@
1
+ require "flip"