flipflop 2.4.0 → 2.7.0

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2d991f9cd5e93e4a837c0e6835da5ae2339498bf74a26fc6e21cee4503793ca6
4
- data.tar.gz: 943ba8e6ea38bf2afbc45454890f8111a3b9029383018e5ddf3a76f8faac0732
3
+ metadata.gz: 5d280de2ff4eb5a0d3437866c2f6d08e6069564183d670cb437cc7741e7eeac3
4
+ data.tar.gz: e16c3770910db3cfc4a442e2094acf44426651f0456235992f8ea0166e53e6d9
5
5
  SHA512:
6
- metadata.gz: a81cbf3b37f9d91ec310bc971eec5c06b2d4b0581bd42191366503782bb8d95ef6b51fef33588bd28ec571a57ebc03f51cd1c5f9514ac8151f1a855e4072e085
7
- data.tar.gz: 30a184287b36b7a346cc72fb0bf4f2ed4cb59083461f3859d70d1ec9908c37b3c1b65e54dc3536ed472fed85f94465ea191c6d4eeb7b049679b4056cb3585b05
6
+ metadata.gz: b2ab3024b4106125d0f67a6333c5d2fc6b6e32d744e3fca9775894e9291cca0e2ecb3b74d6343598fc91c44cf0077b7496e79dade91fa27ce6418bb7fb3a534a
7
+ data.tar.gz: 6c55c846ba7759fa595485283a8d7d5eeaeb919e238a5752310b8bbfb6915f486cf4f750ab96acef3ec94da4295dab144fd6673e5802a0b26d4b3481d16654ba
data/.travis.yml CHANGED
@@ -2,7 +2,7 @@ language: ruby
2
2
  cache: bundler
3
3
  rvm:
4
4
  - 2.5.0
5
- - 2.4.2
5
+ - 2.4.4
6
6
  - 2.3.5
7
7
  - jruby-9.1.13.0
8
8
  - ruby-head
@@ -18,16 +18,12 @@ env:
18
18
  - RAILS_VERSION=5.0 RAILS_API_ONLY=1
19
19
  matrix:
20
20
  include:
21
- - rvm: 2.3.5
22
- env: RAILS_VERSION=4.2
23
21
  - rvm: 2.2.8
24
22
  env: RAILS_VERSION=4.2
25
23
  - rvm: 2.1.10
26
24
  env: RAILS_VERSION=4.2
27
25
  - rvm: 2.0.0
28
26
  env: RAILS_VERSION=4.2
29
- - rvm: 2.3.5
30
- env: RAILS_VERSION=4.1
31
27
  - rvm: 2.2.8
32
28
  env: RAILS_VERSION=4.1
33
29
  - rvm: 2.1.10
@@ -35,6 +31,10 @@ matrix:
35
31
  - rvm: 2.0.0
36
32
  env: RAILS_VERSION=4.1
37
33
  exclude:
34
+ - rvm: 2.4.4
35
+ env: RAILS_VERSION=master
36
+ - rvm: 2.4.4
37
+ env: RAILS_VERSION=master RAILS_API_ONLY=1
38
38
  - rvm: 2.3.5
39
39
  env: RAILS_VERSION=master
40
40
  - rvm: 2.3.5
@@ -52,4 +52,4 @@ deploy:
52
52
  on:
53
53
  tags: true
54
54
  repo: voormedia/flipflop
