From fb280e678d5edf5b44dc628bcb3bafa08f4b9a35 Mon Sep 17 00:00:00 2001 From: dominh <11135943+dominh@users.noreply.github.com> Date: Sat, 30 Mar 2024 23:11:21 +0000 Subject: [PATCH] Allow arrays to be passed through env variables (#354) * Allow arrays to be passed through env variables * Fix "Config::Sources::EnvSource configuration options default configuration arrays when loading nested configurations retains hashes for mixed types" spec * add `Config.env_parse_arrays` --------- Co-authored-by: dominh <> Co-authored-by: Dominik Mlynek Co-authored-by: Chris LaRose --- lib/config.rb | 1 + lib/config/sources/env_source.rb | 25 +++++++++++++++++-- spec/config_env_spec.rb | 31 +++++++++++++++++++++++ spec/sources/env_source_spec.rb | 43 ++++++++++++++++++++++++++++++++ 4 files changed, 98 insertions(+), 2 deletions(-) diff --git a/lib/config.rb b/lib/config.rb index db06bcbc..e98d1e70 100644 --- a/lib/config.rb +++ b/lib/config.rb @@ -18,6 +18,7 @@ module Config env_separator: '.', env_converter: :downcase, env_parse_values: true, + env_parse_arrays: false, fail_on_missing: false, file_name: 'settings', dir_name: 'settings', diff --git a/lib/config/sources/env_source.rb b/lib/config/sources/env_source.rb index df59e35e..ea527db4 100644 --- a/lib/config/sources/env_source.rb +++ b/lib/config/sources/env_source.rb @@ -6,17 +6,20 @@ class EnvSource attr_reader :separator attr_reader :converter attr_reader :parse_values + attr_reader :parse_arrays def initialize(env, prefix: Config.env_prefix || Config.const_name, separator: Config.env_separator, converter: Config.env_converter, - parse_values: Config.env_parse_values) + parse_values: Config.env_parse_values, + parse_arrays: Config.env_parse_arrays) @env = env @prefix = prefix.to_s.split(separator) @separator = separator @converter = converter @parse_values = parse_values + @parse_arrays = parse_arrays end def load @@ -52,10 +55,28 @@ def load leaf[keys.last] = parse_values ? __value(value) : value end - hash + parse_arrays ? convert_hashes_to_arrays(hash) : hash end private + def convert_hashes_to_arrays(hash) + hash.each_with_object({}) do |(key, value), new_hash| + if value.is_a?(Hash) + value = convert_hashes_to_arrays(value) + if consecutive_numeric_keys?(value.keys) + new_hash[key] = value.keys.sort_by(&:to_i).map { |k| value[k] } + else + new_hash[key] = value + end + else + new_hash[key] = value + end + end + end + + def consecutive_numeric_keys?(keys) + keys.map(&:to_i).sort == (0...keys.size).to_a && keys.all? { |k| k == k.to_i.to_s } + end # Try to convert string to a correct type def __value(v) diff --git a/spec/config_env_spec.rb b/spec/config_env_spec.rb index 65e7da8b..a5eee612 100644 --- a/spec/config_env_spec.rb +++ b/spec/config_env_spec.rb @@ -22,6 +22,7 @@ Config.env_separator = '.' Config.env_converter = :downcase Config.env_parse_values = true + Config.env_parse_arrays = true end it 'should add new setting from ENV variable' do @@ -96,6 +97,36 @@ end end + context 'and parsing ENV variables arrays' do + context 'is enabled' do + before :each do + Config.env_parse_arrays = true + end + + it 'should recognize ENV variables with subsequent numeric suffixes starting from 0 as array' do + ENV['Settings.SomeConfig.0'] = 'first' + ENV['Settings.SomeConfig.1'] = 'second' + + expect(config.someconfig).to eq(['first', 'second']) + end + end + + context 'is disabled' do + before :each do + Config.env_parse_arrays = false + end + + it 'should not recognize ENV variables with subsequent numeric suffixes starting from 0 as array' do + ENV['Settings.SomeConfig.0'] = 'first' + ENV['Settings.SomeConfig.1'] = 'second' + + expect(config.someconfig).to be_a Config::Options + expect(config.someconfig['0']).to eq('first') + expect(config.someconfig['1']).to eq('second') + end + end + end + context 'and custom ENV variables prefix is defined' do before :each do Config.env_prefix = 'MyConfig' diff --git a/spec/sources/env_source_spec.rb b/spec/sources/env_source_spec.rb index ad86f9a7..08511a30 100644 --- a/spec/sources/env_source_spec.rb +++ b/spec/sources/env_source_spec.rb @@ -43,6 +43,49 @@ module Config::Sources results = source.load expect(results['action_mailer']['enabled']).to eq('true') end + + describe 'arrays' do + before(:each) do + Config.env_parse_arrays = true + end + + let(:source) do + Config.env_converter = nil + EnvSource.new({ + 'Settings.SomeConfig.0.0' => 'value1', + 'Settings.SomeConfig.0.1' => 'value2', + 'Settings.SomeConfig.1.1' => 'value3', + 'Settings.SomeConfig.1.2' => 'value4', + 'Settings.MixedConfig.1.0' => 'value5', + 'Settings.MixedConfig.1.1' => 'value6', + 'Settings.MixedConfig.1.custom' => 'value7' + }) + end + + let(:results) { source.load } + + context 'when loading nested configurations' do + it 'converts numeric-keyed hashes to arrays' do + expect(results['SomeConfig']).to be_an Array + expect(results['SomeConfig'][0]).to be_an Array + expect(results['SomeConfig'][0][0]).to eq('value1') + expect(results['SomeConfig'][0][1]).to eq('value2') + end + + it 'retains hashes for non-sequential numeric keys' do + expect(results['SomeConfig'][1]).to be_a Hash + expect(results['SomeConfig'][1]['1']).to eq('value3') + expect(results['SomeConfig'][1]['2']).to eq('value4') + end + + it 'retains hashes for mixed types' do + expect(results['MixedConfig']['1']).to be_a Hash + expect(results['MixedConfig']['1']['0']).to eq('value5') + expect(results['MixedConfig']['1']['1']).to eq('value6') + expect(results['MixedConfig']['1']['custom']).to eq('value7') + end + end + end end context 'configuration overrides' do