fickle 0.0.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.
- data/.gitignore +5 -0
- data/.rspec +1 -0
- data/Gemfile +4 -0
- data/README.md +132 -0
- data/Rakefile +5 -0
- data/app/controllers/fickle/application_controller.rb +22 -0
- data/app/controllers/fickle/features_controller.rb +37 -0
- data/app/controllers/fickle/model_features_controller.rb +34 -0
- data/app/views/fickle/features/index.html.erb +9 -0
- data/app/views/fickle/model_features/index.html.erb +5 -0
- data/app/views/layouts/fickle.html.erb +21 -0
- data/fickle.gemspec +25 -0
- data/lib/fickle.rb +39 -0
- data/lib/fickle/abstract_backend.rb +59 -0
- data/lib/fickle/abstract_feature_set.rb +52 -0
- data/lib/fickle/backends.rb +5 -0
- data/lib/fickle/backends/memory.rb +42 -0
- data/lib/fickle/backends/memory/feature_set.rb +39 -0
- data/lib/fickle/class_methods.rb +14 -0
- data/lib/fickle/config.rb +29 -0
- data/lib/fickle/controllers.rb +5 -0
- data/lib/fickle/model_extensions.rb +28 -0
- data/lib/fickle/railtie.rb +8 -0
- data/lib/fickle/routes.rb +12 -0
- data/lib/fickle/version.rb +3 -0
- data/lib/fickle/view_helpers.rb +7 -0
- data/lib/generators/fickle/install/USAGE +8 -0
- data/lib/generators/fickle/install/install_generator.rb +22 -0
- data/lib/generators/fickle/install/templates/fickle.css +112 -0
- data/lib/generators/fickle/install/templates/fickle.js +0 -0
- data/lib/generators/fickle/install/templates/fickle.rb.erb +4 -0
- data/spec/class_methods_spec.rb +22 -0
- data/spec/feature_set_spec.rb +30 -0
- data/spec/fickle_spec.rb +21 -0
- data/spec/model_extensions_spec.rb +24 -0
- data/spec/spec_helper.rb +1 -0
- data/spec/view_helpers_spec.rb +34 -0
- metadata +142 -0
data/.gitignore
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--color
|
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,132 @@
|
|
1
|
+
fickle
|
2
|
+
======
|
3
|
+
*Don't make up your mind.*
|
4
|
+
|
5
|
+
Fickle is a feature engine for Rails 3. It is currently under heavy development and
|
6
|
+
is not stable in any sense of the word. Especially not the emotional sense.
|
7
|
+
|
8
|
+
Unlike other solutions, fickle lets you manage your features without __opening a file__,
|
9
|
+
__committing to a repository__, __performing a deploy__, or __breaking out ssh__. Just
|
10
|
+
point a browser at your application and bend it to your will!
|
11
|
+
|
12
|
+
Not only that, but fickle allows you to override your global features by mixing feature-set
|
13
|
+
functionality into __any model in your application__. If you want to restrict some experimental
|
14
|
+
features to a group of beta testers, it's easy to implement. If you want to enable every
|
15
|
+
feature when you're logged in as, say, "guinea_pig," you got it.
|
16
|
+
|
17
|
+
Quickstart
|
18
|
+
----------
|
19
|
+
|
20
|
+
1. Add fickle to your Gemfile
|
21
|
+
|
22
|
+
gem 'fickle'
|
23
|
+
|
24
|
+
2. Run the generator
|
25
|
+
|
26
|
+
> ./script/rails generate fickle:install
|
27
|
+
|
28
|
+
3. Add feature switches
|
29
|
+
|
30
|
+
<%- if feature_enabled? :my_totally_sweet_feature %>
|
31
|
+
<!--
|
32
|
+
My totally sweet code
|
33
|
+
-->
|
34
|
+
<%- else %>
|
35
|
+
<!--
|
36
|
+
My slightly less sweet code
|
37
|
+
-->
|
38
|
+
<%- end %>
|
39
|
+
|
40
|
+
|
41
|
+
4. Administer!
|
42
|
+
|
43
|
+

