flip2 1.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +4 -0
- data/.rspec +1 -0
- data/.travis.yml +5 -0
- data/Gemfile +2 -0
- data/README.md +180 -0
- data/Rakefile +11 -0
- data/TODO +3 -0
- data/app/assets/stylesheets/flip.css +70 -0
- data/app/controllers/flip/features_controller.rb +47 -0
- data/app/controllers/flip/strategies_controller.rb +31 -0
- data/app/helpers/flip_helper.rb +9 -0
- data/app/views/flip/features/index.html.erb +62 -0
- data/config/routes.rb +14 -0
- data/flip.gemspec +27 -0
- data/lib/flip/abstract_strategy.rb +26 -0
- data/lib/flip/cacheable.rb +25 -0
- data/lib/flip/controller_filters.rb +25 -0
- data/lib/flip/cookie_strategy.rb +67 -0
- data/lib/flip/database_strategy.rb +46 -0
- data/lib/flip/declarable.rb +24 -0
- data/lib/flip/declaration_strategy.rb +20 -0
- data/lib/flip/definition.rb +21 -0
- data/lib/flip/engine.rb +9 -0
- data/lib/flip/facade.rb +18 -0
- data/lib/flip/feature_set.rb +57 -0
- data/lib/flip/forbidden.rb +7 -0
- data/lib/flip/version.rb +3 -0
- data/lib/flip2.rb +28 -0
- data/lib/generators/flip/install/install_generator.rb +9 -0
- data/lib/generators/flip/migration/USAGE +5 -0
- data/lib/generators/flip/migration/migration_generator.rb +22 -0
- data/lib/generators/flip/migration/templates/create_features.rb +11 -0
- data/lib/generators/flip/model/USAGE +8 -0
- data/lib/generators/flip/model/model_generator.rb +8 -0
- data/lib/generators/flip/model/templates/feature.rb +15 -0
- data/lib/generators/flip/routes/USAGE +7 -0
- data/lib/generators/flip/routes/routes_generator.rb +7 -0
- data/lib/generators/flip/views/USAGE +8 -0
- data/lib/generators/flip/views/templates/index.html.erb +54 -0
- data/lib/generators/flip/views/views_generator.rb +8 -0
- data/spec/abstract_strategy_spec.rb +11 -0
- data/spec/cacheable_spec.rb +49 -0
- data/spec/controller_filters_spec.rb +27 -0
- data/spec/cookie_strategy_spec.rb +112 -0
- data/spec/database_strategy_spec.rb +110 -0
- data/spec/declarable_spec.rb +32 -0
- data/spec/declaration_strategy_spec.rb +39 -0
- data/spec/definition_spec.rb +19 -0
- data/spec/feature_set_spec.rb +67 -0
- data/spec/flip_spec.rb +33 -0
- data/spec/spec_helper.rb +2 -0
- metadata +172 -0
data/config/routes.rb
ADDED
data/flip.gemspec
ADDED
@@ -0,0 +1,27 @@
|
|
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 = "flip2"
|
7
|
+
s.version = Flip::VERSION
|
8
|
+
s.platform = Gem::Platform::RUBY
|
9
|
+
s.authors = [""]
|
10
|
+
s.email = [""]
|
11
|
+
s.homepage = ""
|
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
|
+
s.license = "MIT"
|
15
|
+
|
16
|
+
s.files = `git ls-files`.split("\n")
|
17
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
18
|
+
s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
19
|
+
s.require_paths = ["lib"]
|
20
|
+
|
21
|
+
s.add_dependency("activesupport", ">= 3.0", "< 5.1")
|
22
|
+
s.add_dependency("i18n")
|
23
|
+
|
24
|
+
s.add_development_dependency("rspec", "~> 2.5")
|
25
|
+
s.add_development_dependency("rspec-its")
|
26
|
+
s.add_development_dependency("rake")
|
27
|
+
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,25 @@
|
|
1
|
+
module Flip
|
2
|
+
module Cacheable
|
3
|
+
|
4
|
+
def use_feature_cache=(value)
|
5
|
+
@use_feature_cache = value
|
6
|
+
end
|
7
|
+
|
8
|
+
def use_feature_cache
|
9
|
+
@use_feature_cache
|
10
|
+
end
|
11
|
+
|
12
|
+
def start_feature_cache
|
13
|
+
@use_feature_cache = true
|
14
|
+
@features = nil
|
15
|
+
end
|
16
|
+
|
17
|
+
def feature_cache
|
18
|
+
return @features if @features
|
19
|
+
@features = {}
|
20
|
+
all.each { |f| @features[f.key] = f }
|
21
|
+
@features
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
module Flip
|
2
|
+
# ControllerFilters is a name that refers to the fact that Rails
|
3
|
+
# before_action and after_action used to be before_filter and
|
4
|
+
# after_filter.
|
5
|
+
module ControllerFilters
|
6
|
+
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
module ClassMethods
|
10
|
+
|
11
|
+
def require_feature key, options = {}
|
12
|
+
before_action options do
|
13
|
+
flip_feature_disabled key unless Flip.on? key
|
14
|
+
end
|
15
|
+
end
|
16
|
+
|
17
|
+
end
|
18
|
+
|
19
|
+
def flip_feature_disabled key
|
20
|
+
redirect_to root_path
|
21
|
+
# raise Flip::Forbidden.new(key)
|
22
|
+
end
|
23
|
+
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,67 @@
|
|
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
|
+
cookie = cookies[cookie_name(definition)]
|
15
|
+
cookie_value = cookie.is_a?(Hash) ? cookie['value'] : cookie
|
16
|
+
cookie_value === 'true'
|
17
|
+
end
|
18
|
+
|
19
|
+
def switchable?
|
20
|
+
true
|
21
|
+
end
|
22
|
+
|
23
|
+
def switch! key, on
|
24
|
+
cookies[cookie_name(key)] = {
|
25
|
+
'value' => (on ? "true" : "false"),
|
26
|
+
'domain' => :all
|
27
|
+
}
|
28
|
+
end
|
29
|
+
|
30
|
+
def delete! key
|
31
|
+
cookies.delete cookie_name(key)
|
32
|
+
end
|
33
|
+
|
34
|
+
def self.cookies= cookies
|
35
|
+
@cookies = cookies
|
36
|
+
end
|
37
|
+
|
38
|
+
def cookie_name(definition)
|
39
|
+
definition = definition.key unless definition.is_a? Symbol
|
40
|
+
"flip_#{definition}"
|
41
|
+
end
|
42
|
+
|
43
|
+
private
|
44
|
+
|
45
|
+
def cookies
|
46
|
+
self.class.instance_variable_get(:@cookies) || {}
|
47
|
+
end
|
48
|
+
|
49
|
+
# Include in ApplicationController to push cookies into CookieStrategy.
|
50
|
+
# Uses before_action and after_action rather than around_action to
|
51
|
+
# avoid pointlessly adding to stack depth.
|
52
|
+
module Loader
|
53
|
+
extend ActiveSupport::Concern
|
54
|
+
included do
|
55
|
+
before_action :flip_cookie_strategy_before
|
56
|
+
after_action :flip_cookie_strategy_after
|
57
|
+
end
|
58
|
+
def flip_cookie_strategy_before
|
59
|
+
CookieStrategy.cookies = cookies
|
60
|
+
end
|
61
|
+
def flip_cookie_strategy_after
|
62
|
+
CookieStrategy.cookies = nil
|
63
|
+
end
|
64
|
+
end
|
65
|
+
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,46 @@
|
|
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
|
+
record = @klass.where(key: key.to_s).first_or_initialize
|
27
|
+
record.enabled = enable
|
28
|
+
record.save!
|
29
|
+
end
|
30
|
+
|
31
|
+
def delete! key
|
32
|
+
@klass.where(key: key.to_s).first.try(:destroy)
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
def feature(definition)
|
38
|
+
if @klass.respond_to?(:use_feature_cache) && @klass.use_feature_cache
|
39
|
+
@klass.feature_cache[definition.key.to_s]
|
40
|
+
else
|
41
|
+
@klass.where(key: definition.key.to_s).first
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
end
|
46
|
+
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
|
data/lib/flip/engine.rb
ADDED
data/lib/flip/facade.rb
ADDED
@@ -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}. Valid strategies are #{@strategies.keys}" }
|
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
|
data/lib/flip/version.rb
ADDED
data/lib/flip2.rb
ADDED
@@ -0,0 +1,28 @@
|
|
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
|
+
cacheable
|
13
|
+
controller_filters
|
14
|
+
cookie_strategy
|
15
|
+
database_strategy
|
16
|
+
declarable
|
17
|
+
declaration_strategy
|
18
|
+
definition
|
19
|
+
facade
|
20
|
+
feature_set
|
21
|
+
forbidden
|
22
|
+
}.each { |name| require "flip/#{name}" }
|
23
|
+
|
24
|
+
require "flip/engine" if defined?(Rails)
|
25
|
+
|
26
|
+
module Flip
|
27
|
+
extend Facade
|
28
|
+
end
|