flip2 1.1.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.gitignore +4 -0
  3. data/.rspec +1 -0
  4. data/.travis.yml +5 -0
  5. data/Gemfile +2 -0
  6. data/README.md +180 -0
  7. data/Rakefile +11 -0
  8. data/TODO +3 -0
  9. data/app/assets/stylesheets/flip.css +70 -0
  10. data/app/controllers/flip/features_controller.rb +47 -0
  11. data/app/controllers/flip/strategies_controller.rb +31 -0
  12. data/app/helpers/flip_helper.rb +9 -0
  13. data/app/views/flip/features/index.html.erb +62 -0
  14. data/config/routes.rb +14 -0
  15. data/flip.gemspec +27 -0
  16. data/lib/flip/abstract_strategy.rb +26 -0
  17. data/lib/flip/cacheable.rb +25 -0
  18. data/lib/flip/controller_filters.rb +25 -0
  19. data/lib/flip/cookie_strategy.rb +67 -0
  20. data/lib/flip/database_strategy.rb +46 -0
  21. data/lib/flip/declarable.rb +24 -0
  22. data/lib/flip/declaration_strategy.rb +20 -0
  23. data/lib/flip/definition.rb +21 -0
  24. data/lib/flip/engine.rb +9 -0
  25. data/lib/flip/facade.rb +18 -0
  26. data/lib/flip/feature_set.rb +57 -0
  27. data/lib/flip/forbidden.rb +7 -0
  28. data/lib/flip/version.rb +3 -0
  29. data/lib/flip2.rb +28 -0
  30. data/lib/generators/flip/install/install_generator.rb +9 -0
  31. data/lib/generators/flip/migration/USAGE +5 -0
  32. data/lib/generators/flip/migration/migration_generator.rb +22 -0
  33. data/lib/generators/flip/migration/templates/create_features.rb +11 -0
  34. data/lib/generators/flip/model/USAGE +8 -0
  35. data/lib/generators/flip/model/model_generator.rb +8 -0
  36. data/lib/generators/flip/model/templates/feature.rb +15 -0
  37. data/lib/generators/flip/routes/USAGE +7 -0
  38. data/lib/generators/flip/routes/routes_generator.rb +7 -0
  39. data/lib/generators/flip/views/USAGE +8 -0
  40. data/lib/generators/flip/views/templates/index.html.erb +54 -0
  41. data/lib/generators/flip/views/views_generator.rb +8 -0
  42. data/spec/abstract_strategy_spec.rb +11 -0
  43. data/spec/cacheable_spec.rb +49 -0
  44. data/spec/controller_filters_spec.rb +27 -0
  45. data/spec/cookie_strategy_spec.rb +112 -0
  46. data/spec/database_strategy_spec.rb +110 -0
  47. data/spec/declarable_spec.rb +32 -0
  48. data/spec/declaration_strategy_spec.rb +39 -0
  49. data/spec/definition_spec.rb +19 -0
  50. data/spec/feature_set_spec.rb +67 -0
  51. data/spec/flip_spec.rb +33 -0
  52. data/spec/spec_helper.rb +2 -0
  53. metadata +172 -0
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,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
@@ -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
@@ -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
@@ -0,0 +1,7 @@
1
+ module Flip
2
+ class Forbidden < StandardError
3
+ def initialize(key)
4
+ super("requires :#{key} feature")
5
+ end
6
+ end
7
+ end
@@ -0,0 +1,3 @@
1
+ module Flip
2
+ VERSION = "1.1.1"
3
+ end
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