|
44
|
+
|
45
|
+
Web Interface
|
46
|
+
-------------
|
47
|
+
|
48
|
+
Fickle includes a web interface for managing your features. You can globally turn features on and off,
|
49
|
+
and you can manage them on a model-by-model basis. You can even hardstop a feature if it starts causing
|
50
|
+
you trouble.
|
51
|
+
|
52
|
+
The `fickle:install` generator adds this line to your `config/routes.rb` file:
|
53
|
+
|
54
|
+
fickle_admin 'features'
|
55
|
+
|
56
|
+
You can change this line for more advanced control, or you can remove it altogether if you don't want
|
57
|
+
to use the web interface.
|
58
|
+
|
59
|
+
The following options are currently supported:
|
60
|
+
|
61
|
+
path - the first argument, where the fickle admin panel should live.
|
62
|
+
:restrict_on - symbol which refers to a method in ApplicationController for
|
63
|
+
access restriction (ex. :current_user). If provided, fickle
|
64
|
+
will return a 404 unless the result of this method call returns
|
65
|
+
true for "can_administer_fickle?"
|
66
|
+
:controller - The controller to use. If you would like to override fickle's
|
67
|
+
behavior, you can subclass "Fickle::FeaturesController" with
|
68
|
+
your own controller and provide the name here.
|
69
|
+
|
70
|
+
|
71
|
+
Advanced Features
|
72
|
+
-----------------
|
73
|
+
|
74
|
+
Let's say you want to limit features on a per-user basis.
|
75
|
+
|
76
|
+
### app/models/user.rb
|
77
|
+
|
78
|
+
class User < ActiveRecord::Base
|
79
|
+
|
80
|
+
has_feature_set
|
81
|
+
|
82
|
+
end
|
83
|
+
|
84
|
+
### app/controllers/experimental_controller.rb
|
85
|
+
|
86
|
+
class ExperimentalController
|
87
|
+
|
88
|
+
before_filter :authenticate
|
89
|
+
|
90
|
+
def secret_feature
|
91
|
+
status 404 and return unless current_user.has_feature? :my_secret_new_feature
|
92
|
+
# Do secret stuff
|
93
|
+
end
|
94
|
+
|
95
|
+
end
|
96
|
+
|
97
|
+
You can also access this method in the views. Cause, you know, it's on the model.
|
98
|
+
|
99
|
+
Tips/tricks
|
100
|
+
-----------
|
101
|
+
|
102
|
+
|
103
|
+
Want a user who gets all the goodies? It's ruby!
|
104
|
+
|
105
|
+
### app/models/user.rb
|
106
|
+
|
107
|
+
def has_feature?(feature)
|
108
|
+
admin? || super
|
109
|
+
end
|
110
|
+
|
111
|
+
Oh, snap that was easy.
|
112
|
+
|
113
|
+
|
114
|
+
Copyright (c) 2011 chrisrhoden && lazerclub industries
|
115
|
+
|
116
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
117
|
+
of this software and associated documentation files (the "Software"), to deal
|
118
|
+
in the Software without restriction, including without limitation the rights
|
119
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
120
|
+
copies of the Software, and to permit persons to whom the Software is
|
121
|
+
furnished to do so, subject to the following conditions:
|
122
|
+
|
123
|
+
The above copyright notice and this permission notice shall be included in
|
124
|
+
all copies or substantial portions of the Software.
|
125
|
+
|
126
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
127
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
128
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
129
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
130
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
131
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
132
|
+
THE SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1,22 @@
|
|
1
|
+
module Fickle
|
2
|
+
class ApplicationController < ActionController::Base
|
3
|
+
layout 'fickle'
|
4
|
+
before_filter :get_models, :get_features, :get_backend, :get_overrides
|
5
|
+
|
6
|
+
def get_models
|
7
|
+
@models = Fickle::Config.backend.models
|
8
|
+
end
|
9
|
+
|
10
|
+
def get_features
|
11
|
+
@features = Fickle::Config.backend.feature_set('global')
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_backend
|
15
|
+
@backend = Fickle::Config.backend
|
16
|
+
end
|
17
|
+
|
18
|
+
def get_overrides
|
19
|
+
@overrides = Fickle::Config.backend.feature_set('master')
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
module Fickle
|
2
|
+
class FeaturesController < ApplicationController
|
3
|
+
|
4
|
+
|
5
|
+
def index
|
6
|
+
|
7
|
+
end
|
8
|
+
|
9
|
+
def update
|
10
|
+
if params[:enabled] == 'true'
|
11
|
+
@backend.global_enable(params[:id])
|
12
|
+
elsif params[:enabled] == 'false'
|
13
|
+
@backend.global_disable(params[:id])
|
14
|
+
elsif params[:hard_stop] == 'true'
|
15
|
+
@backend.override_disable(params[:id])
|
16
|
+
elsif params[:hard_stop] == 'false'
|
17
|
+
@backend.remove_override(params[:id])
|
18
|
+
end
|
19
|
+
|
20
|
+
respond_to do |format|
|
21
|
+
format.js { render :js => 'true'}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def create
|
26
|
+
|
27
|
+
end
|
28
|
+
|
29
|
+
def edit
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def show
|
34
|
+
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module Fickle
|
2
|
+
class ModelFeaturesController < ApplicationController
|
3
|
+
before_filter :find_model
|
4
|
+
|
5
|
+
def index
|
6
|
+
@features = @backend.model_feature_sets[@model]
|
7
|
+
end
|
8
|
+
|
9
|
+
def show; end
|
10
|
+
|
11
|
+
def update
|
12
|
+
@feature_set = @backend.feature_set(params[:id], @model)
|
13
|
+
|
14
|
+
if params[:enabled] == 'true'
|
15
|
+
@feature_set.enable_feature(params[:feature])
|
16
|
+
elsif params[:enabled] == 'false'
|
17
|
+
@feature_set.enable_feature(params[:feature])
|
18
|
+
end
|
19
|
+
|
20
|
+
respond_to do |format|
|
21
|
+
format.js { render :js => 'true'}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
|
26
|
+
private
|
27
|
+
def find_model
|
28
|
+
@model = (Fickle::Config.backend.models & [params[:model]]).first
|
29
|
+
unless @model
|
30
|
+
render :text => 'Not Found', :status => 404
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,9 @@
|
|
1
|
+
<ul id="features">
|
2
|
+
<%- @features.each do |name, enabled| %>
|
3
|
+
<li>
|
4
|
+
<span class="feature_name"><%= name.to_s.humanize %></span>
|
5
|
+
<input type='checkbox' class='hardstop' name='<%= name %>'<%= ' checked' if @overrides.enabled?(name) == false %>/>
|
6
|
+
<input type='checkbox' class='feature' name='<%= name %>'<%= ' checked' if enabled %>/>
|
7
|
+
</li>
|
8
|
+
<%- end %>
|
9
|
+
</ul>
|
@@ -0,0 +1,21 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html>
|
3
|
+
<head>
|
4
|
+
<title>Fickle!</title>
|
5
|
+
<%= stylesheet_link_tag 'fickle/fickle' %>
|
6
|
+
</head>
|
7
|
+
<body>
|
8
|
+
<h1>Fickle</h1>
|
9
|
+
<ul id="tabs">
|
10
|
+
<li><%= link_to 'Features', fickle_features_path %>
|
11
|
+
<%# @models.each do |model|
|
12
|
+
<li><%= link_to model.to_s.pluralize, fickle_model_features_path(model) </li>
|
13
|
+
end %>
|
14
|
+
</ul>
|
15
|
+
<div id="main">
|
16
|
+
<%= yield %>
|
17
|
+
</div>
|
18
|
+
<%= javascript_include_tag 'http://ajax.googleapis.com/ajax/libs/jquery/1.5.1/jquery.min.js' %>
|
19
|
+
<%= javascript_include_tag 'fickle' %>
|
20
|
+
</body>
|
21
|
+
</html>
|
data/fickle.gemspec
ADDED
@@ -0,0 +1,25 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "fickle/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "fickle"
|
7
|
+
s.version = Fickle::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = ["chrisrhoden"]
|
10
|
+
s.email = ["chris@lazerclub.co"]
|
11
|
+
s.homepage = ""
|
12
|
+
s.summary = %q{The feature library for rails that can't make up its mind}
|
13
|
+
s.description = %q{Fickle lets you turn features on and off at will in your rails app using a web interface.
|
14
|
+
It also lets you manage features on a model-by-model basis (maybe users?)}.gsub(' ','')
|
15
|
+
|
16
|
+
s.rubyforge_project = "fickle"
|
17
|
+
|
18
|
+
s.add_development_dependency 'rspec', '~> 2.4.0'
|
19
|
+
s.add_development_dependency 'rails', '~> 3.0.0'
|
20
|
+
|
21
|
+
s.files = `git ls-files`.split("\n")
|
22
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
23
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
24
|
+
s.require_paths = ["lib"]
|
25
|
+
end
|
data/lib/fickle.rb
ADDED
@@ -0,0 +1,39 @@
|
|
1
|
+
module Fickle
|
2
|
+
|
3
|
+
MODELS = []
|
4
|
+
|
5
|
+
def self.setup!
|
6
|
+
Class.send :include, Fickle::ClassMethods unless Class.respond_to? :has_featureset
|
7
|
+
begin
|
8
|
+
ApplicationHelper
|
9
|
+
rescue Exception; nil
|
10
|
+
else
|
11
|
+
ApplicationHelper.send :include, Fickle::ViewHelpers
|
12
|
+
end
|
13
|
+
begin
|
14
|
+
ActionDispatch::Routing::Mapper
|
15
|
+
rescue Exception; nil
|
16
|
+
else
|
17
|
+
ActionDispatch::Routing::Mapper.send :include, Fickle::Routes
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def self.config
|
22
|
+
yield Fickle::Config if block_given?
|
23
|
+
end
|
24
|
+
|
25
|
+
autoload :FeatureSet, 'fickle/feature_set'
|
26
|
+
autoload :ClassMethods, 'fickle/class_methods'
|
27
|
+
autoload :Railtie, 'fickle/railtie'
|
28
|
+
autoload :VERSION, 'fickle/version'
|
29
|
+
autoload :ViewHelpers, 'fickle/view_helpers'
|
30
|
+
autoload :ModelExtensions, 'fickle/model_extensions'
|
31
|
+
autoload :Routes, 'fickle/routes'
|
32
|
+
autoload :Config, 'fickle/config'
|
33
|
+
autoload :Backends, 'fickle/backends'
|
34
|
+
autoload :AbstractFeatureSet, 'fickle/abstract_feature_set'
|
35
|
+
autoload :AbstractBackend, 'fickle/abstract_backend'
|
36
|
+
|
37
|
+
require 'fickle/railtie' if defined? Rails
|
38
|
+
|
39
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
module Fickle
|
2
|
+
class AbstractBackend
|
3
|
+
|
4
|
+
def initialize(options)
|
5
|
+
@_klass = options[:feature_set_class]
|
6
|
+
add_feature_set('global', new_feature_set('global', :set_default => true))
|
7
|
+
add_feature_set('master', new_feature_set('master'))
|
8
|
+
end
|
9
|
+
|
10
|
+
def new_feature_set(feature_set_key, options={})
|
11
|
+
options.merge!(:key => feature_set_key)
|
12
|
+
if !['master', 'global'].include?(feature_set_key)
|
13
|
+
options[:override] = feature_set('master')
|
14
|
+
options[:fallthrough] = feature_set('global')
|
15
|
+
end
|
16
|
+
@_klass.new(options)
|
17
|
+
end
|
18
|
+
|
19
|
+
def global_enable(feature)
|
20
|
+
feature_set('global').enable_feature(feature)
|
21
|
+
end
|
22
|
+
|
23
|
+
def global_disable(feature)
|
24
|
+
feature_set('global').disable_feature(feature)
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_global(feature)
|
28
|
+
feature_set('global').remove_feature(feature)
|
29
|
+
end
|
30
|
+
|
31
|
+
def override_enable(feature)
|
32
|
+
feature_set('master').enable_feature(feature)
|
33
|
+
end
|
34
|
+
|
35
|
+
def override_disable(feature)
|
36
|
+
feature_set('master').disable_feature(feature)
|
37
|
+
end
|
38
|
+
|
39
|
+
def remove_override(feature)
|
40
|
+
feature_set('master').remove_feature(feature)
|
41
|
+
end
|
42
|
+
|
43
|
+
def feature_set(key, model=nil)
|
44
|
+
if model
|
45
|
+
feature_set = get_feature_set("#{model}-#{key}")
|
46
|
+
add_model_feature_set("#{model}-#{key}", model)
|
47
|
+
else
|
48
|
+
feature_set = get_feature_set(key)
|
49
|
+
end
|
50
|
+
return feature_set
|
51
|
+
end
|
52
|
+
|
53
|
+
def inspect
|
54
|
+
"#<Fickle::Backend overrides:#{feature_set('master').inspect} fallthroughs:#{feature_set('global').inspect}>"
|
55
|
+
end
|
56
|
+
|
57
|
+
alias_method :to_s, :inspect
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
module Fickle
|
2
|
+
class AbstractFeatureSet
|
3
|
+
|
4
|
+
include Enumerable
|
5
|
+
|
6
|
+
def initialize(options = {})
|
7
|
+
@_override = options[:override]
|
8
|
+
@_fallthrough = options[:fallthrough]
|
9
|
+
@_set_default = options[:set_default]
|
10
|
+
@_name = options[:key]
|
11
|
+
end
|
12
|
+
|
13
|
+
def enabled?(feature)
|
14
|
+
return overridden(feature) if overridden?(feature)
|
15
|
+
return feature_enabled?(feature) if feature_defined?(feature)
|
16
|
+
return fallthrough(feature)
|
17
|
+
end
|
18
|
+
|
19
|
+
def inspect
|
20
|
+
"#<Fickle::FeatureSet #@_name#{map{|key, value| " #{key}:#{value}"}.join}>"
|
21
|
+
end
|
22
|
+
|
23
|
+
alias_method :to_s, :inspect
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def overridden?(feature)
|
28
|
+
return false unless @_override
|
29
|
+
if !@_override.feature_enabled?(feature).nil?
|
30
|
+
return true
|
31
|
+
end
|
32
|
+
return false
|
33
|
+
end
|
34
|
+
|
35
|
+
def overridden(feature)
|
36
|
+
@_override.feature_enabled?(feature)
|
37
|
+
end
|
38
|
+
|
39
|
+
def feature_defined?(feature)
|
40
|
+
!feature_enabled?(feature).nil?
|
41
|
+
end
|
42
|
+
|
43
|
+
def fallthrough(feature)
|
44
|
+
return @_fallthrough.enabled?(feature) if @_fallthrough
|
45
|
+
if @_set_default
|
46
|
+
Fickle::Config.default_state ? enable_feature(feature) : disable_feature(feature)
|
47
|
+
return feature_enabled?(feature)
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module Fickle
|
2
|
+
module Backends
|
3
|
+
class Memory < Fickle::AbstractBackend
|
4
|
+
|
5
|
+
autoload :FeatureSet, 'fickle/backends/memory/feature_set'
|
6
|
+
|
7
|
+
def initialize(options={})
|
8
|
+
@models = []
|
9
|
+
@model_feature_sets = {}
|
10
|
+
@sets = {}
|
11
|
+
super(options.merge(:feature_set_class => Fickle::Backends::Memory::FeatureSet))
|
12
|
+
end
|
13
|
+
|
14
|
+
def get_feature_set(feature_set_key)
|
15
|
+
@sets[feature_set_key] ||= new_feature_set(feature_set_key)
|
16
|
+
end
|
17
|
+
|
18
|
+
def add_model_feature_set(feature_set_key, model)
|
19
|
+
@model_feature_sets[model] ||= []
|
20
|
+
@model_feature_sets[model] = (@model_feature_sets[model] | [feature_set_key])
|
21
|
+
end
|
22
|
+
|
23
|
+
def add_model(model)
|
24
|
+
@models = (@models | [model])
|
25
|
+
end
|
26
|
+
|
27
|
+
def remove_model(model)
|
28
|
+
@models = (@models - [model])
|
29
|
+
end
|
30
|
+
|
31
|
+
attr_reader :models, :model_feature_sets
|
32
|
+
|
33
|
+
def add_feature_set(feature_set_key, feature_set)
|
34
|
+
@sets[feature_set_key] = feature_set
|
35
|
+
end
|
36
|
+
|
37
|
+
def feature_sets
|
38
|
+
@sets
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
module Fickle
|
2
|
+
module Backends
|
3
|
+
class Memory
|
4
|
+
class FeatureSet < Fickle::AbstractFeatureSet
|
5
|
+
|
6
|
+
def initialize(options={})
|
7
|
+
super
|
8
|
+
@features = {}
|
9
|
+
end
|
10
|
+
|
11
|
+
def feature_enabled?(feature)
|
12
|
+
features[feature.to_s]
|
13
|
+
end
|
14
|
+
|
15
|
+
def enable_feature(feature)
|
16
|
+
features[feature.to_s] = true
|
17
|
+
end
|
18
|
+
|
19
|
+
def disable_feature(feature)
|
20
|
+
features[feature.to_s] = false
|
21
|
+
end
|
22
|
+
|
23
|
+
def remove_feature(feature)
|
24
|
+
features.delete(feature.to_s)
|
25
|
+
end
|
26
|
+
|
27
|
+
def each
|
28
|
+
features.each do |feature, enabled|
|
29
|
+
yield feature, enabled
|
30
|
+
end
|
31
|
+
end
|
32
|
+
|
33
|
+
private
|
34
|
+
attr_reader :features
|
35
|
+
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
module Fickle
|
2
|
+
module Config
|
3
|
+
|
4
|
+
# Please ignore the mess. Memoizing on a ModuleAccessor without ActiveSupport
|
5
|
+
def self.default_state
|
6
|
+
@default_state ||= false
|
7
|
+
def self.default_state
|
8
|
+
@default_state
|
9
|
+
end
|
10
|
+
@default_state
|
11
|
+
end
|
12
|
+
|
13
|
+
def self.backend=(backend)
|
14
|
+
@backend = backend
|
15
|
+
end
|
16
|
+
|
17
|
+
def self.backend
|
18
|
+
@backend ||= Fickle::Backends::Memory.new
|
19
|
+
def self.backend
|
20
|
+
@backend
|
21
|
+
end
|
22
|
+
@backend
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.default_state=(default_state)
|
26
|
+
@default_state = default_state
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Fickle
|
2
|
+
module ModelExtensions
|
3
|
+
def has_feature?(feature)
|
4
|
+
feature_set.enabled?(feature)
|
5
|
+
end
|
6
|
+
|
7
|
+
def add_feature(feature)
|
8
|
+
feature_set.enable_feature(feature)
|
9
|
+
end
|
10
|
+
|
11
|
+
def remove_feature(feature)
|
12
|
+
feature_set.disable_feature(feature)
|
13
|
+
end
|
14
|
+
|
15
|
+
def feature_set
|
16
|
+
@feature_set = Fickle::Config.backend.feature_set(fickle_id, self.class.name)
|
17
|
+
end
|
18
|
+
|
19
|
+
def fickle_id
|
20
|
+
return @fickle_id if @fickle_id
|
21
|
+
@fickle_id = case self
|
22
|
+
when (defined? ActiveRecord and ActiveRecord::Base) then id
|
23
|
+
when (defined? Mongoid and Mongoid::Document) then id
|
24
|
+
else hash
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
module Fickle
|
2
|
+
module Routes
|
3
|
+
def fickle_admin(path, options = {})
|
4
|
+
options = {:controller => 'Fickle::Features', :model_controller => 'Fickle::ModelFeatures'}.merge(options)
|
5
|
+
|
6
|
+
namespace 'fickle', :module => nil, :path => path do
|
7
|
+
resources :model_features, :path => 'models/:model', :controller => options.delete(:model_controller)
|
8
|
+
resources :features, options.merge(:path => '')
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module Fickle
|
2
|
+
class InstallGenerator < Rails::Generators::Base
|
3
|
+
source_root File.expand_path('../templates', __FILE__)
|
4
|
+
|
5
|
+
class_option :route, :type => :boolean, :default => true, :desc => "Add route for the Fickle web interface"
|
6
|
+
class_option :default_enabled, :type => :boolean, :default => false, :desc => "Default all features to be enabled"
|
7
|
+
|
8
|
+
def generate_route
|
9
|
+
route("fickle_admin 'features'") if options.route
|
10
|
+
end
|
11
|
+
|
12
|
+
def generate_config_file
|
13
|
+
template "fickle.rb.erb", "config/initializers/fickle.rb"
|
14
|
+
end
|
15
|
+
|
16
|
+
def generate_public_files
|
17
|
+
copy_file 'fickle.css', 'public/stylesheets/fickle/fickle.css'
|
18
|
+
copy_file 'fickle.js', 'public/javascripts/fickle.js'
|
19
|
+
end
|
20
|
+
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
body, html {
|
2
|
+
font-family: "Helvetica Neue", "Arial", "Sans-Serif";
|
3
|
+
font-weight: 100;
|
4
|
+
letter-spacing: 0.1em;
|
5
|
+
}
|
6
|
+
|
7
|
+
h1 {
|
8
|
+
width: 500px;
|
9
|
+
margin: 0 auto;
|
10
|
+
position: relative;
|
11
|
+
top: 1.0em;
|
12
|
+
font-weight: 100;
|
13
|
+
letter-spacing:0.2em;
|
14
|
+
position: relative;
|
15
|
+
z-index: 0;
|
16
|
+
}
|
17
|
+
|
18
|
+
#main {
|
19
|
+
width: 535px;
|
20
|
+
margin: 0 auto;
|
21
|
+
background: #347af4;
|
22
|
+
padding: 1px 0;
|
23
|
+
border-radius: 15px;
|
24
|
+
}
|
25
|
+
|
26
|
+
#features {
|
27
|
+
width: 500px;
|
28
|
+
background: #333;
|
29
|
+
color: #fff;
|
30
|
+
list-style-type: none;
|
31
|
+
padding:35px 10px;
|
32
|
+
font-size: 2.0em;
|
33
|
+
border-radius: 10px;
|
34
|
+
margin: 7px auto;
|
35
|
+
text-shadow: 0 -1px 0 #000;
|
36
|
+
}
|
37
|
+
|
38
|
+
#tabs {
|
39
|
+
width: 515px;
|
40
|
+
margin: 0 auto;
|
41
|
+
list-style-type: none;
|
42
|
+
padding: 0;
|
43
|
+
text-align:right;
|
44
|
+
font-weight: 100;
|
45
|
+
position: relative;
|
46
|
+
z-index: 5;
|
47
|
+
}
|
48
|
+
|
49
|
+
#tabs li {
|
50
|
+
display: inline-block;
|
51
|
+
padding: 5px 10px;
|
52
|
+
margin: 0 5px;
|
53
|
+
border-top-left-radius: 10px;
|
54
|
+
border-top-right-radius: 10px;
|
55
|
+
background: #347af4;
|
56
|
+
color: #fff;
|
57
|
+
text-shadow: 0 -1px 0 #333;
|
58
|
+
}
|
59
|
+
|
60
|
+
#tabs li a {
|
61
|
+
font-weight: 400;
|
62
|
+
color: #FFF;
|
63
|
+
text-decoration: none;
|
64
|
+
}
|
65
|
+
|
66
|
+
.toggle {
|
67
|
+
width: 150px;
|
68
|
+
color: #FFF;
|
69
|
+
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.7);
|
70
|
+
position: relative;
|
71
|
+
height: 36px;
|
72
|
+
margin: 0 30px 0 0;
|
73
|
+
border-radius: 5px;
|
74
|
+
float: right;
|
75
|
+
font-weight: 400;
|
76
|
+
}
|
77
|
+
|
78
|
+
.toggle .on, .toggle .off {
|
79
|
+
display:inline-block;
|
80
|
+
width: 75px;
|
81
|
+
margin: 0;
|
82
|
+
text-align: center;
|
83
|
+
height: 36px;
|
84
|
+
-webkit-box-shadow: 0 1px 0 rgba(255,255,255,0.4), inset 0 2px 3px rgba(0,0,0,0.6);
|
85
|
+
|
86
|
+
}
|
87
|
+
.toggle .on {
|
88
|
+
border-top-right-radius: 5px;
|
89
|
+
border-bottom-right-radius: 5px;
|
90
|
+
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #347af4), color-stop(0.5, #3680f8), color-stop(0.501, #5091f3), color-stop(1, #75acfc));
|
91
|
+
}
|
92
|
+
|
93
|
+
.toggle .off {
|
94
|
+
border-top-left-radius: 5px;
|
95
|
+
border-bottom-left-radius: 5px;
|
96
|
+
color: #333;
|
97
|
+
text-shadow: 0 1px 0 #fff;
|
98
|
+
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #999), color-stop(0.5, #AAA), color-stop(0.501, #bbb), color-stop(1, #ddd));
|
99
|
+
}
|
100
|
+
|
101
|
+
.toggle .slider {
|
102
|
+
cursor:pointer;
|
103
|
+
width: 78px;
|
104
|
+
height: 33px;
|
105
|
+
top:1px;
|
106
|
+
left:1px;
|
107
|
+
border-top: 1px solid #bbb;
|
108
|
+
background: -webkit-gradient(linear, left top, left bottom, color-stop(0, #666), color-stop(0.5, #777), color-stop(1, #555));
|
109
|
+
-webkit-box-shadow: 0 0 1px 1px #222;
|
110
|
+
position: absolute;
|
111
|
+
border-radius: 5px;
|
112
|
+
}
|
File without changes
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fickle::ClassMethods do
|
4
|
+
|
5
|
+
describe '#has_featureset' do
|
6
|
+
|
7
|
+
before :all do
|
8
|
+
Fickle.setup!
|
9
|
+
class MagicClass
|
10
|
+
has_featureset
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should add the class to the models pile' do
|
15
|
+
Fickle::MODELS.should include MagicClass
|
16
|
+
end
|
17
|
+
|
18
|
+
it 'should mix in the feature-specific methods' do
|
19
|
+
MagicClass.new.should respond_to :has_feature?
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fickle::FeatureSet do
|
4
|
+
it 'should allow the enabling of features' do
|
5
|
+
subject.enable_feature(:kill)
|
6
|
+
subject.enabled?(:kill).should be true
|
7
|
+
end
|
8
|
+
|
9
|
+
it 'should allow the disabling of features' do
|
10
|
+
subject.disable_feature(:kill)
|
11
|
+
subject.enabled?(:kill).should be false
|
12
|
+
end
|
13
|
+
|
14
|
+
it 'should default to false' do
|
15
|
+
subject.enabled?(:jabberwocky).should be false
|
16
|
+
end
|
17
|
+
|
18
|
+
context 'persistence' do
|
19
|
+
before :each do
|
20
|
+
@feature_set_one = Fickle::FeatureSet.for('kings')
|
21
|
+
@feature_set_two = Fickle::FeatureSet.for('kings')
|
22
|
+
end
|
23
|
+
|
24
|
+
it 'should work' do
|
25
|
+
@feature_set_one.enable_feature(:drawing)
|
26
|
+
@feature_set_two.enabled?(:drawing).should be true
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
end
|
data/spec/fickle_spec.rb
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fickle do
|
4
|
+
it { should respond_to :setup! }
|
5
|
+
|
6
|
+
context 'in a rails app' do
|
7
|
+
before :all do
|
8
|
+
ApplicationHelper = Class.new
|
9
|
+
Fickle.setup!
|
10
|
+
end
|
11
|
+
|
12
|
+
it 'should include the view helpers in ApplicationHelper' do
|
13
|
+
ApplicationHelper.new.should respond_to :feature_enabled?
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should extend class to include the has_featureset method' do
|
17
|
+
class TestClass; end
|
18
|
+
TestClass.should respond_to :has_featureset
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
@@ -0,0 +1,24 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
Fickle.setup!
|
3
|
+
|
4
|
+
class Genius
|
5
|
+
has_featureset
|
6
|
+
end
|
7
|
+
|
8
|
+
describe Genius do
|
9
|
+
it { should respond_to :has_feature? }
|
10
|
+
|
11
|
+
it 'should allow features to be enabled' do
|
12
|
+
subject.add_feature :thrill
|
13
|
+
subject.has_feature?(:thrill).should be true
|
14
|
+
end
|
15
|
+
|
16
|
+
it 'should allow features to be disabled' do
|
17
|
+
subject.remove_feature :chill
|
18
|
+
subject.has_feature?(:chill).should be false
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should default to false for new features' do
|
22
|
+
subject.has_feature?(:evil).should be false
|
23
|
+
end
|
24
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require 'test_project/config/application'
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'spec_helper'
|
2
|
+
|
3
|
+
describe Fickle::ViewHelpers do
|
4
|
+
|
5
|
+
class TestClass
|
6
|
+
include Fickle::ViewHelpers
|
7
|
+
end
|
8
|
+
|
9
|
+
subject do
|
10
|
+
TestClass.new
|
11
|
+
end
|
12
|
+
|
13
|
+
it { should respond_to :feature_enabled? }
|
14
|
+
|
15
|
+
describe '#feature_enabled?' do
|
16
|
+
|
17
|
+
before :each do
|
18
|
+
@feature_set = Fickle::FeatureSet.for('global')
|
19
|
+
end
|
20
|
+
|
21
|
+
it 'should create features that have not been tested before and default to false' do
|
22
|
+
subject.feature_enabled?(:feature).should be false
|
23
|
+
end
|
24
|
+
it 'should return false when a feature is not enabled' do
|
25
|
+
@feature_set.disable_feature(:kill_robots)
|
26
|
+
subject.feature_enabled?(:kill_robots).should be false
|
27
|
+
end
|
28
|
+
it 'should return true when a feature is enabled' do
|
29
|
+
@feature_set.enable_feature(:kill_robots)
|
30
|
+
subject.feature_enabled?(:kill_robots).should be true
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
end
|
metadata
ADDED
@@ -0,0 +1,142 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: fickle
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 29
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 0
|
8
|
+
- 0
|
9
|
+
- 1
|
10
|
+
version: 0.0.1
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- chrisrhoden
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2011-03-20 00:00:00 -04:00
|
19
|
+
default_executable:
|
20
|
+
dependencies:
|
21
|
+
- !ruby/object:Gem::Dependency
|
22
|
+
name: rspec
|
23
|
+
prerelease: false
|
24
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
25
|
+
none: false
|
26
|
+
requirements:
|
27
|
+
- - ~>
|
28
|
+
- !ruby/object:Gem::Version
|
29
|
+
hash: 31
|
30
|
+
segments:
|
31
|
+
- 2
|
32
|
+
- 4
|
33
|
+
- 0
|
34
|
+
version: 2.4.0
|
35
|
+
type: :development
|
36
|
+
version_requirements: *id001
|
37
|
+
- !ruby/object:Gem::Dependency
|
38
|
+
name: rails
|
39
|
+
prerelease: false
|
40
|
+
requirement: &id002 !ruby/object:Gem::Requirement
|
41
|
+
none: false
|
42
|
+
requirements:
|
43
|
+
- - ~>
|
44
|
+
- !ruby/object:Gem::Version
|
45
|
+
hash: 7
|
46
|
+
segments:
|
47
|
+
- 3
|
48
|
+
- 0
|
49
|
+
- 0
|
50
|
+
version: 3.0.0
|
51
|
+
type: :development
|
52
|
+
version_requirements: *id002
|
53
|
+
description: |-
|
54
|
+
Fickle lets you turn features on and off at will in your rails app using a web interface.
|
55
|
+
It also lets you manage features on a model-by-model basis (maybe users?)
|
56
|
+
email:
|
57
|
+
- chris@lazerclub.co
|
58
|
+
executables: []
|
59
|
+
|
60
|
+
extensions: []
|
61
|
+
|
62
|
+
extra_rdoc_files: []
|
63
|
+
|
64
|
+
files:
|
65
|
+
- .gitignore
|
66
|
+
- .rspec
|
67
|
+
- Gemfile
|
68
|
+
- README.md
|
69
|
+
- Rakefile
|
70
|
+
- app/controllers/fickle/application_controller.rb
|
71
|
+
- app/controllers/fickle/features_controller.rb
|
72
|
+
- app/controllers/fickle/model_features_controller.rb
|
73
|
+
- app/views/fickle/features/index.html.erb
|
74
|
+
- app/views/fickle/model_features/index.html.erb
|
75
|
+
- app/views/layouts/fickle.html.erb
|
76
|
+
- fickle.gemspec
|
77
|
+
- lib/fickle.rb
|
78
|
+
- lib/fickle/abstract_backend.rb
|
79
|
+
- lib/fickle/abstract_feature_set.rb
|
80
|
+
- lib/fickle/backends.rb
|
81
|
+
- lib/fickle/backends/memory.rb
|
82
|
+
- lib/fickle/backends/memory/feature_set.rb
|
83
|
+
- lib/fickle/class_methods.rb
|
84
|
+
- lib/fickle/config.rb
|
85
|
+
- lib/fickle/controllers.rb
|
86
|
+
- lib/fickle/model_extensions.rb
|
87
|
+
- lib/fickle/railtie.rb
|
88
|
+
- lib/fickle/routes.rb
|
89
|
+
- lib/fickle/version.rb
|
90
|
+
- lib/fickle/view_helpers.rb
|
91
|
+
- lib/generators/fickle/install/USAGE
|
92
|
+
- lib/generators/fickle/install/install_generator.rb
|
93
|
+
- lib/generators/fickle/install/templates/fickle.css
|
94
|
+
- lib/generators/fickle/install/templates/fickle.js
|
95
|
+
- lib/generators/fickle/install/templates/fickle.rb.erb
|
96
|
+
- spec/class_methods_spec.rb
|
97
|
+
- spec/feature_set_spec.rb
|
98
|
+
- spec/fickle_spec.rb
|
99
|
+
- spec/model_extensions_spec.rb
|
100
|
+
- spec/spec_helper.rb
|
101
|
+
- spec/view_helpers_spec.rb
|
102
|
+
has_rdoc: true
|
103
|
+
homepage: ""
|
104
|
+
licenses: []
|
105
|
+
|
106
|
+
post_install_message:
|
107
|
+
rdoc_options: []
|
108
|
+
|
109
|
+
require_paths:
|
110
|
+
- lib
|
111
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
112
|
+
none: false
|
113
|
+
requirements:
|
114
|
+
- - ">="
|
115
|
+
- !ruby/object:Gem::Version
|
116
|
+
hash: 3
|
117
|
+
segments:
|
118
|
+
- 0
|
119
|
+
version: "0"
|
120
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
121
|
+
none: false
|
122
|
+
requirements:
|
123
|
+
- - ">="
|
124
|
+
- !ruby/object:Gem::Version
|
125
|
+
hash: 3
|
126
|
+
segments:
|
127
|
+
- 0
|
128
|
+
version: "0"
|
129
|
+
requirements: []
|
130
|
+
|
131
|
+
rubyforge_project: fickle
|
132
|
+
rubygems_version: 1.4.2
|
133
|
+
signing_key:
|
134
|
+
specification_version: 3
|
135
|
+
summary: The feature library for rails that can't make up its mind
|
136
|
+
test_files:
|
137
|
+
- spec/class_methods_spec.rb
|
138
|
+
- spec/feature_set_spec.rb
|
139
|
+
- spec/fickle_spec.rb
|
140
|
+
- spec/model_extensions_spec.rb
|
141
|
+
- spec/spec_helper.rb
|
142
|
+
- spec/view_helpers_spec.rb
|