flip2 1.1.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.
- 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
|