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