55
- ruby: 2.3.5
55
+ ruby: 2.5.0
data/CHANGES.md CHANGED
@@ -1,3 +1,18 @@
1
+ ## 2.7.0
2
+
3
+ * Removing .css extension from render partial and adding it to html handler to correctly render the erb file.
4
+ * Fixes Rails 6 & 7 deprecation: `Render file with extension is deprecated`
5
+ * Ruby 3.x compatible (Credits: https://github.com/voormedia/flipflop/pull/36)
6
+
7
+ ## 2.6.0
8
+
9
+ * Failure to load strategies in test environments will result in a warning instead of an error. This should aid in running Rake tasks.
10
+
11
+ ## 2.5.0
12
+
13
+ * Add Sequel strategy. ActiveRecord/Sequel can be used side by side.
14
+ * Rails 6 compatibility.
15
+
1
16
  ## 2.4.0
2
17
 
3
18
  * Add location of feature definition.
data/Gemfile CHANGED
@@ -13,7 +13,16 @@ group :test do
13
13
  gem "bootstrap", "= 4.0.0.alpha6", require: false
14
14
 
15
15
  gem "fakeredis", require: false
16
- gem "sqlite3", ">= 1.3", platform: :ruby
16
+
17
+ if version == "master" || version >= "6"
18
+ gem "sqlite3", "~> 1.4.0", platform: :ruby
19
+ else
20
+ gem "sqlite3", "~> 1.3.6", platform: :ruby
21
+ end
22
+ if version >= "5.2" || Gem::Version.new(RUBY_VERSION) > Gem::Version.new("2.4.4")
23
+ gem 'sassc-rails'
24
+ end
25
+
17
26
  gem "activerecord-jdbcsqlite3-adapter", platform: :jruby,
18
27
  github: "jruby/activerecord-jdbc-adapter"
19
28
 
data/LICENSE CHANGED
@@ -1,7 +1,7 @@
1
1
  The MIT License
2
2
 
3
3
  Copyright (c) 2011-2013 Learnable Pty Ltd
4
- Copyright (c) 2016-2017 Voormedia
4
+ Copyright (c) 2016-2019 Voormedia
5
5
 
6
6
  Permission is hereby granted, free of charge, to any person obtaining a copy
7
7
  of this software and associated documentation files (the "Software"), to deal
data/README.md CHANGED
@@ -4,11 +4,14 @@
4
4
 
5
5
  **Flipflop** provides a declarative, layered way of enabling and disabling
6
6
  application functionality at run-time. It is originally based on
7
- [Flip](https://github.com/pda/flip). Compared to the original gem **Flipflop** has:
7
+ [Flip](https://github.com/pda/flip). **Flipflop** has the following features:
8
+ * simple configuration
9
+ * ease of use for developers
8
10
  * an improved dashboard
11
+ * manage features via console (using rake tasks)
9
12
  * thread safety
10
13
  * better database performance due to per-request caching, enabled by default
11
- * more strategies (Redis, query strings, sessions, custom code)
14
+ * more strategies (Sequel, Redis, query strings, sessions, custom code)
12
15
  * more strategy options (cookie options, strategy names and descriptions, custom database models)
13
16
  * the ability to use the same strategy twice, with different options
14
17
  * configuration in a fixed location (`config/features.rb`) that is usable even if you don't use the database strategy
@@ -21,7 +24,7 @@ application functionality at run-time. It is originally based on
21
24
  You can configure strategy layers that will evaluate if a feature is currently
22
25
  enabled or disabled. Available strategies are:
23
26
  * a per-feature default setting
24
- * database (with Active Record or Redis), to flipflop features site-wide for all users
27
+ * database (with Active Record, Sequel, or Redis), to flipflop features site-wide for all users
25
28
  * cookie or session, to flipflop features for single users
26
29
  * query string parameters, to flipflop features occasionally (in development mode for example)
27
30
  * custom strategy code
@@ -30,6 +33,19 @@ Flipflop has a dashboard interface that's easy to understand and use.
30
33
 
31
34
  [<img src="https://raw.githubusercontent.com/voormedia/flipflop/screenshots/dashboard.png" alt="Dashboard">](https://raw.githubusercontent.com/voormedia/flipflop/screenshots/dashboard.png)
32
35
 
36
+ If you prefer, you can use the included rake tasks to enable or disable features.
37
+
38
+ ```
39
+ rake flipflop:features # Shows features table
40
+ rake flipflop:turn_on[feature,strategy] # Enables a feature with the specified strategy
41
+ rake flipflop:turn_off[feature,strategy] # Disables a feature with the specified strategy
42
+ rake flipflop:clear[feature,strategy] # Clears a feature with the specified strategy
43
+ ```
44
+
45
+ ## Rails requirements
46
+
47
+ This gem requires Rails 4, 5, 6 or 7. Using an ORM layer is entirely optional.
48
+
33
49
  ## Installation
34
50
 
35
51
  Add the gem to your `Gemfile`:
@@ -58,7 +74,7 @@ Features and strategies are declared in `config/features.rb`:
58
74
  Flipflop.configure do
59
75
  # Strategies will be used in the order listed here.
60
76
  strategy :cookie
61
- strategy :active_record
77
+ strategy :active_record # or :sequel, :redis
62
78
  strategy :default
63
79
 
64
80
  # Basic feature declaration:
@@ -86,8 +102,8 @@ Feature definitions support these options:
86
102
  ## Strategies
87
103
 
88
104
  The following strategies are provided:
89
- * `:active_record` – Save feature settings in the database.
90
- * `:class` – Provide the feature model. `Flipflop::Feature` by default (which uses the table `flipflop_features`). Honors `default_scope` when features are resolved or switched on/off.
105
+ * `:active_record`/`:sequel` – Save feature settings in the database.
106
+ * `:class` – Provide the feature model. `Flipflop::Feature` by default (which is defined automatically and uses the table `flipflop_features`). The `ActiveRecord` version honors `default_scope` when features are resolved or switched on/off.
91
107
  * `:cookie` – Save feature settings in browser cookies for the current user.
92
108
  * `:prefix` – String prefix for all cookie names. Defaults to no prefix.
93
109
  * `:path` – The path for which the cookies apply. Defaults to the root of the application.
@@ -0,0 +1,3 @@
1
+ //= link_tree ../images
2
+ //= link_directory ../javascripts .js
3
+ //= link_directory ../stylesheets .css
@@ -25,7 +25,12 @@ module Flipflop
25
25
  @strategies = @feature_set.strategies.reject(&:hidden?)
26
26
  @grouped_features = @feature_set.features.group_by(&:group)
27
27
 
28
- @application_name = Rails.application.class.parent_name.underscore.titleize
28
+ app_class = Rails.application.class
29
+ application_name = app_class.respond_to?(:module_parent_name) ?
30
+ app_class.module_parent_name :
31
+ app_class.parent_name
32
+
33
+ @application_name = application_name.underscore.titleize
29
34
  end
30
35
 
31
36
  def grouped?
@@ -2,7 +2,7 @@
2
2
  <html>
3
3
  <head>
4
4
  <title><%= yield :title -%></title>
5
- <style><%= render partial: "flipflop/stylesheets/flipflop.css" %></style>
5
+ <style><%= render partial: "flipflop/stylesheets/flipflop" %></style>
6
6
  </head>
7
7
  <body><%= yield -%></body>
8
8
  </html>
data/flipflop.gemspec CHANGED
@@ -14,6 +14,7 @@ Gem::Specification.new do |s|
14
14
  s.license = "MIT"
15
15
 
16
16
  s.add_dependency("activesupport", ">= 4.0")
17
+ s.add_dependency('terminal-table', '>= 1.8')
17
18
  s.files = `git ls-files`.split("\n")
18
19
  s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
19
20
  s.executables = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
@@ -32,6 +32,12 @@ module Flipflop
32
32
  end
33
33
 
34
34
  FeatureSet.current.use(strategy)
35
+ rescue StandardError => err
36
+ if FeatureSet.current.raise_strategy_errors
37
+ raise err
38
+ else
39
+ warn "WARNING: Unable to load Flipflop strategy #{strategy}: #{err}"
40
+ end
35
41
  end
36
42
  end
37
43
  end
@@ -14,6 +14,12 @@ module Flipflop
14
14
 
15
15
  config.flipflop = ActiveSupport::OrderedOptions.new
16
16
 
17
+ initializer "flipflop.config" do |app|
18
+ raise_errors = config.flipflop.raise_strategy_errors
19
+ raise_errors = (ENV["RACK_ENV"] || ENV["RAILS_ENV"]) != "test" if raise_errors.nil?
20
+ FeatureSet.current.raise_strategy_errors = raise_errors
21
+ end
22
+
17
23
  initializer "flipflop.features_path" do |app|
18
24
  FeatureLoader.current.append(app)
19
25
  end
@@ -37,8 +43,13 @@ module Flipflop
37
43
 
38
44
  initializer "flipflop.request_interceptor" do |app|
39
45
  interceptor = Strategies::AbstractStrategy::RequestInterceptor
40
- ActionController::Base.send(:include, interceptor)
41
- ActionController::API.send(:include, interceptor) if defined?(ActionController::API)
46
+ ActiveSupport.on_load(:action_controller_base) do
47
+ ActionController::Base.send(:include, interceptor)
48
+ end
49
+
50
+ ActiveSupport.on_load(:action_controller_api) do
51
+ ActionController::API.send(:include, interceptor)
52
+ end
42
53
  end
43
54
 
44
55
  def run_tasks_blocks(app)
@@ -49,9 +60,9 @@ module Flipflop
49
60
 
50
61
  private
51
62
 
52
- def to_prepare
63
+ def to_prepare(&block)
53
64
  klass = defined?(ActiveSupport::Reloader) ? ActiveSupport::Reloader : ActionDispatch::Reloader
54
- klass.to_prepare(&Proc.new)
65
+ klass.to_prepare(&block)
55
66
  end
56
67
  end
57
68
  end
@@ -1,3 +1,5 @@
1
+ require "forwardable"
2
+
1
3
  module Flipflop
2
4
  module Facade
3
5
  extend Forwardable
@@ -28,15 +28,17 @@ module Flipflop
28
28
  private :new
29
29
  end
30
30
 
31
+ attr_accessor :raise_strategy_errors
32
+
31
33
  def initialize
32
34
  @features = {}
33
35
  @strategies = {}
34
36
  end
35
37
 
36
- def configure
38
+ def configure(&block)
37
39
  Module.new do
38
40
  extend Configurable
39
- instance_exec(&Proc.new)
41
+ instance_exec(&block)
40
42
  end
41
43
  self
42
44
  end
@@ -5,10 +5,17 @@ module Flipflop
5
5
  def default_description
6
6
  "Stores features in database. Applies to all users."
7
7
  end
8
+
9
+ def define_feature_class
10
+ return Flipflop::Feature if defined?(Flipflop::Feature)
11
+
12
+ model = Class.new(ActiveRecord::Base)
13
+ Flipflop.const_set(:Feature, model)
14
+ end
8
15
  end
9
16
 
10
17
  def initialize(**options)
11
- @class = options.delete(:class) || ::Flipflop::Feature
18
+ @class = options.delete(:class) || self.class.define_feature_class
12
19
  if !@class.kind_of?(Class)
13
20
  @class = ActiveSupport::Inflector.constantize(@class.to_s)
14
21
  end
@@ -0,0 +1,57 @@
1
+ module Flipflop
2
+ module Strategies
3
+ class SequelStrategy < AbstractStrategy
4
+ class << self
5
+ def default_description
6
+ "Stores features in database. Applies to all users."
7
+ end
8
+
9
+ def define_feature_class
10
+ return Flipflop::Feature if defined?(Flipflop::Feature)
11
+
12
+ model = Class.new(Sequel::Model(:flipflop_features))
13
+ model.plugin(:timestamps, force: true, update_on_create: true)
14
+ model.raise_on_save_failure = true
15
+
16
+ Flipflop.const_set(:Feature, model)
17
+ end
18
+ end
19
+
20
+ def initialize(**options)
21
+ @class = options.delete(:class) || self.class.define_feature_class
22
+ if !@class.kind_of?(Class)
23
+ @class = ActiveSupport::Inflector.constantize(@class.to_s)
24
+ end
25
+ super(**options)
26
+ end
27
+
28
+ def switchable?
29
+ true
30
+ end
31
+
32
+ def enabled?(feature)
33
+ find(feature).try(:enabled?)
34
+ end
35
+
36
+ def switch!(feature, enabled)
37
+ record = find_or_new(feature)
38
+ record.enabled = enabled
39
+ record.save
40
+ end
41
+
42
+ def clear!(feature)
43
+ find(feature).try(:destroy)
44
+ end
45
+
46
+ protected
47
+
48
+ def find_or_new(feature)
49
+ find(feature) || @class.new(key: feature.to_s)
50
+ end
51
+
52
+ def find(feature)
53
+ @class.where(key: feature.to_s).first
54
+ end
55
+ end
56
+ end
57
+ end
@@ -1,3 +1,3 @@
1
1
  module Flipflop
2
- VERSION = "2.4.0"
2
+ VERSION = "2.7.0"
3
3
  end
data/lib/flipflop.rb CHANGED
@@ -22,6 +22,7 @@ require "flipflop/strategies/lambda_strategy"
22
22
  require "flipflop/strategies/query_string_strategy"
23
23
  require "flipflop/strategies/redis_strategy"
24
24
  require "flipflop/strategies/session_strategy"
25
+ require "flipflop/strategies/sequel_strategy"
25
26
  require "flipflop/strategies/test_strategy"
26
27
 
27
28
  require "flipflop/engine" if defined?(Rails)
@@ -6,8 +6,10 @@ Flipflop.configure do
6
6
 
7
7
  # Other strategies:
8
8
  #
9
- # strategy :query_string
9
+ # strategy :sequel
10
10
  # strategy :redis
11
+ #
12
+ # strategy :query_string
11
13
  # strategy :session
12
14
  #
13
15
  # strategy :my_strategy do |feature|
@@ -24,6 +24,10 @@ class Flipflop::InstallGenerator < Rails::Generators::Base
24
24
  # Before filter for Flipflop dashboard. Replace with a lambda or method name
25
25
  # defined in ApplicationController to implement access control.
26
26
  config.flipflop.dashboard_access_filter = #{access_filter}
27
+
28
+ # By default, when set to `nil`, strategy loading errors are suppressed in test
29
+ # mode. Set to `true` to always raise errors, or `false` to always warn.
30
+ config.flipflop.raise_strategy_errors = nil
27
31
  RUBY
28
32
  end
29
33
 
@@ -0,0 +1,31 @@
1
+ require_relative 'support/methods'
2
+
3
+ namespace :flipflop do
4
+ # Encapsulates support methods to prevent name collision with global
5
+ # rake namespace
6
+ m = Object.new
7
+ m.extend Flipflop::Rake::SupportMethods
8
+
9
+ desc 'Enables a feature with the specified strategy.'
10
+ task :turn_on, %i[feature strategy] => :environment do |_task, args|
11
+ m.switch_feature! args[:feature], args[:strategy], true
12
+ puts "Feature :#{args[:feature]} enabled!"
13
+ end
14
+
15
+ desc 'Disables a feature with the specified strategy.'
16
+ task :turn_off, %i[feature strategy] => :environment do |_task, args|
17
+ m.switch_feature! args[:feature], args[:strategy], false
18
+ puts "Feature :#{args[:feature]} disabled!"
19
+ end
20
+
21
+ desc 'Clears a feature with the specified strategy.'
22
+ task :clear, %i[feature strategy] => :environment do |_task, args|
23
+ m.clear_feature! args[:feature], args[:strategy]
24
+ puts "Feature :#{args[:feature]} cleared!"
25
+ end
26
+
27
+ desc 'Shows features table'
28
+ task features: :environment do
29
+ puts m.build_features_table
30
+ end
31
+ end
@@ -0,0 +1,71 @@
1
+ require 'terminal-table'
2
+
3
+ module Flipflop
4
+ module Rake
5
+ module SupportMethods
6
+ def status_label(enabled)
7
+ enabled.nil? ? '' : (enabled ? 'ON' : 'OFF')
8
+ end
9
+
10
+ def switch_feature!(feature_name, strategy_name, value)
11
+ feature = find_feature_by_name(feature_name)
12
+ strategy = find_strategy_by_name(strategy_name)
13
+
14
+ if strategy.switchable?
15
+ strategy.switch!(feature.key, value)
16
+ else
17
+ raise "The :#{strategy_name} strategy doesn't support switching."
18
+ end
19
+ end
20
+
21
+ def clear_feature!(feature_name, strategy_name)
22
+ feature = find_feature_by_name(feature_name)
23
+ strategy = find_strategy_by_name(strategy_name)
24
+
25
+ strategy.clear!(feature.key)
26
+ end
27
+
28
+ def build_features_table
29
+ table_class.new(headings: table_header, rows: table_rows)
30
+ end
31
+
32
+ protected
33
+
34
+ def features
35
+ Flipflop.feature_set.features
36
+ end
37
+
38
+ def strategies
39
+ Flipflop.feature_set.strategies
40
+ end
41
+
42
+ def find_feature_by_name(name)
43
+ features.find { |f| f.name == name } || raise("Feature :#{name} is not defined.")
44
+ end
45
+
46
+ def find_strategy_by_name(name)
47
+ strategies.find { |s| s.name == name } || raise("Strategy :#{name} is not available.")
48
+ end
49
+
50
+ def table_header
51
+ %w[feature description] + strategies.map(&:name)
52
+ end
53
+
54
+ def table_class
55
+ Terminal::Table
56
+ end
57
+
58
+ def table_rows
59
+ features.inject([]) do |array, feature|
60
+ array << build_row_for(feature)
61
+ end
62
+ end
63
+
64
+ def build_row_for(feature)
65
+ strategies.inject([feature.name, feature.description]) do |row, strategy|
66
+ row << status_label(strategy.enabled?(feature.key))
67
+ end
68
+ end
69
+ end
70
+ end
71
+ end
data/test/test_helper.rb CHANGED
@@ -31,6 +31,20 @@ def create_request
31
31
  request
32
32
  end
33
33
 
34
+ def capture_stdout
35
+ stdout, $stdout = $stdout, StringIO.new
36
+ yield rescue nil
37
+ stdout, $stdout = $stdout, stdout
38
+ stdout.string
39
+ end
40
+
41
+ def capture_stderr
42
+ stderr, $stderr = $stderr, StringIO.new
43
+ yield rescue nil
44
+ stderr, $stderr = $stderr, stderr
45
+ stderr.string
46
+ end
47
+
34
48
  def reload_constant(name)
35
49
  ActiveSupport::Dependencies.remove_constant(name.to_s)
36
50
  path = ActiveSupport::Dependencies.search_for_file(name.to_s.underscore).sub!(/\.rb\z/, "")
@@ -100,7 +114,11 @@ class TestApp
100
114
  quiet: true,
101
115
  api: ENV["RAILS_API_ONLY"].to_i.nonzero?,
102
116
  skip_active_job: true,
117
+ skip_active_storage: true,
118
+ skip_action_cable: true,
119
+ skip_bootsnap: true,
103
120
  skip_bundle: true,
121
+ skip_puma: true,
104
122
  skip_gemfile: true,
105
123
  skip_git: true,
106
124
  skip_javascript: true,
@@ -128,11 +146,26 @@ class TestApp
128
146
  require "rails"
129
147
  require "flipflop/engine"
130
148
 
149
+ ActiveSupport::Dependencies.autoloaded_constants.clear
131
150
  load File.expand_path("../../#{path}/config/application.rb", __FILE__)
132
151
  load File.expand_path("../../#{path}/config/environments/test.rb", __FILE__)
133
152
  Rails.application.config.cache_classes = false
134
153
  Rails.application.config.action_view.raise_on_missing_translations = true
135
154
  Rails.application.config.i18n.enforce_available_locales = false
155
+ Rails.application.config.autoloader = :classic # Disable Zeitwerk in Rails 6+
156
+
157
+ # Avoid Rails 6+ deprecation warning
158
+ if defined?(ActiveRecord::ConnectionAdapters::SQLite3Adapter) &&
159
+ ActiveRecord::ConnectionAdapters::SQLite3Adapter.respond_to?(:represent_boolean_as_integer=) &&
160
+ Rails.application.config.active_record.sqlite3.nil?
161
+ Rails.application.config.active_record.sqlite3 = ActiveSupport::OrderedOptions.new
162
+ end
163
+
164
+ # Avoid Rails 6+ deprecation warning
165
+ if defined?(ActionView::Railtie::NULL_OPTION)
166
+ Rails.application.config.action_view.finalize_compiled_template_methods = ActionView::Railtie::NULL_OPTION
167
+ end
168
+
136
169
  Rails.application.initialize!
137
170
 
138
171
  I18n.locale = :en
@@ -143,18 +176,20 @@ class TestApp
143
176
  def migrate!
144
177
  ActiveRecord::Base.establish_connection
145
178
 
146
- silence_stdout { ActiveRecord::Tasks::DatabaseTasks.create_current }
179
+ capture_stdout { ActiveRecord::Tasks::DatabaseTasks.create_current }
147
180
  ActiveRecord::Migration.verbose = false
148
181
 
149
182
  if defined?(ActiveRecord::Migrator.migrate)
150
183
  ActiveRecord::Migrator.migrate(Rails.application.paths["db/migrate"].to_a)
184
+ elsif ActiveRecord::Base.connection.respond_to?(:schema_migration)
185
+ ActiveRecord::MigrationContext.new(Rails.application.paths["db/migrate"], ActiveRecord::Base.connection.schema_migration).migrate
151
186
  else
152
- # Rails 5.2+
153
187
  ActiveRecord::MigrationContext.new(Rails.application.paths["db/migrate"]).migrate
154
188
  end
155
189
  end
156
190
 
157
191
  def unload!
192
+ ENV["RAILS_ENV"] = nil
158
193
  Flipflop::Strategies::AbstractStrategy::RequestInterceptor.request = nil
159
194
  Flipflop::FeatureLoader.instance_variable_set(:@current, nil)
160
195
 
@@ -173,12 +208,6 @@ class TestApp
173
208
 
174
209
  private
175
210
 
176
- def silence_stdout
177
- stdout, $stdout = $stdout, StringIO.new
178
- yield rescue nil
179
- $stdout = stdout
180
- end
181
-
182
211
  def path
183
212
  "tmp/" + name
184
213
  end
@@ -1,5 +1,12 @@
1
1
  require File.expand_path("../../test_helper", __FILE__)
2
2
 
3
+ class FailingStrategy < Flipflop::Strategies::AbstractStrategy
4
+ def initialize(*)
5
+ raise "Oops"
6
+ end
7
+ end
8
+
9
+
3
10
  describe Flipflop::Configurable do
4
11
  subject do
5
12
  Flipflop::FeatureSet.current.send(:initialize)
@@ -100,5 +107,20 @@ describe Flipflop::Configurable do
100
107
  assert_equal ["awesome"],
101
108
  Flipflop::FeatureSet.current.strategies.map(&:description)
102
109
  end
110
+
111
+ it "should raise error when strategy fails to load if unsuppressed" do
112
+ subject
113
+ Flipflop::FeatureSet.current.raise_strategy_errors = true
114
+ assert_raises "Oops" do
115
+ subject.strategy(FailingStrategy)
116
+ end
117
+ end
118
+
119
+ it "should not raise error when strategy fails to load if suppressed" do
120
+ subject
121
+ Flipflop::FeatureSet.current.raise_strategy_errors = false
122
+ assert_equal "WARNING: Unable to load Flipflop strategy FailingStrategy: Oops\n",
123
+ capture_stderr { subject.strategy(FailingStrategy) }
124
+ end
103
125
  end
104
126
  end
@@ -6,7 +6,7 @@ class ResultSet
6
6
  end
7
7
 
8
8
  def first_or_initialize
9
- @results.first or My::Feature.new(@key, false)
9
+ @results.first or MyAr::Feature.new(@key, false)
10
10
  end
11
11
 
12
12
  def first
@@ -14,7 +14,7 @@ class ResultSet
14
14
  end
15
15
  end
16
16
 
17
- module My
17
+ module MyAr
18
18
  class Feature < Struct.new(:key, :enabled)
19
19
  class << self
20
20
  attr_accessor :results
@@ -27,11 +27,11 @@ module My
27
27
  alias_method :enabled?, :enabled
28
28
 
29
29
  def destroy
30
- My::Feature.results[key] = ResultSet.new(key)
30
+ MyAr::Feature.results[key] = ResultSet.new(key)
31
31
  end
32
32
 
33
33
  def save!
34
- My::Feature.results[key] = ResultSet.new(key, [self])
34
+ MyAr::Feature.results[key] = ResultSet.new(key, [self])
35
35
  end
36
36
  end
37
37
  end
@@ -39,7 +39,7 @@ end
39
39
  describe Flipflop::Strategies::ActiveRecordStrategy do
40
40
  describe "with defaults" do
41
41
  subject do
42
- Flipflop::Strategies::ActiveRecordStrategy.new(class: My::Feature).freeze
42
+ Flipflop::Strategies::ActiveRecordStrategy.new(class: MyAr::Feature).freeze
43
43
  end
44
44
 
45
45
  it "should have default name" do
@@ -65,8 +65,8 @@ describe Flipflop::Strategies::ActiveRecordStrategy do
65
65
 
66
66
  describe "with enabled feature" do
67
67
  before do
68
- My::Feature.results = {
69
- one: ResultSet.new(:one, [My::Feature.new(:one, true)]),
68
+ MyAr::Feature.results = {
69
+ one: ResultSet.new(:one, [MyAr::Feature.new(:one, true)]),
70
70
  }
71
71
  end
72
72
 
@@ -87,8 +87,8 @@ describe Flipflop::Strategies::ActiveRecordStrategy do
87
87
 
88
88
  describe "with disabled feature" do
89
89
  before do
90
- My::Feature.results = {
91
- two: ResultSet.new(:two, [My::Feature.new(:two, false)]),
90
+ MyAr::Feature.results = {
91
+ two: ResultSet.new(:two, [MyAr::Feature.new(:two, false)]),
92
92
  }
93
93
  end
94
94
 
@@ -109,7 +109,7 @@ describe Flipflop::Strategies::ActiveRecordStrategy do
109
109
 
110
110
  describe "with unsaved feature" do
111
111
  before do
112
- My::Feature.results = {
112
+ MyAr::Feature.results = {
113
113
  three: ResultSet.new(:three),
114
114
  }
115
115
  end
@@ -127,12 +127,12 @@ describe Flipflop::Strategies::ActiveRecordStrategy do
127
127
 
128
128
  describe "with string class name" do
129
129
  subject do
130
- Flipflop::Strategies::ActiveRecordStrategy.new(class: "My::Feature").freeze
130
+ Flipflop::Strategies::ActiveRecordStrategy.new(class: "MyAr::Feature").freeze
131
131
  end
132
132
 
133
133
  before do
134
- My::Feature.results = {
135
- one: ResultSet.new(:one, [My::Feature.new(:one, true)]),
134
+ MyAr::Feature.results = {
135
+ one: ResultSet.new(:one, [MyAr::Feature.new(:one, true)]),
136
136
  }
137
137
  end
138
138
 
@@ -144,12 +144,12 @@ describe Flipflop::Strategies::ActiveRecordStrategy do
144
144
 
145
145
  describe "with symbol class name" do
146
146
  subject do
147
- Flipflop::Strategies::ActiveRecordStrategy.new(class: :"My::Feature").freeze
147
+ Flipflop::Strategies::ActiveRecordStrategy.new(class: :"MyAr::Feature").freeze
148
148
  end
149
149
 
150
150
  before do
151
- My::Feature.results = {
152
- one: ResultSet.new(:one, [My::Feature.new(:one, true)]),
151
+ MyAr::Feature.results = {
152
+ one: ResultSet.new(:one, [MyAr::Feature.new(:one, true)]),
153
153
  }
154
154
  end
155
155
 
@@ -112,7 +112,7 @@ describe Flipflop::Strategies::CookieStrategy do
112
112
  subject.switch!(:one, true)
113
113
  subject.clear!(:one)
114
114
  subject.send(:request).cookie_jar.write(headers = {})
115
- assert_equal "my_cookie_one=; domain=.example.com; path=/foo; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 -0000; HttpOnly",
115
+ assert_equal "my_cookie_one=; domain=.example.com; path=/foo; max-age=0; expires=Thu, 01 Jan 1970 00:00:00 GMT; HttpOnly",
116
116
  headers["Set-Cookie"]
117
117
  end
118
118
  end
@@ -0,0 +1,165 @@
1
+ require File.expand_path("../../../test_helper", __FILE__)
2
+
3
+ class ResultSet
4
+ def initialize(key, results = [])
5
+ @key, @results = key, results
6
+ end
7
+
8
+ def first
9
+ @results.first
10
+ end
11
+ end
12
+
13
+ module MySq
14
+ class Feature < Struct.new(:key, :enabled)
15
+ class << self
16
+ attr_accessor :results
17
+
18
+ def where(conditions)
19
+ results[conditions[:key].to_sym]
20
+ end
21
+ end
22
+
23
+ alias_method :enabled?, :enabled
24
+
25
+ def initialize(key, value = false)
26
+ if key.kind_of?(Hash)
27
+ key.each { |k, v| self[k] = v.to_sym }
28
+ else
29
+ super
30
+ end
31
+ end
32
+
33
+ def destroy
34
+ MySq::Feature.results[key] = ResultSet.new(key)
35
+ end
36
+
37
+ def save
38
+ MySq::Feature.results[key] = ResultSet.new(key, [self])
39
+ end
40
+ end
41
+ end
42
+
43
+ describe Flipflop::Strategies::SequelStrategy do
44
+ describe "with defaults" do
45
+ subject do
46
+ Flipflop::Strategies::SequelStrategy.new(class: MySq::Feature).freeze
47
+ end
48
+
49
+ it "should have default name" do
50
+ assert_equal "sequel", subject.name
51
+ end
52
+
53
+ it "should have title derived from name" do
54
+ assert_equal "Sequel", subject.title
55
+ end
56
+
57
+ it "should have default description" do
58
+ assert_equal "Stores features in database. Applies to all users.",
59
+ subject.description
60
+ end
61
+
62
+ it "should be switchable" do
63
+ assert_equal true, subject.switchable?
64
+ end
65
+
66
+ it "should have unique key" do
67
+ assert_match /^\w+$/, subject.key
68
+ end
69
+
70
+ describe "with enabled feature" do
71
+ before do
72
+ MySq::Feature.results = {
73
+ one: ResultSet.new(:one, [MySq::Feature.new(:one, true)]),
74
+ }
75
+ end
76
+
77
+ it "should have feature enabled" do
78
+ assert_equal true, subject.enabled?(:one)
79
+ end
80
+
81
+ it "should be able to switch feature off" do
82
+ subject.switch!(:one, false)
83
+ assert_equal false, subject.enabled?(:one)
84
+ end
85
+
86
+ it "should be able to clear feature" do
87
+ subject.clear!(:one)
88
+ assert_nil subject.enabled?(:one)
89
+ end
90
+ end
91
+
92
+ describe "with disabled feature" do
93
+ before do
94
+ MySq::Feature.results = {
95
+ two: ResultSet.new(:two, [MySq::Feature.new(:two, false)]),
96
+ }
97
+ end
98
+
99
+ it "should not have feature enabled" do
100
+ assert_equal false, subject.enabled?(:two)
101
+ end
102
+
103
+ it "should be able to switch feature on" do
104
+ subject.switch!(:two, true)
105
+ assert_equal true, subject.enabled?(:two)
106
+ end
107
+
108
+ it "should be able to clear feature" do
109
+ subject.clear!(:two)
110
+ assert_nil subject.enabled?(:two)
111
+ end
112
+ end
113
+
114
+ describe "with unsaved feature" do
115
+ before do
116
+ MySq::Feature.results = {
117
+ three: ResultSet.new(:three),
118
+ }
119
+ end
120
+
121
+ it "should not know feature" do
122
+ assert_nil subject.enabled?(:three)
123
+ end
124
+
125
+ it "should be able to switch feature on" do
126
+ subject.switch!(:three, true)
127
+ assert_equal true, subject.enabled?(:three)
128
+ end
129
+ end
130
+ end
131
+
132
+ describe "with string class name" do
133
+ subject do
134
+ Flipflop::Strategies::SequelStrategy.new(class: "MySq::Feature").freeze
135
+ end
136
+
137
+ before do
138
+ MySq::Feature.results = {
139
+ one: ResultSet.new(:one, [MySq::Feature.new(:one, true)]),
140
+ }
141
+ end
142
+
143
+ it "should be able to switch feature off" do
144
+ subject.switch!(:one, false)
145
+ assert_equal false, subject.enabled?(:one)
146
+ end
147
+ end
148
+
149
+ describe "with symbol class name" do
150
+ subject do
151
+ Flipflop::Strategies::SequelStrategy.new(class: :"MySq::Feature").freeze
152
+ end
153
+
154
+ before do
155
+ MySq::Feature.results = {
156
+ one: ResultSet.new(:one, [MySq::Feature.new(:one, true)]),
157
+ }
158
+ end
159
+
160
+ it "should be able to switch feature off" do
161
+ subject.switch!(:one, false)
162
+ assert_equal false, subject.enabled?(:one)
163
+ end
164
+ end
165
+ end
@@ -0,0 +1,68 @@
1
+ require File.expand_path('../../../../test_helper', __FILE__)
2
+ require File.expand_path('../../../../../lib/tasks/support/methods', __FILE__)
3
+
4
+ describe Flipflop::Rake::SupportMethods do
5
+ subject do
6
+ object = Object.new
7
+ object.extend Flipflop::Rake::SupportMethods
8
+ end
9
+
10
+ describe '#status_label' do
11
+ it 'returns the "enabled" label when its argument is "true"' do
12
+ assert_equal 'ON', subject.status_label(true)
13
+ end
14
+
15
+ it 'returns the "disabled" label when its argument is "false"' do
16
+ assert_equal 'OFF', subject.status_label(false)
17
+ end
18
+
19
+ it 'returns the "unset" label when its argument is "nil"' do
20
+ assert_equal '', subject.status_label(nil)
21
+ end
22
+ end
23
+
24
+ def with_feature_and_strategy(strategy = 'Test')
25
+ # Stubs finder methods to avoid going into FeatureSet code.
26
+ feature = Flipflop::FeatureDefinition.new(:world_domination)
27
+ subject.stub :find_feature_by_name, feature do
28
+ classname = "#{strategy}Strategy"
29
+ strategy = Flipflop::Strategies.const_get(classname).new(name: strategy)
30
+ subject.stub :find_strategy_by_name, strategy do
31
+ yield(strategy, feature) if block_given?
32
+ end
33
+ end
34
+ end
35
+
36
+ describe '#switch_feature!' do
37
+ it 'enables a feature using a strategy' do
38
+ with_feature_and_strategy do |strategy, feature|
39
+ subject.switch_feature! 'world_domination', 'test', true
40
+ assert_equal true, strategy.enabled?(feature.key)
41
+ end
42
+ end
43
+
44
+ it 'disables a feature using a strategy' do
45
+ with_feature_and_strategy do |strategy, feature|
46
+ subject.switch_feature! 'world_domination', 'test', false
47
+ assert_equal false, strategy.enabled?(feature.key)
48
+ end
49
+ end
50
+
51
+ describe 'when the strategy is not switchable' do
52
+ it 'raises an error' do
53
+ with_feature_and_strategy 'Lambda' do |strategy, feature|
54
+ -> { subject.switch_feature!('world_domination', 'lambda', true) }.must_raise
55
+ end
56
+ end
57
+ end
58
+ end
59
+
60
+ describe '#clear_feature!' do
61
+ it 'clears a feature using a strategy' do
62
+ with_feature_and_strategy do |strategy, feature|
63
+ subject.clear_feature! 'world_domination', 'test'
64
+ assert_nil strategy.enabled?(feature.key)
65
+ end
66
+ end
67
+ end
68
+ end
metadata CHANGED
@@ -1,16 +1,16 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: flipflop
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.4.0
4
+ version: 2.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Paul Annesley
8
8
  - Rolf Timmermans
9
9
  - Jippe Holwerda
10
- autorequire:
10
+ autorequire:
11
11
  bindir: bin
12
12
  cert_chain: []
13
- date: 2018-04-26 00:00:00.000000000 Z
13
+ date: 2022-06-29 00:00:00.000000000 Z
14
14
  dependencies:
15
15
  - !ruby/object:Gem::Dependency
16
16
  name: activesupport
@@ -26,6 +26,20 @@ dependencies:
26
26
  - - ">="
27
27
  - !ruby/object:Gem::Version
28
28
  version: '4.0'
29
+ - !ruby/object:Gem::Dependency
30
+ name: terminal-table
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '1.8'
36
+ type: :runtime
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '1.8'
29
43
  description: Declarative API for specifying features, switchable in declaration, database
30
44
  and cookies.
31
45
  email:
@@ -43,11 +57,11 @@ files:
43
57
  - LICENSE
44
58
  - README.md
45
59
  - Rakefile
60
+ - app/assets/config/manifest.js
46
61
  - app/controllers/flipflop/features_controller.rb
47
62
  - app/controllers/flipflop/strategies_controller.rb
48
- - app/models/flipflop/feature.rb
49
63
  - app/views/flipflop/features/index.html.erb
50
- - app/views/flipflop/stylesheets/_flipflop.css
64
+ - app/views/flipflop/stylesheets/_flipflop.html.erb
51
65
  - app/views/layouts/flipflop.html.erb
52
66
  - config/features.rb
53
67
  - config/locales/en.yml
@@ -70,6 +84,7 @@ files:
70
84
  - lib/flipflop/strategies/options_hasher.rb
71
85
  - lib/flipflop/strategies/query_string_strategy.rb
72
86
  - lib/flipflop/strategies/redis_strategy.rb
87
+ - lib/flipflop/strategies/sequel_strategy.rb
73
88
  - lib/flipflop/strategies/session_strategy.rb
74
89
  - lib/flipflop/strategies/test_strategy.rb
75
90
  - lib/flipflop/version.rb
@@ -82,6 +97,8 @@ files:
82
97
  - lib/generators/flipflop/migration/templates/create_features.rb
83
98
  - lib/generators/flipflop/routes/USAGE
84
99
  - lib/generators/flipflop/routes/routes_generator.rb
100
+ - lib/tasks/flipflop.rake
101
+ - lib/tasks/support/methods.rb
85
102
  - src/stylesheets/_flipflop.scss
86
103
  - test/integration/app_test.rb
87
104
  - test/integration/dashboard_test.rb
@@ -105,14 +122,16 @@ files:
105
122
  - test/unit/strategies/options_hasher_test.rb
106
123
  - test/unit/strategies/query_string_strategy_test.rb
107
124
  - test/unit/strategies/redis_strategy_test.rb
125
+ - test/unit/strategies/sequel_strategy_test.rb
108
126
  - test/unit/strategies/session_strategy_test.rb
109
127
  - test/unit/strategies/test_strategy_test.rb
110
128
  - test/unit/strategies_controller_test.rb
129
+ - test/unit/tasks/support/methods_test.rb
111
130
  homepage: https://github.com/voormedia/flipflop
112
131
  licenses:
113
132
  - MIT
114
133
  metadata: {}
115
- post_install_message:
134
+ post_install_message:
116
135
  rdoc_options: []
117
136
  require_paths:
118
137
  - lib
@@ -127,9 +146,35 @@ required_rubygems_version: !ruby/object:Gem::Requirement
127
146
  - !ruby/object:Gem::Version
128
147
  version: '0'
129
148
  requirements: []
130
- rubyforge_project:
131
- rubygems_version: 2.7.6
132
- signing_key:
149
+ rubygems_version: 3.1.6
150
+ signing_key:
133
151
  specification_version: 4
134
152
  summary: A feature flipflopper for Rails web applications.
135
- test_files: []
153
+ test_files:
154
+ - test/integration/app_test.rb
155
+ - test/integration/dashboard_test.rb
156
+ - test/templates/nl.yml
157
+ - test/templates/test_app_features.rb
158
+ - test/templates/test_engine.rb
159
+ - test/templates/test_engine_features.rb
160
+ - test/test_helper.rb
161
+ - test/unit/configurable_test.rb
162
+ - test/unit/feature_cache_test.rb
163
+ - test/unit/feature_definition_test.rb
164
+ - test/unit/feature_set_test.rb
165
+ - test/unit/flipflop_test.rb
166
+ - test/unit/group_definition_test.rb
167
+ - test/unit/strategies/abstract_strategy_request_test.rb
168
+ - test/unit/strategies/abstract_strategy_test.rb
169
+ - test/unit/strategies/active_record_strategy_test.rb
170
+ - test/unit/strategies/cookie_strategy_test.rb
171
+ - test/unit/strategies/default_strategy_test.rb
172
+ - test/unit/strategies/lambda_strategy_test.rb
173
+ - test/unit/strategies/options_hasher_test.rb
174
+ - test/unit/strategies/query_string_strategy_test.rb
175
+ - test/unit/strategies/redis_strategy_test.rb
176
+ - test/unit/strategies/sequel_strategy_test.rb
177
+ - test/unit/strategies/session_strategy_test.rb
178
+ - test/unit/strategies/test_strategy_test.rb
179
+ - test/unit/strategies_controller_test.rb
180
+ - test/unit/tasks/support/methods_test.rb
@@ -1,4 +0,0 @@
1
- if defined?(ActiveRecord)
2
- class Flipflop::Feature < ActiveRecord::Base
3
- end
4
- end