From 5ed340c83e138b9ecad07e7a3b1ff4d5a6fb15fe Mon Sep 17 00:00:00 2001 From: Daniel Morton Date: Thu, 12 Sep 2024 11:44:51 -0400 Subject: [PATCH] Configurably allow duplicates in FileDataSourceImpl This PR modifies the `FileDataSourceImpl` to accept an additional value in the options hash called `allow_duplicates`. `false` will be used if no value is provided which will cause this to operate exactly as it does prior to this change. Configuring `allow_duplicates` to be `true` will not raise an error when flag or segment values are loaded from multiple files and a subsequent file contains a key found in a previous file. The purpose of this is to allow a notion of a 'local' override during development. A project may have a `/config/feature_flags.yml` and a developer adds a flag to it for a feature they are developing. If they want to commit code with this feature, but don't want other developers encountering it yet, they will have to remember to turn the feature off prior to commiting their change. With this change, a project could have a `/config/feature_flags.yml` which contains the flag values other developers should see, and a `/config/feature_flags.local.yml` file, ignored by source control and not checked in. As a developer is working on the new feature, they can have it enabled in the `.local` file so they can see the feature and work with it, but disabled in the commited file so it does not impact other developers. --- .../impl/integrations/file_data_source.rb | 3 ++- lib/ldclient-rb/integrations/file_data.rb | 3 +++ spec/integrations/file_data_source_spec.rb | 27 +++++++++++++++++++ 3 files changed, 32 insertions(+), 1 deletion(-) diff --git a/lib/ldclient-rb/impl/integrations/file_data_source.rb b/lib/ldclient-rb/impl/integrations/file_data_source.rb index 1e7ad078..8c62c909 100644 --- a/lib/ldclient-rb/impl/integrations/file_data_source.rb +++ b/lib/ldclient-rb/impl/integrations/file_data_source.rb @@ -35,6 +35,7 @@ def initialize(data_store, data_source_update_sink, logger, options={}) if @paths.is_a? String @paths = [ @paths ] end + @allow_duplicates = options[:allow_duplicates] || false @auto_update = options[:auto_update] @use_listen = @auto_update && @@have_listen && !options[:force_polling] @poll_interval = options[:poll_interval] || 1 @@ -139,7 +140,7 @@ def add_item(all_data, kind, item) items = all_data[kind] raise ArgumentError, "Received unknown item kind #{kind[:namespace]} in add_data" if items.nil? # shouldn't be possible since we preinitialize the hash key = item[:key].to_sym - unless items[key].nil? + unless items[key].nil? || @allow_duplicates raise ArgumentError, "#{kind[:namespace]} key \"#{item[:key]}\" was used more than once" end items[key] = Model.deserialize(kind, item) diff --git a/lib/ldclient-rb/integrations/file_data.rb b/lib/ldclient-rb/integrations/file_data.rb index fb85ad98..36e847ca 100644 --- a/lib/ldclient-rb/integrations/file_data.rb +++ b/lib/ldclient-rb/integrations/file_data.rb @@ -97,6 +97,9 @@ module FileData # @option options [Float] :poll_interval The minimum interval, in seconds, between checks for # file modifications - used only if auto_update is true, and if the native file-watching # mechanism from 'listen' is not being used. The default value is 1 second. + # @option options [Boolean] :allow_duplicates Do not raise an error if using multiple files + # that contain the same flag or segment key. If this is true, the last value for a given key + # will be used. The default is false. # @return an object that can be stored in {Config#data_source} # def self.data_source(options={}) diff --git a/spec/integrations/file_data_source_spec.rb b/spec/integrations/file_data_source_spec.rb index 8b22e902..7fa6f5b8 100644 --- a/spec/integrations/file_data_source_spec.rb +++ b/spec/integrations/file_data_source_spec.rb @@ -37,6 +37,22 @@ module Integrations EOF } + let(:alternate_flag_only_json) { <<-EOF +{ + "flags": { + "flag1": { + "key": "flag1", + "on": false, + "fallthrough": { + "variation": 2 + }, + "variations": [ "fall", "off", "on" ] + } + } +} +EOF + } + let(:segment_only_json) { <<-EOF { "segments": { @@ -243,6 +259,17 @@ def with_data_source(options, initialize_to_valid = false) end end + it "allows duplicate keys and uses the last loaded version when allow-duplicates is true" do + file1 = make_temp_file(flag_only_json) + file2 = make_temp_file(alternate_flag_only_json) + with_data_source({ paths: [ file1.path, file2.path ], allow_duplicates: true }) do |ds| + ds.start + expect(@store.initialized?).to eq(true) + expect(@store.all(LaunchDarkly::FEATURES).keys).to_not eq([]) + expect(@store.all(LaunchDarkly::FEATURES)[:flag1][:on]).to eq(false) + end + end + it "does not reload modified file if auto-update is off" do file = make_temp_file(flag_only_json)