diff --git a/examples/naming_style.rb b/examples/naming_style.rb new file mode 100644 index 00000000..bca0ffed --- /dev/null +++ b/examples/naming_style.rb @@ -0,0 +1,19 @@ +require 'bundler/setup' +require 'flipper' +require 'flipper/adapters/naming_style' + +Flipper.configure do |config| + config.use Flipper::Adapters::NamingStyle, :snake # or :camel, :kebab, :screaming_snake, or a Regexp +end + +# This will work because the feature key is in snake_case. +Flipper.enable(:snake_case) + +begin + # This will raise an error because the feature key is in CamelCase. + Flipper.enable(:CamelCase) +rescue Flipper::Adapters::NamingStyle::InvalidFormat => e + puts "#{e.class}: #{e.message}" +else + fail "An error should have been raised, but wasn't." +end diff --git a/lib/flipper/adapters/naming_style.rb b/lib/flipper/adapters/naming_style.rb new file mode 100644 index 00000000..a04d1b80 --- /dev/null +++ b/lib/flipper/adapters/naming_style.rb @@ -0,0 +1,42 @@ +module Flipper + module Adapters + # An adapter that enforces a naming style for added features. + # + # Flipper.configure do |config| + # config.use Flipper::Adapters::NamingStyle, :snake # or :camel, :kebab, :screaming_snake, or a Regexp + # end + # + class NamingStyle < Wrapper + InvalidFormat = Class.new(Flipper::Error) + + PRESETS = { + camel: /^([A-Z][a-z0-9]*)+$/, # CamelCase + snake: /^[a-z0-9]+(_[a-z0-9]+)*$/, # snake_case + kebab: /^[a-z0-9]+(-[a-z0-9]+)*$/, # kebab-case + screaming_snake: /^[A-Z0-9]+(_[A-Z0-9]+)*$/, # SCREAMING_SNAKE_CASE + } + + attr_reader :format + + def initialize(adapter, format = :snake) + @format = format.is_a?(Regexp) ? format : PRESETS.fetch(format) { + raise ArgumentError, "Unknown format: #{format.inspect}. Must be a Regexp or one of #{PRESETS.keys.join(', ')}" + } + + super(adapter) + end + + def add(feature) + unless valid?(feature.key) + raise InvalidFormat, "Feature key #{feature.key.inspect} does not match format #{format.inspect}" + end + + super feature + end + + def valid?(name) + format.match?(name) + end + end + end +end diff --git a/spec/flipper/adapters/naming_style_spec.rb b/spec/flipper/adapters/naming_style_spec.rb new file mode 100644 index 00000000..22096aab --- /dev/null +++ b/spec/flipper/adapters/naming_style_spec.rb @@ -0,0 +1,70 @@ +require "flipper/adapters/naming_style" + +RSpec.describe Flipper::Adapters::NamingStyle do + it_should_behave_like "a flipper adapter" do + let(:format) { /.*/ } + let(:memory) { Flipper::Adapters::Memory.new } + let(:adapter) { described_class.new(memory, format) } + + subject { adapter } + + describe "#initialize" do + it "accepts a regex" do + expect { described_class.new(memory, format) }.not_to raise_error + end + + it "accepts a symbol" do + [:camel, :snake, :kebab, :screaming_snake].each do |format| + expect { described_class.new(memory, format) }.not_to raise_error + end + end + + it "raises an error if the format is an unknown symbol" do + expect { described_class.new(memory, :Pascal) }.to raise_error(ArgumentError) + end + end + + describe "#add" do + { + /\A(breaker|feature)\// => { + valid: %w[breaker/search feature/search], + invalid: %w[search breaker_search breaker], + }, + camel: { + valid: %w[Camel CamelCase SCREAMINGCamelCase CamelCase1 Camel1Case], + invalid: %w[snake_case Camel-Kebab lowercase], + }, + snake: { + valid: %w[lower snake_case snake_case_1], + invalid: %w[CamelCase cobraCase double__underscore], + }, + kebab: { + valid: %w[kebab kebab-case kebab-case-1 htt-party], + invalid: %w[CamelCase CamelCase1 double__dash], + }, + screaming_snake: { + valid: %w[SCREAMING SCREAMING_SNAKE SCREAMING_SNAKE_1 HTTP_THING], + invalid: %w[CamelCase CamelCase1 double__underscore], + } + }.each do |format, examples| + context "with format=#{format.inspect}" do + let(:format) { format } + + examples[:valid].each do |feature| + it "adds feature named #{feature}" do + expect(subject.add(flipper[feature])).to eq(true) + expect(subject.features).to eq(Set[feature]) + end + end + + examples[:invalid].each do |feature| + it "raises an error for feature named #{feature}" do + expect { adapter.add(flipper[feature]) }.to raise_error(Flipper::Adapters::NamingStyle::InvalidFormat) + expect(subject.features).to eq(Set[]) + end + end + end + end + end + end +end