diff --git a/Gemfile b/Gemfile index d44bd83..1e337a3 100644 --- a/Gemfile +++ b/Gemfile @@ -14,6 +14,7 @@ group :test do end gem "sqlite3", ">= 1.3", platform: :ruby + gem "fakeredis" gem "activerecord-jdbcsqlite3-adapter", platform: :jruby gem "minitest", ">= 4.2" gem "capybara", ">= 2.6" diff --git a/README.md b/README.md index b72c727..8548c0c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,9 @@ The following strategies are provided: * `:httponly` – Whether the cookies are accessible via scripting or only HTTP. Default is `false`. * `:query_string` – Interpret query string parameters as features. This strategy is only used for resolving. It does not allow switching features on/off. * `:prefix` – String prefix for all query string parameters. Defaults to no prefix. +* `:redis` – Save feature settings in Redis. + * `:client` – Use the specified Redis client instead of `Redis.new`. + * `:prefix` – String prefix for all Redis keys. Defaults to no prefix. * `:session` – Save feature settings in the current user's application session. * `:prefix` – String prefix for all session variables. Defaults to no prefix. * `:default` – Not strictly needed, all feature defaults will be applied if no strategies match a feature. Include this strategy to determine the order of using the default value, and to make it appear in the dashboard. diff --git a/Rakefile b/Rakefile index 535f93a..aca4408 100644 --- a/Rakefile +++ b/Rakefile @@ -34,6 +34,6 @@ namespace :assets do end task :clean do - FileUtils.rm(stylesheet_path) + FileUtils.rm(stylesheet_path) rescue nil end end diff --git a/lib/flipflop.rb b/lib/flipflop.rb index ab8216b..ffc6951 100644 --- a/lib/flipflop.rb +++ b/lib/flipflop.rb @@ -18,6 +18,7 @@ require "flipflop/strategies/default_strategy" require "flipflop/strategies/lambda_strategy" require "flipflop/strategies/query_string_strategy" +require "flipflop/strategies/redis_strategy" require "flipflop/strategies/session_strategy" require "flipflop/strategies/test_strategy" diff --git a/lib/flipflop/strategies/active_record_strategy.rb b/lib/flipflop/strategies/active_record_strategy.rb index 5113e1c..6dadf14 100644 --- a/lib/flipflop/strategies/active_record_strategy.rb +++ b/lib/flipflop/strategies/active_record_strategy.rb @@ -8,7 +8,7 @@ def default_description end def initialize(**options) - @class = options.delete(:class) || "Flipflop::Feature" + @class = options.delete(:class) || ::Flipflop::Feature if !@class.kind_of?(Class) @class = ActiveSupport::Inflector.constantize(@class.to_s) end diff --git a/lib/flipflop/strategies/query_string_strategy.rb b/lib/flipflop/strategies/query_string_strategy.rb index 4053b44..56bd797 100644 --- a/lib/flipflop/strategies/query_string_strategy.rb +++ b/lib/flipflop/strategies/query_string_strategy.rb @@ -15,7 +15,7 @@ def initialize(**options) def enabled?(feature) return unless request? return unless request.params.has_key?(param_key(feature)) - request.params[param_key(feature)].to_s != "0" + request.params[param_key(feature)] != "0" end protected diff --git a/lib/flipflop/strategies/redis_strategy.rb b/lib/flipflop/strategies/redis_strategy.rb new file mode 100644 index 0000000..cd6a57f --- /dev/null +++ b/lib/flipflop/strategies/redis_strategy.rb @@ -0,0 +1,41 @@ +module Flipflop + module Strategies + class RedisStrategy < AbstractStrategy + class << self + def default_description + "Stores features in Redis. Applies to all users." + end + end + + def initialize(**options) + @client = options.delete(:client) || ::Redis.new + @prefix = options.delete(:prefix).to_s.freeze + super(**options) + end + + def switchable? + true + end + + def enabled?(feature) + redis_value = @client.get(redis_key(feature)) + return if redis_value.nil? + redis_value === "1" + end + + def switch!(feature, enabled) + @client.set(redis_key(feature), enabled ? "1" : "0") + end + + def clear!(feature) + @client.del(redis_key(feature)) + end + + protected + + def redis_key(feature) + @prefix + feature.to_s + end + end + end +end diff --git a/lib/generators/flipflop/features/templates/features.rb b/lib/generators/flipflop/features/templates/features.rb index dc36c42..fe823f8 100644 --- a/lib/generators/flipflop/features/templates/features.rb +++ b/lib/generators/flipflop/features/templates/features.rb @@ -6,8 +6,9 @@ # Other strategies: # - # strategy :session # strategy :query_string + # strategy :redis + # strategy :session # # strategy :my_strategy do |feature| # # ... your custom code here; return true/false/nil. diff --git a/test/unit/strategies/redis_strategy_test.rb b/test/unit/strategies/redis_strategy_test.rb new file mode 100644 index 0000000..ae506c5 --- /dev/null +++ b/test/unit/strategies/redis_strategy_test.rb @@ -0,0 +1,97 @@ +require File.expand_path("../../../test_helper", __FILE__) + +require "fakeredis" + +describe Flipflop::Strategies::RedisStrategy do + before do + Redis.new.flushall + end + + describe "with defaults" do + subject do + Flipflop::Strategies::RedisStrategy.new.freeze + end + + it "should have default name" do + assert_equal "redis", subject.name + end + + it "should have default description" do + assert_equal "Stores features in Redis. Applies to all users.", + subject.description + end + + it "should be switchable" do + assert_equal true, subject.switchable? + end + + it "should have unique key" do + assert_match /^\w+$/, subject.key + end + + describe "with enabled feature" do + before do + Redis.new.set("one", 1) + end + + it "should have feature enabled" do + assert_equal true, subject.enabled?(:one) + end + + it "should be able to switch feature off" do + subject.switch!(:one, false) + assert_equal false, subject.enabled?(:one) + end + + it "should be able to clear feature" do + subject.clear!(:one) + assert_nil subject.enabled?(:one) + end + end + + describe "with disabled feature" do + before do + Redis.new.set("two", 0) + end + + it "should not have feature enabled" do + assert_equal false, subject.enabled?(:two) + end + + it "should be able to switch feature on" do + subject.switch!(:two, true) + assert_equal true, subject.enabled?(:two) + end + + it "should be able to clear feature" do + subject.clear!(:two) + assert_nil subject.enabled?(:two) + end + end + + describe "with unsaved feature" do + it "should not know feature" do + assert_nil subject.enabled?(:three) + end + + it "should be able to switch feature on" do + subject.switch!(:three, true) + assert_equal true, subject.enabled?(:three) + end + end + end + + describe "with options" do + subject do + Flipflop::Strategies::RedisStrategy.new( + client: Redis.new(db: 1), + prefix: "my_feature:", + ).freeze + end + + it "should use prefix and database to resolve parameters" do + Redis.new(db: 1).set("my_feature:one", 1) + assert_equal true, subject.enabled?(:one) + end + end +end