Skip to content

Instantly share code, notes, and snippets.

@chunpan
Created February 26, 2017 05:12
Show Gist options
  • Save chunpan/2a5b18b7a729b2f96aa4b81cb5d4fa7a to your computer and use it in GitHub Desktop.
Save chunpan/2a5b18b7a729b2f96aa4b81cb5d4fa7a to your computer and use it in GitHub Desktop.
A Simple File-Based Feature Toggler
# A service to properly route feature toggles, similar to Promote's Gate mechanism,
# but greatly simplified.
# @see https://martinfowler.com/articles/feature-toggles.html
class FeatureToggleService
attr_reader :config
DEFAULT_CONFIG_PATH = Rails.root.join('config', 'feature_toggles.yml')
def initialize(config: nil)
@config =
if config.blank? || !config.is_a?(Hash)
YAML.load_file(DEFAULT_CONFIG_PATH)
else
config
end
end
# Sets enabled/disabled state of a feature in this instance of service only, great for test.
# To permanently enable or disable a feature, update the `config/feature_toggles.yml` file.
# @param feature, required, a String with dot notation,
# e.g. <code>'scheduled_send.use_local_time'</code>
# @param enabled, Boolean value, default to <code>true</code>
def set(feature:, enabled: true)
if feature.present?
keys = feature.to_s.split('.').map(&:to_sym)
last_config_node = @config
keys.each_with_index do |key, index|
# last key gets assigned the toggle
if index == keys.size - 1
last_config_node[key] = enabled
else
last_config_node[key] ||= {}
last_config_node = last_config_node[key]
end
end
return true
end
false
end
# Gets enabled/disabled state of a feature.
# @return <code>true</code> if a feature key is found with a <code>Boolean</code> true value.
def enabled?(feature:)
if feature.present?
keys = feature.to_s.split('.').map(&:to_sym)
last_config_node = config
keys.each_with_index do |key, index|
if index == keys.size - 1
return last_config_node.try(:[], key) == true
else
return false unless last_config_node.try(:[], key)
last_config_node = last_config_node[key]
end
end
end
false
end
end
require 'rails_helper'
describe FeatureToggleService do
let(:service) { described_class.new(config: config) }
let(:config) do
{
scheduled_send: {
use_local_time: false,
finders: {
utc_offset_finder: true,
}
}
}
end
describe 'Constructor' do
subject { service }
it { is_expected.to be_a described_class }
it 'stores the provided config' do
expect(subject.config).to eq config
end
end
describe '#set' do
subject { service.set(feature: feature, enabled: enabled) }
let(:enabled) { true }
context 'with empty feature string' do
let(:feature) { nil }
it { is_expected.to be false }
end
context 'with existing feature string' do
let(:feature) { 'scheduled_send.use_local_time' }
it { is_expected.to be true }
it 'sets the feature state to `true`' do
subject
expect(service.config[:scheduled_send][:use_local_time]).to be true
end
end
context 'with new feature string' do
let(:feature) { 'triggered_email.new_feature' }
let(:enabled) { false }
it { is_expected.to be true }
it 'sets the new feature state to `false`' do
subject
expect(service.config[:triggered_email][:new_feature]).to be false
end
end
end
describe '#enabled?' do
subject { service.enabled?(feature: feature) }
context 'feature of using local time' do
let(:feature) { 'scheduled_send.use_local_time ' }
it { is_expected.to be false }
end
context 'feature of utc_offset_finder' do
let(:feature) { 'scheduled_send.finders.utc_offset_finder' }
it { is_expected.to be true }
end
end
end
---
scheduled_send:
use_local_time: false
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment