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