flip_fork 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA1:
3
+ metadata.gz: 13d411c043eedc95154d47015e144ecdbc709a19
4
+ data.tar.gz: 9aabaffe9b990127475615b59e3ba06a06c038e4
5
+ SHA512:
6
+ metadata.gz: 1aece50a74ff32c555fadf7cf4348cc9dcaa50ce104a23f266a9763dfc86cd16cba3c204e58bae87b86565ac401a91980d8a9f0b7498b306fced53fa2a569c97
7
+ data.tar.gz: e5a34425077ce23c1f5abb8b77c65472a2339a31502878affae1e052611ded057886d542955151c64542c4d5a2f764048214e1661ddfaec948dbc92d2ec9f14d
data/.gitignore ADDED
@@ -0,0 +1,4 @@
1
+ *.gem
2
+ .bundle
3
+ Gemfile.lock
4
+ pkg/*
data/.travis.yml ADDED
@@ -0,0 +1,7 @@
1
+ language: ruby
2
+ rvm:
3
+ - 2.0.0
4
+ - 1.9.3
5
+ - 1.9.2
6
+ - jruby-19mode
7
+ - rbx-19mode
data/Gemfile ADDED
@@ -0,0 +1,2 @@
1
+ source "https://rubygems.org"
2
+ gemspec
data/README.md ADDED
@@ -0,0 +1,161 @@
1
+ Flip — flip your features
2
+ ================
3
+
4
+ [![Build Status](https://travis-ci.org/pda/flip.png)](https://travis-ci.org/pda/flip)
5
+
6
+ **Flip** provides a declarative, layered way of enabling and disabling application functionality at run-time.
7
+
8
+ This gem optimizes for:
9
+
10
+ * developer ease-of-use,
11
+ * visibility and control for other stakeholders (like marketing); and
12
+ * run-time performance
13
+
14
+ There are three layers of strategies per feature:
15
+
16
+ * default
17
+ * database, to flip features site-wide for all users
18
+ * cookie, to flip features just for you (or someone else)
19
+
20
+ There is also a configurable system-wide default - !Rails.env.production?` works nicely.
21
+
22
+ Flip has a dashboard UI that's easy to understand and use.
23
+
24
+ ![Feature Flipper Dashboard](https://dl.dropbox.com/u/13833591/flip-gem-dashboard.png "Feature Flipper Dashboard")
25
+
26
+ Install
27
+ -------
28
+
29
+ **Rails 3.0, 3.1 and 3.2+**
30
+
31
+ # Gemfile
32
+ gem "flip"
33
+
34
+ # Generate the model and migration
35
+ > rails g flip:install
36
+
37
+ # Run the migration
38
+ > rake db:migrate
39
+
40
+
41
+ Declaring Features
42
+ ------------------
43
+
44
+ ```ruby
45
+ # This is the model class generated by rails g flip:install
46
+ class Feature < ActiveRecord::Base
47
+ include Flip::Declarable
48
+
49
+ # The recommended Flip strategy stack.
50
+ strategy Flip::CookieStrategy
51
+ strategy Flip::DatabaseStrategy
52
+ strategy Flip::DefaultStrategy
53
+ default false
54
+
55
+ # A basic feature declaration.
56
+ feature :shiny_things
57
+
58
+ # Override the system-wide default.
59
+ feature :world_domination, default: true
60
+
61
+ # Enabled half the time..? Sure, we can do that.
62
+ feature :flakey,
63
+ default: proc { rand(2).zero? }
64
+
65
+ # Provide a description, normally derived from the feature name.
66
+ feature :something,
67
+ default: true,
68
+ description: "Ability to purchase enrollments in courses",
69
+
70
+ end
71
+ ```
72
+
73
+
74
+ Checking Features
75
+ -----------------
76
+
77
+ `Flip.on?` or the dynamic predicate methods are used to check feature state:
78
+
79
+ ```ruby
80
+ Flip.on? :world_domination # true
81
+ Flip.world_domination? # true
82
+
83
+ Flip.on? :shiny_things # false
84
+ Flip.shiny_things? # false
85
+ ```
86
+
87
+ Views and controllers use the `feature?(key)` method:
88
+
89
+ ```erb
90
+ <div>
91
+ <% if feature? :world_domination %>
92
+ <%= link_to "Dominate World", world_dominations_path %>
93
+ <% end %>
94
+ </div>
95
+ ```
96
+
97
+
98
+ Feature Flipping Controllers
99
+ ----------------------------
100
+
101
+ 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:
102
+
103
+ ```ruby
104
+ class SampleController < ApplicationController
105
+
106
+ require_feature :something, :except => :index
107
+
108
+ def show
109
+ end
110
+
111
+ def index
112
+ end
113
+
114
+ end
115
+ ```
116
+
117
+ Dashboard
118
+ ---------
119
+
120
+ The dashboard provides visibility and control over the features.
121
+
122
+ The gem includes some basic styles:
123
+
124
+ ```haml
125
+ = content_for :stylesheets_head do
126
+ = stylesheet_link_tag "flip"
127
+ ```
128
+
129
+ You probably don't want the dashboard to be public. Here's one way of implementing access control.
130
+
131
+ app/controllers/admin/features_controller.rb:
132
+
133
+ ```ruby
134
+ class Admin::FeaturesController < Flip::FeaturesController
135
+ before_filter :assert_authenticated_as_admin
136
+ end
137
+ ```
138
+
139
+ app/controllers/admin/feature_strategies_controller.rb:
140
+
141
+ ```ruby
142
+ class Admin::FeatureStrategiesController < Flip::FeaturesController
143
+ before_filter :assert_authenticated_as_admin
144
+ end
145
+ ```
146
+
147
+ routes.rb:
148
+
149
+ ```ruby
150
+ namespace :admin do
151
+ resources :features, only: [ :index ] do
152
+ resources :feature_strategies, only: [ :update, :destroy ]
153
+ end
154
+ end
155
+
156
+ mount Flip::Engine => "/admin/features"
157
+ ```
158
+
159
+ ----
160
+ Created by Paul Annesley
161
+ Copyright © 2011-2013 Learnable Pty Ltd, [MIT Licence](http://www.opensource.org/licenses/mit-license.php).
data/Rakefile ADDED
@@ -0,0 +1,10 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ desc "Run all tests"
5
+ task :default => :spec
6
+
7
+ desc "Run specs"
8
+ task :spec do
9
+ system 'bundle exec rspec --color --format documentation spec/*_spec.rb'
10
+ end
data/TODO ADDED
@@ -0,0 +1,3 @@
1
+ FlipHelper view/controller helper.
2
+ Optimize DatabaseStrategy.
3
+ CookieStrategy::Loader before/after, not around.
@@ -0,0 +1,70 @@
1
+ /* Flip */
2
+
3
+ .flip {
4
+ margin: 0;
5
+ }
6
+
7
+ .flip h1 {
8
+ color:#666666;
9
+ font-size: 229%;
10
+ line-height: 44.928px;
11
+ margin: 13.5px 0;
12
+ }
13
+
14
+ .flip th.name, .flip th.description, .flip th.status {
15
+ visibility: hidden;
16
+ }
17
+
18
+ .flip td.name {
19
+ font-family: Monaco, sans-serif;
20
+ font-weight: bold;
21
+ }
22
+
23
+ .flip td.name, .flip td.description {
24
+ vertical-align: top;
25
+ }
26
+
27
+ .flip th {
28
+ font-weight: normal;
29
+ text-align: left;
30
+ vertical-align: top;
31
+ }
32
+
33
+ .flip th .description {
34
+ font-weight: normal;
35
+ display: block;
36
+ font-size: 80%;
37
+ }
38
+
39
+ .flip th, .flip td {
40
+ padding: 5px 10px;
41
+ width: 160px;
42
+ height: 40px;
43
+ }
44
+
45
+ .flip td.off, .flip td.on, .flip td.pass {
46
+ text-align: center;
47
+ text-transform: capitalize;
48
+ }
49
+
50
+ .flip td.off {
51
+ background-color: #fbb;
52
+ }
53
+
54
+ .flip td.on {
55
+ background-color: #cfc;
56
+ }
57
+
58
+ .flip td.pass {
59
+ background-color: #eef;
60
+ }
61
+
62
+ .flip form {
63
+ display: inline;
64
+ }
65
+
66
+ .flip form input[type=submit] {
67
+ font-size: 80%;
68
+ padding: 2px 5px;
69
+ margin: 0;
70
+ }
@@ -0,0 +1,47 @@
1
+ module Flip
2
+ class FeaturesController < ApplicationController
3
+
4
+ def index
5
+ @p = FeaturesPresenter.new(FeatureSet.instance)
6
+ end
7
+
8
+ class FeaturesPresenter
9
+
10
+ include Flip::Engine.routes.url_helpers
11
+
12
+ def initialize(feature_set)
13
+ @feature_set = feature_set
14
+ end
15
+
16
+ def strategies
17
+ @feature_set.strategies
18
+ end
19
+
20
+ def definitions
21
+ @feature_set.definitions
22
+ end
23
+
24
+ def status(definition)
25
+ @feature_set.on?(definition.key) ? "on" : "off"
26
+ end
27
+
28
+ def default_status(definition)
29
+ @feature_set.default_for(definition) ? "on" : "off"
30
+ end
31
+
32
+ def strategy_status(strategy, definition)
33
+ if strategy.knows? definition
34
+ strategy.on?(definition) ? "on" : "off"
35
+ end
36
+ end
37
+
38
+ def switch_url(strategy, definition)
39
+ feature_strategy_path \
40
+ definition.key,
41
+ strategy.name.underscore
42
+ end
43
+
44
+ end
45
+
46
+ end
47
+ end
@@ -0,0 +1,31 @@
1
+ module Flip
2
+ class StrategiesController < ApplicationController
3
+
4
+ include Flip::Engine.routes.url_helpers
5
+
6
+ def update
7
+ strategy.switch! feature_key, turn_on?
8
+ redirect_to features_url
9
+ end
10
+
11
+ def destroy
12
+ strategy.delete! feature_key
13
+ redirect_to features_url
14
+ end
15
+
16
+ private
17
+
18
+ def turn_on?
19
+ params[:commit] == "Switch On"
20
+ end
21
+
22
+ def feature_key
23
+ params[:feature_id].to_sym
24
+ end
25
+
26
+ def strategy
27
+ FeatureSet.instance.strategy(params[:id])
28
+ end
29
+
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # Access to feature-flipping configuration.
2
+ module FlipHelper
3
+
4
+ # Whether the given feature is switched on
5
+ def feature?(key)
6
+ Flip.on? key
7
+ end
8
+
9
+ end
@@ -0,0 +1,62 @@
1
+ <div class="flip">
2
+ <h1>Feature Flippers</h1>
3
+
4
+ <table>
5
+ <thead>
6
+ <th class="name">Feature Name</th>
7
+ <th class="description">Description</th>
8
+ <th class="status">Status</th>
9
+ <% @p.strategies.each do |strategy| %>
10
+ <th>
11
+ <%= strategy.name %>
12
+ <span class="description"><%= strategy.description %></span>
13
+ </th>
14
+ <% end %>
15
+ <th>
16
+ Default
17
+ <span class="description">The system default when no strategies match.</span>
18
+ </th>
19
+ </thead>
20
+ <tbody>
21
+ <% @p.definitions.each do |definition| %>
22
+ <tr>
23
+ <td class="name"><%= definition.name %></td>
24
+
25
+ <td class="description"><%= definition.description %></td>
26
+
27
+ <%= content_tag :td, class: @p.status(definition) do %>
28
+ <%= @p.status definition %>
29
+ <% end %>
30
+
31
+ <% @p.strategies.each do |strategy| %>
32
+ <%= content_tag :td, class: @p.strategy_status(strategy, definition) || "pass" do %>
33
+ <%= @p.strategy_status strategy, definition %>
34
+
35
+ <% if strategy.switchable? %>
36
+ <%= form_tag(@p.switch_url(strategy, definition), method: :put) do %>
37
+ <% unless @p.strategy_status(strategy, definition) == "on" %>
38
+ <%= submit_tag "Switch On" %>
39
+ <% end %>
40
+ <% unless @p.strategy_status(strategy, definition) == "off" %>
41
+ <%= submit_tag "Switch Off" %>
42
+ <% end %>
43
+ <% end %>
44
+ <% unless @p.strategy_status(strategy, definition).blank? %>
45
+ <%= form_tag(@p.switch_url(strategy, definition), method: :delete) do %>
46
+ <%= submit_tag "Delete" %>
47
+ <% end %>
48
+ <% end %>
49
+ <% end %>
50
+
51
+ <% end %>
52
+ <% end %>
53
+
54
+ <%= content_tag :td, class: @p.default_status(definition) do %>
55
+ <%= @p.default_status definition %>
56
+ <% end %>
57
+
58
+ </tr>
59
+ <% end %>
60
+ </tbody>
61
+ </table>
62
+ </div>
data/config/routes.rb ADDED
@@ -0,0 +1,14 @@
1
+ Flip::Engine.routes.draw do
2
+
3
+ scope module: "Flip" do
4
+
5
+ resources :features, path: "", only: [ :index ] do
6
+
7
+ resources :strategies,
8
+ only: [ :update, :destroy ]
9
+
10
+ end
11
+
12
+ end
13
+
14
+ end
data/flip.gemspec ADDED
@@ -0,0 +1,25 @@
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_fork"
7
+ s.version = Flip::VERSION
8
+ s.platform = Gem::Platform::RUBY
9
+ s.authors = ["Paul Annesley", "Brandt Lareau"]
10
+ s.email = ["paul@annesley.cc, brandt.lareau@gmail.com"]
11
+ s.homepage = "https://github.com/newdark/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.files = `git ls-files`.split("\n")
16
+ s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
17
+ s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
18
+ s.require_paths = ["lib"]
19
+
20
+ s.add_dependency("activesupport", "~> 3.0")
21
+ s.add_dependency("i18n")
22
+
23
+ s.add_development_dependency("rspec", "~> 2.5")
24
+ s.add_development_dependency("rake")
25
+ 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
+ raise Flip::Forbidden.new(key)
18
+ end
19
+
20
+ end
21
+ end
@@ -0,0 +1,62 @@
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
+ self.class.instance_variable_get(:@cookies) || {}
42
+ end
43
+
44
+ # Include in ApplicationController to push cookies into CookieStrategy.
45
+ # Users before_filter and after_filter rather than around_filter to
46
+ # avoid pointlessly adding to stack depth.
47
+ module Loader
48
+ extend ActiveSupport::Concern
49
+ included do
50
+ before_filter :flip_cookie_strategy_before
51
+ after_filter :flip_cookie_strategy_after
52
+ end
53
+ def flip_cookie_strategy_before
54
+ CookieStrategy.cookies = cookies
55
+ end
56
+ def flip_cookie_strategy_after
57
+ CookieStrategy.cookies = nil
58
+ end
59
+ end
60
+
61
+ end
62
+ end
@@ -0,0 +1,40 @@
1
+ # Database backed system-wide
2
+ module Flip
3
+ class DatabaseStrategy < AbstractStrategy
4
+
5
+ def initialize(model_klass = Feature)
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).enabled?
19
+ end
20
+
21
+ def switchable?
22
+ true
23
+ end
24
+
25
+ def switch! key, enable
26
+ @klass.find_or_initialize_by_key(key.to_s).update_attributes! enabled: enable
27
+ end
28
+
29
+ def delete! key
30
+ @klass.find_by_key(key.to_s).try(:destroy)
31
+ end
32
+
33
+ private
34
+
35
+ def feature(definition)
36
+ @klass.find_by_key definition.key.to_s
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,24 @@
1
+ module Flip
2
+ module Declarable
3
+
4
+ def self.extended(base)
5
+ FeatureSet.reset
6
+ end
7
+
8
+ # Adds a new feature definition, creates predicate method.
9
+ def feature(key, options = {})
10
+ FeatureSet.instance << Flip::Definition.new(key, options)
11
+ end
12
+
13
+ # Adds a strategy for determining feature status.
14
+ def strategy(strategy)
15
+ FeatureSet.instance.add_strategy strategy
16
+ end
17
+
18
+ # The default response, boolean or a Proc to be called.
19
+ def default(default)
20
+ FeatureSet.instance.default = default
21
+ end
22
+
23
+ end
24
+ 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[:default].nil?
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
@@ -0,0 +1,9 @@
1
+ module Flip
2
+ class Engine < ::Rails::Engine
3
+
4
+ initializer "flip.blarg" do
5
+ ActionController::Base.send(:include, Flip::CookieStrategy::Loader)
6
+ end
7
+
8
+ end
9
+ end