feature-1.4.0/0000755000175000017500000000000013415353423011550 5ustar aleealeefeature-1.4.0/Rakefile0000755000175000017500000000072413415353423013223 0ustar aleealeerequire 'rake/testtask' require 'rspec/core/rake_task' require 'rubocop/rake_task' RuboCop::RakeTask.new RSpec::Core::RakeTask.new(:spec) do |spec| spec.pattern = 'spec/**/*_spec.rb' spec.rspec_opts = ['--colour', '-f documentation', '--backtrace'] end task :mutant do require 'mutant' result = Mutant::CLI.run(%w(--include lib --require feature --use rspec Feature*)) raise unless result == Mutant::CLI::EXIT_SUCCESS end task default: [:spec, :rubocop] feature-1.4.0/spec/0000755000175000017500000000000013415353423012502 5ustar aleealeefeature-1.4.0/spec/integration/0000755000175000017500000000000013415353423015025 5ustar aleealeefeature-1.4.0/spec/integration/rails/0000755000175000017500000000000013415353423016137 5ustar aleealeefeature-1.4.0/spec/integration/rails/test-against-specific-rails-version.sh0000755000175000017500000000067413415353423025466 0ustar aleealee#!/bin/bash set -e export BUNDLE_GEMFILE=gemfiles/rails${RAILS_VERSION}.gemfile TESTAPP_NAME=testapp if [ -d $TESTAPP_NAME ]; then rm -Rf $TESTAPP_NAME fi bundle install bundle exec rails new $TESTAPP_NAME --skip-bundle unset BUNDLE_GEMFILE echo "gem 'feature', path: '../../../..'" >> $TESTAPP_NAME/Gemfile cd $TESTAPP_NAME bundle install bundle exec rails g feature:install bundle exec rake db:migrate cd .. rm -Rf $TESTAPP_NAME feature-1.4.0/spec/integration/rails/test-against-several-rails-versions.sh0000755000175000017500000000017513415353423025521 0ustar aleealee#!/bin/bash set -e for rails_version in 4; do RAILS_VERSION=$rails_version ./test-against-specific-rails-version.sh done feature-1.4.0/spec/integration/rails/gemfiles/0000755000175000017500000000000013415353423017732 5ustar aleealeefeature-1.4.0/spec/integration/rails/gemfiles/rails4.gemfile0000644000175000017500000000006713415353423022465 0ustar aleealeesource 'https://rubygems.org' gem 'rails', '~> 4.2.1' feature-1.4.0/spec/spec_helper.rb0000755000175000017500000000052113415353423015321 0ustar aleealeerequire 'pathname' require 'timecop' require 'coveralls' require 'fakeredis/rspec' Coveralls.wear! Coveralls::Output.silent = true $LOAD_PATH.unshift(File.join(File.dirname(__FILE__), '..', 'lib')) $LOAD_PATH.unshift(File.dirname(__FILE__)) SPEC_ROOT = Pathname(__FILE__).dirname.expand_path require SPEC_ROOT.parent + 'lib/feature' feature-1.4.0/spec/feature/0000755000175000017500000000000013415353423014135 5ustar aleealeefeature-1.4.0/spec/feature/yaml_repository_spec.rb0000644000175000017500000001030213415353423020731 0ustar aleealeerequire 'spec_helper' require 'tempfile' include Feature::Repository describe Feature::Repository::YamlRepository do context 'proper config file' do before(:each) do @filename = Tempfile.new(['feature_config', '.yaml']).path fp = File.new(@filename, 'w') fp.write <<"EOF" features: feature_a_active: true feature_b_active: true feature_c_inactive: false feature_d_inactive: false EOF fp.close @repo = YamlRepository.new(@filename) end after(:each) do File.delete(@filename) end it 'should read active features from a config file' do expect(@repo.active_features).to eq([:feature_a_active, :feature_b_active]) end context 're-read config file' do before(:each) do fp = File.new(@filename, 'w') fp.write <<"EOF" features: feature_a_active: true feature_c_inactive: false EOF fp.close end it 'should read active features new on each request' do expect(@repo.active_features).to eq([:feature_a_active]) end end context 'with no features' do before(:each) do fp = File.new(@filename, 'w') fp.write <<"EOF" features: EOF fp.close end it 'should read active features new on each request' do expect(@repo.active_features).to eq([]) end end context 'with optional environment name' do before(:each) do fp = File.new(@filename, 'w') fp.write <<"EOF" development: features: feature_a: true feature_b: true production: features: feature_a: true feature_b: false EOF fp.close end it 'has two active features for development environment' do repo = YamlRepository.new(@filename, 'development') expect(repo.active_features).to eq([:feature_a, :feature_b]) end it 'has one active feature for production environment' do repo = YamlRepository.new(@filename, 'production') expect(repo.active_features).to eq([:feature_a]) end end # Sometimes needed when loading features from ENV variables or are time # based rules Ex: Date.today > Date.strptime('1/2/2012', '%d/%m/%Y') context 'a config file with embedded erb' do before(:each) do @filename = Tempfile.new(['feature_config', '.yaml']).path fp = File.new(@filename, 'w') fp.write <<"EOF" features: feature_a_active: <%= 'true' == 'true' %> feature_b_active: true feature_c_inactive: <%= false %> feature_d_inactive: <%= 1 < 0 %> EOF fp.close @repo = YamlRepository.new(@filename) end it 'should read active features from the config file' do expect(@repo.active_features).to eq([:feature_a_active, :feature_b_active]) end end end it 'should raise exception on no file found' do repo = YamlRepository.new('/this/file/should/not/exist') expect do repo.active_features end.to raise_error(Errno::ENOENT, /No such file or directory/) end it 'should raise exception on invalid yaml' do @filename = Tempfile.new(['feature_config', '.yaml']).path fp = File.new(@filename, 'w') fp.write 'this is not valid feature config' fp.close repo = YamlRepository.new(@filename) expect do repo.active_features end.to raise_error(ArgumentError, 'yaml config does not contain proper config') end it 'should raise exception on yaml without features key' do @filename = Tempfile.new(['feature_config', '.yaml']).path fp = File.new(@filename, 'w') fp.write <<"EOF" fail: feature: true EOF fp.close repo = YamlRepository.new(@filename) expect do repo.active_features end.to raise_error(ArgumentError, 'yaml config does not contain proper config') end it 'should raise exception on not true/false value in config' do @filename = Tempfile.new(['feature_config', '.yaml']).path fp = File.new(@filename, 'w') fp.write <<"EOF" features: invalid_feature: neither_true_or_false EOF fp.close repo = YamlRepository.new(@filename) expect do repo.active_features end.to raise_error(ArgumentError, 'neither_true_or_false is not allowed value in config, use true/false') end end feature-1.4.0/spec/feature/active_record_repository_spec.rb0000644000175000017500000000275113415353423022611 0ustar aleealeerequire 'spec_helper' include Feature::Repository describe Feature::Repository::ActiveRecordRepository do before(:each) do # Mock the model @features = double('FeatureToggle') @repository = ActiveRecordRepository.new(@features) end it 'should have no active features after initialization' do allow(@features).to receive(:where) { [] } expect(@repository.active_features).to eq([]) end it 'should have active features' do allow(@features).to receive(:where).with(active: true) { [double(name: 'active')] } expect(@repository.active_features).to eq([:active]) end it 'should add an active feature' do expect(@features).to receive(:exists?).with(name: 'feature_a').and_return(false) expect(@features).to receive(:create!).with(name: 'feature_a', active: true) @repository.add_active_feature :feature_a end it 'should raise an exception when adding not a symbol as active feature' do expect do @repository.add_active_feature 'feature_a' end.to raise_error(ArgumentError, 'feature_a is not a symbol') end it 'should raise an exception when adding a active feature already added as active' do expect(@features).to receive(:create!).with(name: 'feature_a', active: true) allow(@features).to receive(:exists?).and_return(false, true) @repository.add_active_feature :feature_a expect do @repository.add_active_feature :feature_a end.to raise_error(ArgumentError, 'feature :feature_a already added') end end feature-1.4.0/spec/feature/feature_spec.rb0000644000175000017500000001546013415353423017135 0ustar aleealeerequire 'spec_helper' describe Feature do context 'without FeatureRepository' do it 'should raise an exception when calling active?' do expect do Feature.active?(:feature_a) end.to raise_error('missing Repository for obtaining feature lists') end it 'should raise an exception when calling inactive?' do expect do Feature.inactive?(:feature_a) end.to raise_error('missing Repository for obtaining feature lists') end it 'should raise an exception when calling with' do expect do Feature.with(:feature_a) do end end.to raise_error('missing Repository for obtaining feature lists') end it 'should raise an exception when calling without' do expect do Feature.without(:feature_a) do end end.to raise_error('missing Repository for obtaining feature lists') end it 'should raise an exception when calling active_features' do expect do Feature.active_features end.to raise_error('missing Repository for obtaining feature lists') end end context 'setting Repository' do before(:each) do @repository = Feature::Repository::SimpleRepository.new end context 'with auto_refresh set to false' do before(:each) do Feature.set_repository @repository end it 'should raise an exception when add repository with wrong class' do expect do Feature.set_repository('not a repository') end.to raise_error(ArgumentError, 'given repository does not respond to active_features') end it 'should get active features lazy on first usage' do @repository.add_active_feature(:feature_a) # the line below will be the first usage of feature in this case expect(Feature.active?(:feature_a)).to be_truthy end it 'should get active features from repository once' do Feature.active?(:does_not_matter) @repository.add_active_feature(:feature_a) expect(Feature.active?(:feature_a)).to be_falsey end it 'should reload active features on first call only' do @repository.add_active_feature(:feature_a) expect(@repository).to receive(:active_features).once.and_return(@repository.active_features) Feature.active?(:feature_a) Feature.active?(:feature_a) end end context 'with auto_refresh set to true' do before(:each) do Feature.set_repository @repository, true end it 'should reload active features on every call' do @repository.add_active_feature(:feature_a) expect(@repository).to receive(:active_features).twice.and_return(@repository.active_features) Feature.active?(:feature_a) Feature.active?(:feature_a) end end context 'with timeout set to 30 seconds' do before(:each) do Timecop.freeze(Time.now) Feature.set_repository @repository, 30 @repository.add_active_feature(:feature_a) Feature.active?(:feature_a) end after(:each) do Timecop.return end it 'should not update after 10 seconds' do Timecop.freeze(Time.now + 10) expect(@repository).not_to receive(:active_features) Feature.active?(:feature_a) end it 'should update after 40 seconds' do Timecop.freeze(Time.now + 40) expect(@repository).to receive(:active_features).and_return(@repository.active_features) Feature.active?(:feature_a) end end end context 'refresh features' do before(:each) do @repository = Feature::Repository::SimpleRepository.new Feature.set_repository @repository end it 'should refresh active feature lists from repository' do @repository.add_active_feature(:feature_a) Feature.refresh! expect(Feature.active?(:feature_a)).to be_truthy end end context 'request features' do before(:each) do repository = Feature::Repository::SimpleRepository.new repository.add_active_feature :feature_active Feature.set_repository repository end it 'should affirm active feature is active' do expect(Feature.active?(:feature_active)).to be_truthy end it 'should not affirm active feature is inactive' do expect(Feature.inactive?(:feature_active)).to be_falsey end it 'should affirm inactive feature is inactive' do expect(Feature.inactive?(:feature_inactive)).to be_truthy end it 'should not affirm inactive feature is active' do expect(Feature.active?(:feature_inactive)).to be_falsey end it 'should call block with active feature in active list' do reached = false Feature.with(:feature_active) do reached = true end expect(reached).to be_truthy end it 'should not call block with active feature not in active list' do reached = false Feature.with(:feature_inactive) do reached = true end expect(reached).to be_falsey end it 'should raise exception when no block given to with' do expect do Feature.with(:feature_active) end.to raise_error(ArgumentError, 'no block given to with') end it 'should call block without inactive feature in inactive list' do reached = false Feature.without(:feature_inactive) do reached = true end expect(reached).to be_truthy end it 'should not call block without inactive feature in inactive list' do reached = false Feature.without(:feature_active) do reached = true end expect(reached).to be_falsey end it 'should raise exception when no block given to without' do expect do Feature.without(:feature_inactive) end.to raise_error(ArgumentError, 'no block given to without') end describe 'switch()' do context 'given a value' do it 'should return the first value if the feature is active' do retval = Feature.switch(:feature_active, 1, 2) expect(retval).to eq(1) end it 'should return the second value if the feature is inactive' do retval = Feature.switch(:feature_inactive, 1, 2) expect(retval).to eq(2) end end context 'given a proc/lambda' do it 'should call the first proc/lambda if the feature is active' do retval = Feature.switch(:feature_active, -> { 1 }, -> { 2 }) expect(retval).to eq(1) end it 'should call the second proc/lambda if the feature is active' do retval = Feature.switch(:feature_inactive, -> { 1 }, -> { 2 }) expect(retval).to eq(2) end end end describe 'active_features' do it 'should return an array of active feature flags' do expect(Feature.active_features).to eq([:feature_active]) end end end end feature-1.4.0/spec/feature/redis_repository_spec.rb0000644000175000017500000000305713415353423021106 0ustar aleealeerequire 'spec_helper' include Feature::Repository describe Feature::Repository::RedisRepository do before(:each) do @repository = RedisRepository.new('application_features') end it 'should have no active features after initialization' do expect(@repository.active_features).to eq([]) end it 'should add an active feature' do @repository.add_active_feature :feature_a expect(@repository.active_features).to eq([:feature_a]) end it 'should only show active feature' do Redis.current.hset('application_features', 'inactive_a', false) Redis.current.hset('application_features', 'inactive_b', false) Redis.current.hset('application_features', 'feature_a', true) Redis.current.hset('application_features', 'feature_b', true) expect(@repository.active_features).to eq([:feature_a, :feature_b]) end it 'should raise an exception when adding not a symbol as active feature' do expect do @repository.add_active_feature 'feature_a' end.to raise_error(ArgumentError, 'feature_a is not a symbol') end it 'should raise an exception when adding a active feature already added as active' do @repository.add_active_feature :feature_a expect do @repository.add_active_feature :feature_a end.to raise_error(ArgumentError, 'feature :feature_a already added') end let(:specified_redis) { double } let(:repo) { RedisRepository.new('application_features', specified_redis) } it 'should allow you to specify the redis instance to use' do expect(repo.send(:redis)).to eq specified_redis end end feature-1.4.0/spec/feature/testing_spec.rb0000644000175000017500000000660313415353423017156 0ustar aleealeerequire 'spec_helper' require 'feature/testing' shared_examples 'a testable repository' do before do expect(Feature.active?(:active_feature)).to be_truthy expect(Feature.active?(:another_active_feature)).to be_truthy expect(Feature.active?(:deactive_feature)).to be_falsey expect(Feature.active?(:another_deactive_feature)).to be_falsey end after do expect(Feature.active?(:active_feature)).to be_truthy expect(Feature.active?(:another_active_feature)).to be_truthy expect(Feature.active?(:deactive_feature)).to be_falsey expect(Feature.active?(:another_deactive_feature)).to be_falsey end describe '.run_with_activated' do it 'activates a deactivated feature' do Feature.run_with_activated(:deactive_feature) do expect(Feature.active?(:deactive_feature)).to be_truthy end end it 'activates multiple deactivated features' do Feature.run_with_activated(:deactive_feature, :another_deactive_feature) do expect(Feature.active?(:deactive_feature)).to be_truthy expect(Feature.active?(:another_deactive_feature)).to be_truthy end end end describe '.run_with_deactivated' do it 'deactivates an activated feature' do Feature.run_with_deactivated(:active_feature) do expect(Feature.active?(:active_feature)).to be_falsey end end it 'deactivates multiple activated features' do Feature.run_with_deactivated(:active_feature, :another_active_feature) do expect(Feature.active?(:active_feature)).to be_falsey expect(Feature.active?(:another_active_feature)).to be_falsey end end end end describe 'Feature testing support' do context 'without auto_refresh' do before(:all) do repository = Feature::Repository::SimpleRepository.new repository.add_active_feature(:active_feature) repository.add_active_feature(:another_active_feature) Feature.set_repository(repository) end it_behaves_like 'a testable repository' end context 'with auto_refresh' do before(:all) do repository = Feature::Repository::SimpleRepository.new repository.add_active_feature(:active_feature) repository.add_active_feature(:another_active_feature) Feature.set_repository(repository, true) end it_behaves_like 'a testable repository' describe '.run_with_deactivated' do it 'should disable perform_initial_refresh for the first call to Feature.active?' do Feature.run_with_activated(:deactive_feature) do expect(Feature.active?(:deactive_feature)).to be_truthy end end end describe '.run_with_deactivated' do it 'should disable perform_initial_refresh for the first call to Feature.active?' do Feature.run_with_deactivated(:active_feature) do expect(Feature.active?(:active_feature)).to be_falsey end end end end context 'with no features activated' do before(:all) do repository = Feature::Repository::SimpleRepository.new Feature.set_repository(repository) end describe '.run_with_activated' do it 'should not raise an error' do expect { Feature.run_with_activated(:foo) {} }.to_not raise_error end end describe '.run_with_deactivated' do it 'should not raise an error' do expect { Feature.run_with_deactivated(:foo) {} }.to_not raise_error end end end end feature-1.4.0/spec/feature/simple_repository_spec.rb0000644000175000017500000000215213415353423021264 0ustar aleealeerequire 'spec_helper' include Feature::Repository describe Feature::Repository::SimpleRepository do before(:each) do @repository = SimpleRepository.new end it 'should have no active features after initialization' do expect(@repository.active_features).to eq([]) end it 'should add an active feature' do @repository.add_active_feature :feature_a expect(@repository.active_features).to eq([:feature_a]) end it 'should add an feature without having impact on internal structure' do list = @repository.active_features @repository.add_active_feature :feature_a expect(list).to eq([]) end it 'should raise an exception when adding not a symbol as active feature' do expect do @repository.add_active_feature 'feature_a' end.to raise_error(ArgumentError, 'feature_a is not a symbol') end it 'should raise an exception when adding a active feature already added as active' do @repository.add_active_feature :feature_a expect do @repository.add_active_feature :feature_a end.to raise_error(ArgumentError, 'feature :feature_a already added') end end feature-1.4.0/lib/0000755000175000017500000000000013415353423012316 5ustar aleealeefeature-1.4.0/lib/feature.rb0000755000175000017500000000655413415353423014313 0ustar aleealee# Feature module provides all methods # - to set a feature repository # - to check if a feature (represented by a symbol) is active or inactive # - for conditional block execution with or without a feature # - to refresh the feature lists (request them from repository) # # @note all features not active will be handled has inactive # # Example usage: # repository = SimpleRepository.new # repository.add_active_feature(:feature_name) # # Feature.set_repository(repository) # Feature.active?(:feature_name) # # => true # Feature.inactive?(:inactive_feature) # # => false # # Feature.with(:feature_name) do # # code will be executed # end # module Feature require 'feature/repository' require 'feature/generators/install_generator' @repository = nil @active_features = nil # Set the feature repository # The given repository has to respond to method 'active_features' with an array of symbols # # @param [Object] repository the repository to get the features from # @param [Boolean|Integer] refresh optional (default: false) - auto refresh or refresh after given number of seconds # def self.set_repository(repository, refresh = false) unless repository.respond_to?(:active_features) raise ArgumentError, 'given repository does not respond to active_features' end @perform_initial_refresh = true @repository = repository if [true, false].include?(refresh) @auto_refresh = refresh else @auto_refresh = false @refresh_after = refresh @next_refresh_after = Time.now + @refresh_after end end # Refreshes list of active features from repository. # Useful when using an repository with external source. # def self.refresh! @active_features = @repository.active_features @next_refresh_after = Time.now + @refresh_after if @refresh_after @perform_initial_refresh = false end ## # Requests if feature is active # # @param [Symbol] feature # @return [Boolean] # def self.active?(feature) active_features.include?(feature) end # Requests if feature is inactive (or unknown) # # @param [Symbol] feature # @return [Boolean] # def self.inactive?(feature) !active?(feature) end # Execute the given block if feature is active # # @param [Symbol] feature # def self.with(feature) raise ArgumentError, "no block given to #{__method__}" unless block_given? yield if active?(feature) end # Execute the given block if feature is inactive # # @param [Symbol] feature # def self.without(feature) raise ArgumentError, "no block given to #{__method__}" unless block_given? yield if inactive?(feature) end # Return value or execute Proc/lambda depending on Feature status. # # @param [Symbol] feature # @param [Object] value / lambda to use if feature is active # @param [Object] value / lambda to use if feature is inactive # def self.switch(feature, l1, l2) if active?(feature) l1.instance_of?(Proc) ? l1.call : l1 else l2.instance_of?(Proc) ? l2.call : l2 end end # Return list of active feature flags. # # @return [Array] list of symbols # def self.active_features raise 'missing Repository for obtaining feature lists' unless @repository refresh! if @auto_refresh || @perform_initial_refresh || (@next_refresh_after && Time.now > @next_refresh_after) @active_features end end feature-1.4.0/lib/feature/0000755000175000017500000000000013415353423013751 5ustar aleealeefeature-1.4.0/lib/feature/generators/0000755000175000017500000000000013415353423016122 5ustar aleealeefeature-1.4.0/lib/feature/generators/templates/0000755000175000017500000000000013415353423020120 5ustar aleealeefeature-1.4.0/lib/feature/generators/templates/feature.rb0000644000175000017500000000025313415353423022100 0ustar aleealee# Set repository to ActiveRecord if FeatureToggle.table_exists? repo = Feature::Repository::ActiveRecordRepository.new(FeatureToggle) Feature.set_repository(repo) end feature-1.4.0/lib/feature/generators/install_generator.rb0000644000175000017500000000166413415353423022172 0ustar aleealeeif defined?(Rails) && Rails::VERSION::STRING > '3' require 'rails/generators' module Feature # Rails generator for generating feature ActiveRecord model # and migration step for creating the table class InstallGenerator < Rails::Generators::Base desc 'This generator creates a migration and a model for FeatureToggles.' source_root File.expand_path('../templates', __FILE__) def generate_model generate :model, 'feature_toggle name:string active:boolean' inject_into_class 'app/models/feature_toggle.rb', 'FeatureToggle' do " attr_accessible :name, :active if ActiveRecord::Base.respond_to? :attr_accessible\n"\ " # Feature name should be present and unique\n"\ " validates :name, presence: true, uniqueness: true\n" end end def generate_initializer template 'feature.rb', 'config/initializers/feature.rb' end end end end feature-1.4.0/lib/feature/repository/0000755000175000017500000000000013415353423016170 5ustar aleealeefeature-1.4.0/lib/feature/repository/simple_repository.rb0000644000175000017500000000322613415353423022310 0ustar aleealeemodule Feature module Repository # SimpleRepository for active feature list # Simply add features to that should be active, # no config or data sources required. # # Example usage: # repository = SimpleRepository.new # repository.add_active_feature(:feature_name) # # use repository with Feature # class SimpleRepository # Constructor # def initialize @active_features = [] end # Returns list of active features # # @return [Array] list of active features # def active_features @active_features.dup end # Add an active feature to repository # # @param [Symbol] feature the feature to be added # def add_active_feature(feature) check_feature_is_not_symbol(feature) check_feature_already_in_list(feature) @active_features << feature end # Checks that given feature is a symbol, raises exception otherwise # # @param [Sybmol] feature the feature to be checked # def check_feature_is_not_symbol(feature) raise ArgumentError, "#{feature} is not a symbol" unless feature.instance_of?(Symbol) end private :check_feature_is_not_symbol # Checks if given feature is already added to list of active features # and raises an exception if so # # @param [Symbol] feature the feature to be checked # def check_feature_already_in_list(feature) raise ArgumentError, "feature :#{feature} already added" if @active_features.include?(feature) end private :check_feature_already_in_list end end end feature-1.4.0/lib/feature/repository/redis_repository.rb0000644000175000017500000000417713415353423022133 0ustar aleealeemodule Feature module Repository # RedisRepository for active feature list # # Example usage: # repository = RedisRepository.new("feature_toggles") # repository.add_active_feature(:feature_name) # # 'feature_toggles' can be whatever name you want to use for # the Redis hash that will store all of your feature toggles. # class RedisRepository attr_writer :redis # Constructor # # @param redis_key the key of the redis hash where all the toggles will be stored # def initialize(redis_key, client = nil) @redis_key = redis_key @redis = client unless client.nil? end # Returns list of active features # # @return [Array] list of active features # def active_features redis.hgetall(@redis_key).select { |_k, v| v.to_s == 'true' }.map { |k, _v| k.to_sym } end # Add an active feature to repository # # @param [Symbol] feature the feature to be added # def add_active_feature(feature) check_feature_is_not_symbol(feature) check_feature_already_in_list(feature) redis.hset(@redis_key, feature, true) end # Checks that given feature is a symbol, raises exception otherwise # # @param [Sybmol] feature the feature to be checked # def check_feature_is_not_symbol(feature) raise ArgumentError, "#{feature} is not a symbol" unless feature.instance_of?(Symbol) end private :check_feature_is_not_symbol # Checks if given feature is already added to list of active features # and raises an exception if so # # @param [Symbol] feature the feature to be checked # def check_feature_already_in_list(feature) raise ArgumentError, "feature :#{feature} already added" if redis.hexists(@redis_key, feature) end private :check_feature_already_in_list # Returns the currently specified redis client # # @return [Redis] Currently set redis client # def redis @redis ||= Redis.current end private :redis end end end feature-1.4.0/lib/feature/repository/active_record_repository.rb0000644000175000017500000000337613415353423023636 0ustar aleealeemodule Feature module Repository # AcitveRecordRepository for active feature list # Right now we assume you have at least name:string and active:boolean # defined in your table. # # Example usage: # repository = ActiveRecordRepository.new(FeatureToggle) # repository.add_active_feature(:feature_name) # # use repository with Feature # class ActiveRecordRepository # Constructor # def initialize(model) @model = model end # Returns list of active features # # @return [Array] list of active features # def active_features @model.where(active: true).map { |f| f.name.to_sym } end # Add an active feature to repository # # @param [Symbol] feature the feature to be added # def add_active_feature(feature) check_feature_is_not_symbol(feature) check_feature_already_in_list(feature) @model.create!(name: feature.to_s, active: true) end # Checks if the given feature is a not symbol and raises an exception if so # # @param [Sybmol] feature the feature to be checked # def check_feature_is_not_symbol(feature) raise ArgumentError, "#{feature} is not a symbol" unless feature.instance_of?(Symbol) end private :check_feature_is_not_symbol # Checks if given feature is already added to list of active features # and raises an exception if so # # @param [Symbol] feature the feature to be checked # def check_feature_already_in_list(feature) raise ArgumentError, "feature :#{feature} already added" if @model.exists?(name: feature.to_s) end private :check_feature_already_in_list end end end feature-1.4.0/lib/feature/repository/yaml_repository.rb0000644000175000017500000000530313415353423021757 0ustar aleealeemodule Feature module Repository # YamlRepository for active and inactive features # The yaml config file should look like this: # # features: # an_active_feature: true # an_inactive_feature: false # # Example usage: # repository = YamlRepository.new('/path/to/yaml/file') # # use repository with Feature # # A yaml config also can have this format: # features: # development: # a_feature: true # production: # a_feature: false # # This way you have to use: # repository = YamlRepository.new('/path/to/yaml/file', '_your_environment_') # # use repository with Feature # class YamlRepository require 'erb' require 'yaml' # Constructor # # @param [String] yaml_file_name the yaml config filename # @param [String] environment optional environment to use from config # def initialize(yaml_file_name, environment = '') @yaml_file_name = yaml_file_name @environment = environment end # Returns list of active features # # @return [Array] list of active features # def active_features data = read_file(@yaml_file_name) get_active_features(data, @environment) end # Read given file, perform erb evaluation and yaml parsing # # @param file_name [String] the file name fo the yaml config # @return [Hash] # def read_file(file_name) raw_data = File.read(file_name) evaluated_data = ERB.new(raw_data).result YAML.load(evaluated_data) end private :read_file # Extracts active features from given hash # # @param data [Hash] hash parsed from yaml file # @param selector [String] uses the value for this key as source of feature data # def get_active_features(data, selector) data = data[selector] unless selector.empty? if !data.is_a?(Hash) || !data.key?('features') raise ArgumentError, 'yaml config does not contain proper config' end return [] unless data['features'] check_valid_feature_data(data['features']) data['features'].keys.select { |key| data['features'][key] }.map(&:to_sym) end private :get_active_features # Checks for valid values in given feature hash # # @param features [Hash] feature hash # def check_valid_feature_data(features) features.values.each do |value| unless [true, false].include?(value) raise ArgumentError, "#{value} is not allowed value in config, use true/false" end end end end end end feature-1.4.0/lib/feature/testing.rb0000644000175000017500000000302113415353423015747 0ustar aleealeerequire 'feature' # This file provides functionality for testing your code with features # activated or deactivated. # This file should only be required in test/spec code! # # To enable Feature testing capabilities do: # require 'feature/testing' module Feature # Execute the code block with the given features active # # Example usage: # Feature.run_with_activated(:feature, :another_feature) do # # your test code here # end def self.run_with_activated(*features, &blk) with_stashed_config do @active_features.concat(features).uniq! @auto_refresh = false @perform_initial_refresh = false blk.call end end # Execute the code block with the given features deactive # # Example usage: # Feature.run_with_deactivated(:feature, :another_feature) do # # your test code here # end def self.run_with_deactivated(*features, &blk) with_stashed_config do @active_features -= features @auto_refresh = false @perform_initial_refresh = false blk.call end end # Execute the given code block and store + restore the feature # configuration before/after the execution def self.with_stashed_config @active_features = [] if @active_features.nil? old_features = @active_features.dup old_auto_refresh = @auto_refresh old_perform_initial_refresh = @perform_initial_refresh yield ensure @active_features = old_features @auto_refresh = old_auto_refresh @perform_initial_refresh = old_perform_initial_refresh end end feature-1.4.0/lib/feature/repository.rb0000644000175000017500000000045113415353423016515 0ustar aleealeemodule Feature # Module for holding feature repositories module Repository require 'feature/repository/simple_repository' require 'feature/repository/yaml_repository' require 'feature/repository/active_record_repository' require 'feature/repository/redis_repository' end end feature-1.4.0/feature.gemspec0000644000175000017500000000335613415353423014557 0ustar aleealee######################################################### # This file has been automatically generated by gem2tgz # ######################################################### # -*- encoding: utf-8 -*- # stub: feature 1.4.0 ruby lib Gem::Specification.new do |s| s.name = "feature".freeze s.version = "1.4.0" s.required_rubygems_version = Gem::Requirement.new(">= 0".freeze) if s.respond_to? :required_rubygems_version= s.require_paths = ["lib".freeze] s.authors = ["Markus Gerdes".freeze] s.date = "2016-06-11" s.email = "github@mgsnova.de".freeze s.files = ["CHANGELOG.md".freeze, "Gemfile".freeze, "README.md".freeze, "Rakefile".freeze, "lib/feature.rb".freeze, "lib/feature/generators/install_generator.rb".freeze, "lib/feature/generators/templates/feature.rb".freeze, "lib/feature/repository.rb".freeze, "lib/feature/repository/active_record_repository.rb".freeze, "lib/feature/repository/redis_repository.rb".freeze, "lib/feature/repository/simple_repository.rb".freeze, "lib/feature/repository/yaml_repository.rb".freeze, "lib/feature/testing.rb".freeze, "spec/feature/active_record_repository_spec.rb".freeze, "spec/feature/feature_spec.rb".freeze, "spec/feature/redis_repository_spec.rb".freeze, "spec/feature/simple_repository_spec.rb".freeze, "spec/feature/testing_spec.rb".freeze, "spec/feature/yaml_repository_spec.rb".freeze, "spec/integration/rails/gemfiles/rails4.gemfile".freeze, "spec/integration/rails/test-against-several-rails-versions.sh".freeze, "spec/integration/rails/test-against-specific-rails-version.sh".freeze, "spec/spec_helper.rb".freeze] s.homepage = "http://github.com/mgsnova/feature".freeze s.licenses = ["MIT".freeze] s.rubygems_version = "2.5.2.1".freeze s.summary = "Feature Toggle library for ruby".freeze end feature-1.4.0/CHANGELOG.md0000644000175000017500000000370713415353423013370 0ustar aleealee## 1.4.0 (2016-06-11) * possibility to refresh feature list after certain time (enc) * call to get all active features (pmeskers) * Improve and prettify README (glaucocustodio) * Fixing typos in README (adarsh) ## 1.3.0 (2015-06-12) * Support testing with multiple features (stevenwilkin) * Bugfix when using auto_refresh and testing support (javidjamae) * Fixing an issue with Feature.run_with_activated if @active_features is nil (tommyh) ## 1.2.0 (2014-10-28) * add Support for auto-refresh of feature data (javidjamae) * read list of features at first usage instead of initialization (lazy-load) * add RedisRepository (javidjamae) * remove explicit Rails 3 Support of ActiveRecordRepository ## 1.1.0 (2014-09-30) * generator compatible with Rails 3/4 (mumoshu) * add LICENSE (pivotalfish) * add optional environment flag for YamlRepository (cherbst-medidata) * documentation fixes (asmega, crackofdusk) * update to rspec 3 ## 1.0.0 (2014-03-26) * drop ruby 1.8 support * support Rails 4 generator loading only (mauriciovieira) * add Feature.switch method (FlavourSys) * minor code and doc fixes and cleanup ## 0.7.0 (2013-12-07) * add ActiveRecordRepository and a Rails::Generator (bigzed) ## 0.6.0 (2013-03-24) * add capability for easier testing of activated or deactivated features ## 0.5.0 (2012-09-29) * added erb support for yaml feature config (dscataglini) * fixed travis integration * added code climate link ## 0.4.1 (2012-03-20) * bugfix: occured when having empty feature set using yaml config (benjaminoakes) ## 0.4.0 (2011-11-28) * simplify repositories, they only have to respond to method active_features ## 0.3.0 (2011-11-23) * assume all features as inactive that are not active, also unknown ones * make yaml parsing in yaml repository more strict ## 0.2.1 (2011-11-22) * refactored gemspec ## 0.2.0 (2011-11-22) * add yaml config file repository ## 0.1.0 (2011-11-18) * first version with basic functionality and simple feature repository feature-1.4.0/README.md0000644000175000017500000001346413415353423013037 0ustar aleealee[![Gem Version](https://badge.fury.io/rb/feature.svg)](https://rubygems.org/gems/feature) [![Travis-CI Build Status](https://travis-ci.org/mgsnova/feature.svg)](https://travis-ci.org/mgsnova/feature) [![Coverage Status](http://img.shields.io/coveralls/mgsnova/feature/master.svg)](https://coveralls.io/r/mgsnova/feature) [![Code Climate](https://codeclimate.com/github/mgsnova/feature.svg)](https://codeclimate.com/github/mgsnova/feature) [![Inline docs](http://inch-ci.org/github/mgsnova/feature.svg)](http://inch-ci.org/github/mgsnova/feature) [![Dependency Status](https://gemnasium.com/mgsnova/feature.svg)](https://gemnasium.com/mgsnova/feature) # Feature Feature is a battle-tested [feature toggle](http://martinfowler.com/bliki/FeatureToggle.html) library for ruby. The feature toggle functionality has to be configured by feature repositories. A feature repository simply provides lists of active features (symbols!). Unknown features are assumed inactive. With this approach Feature is highly configurable and not bound to a specific kind of configuration. **NOTE:** The current gem version works with Ruby 2+ and supports Ruby on Rails 4+. **NOTE:** Ruby 1.9 is supported until version 1.2.0, Ruby 1.8 is supported until version 0.7.0. **NOTE:** ActiveRecord / Rails 3 is supported until version 1.1.0. ## Installation gem install feature ## How to use * Setup Feature * Create a repository (for more infos about configuration backends, see section below) ```ruby require 'feature' repo = Feature::Repository::SimpleRepository.new ``` * Set repository to Feature ```ruby Feature.set_repository(repo) ``` * Use Feature in your production code ```ruby Feature.active?(:feature_name) # => true/false Feature.inactive?(:feature_name) # => true/false Feature.active_features # => [:list, :of, :features] Feature.with(:feature_name) do # code end Feature.without(:feature_name) do # code end # this returns value_true if :feature_name is active, otherwise value_false Feature.switch(:feature_name, value_true, value_false) # switch may also take Procs that will be evaluated and it's result returned. Feature.switch(:feature_name, -> { code... }, -> { code... }) ``` * Use Feature in your test code (for reliable testing of feature depending code) ```ruby require 'feature/testing' Feature.run_with_activated(:feature) do # your test code end # you also can give a list of features Feature.run_with_deactivated(:feature, :another_feature) do # your test code end ``` * Feature-toggle caching * By default, Feature will lazy-load the active features from the underlying repository the first time you try to check whether a feature is set or not. * Subsequent calls to Feature will access the cached in-memory representation of the list of features. So changes to toggles in the underlying repository would not be reflected in the application until you restart the application or manually call ```ruby Feature.refresh! ``` * You can optionally pass in true as a second argument on set_repository, to force Feature to auto-refresh the feature list on every feature-toggle check you make. ```ruby Feature.set_repository(your_repository, true) ``` * You can also optionally pass in a number as second argument on set_repository, to force Feature to refresh the feature list after X seconds. This will be done only on demand by a request. ```ruby Feature.set_repository(your_repository, 60) ``` ## How to setup different backends ### SimpleRepository (in-memory) ```ruby # File: Gemfile gem 'feature' ``` ```ruby # setup code require 'feature' repo = Feature::Repository::SimpleRepository.new repo.add_active_feature :be_nice Feature.set_repository repo ``` ### RedisRepository (features configured in redis server) ```ruby # See here to learn how to configure redis: https://github.com/redis/redis-rb # File: Gemfile gem 'feature' gem 'redis' ``` ```ruby # setup code (or Rails initializer: config/initializers/feature.rb) require 'feature' # "feature_toggles" will be the key name in redis repo = Feature::Repository::RedisRepository.new("feature_toggles") Feature.set_repository repo # add/toggle features in Redis Redis.current.hset("feature_toggles", "ActiveFeature", true) Redis.current.hset("feature_toggles", "InActiveFeature", false) ``` ### YamlRepository (features configured in static yml file) ```ruby # File: Gemfile gem 'feature' ``` ``` # File: config/feature.yml features: an_active_feature: true an_inactive_feature: false ``` ```ruby # setup code (or Rails initializer: config/initializers/feature.rb) repo = Feature::Repository::YamlRepository.new("#{Rails.root}/config/feature.yml") Feature.set_repository repo ``` You may also specify a Rails environment to use a new feature in development and test, but not production: ``` # File: config/feature.yml test: features: a_new_feature: true production: features: a_new_feature: false ``` ```ruby # File: config/initializers/feature.rb repo = Feature::Repository::YamlRepository.new("#{Rails.root}/config/feature.yml", Rails.env) Feature.set_repository repo ``` ### ActiveRecordRepository (features configured in a database) using Rails ```ruby # File: Gemfile gem 'feature' ``` ``` # Run generator and migrations $ rails g feature:install $ rake db:migrate ``` ```ruby # Add Features to table FeaturesToggle for example in # File: db/schema.rb FeatureToggle.create!(name: "ActiveFeature", active: true) FeatureToggle.create!(name: "InActiveFeature", active: false) # or in initializer # File: config/initializers/feature.rb repo.add_active_feature(:active_feature) ``` feature-1.4.0/Gemfile0000644000175000017500000000041513415353423013043 0ustar aleealeesource 'https://rubygems.org' gem 'rake' group :test do gem 'rspec' gem 'rspec-mocks' gem 'coveralls', require: false gem 'rubocop', require: false gem 'timecop' gem 'fakeredis' if RUBY_VERSION >= '2.1' gem 'mutant' gem 'mutant-rspec' end end