From 4db94bdc32e4ba2ad74cbdf20714f26416367a77 Mon Sep 17 00:00:00 2001 From: Alex Wheeler Date: Fri, 22 Dec 2017 10:27:15 -0800 Subject: [PATCH] Rollout Adapter for importing rollout data into Flipper --- flipper-rollout.gemspec | 25 +++++ lib/flipper/adapters/rollout.rb | 71 ++++++++++++++ spec/flipper/adapters/rollout_spec.rb | 136 ++++++++++++++++++++++++++ 3 files changed, 232 insertions(+) create mode 100644 flipper-rollout.gemspec create mode 100644 lib/flipper/adapters/rollout.rb create mode 100644 spec/flipper/adapters/rollout_spec.rb diff --git a/flipper-rollout.gemspec b/flipper-rollout.gemspec new file mode 100644 index 000000000..77692c3c0 --- /dev/null +++ b/flipper-rollout.gemspec @@ -0,0 +1,25 @@ +# -*- encoding: utf-8 -*- +require File.expand_path('../lib/flipper/version', __FILE__) + +flipper_rollout_files = lambda do |file| + file =~ /rollout/ +end + +Gem::Specification.new do |gem| + gem.authors = ['John Nunemaker'] + gem.email = ['nunemaker@gmail.com'] + gem.summary = 'Rollout adapter for Flipper' + gem.description = 'Rollout adapter for Flipper' + gem.license = 'MIT' + gem.homepage = 'https://github.com/jnunemaker/flipper' + + gem.files = `git ls-files`.split("\n").select(&flipper_rollout_files) + ['lib/flipper/version.rb'] + gem.test_files = `git ls-files -- {test,spec,features}/*`.split("\n").select(&flipper_rollout_files) + gem.name = 'flipper-rollout' + gem.require_paths = ['lib'] + gem.version = Flipper::VERSION + + gem.add_dependency 'flipper', "~> #{Flipper::VERSION}" + gem.add_dependency 'redis', '>= 2.2', '< 4.1.0' + gem.add_dependency 'rollout', "~> 2.0" +end diff --git a/lib/flipper/adapters/rollout.rb b/lib/flipper/adapters/rollout.rb new file mode 100644 index 000000000..ae2a86348 --- /dev/null +++ b/lib/flipper/adapters/rollout.rb @@ -0,0 +1,71 @@ +module Flipper + module Adapters + class Rollout + class AdapterMethodNotSupportedError < Error + def initialize(message = 'unsupported method called for import adapter') + super(message) + end + end + + # Public: The name of the adapter. + attr_reader :name + + def initialize(rollout) + @rollout = rollout + @name = :rollout + end + + # Public: The set of known features. + def features + @rollout.features + end + + # Public: Gets the values for all gates for a given feature. + # + # Returns a Hash of Flipper::Gate#key => value. + def get(feature) + feature = @rollout.get(feature.key) + percentage = feature.percentage.zero? ? nil : feature.percentage + { + boolean: nil, + groups: Set.new(feature.groups), + actors: Set.new(feature.users), + percentage_of_actors: percentage, + percentage_of_time: nil, + } + end + + def get_multi(_features) + raise AdapterMethodNotSupportedError + end + + def get_all + raise AdapterMethodNotSupportedError + end + + def add(_feature) + raise AdapterMethodNotSupportedError + end + + def remove(_feature) + raise AdapterMethodNotSupportedError + end + + def clear(_feature) + raise AdapterMethodNotSupportedError + end + + def enable(_feature, _gate, _thing) + raise AdapterMethodNotSupportedError + end + + def disable(_feature, _gate, _thing) + raise AdapterMethodNotSupportedError + end + + def import(_source_adapter) + raise AdapterMethodNotSupportedError + end + end + end +end diff --git a/spec/flipper/adapters/rollout_spec.rb b/spec/flipper/adapters/rollout_spec.rb new file mode 100644 index 000000000..40ed47ca0 --- /dev/null +++ b/spec/flipper/adapters/rollout_spec.rb @@ -0,0 +1,136 @@ +require 'helper' +require 'redis' +require 'rollout' +require 'flipper/adapters/rollout' +require 'flipper/adapters/memory' +require 'flipper/spec/shared_adapter_specs' + +RSpec.describe Flipper::Adapters::Rollout do + let(:redis) { Redis.new } + let(:rollout) { Rollout.new(redis) } + let(:source_adapter) { described_class.new(rollout) } + let(:source_flipper) { Flipper.new(source_adapter) } + let(:destination_adapter) { Flipper::Adapters::Memory.new } + let(:destination_flipper) { Flipper.new(destination_adapter) } + + before do + redis.flushdb + end + + describe '#name' do + it 'has name that is a symbol' do + expect(source_adapter.name).not_to be_nil + expect(source_adapter.name).to be_instance_of(Symbol) + end + end + + describe '#get' do + it 'returns hash of gate data' do + rollout.activate_user(:chat, Struct.new(:id).new(1)) + rollout.activate_percentage(:chat, 20) + rollout.activate_group(:chat, :admins) + feature = Struct.new(:key).new(:chat) + expected = { + actors: Set.new(["1"]), + boolean: nil, + groups: Set.new([:admins]), + percentage_of_actors: 20.0, + percentage_of_time: nil, + } + expect(source_adapter.get(feature)).to eq(expected) + end + end + + describe '#features' do + it 'returns all feature keys' do + rollout.activate(:chat) + rollout.activate(:messaging) + rollout.activate(:push_notifications) + expect(source_adapter.features).to match_array([:chat, :messaging, :push_notifications]) + end + end + + it 'can have one feature imported' do + rollout.activate(:search) + destination_flipper.import(source_flipper) + expect(destination_flipper.features.map(&:key)).to eq(["search"]) + end + + it 'can have multiple features imported' do + rollout.activate(:yep) + rollout.activate_group(:preview_features, :developers) + rollout.activate_group(:preview_features, :marketers) + rollout.activate_group(:preview_features, :company) + rollout.activate_group(:preview_features, :early_access) + rollout.activate_user(:preview_features, Struct.new(:id).new(1)) + rollout.activate_user(:preview_features, Struct.new(:id).new(2)) + rollout.activate_user(:preview_features, Struct.new(:id).new(3)) + rollout.activate_percentage(:issues_next, 25) + + destination_flipper.import(source_flipper) + + feature = destination_flipper[:yep] + expect(feature.boolean_value).to eq(true) + + feature = destination_flipper[:preview_features] + expect(feature.boolean_value).to be(false) + expect(feature.actors_value).to eq(Set['1', '2', '3']) + expected_groups = Set['developers', 'marketers', 'company', 'early_access'] + expect(feature.groups_value).to eq(expected_groups) + expect(feature.percentage_of_actors_value).to be(0) + + feature = destination_flipper[:issues_next] + expect(feature.boolean_value).to eq(false) + expect(feature.actors_value).to eq(Set.new) + expect(feature.groups_value).to eq(Set.new) + expect(feature.percentage_of_actors_value).to be(25.0) + + feature = destination_flipper[:verbose_logging] + expect(feature.boolean_value).to eq(false) + expect(feature.actors_value).to eq(Set.new) + expect(feature.groups_value).to eq(Set.new) + expect(feature.percentage_of_actors_value).to be(0) + end + + describe 'unsupported methods' do + it 'raises on get_multi' do + expect { source_adapter.get_multi([]) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on get_all' do + expect { source_adapter.get_all } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on add' do + expect { source_adapter.add(:feature) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on remove' do + expect { source_adapter.remove(:feature) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on clear' do + expect { source_adapter.clear(:feature) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on enable' do + expect { source_adapter.enable(:feature, :gate, :thing) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on disable' do + expect { source_adapter.disable(:feature, :gate, :thing) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + + it 'raises on import' do + expect { source_adapter.import(:source_adapter) } + .to raise_error(Flipper::Adapters::Rollout::AdapterMethodNotSupportedError) + end + end +end