From d99d1d7769dacd880c758cdb4d6d3ea8d0dbffdc Mon Sep 17 00:00:00 2001 From: Daniel Cazzulino Date: Mon, 26 Apr 2021 11:41:54 -0300 Subject: [PATCH] When saving to an aggregate config, use fallback locations We were previously assuming that there would always be a default config file (plain .netconfig in a non-system/global location) in the aggregate. This was problematic if you had opened a CLI app from the user's profile dir, which is even the default when running cmd, for example), since in that case there would be no default .netconfig, only the global config as the first entry. So we instead use a smarter probing with fallbacks in place so that we try: 1. Default config if any found 2. Non-local (.user) file next 3. Local (.user) file if none found. Fixes #51 --- src/Config.Tests/ConfigTests.cs | 20 +++++++++++++++++ src/Config/AggregateConfig.cs | 40 ++++++++++++++++++++------------- src/Config/Config.cs | 37 +++++++++++++++++++----------- src/Config/ConfigLevel.cs | 5 +++++ 4 files changed, 73 insertions(+), 29 deletions(-) diff --git a/src/Config.Tests/ConfigTests.cs b/src/Config.Tests/ConfigTests.cs index 24f4fec..4d511b3 100644 --- a/src/Config.Tests/ConfigTests.cs +++ b/src/Config.Tests/ConfigTests.cs @@ -9,10 +9,12 @@ namespace DotNetConfig.Tests { public class ConfigTests { + static readonly string currentDir = Directory.GetCurrentDirectory(); readonly ITestOutputHelper output; public ConfigTests(ITestOutputHelper output) { + Directory.SetCurrentDirectory(currentDir); Config.GlobalLocation = Path.Combine(Directory.GetCurrentDirectory(), "Content", "global.netconfig"); Config.SystemLocation = Path.Combine(Directory.GetCurrentDirectory(), "Content", "system.netconfig"); CultureInfo.CurrentCulture = CultureInfo.InvariantCulture; @@ -385,5 +387,23 @@ public void when_resolving_path_then_resolves_relative_to_config() Assert.Equal(Path.Combine(parentDir, "file.txt"), section.GetNormalizedPath("parent")); Assert.Equal(Path.Combine(localDir, "file.txt"), section.GetNormalizedPath("local")); } + + [Fact] + public void when_setting_variable_in_global_dir_then_writes_global_file() + { + Config.GlobalLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), ".netconfig"); + Config.SystemLocation = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString(), ".netconfig"); + + // Simulates opening a cli app from the user's profile dir + Directory.CreateDirectory(Path.GetDirectoryName(Config.GlobalLocation)); + Directory.SetCurrentDirectory(Path.GetDirectoryName(Config.GlobalLocation)); + var config = Config.Build(); + + config.SetString("foo", "bar", "baz"); + + var global = Config.Build(ConfigLevel.Global); + + Assert.Equal("baz", global.GetString("foo", "bar")); + } } } diff --git a/src/Config/AggregateConfig.cs b/src/Config/AggregateConfig.cs index 331f8b5..5c83773 100644 --- a/src/Config/AggregateConfig.cs +++ b/src/Config/AggregateConfig.cs @@ -13,16 +13,16 @@ public AggregateConfig(params Config[] configs) public List Files { get; } public override void AddBoolean(string section, string? subsection, string variable, bool value) - => Files.First(x => x.Level == null).AddBoolean(section, subsection, variable, value); + => GetConfig().AddBoolean(section, subsection, variable, value); public override void AddDateTime(string section, string? subsection, string variable, DateTime value) - => Files.First(x => x.Level == null).AddDateTime(section, subsection, variable, value); + => GetConfig().AddDateTime(section, subsection, variable, value); public override void AddNumber(string section, string? subsection, string variable, long value) - => Files.First(x => x.Level == null).AddNumber(section, subsection, variable, value); + => GetConfig().AddNumber(section, subsection, variable, value); public override void AddString(string section, string? subsection, string variable, string value) - => Files.First(x => x.Level == null).AddString(section, subsection, variable, value); + => GetConfig().AddString(section, subsection, variable, value); public override IEnumerable GetAll(string section, string? subsection, string variable, string? valueRegex) => Files.SelectMany(x => x.GetAll(section, subsection, variable, valueRegex)); @@ -34,34 +34,34 @@ public override IEnumerable GetRegex(string nameRegex, string? valu => Files.Select(config => config.GetNormalizedPath(section, subsection, variable)).Where(x => x != null).FirstOrDefault(); public override void RemoveSection(string section, string? subsection = null) - => Files.First(x => x.Level == null).RemoveSection(section, subsection); + => GetConfig().RemoveSection(section, subsection); public override void RenameSection(string oldSection, string? oldSubsection, string newSection, string? newSubsection) - => Files.First(x => x.Level == null).RenameSection(oldSection, oldSubsection, newSection, newSubsection); + => GetConfig().RenameSection(oldSection, oldSubsection, newSection, newSubsection); public override void SetAllBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllBoolean(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllBoolean(section, subsection, variable, value, valueRegex); public override void SetAllDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllDateTime(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllDateTime(section, subsection, variable, value, valueRegex); public override void SetAllNumber(string section, string? subsection, string variable, long value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllNumber(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllNumber(section, subsection, variable, value, valueRegex); public override void SetAllString(string section, string? subsection, string variable, string value, string? valueRegex) - => Files.First(x => x.Level == null).SetAllString(section, subsection, variable, value, valueRegex); + => GetConfig().SetAllString(section, subsection, variable, value, valueRegex); public override void SetBoolean(string section, string? subsection, string variable, bool value, string? valueRegex) - => Files.First(x => x.Level == null).SetBoolean(section, subsection, variable, value, valueRegex); + => GetConfig().SetBoolean(section, subsection, variable, value, valueRegex); public override void SetDateTime(string section, string? subsection, string variable, DateTime value, string? valueRegex) - => Files.First(x => x.Level == null).SetDateTime(section, subsection, variable, value, valueRegex); + => GetConfig().SetDateTime(section, subsection, variable, value, valueRegex); public override void SetNumber(string section, string? subsection, string variable, long value, string? valueRegex) - => Files.First(x => x.Level == null).SetNumber(section, subsection, variable, value, valueRegex); + => GetConfig().SetNumber(section, subsection, variable, value, valueRegex); public override void SetString(string section, string? subsection, string variable, string value, string? valueRegex) - => Files.First(x => x.Level == null).SetString(section, subsection, variable, value, valueRegex); + => GetConfig().SetString(section, subsection, variable, value, valueRegex); public override bool TryGetBoolean(string section, string? subsection, string variable, out bool value) { @@ -112,10 +112,18 @@ public override bool TryGetString(string section, string? subsection, string var } public override void Unset(string section, string? subsection, string variable) - => Files.First(x => x.Level == null).Unset(section, subsection, variable); + => GetConfig().Unset(section, subsection, variable); public override void UnsetAll(string section, string? subsection, string variable, string? valueRegex) - => Files.First(x => x.Level == null).UnsetAll(section, subsection, variable, valueRegex); + => GetConfig().UnsetAll(section, subsection, variable, valueRegex); + + // When writing via the aggregate config without specifying a ConfigLevel, we first try the default + // .netconfig location, followed by a non-local one (i.e. global/system if that's the first we find), + // followed by whatever is first last. + Config GetConfig() + => Files.FirstOrDefault(x => x.Level == null) ?? + Files.FirstOrDefault(x => x.Level != ConfigLevel.Local) ?? + Files.First(); protected override IEnumerable GetEntries() => Files.SelectMany(x => x); diff --git a/src/Config/Config.cs b/src/Config/Config.cs index 01bd3ba..b156108 100644 --- a/src/Config/Config.cs +++ b/src/Config/Config.cs @@ -91,20 +91,31 @@ public static Config Build(string? path = null) dir = new DirectoryInfo(Path.GetDirectoryName(path)).Parent; } - // [config] root = true stops the directory walking - while (configs.GetBoolean("config", "root") != true && dir != null && dir.Exists) + // If the path is not Global or System, we start walking the directory + // tree up to build the config hierarchy + if (!GlobalLocation.Equals(path, StringComparison.OrdinalIgnoreCase) && + !SystemLocation.Equals(path, StringComparison.OrdinalIgnoreCase)) { - var file = Path.Combine(dir.FullName, FileName + UserExtension); - if (File.Exists(file)) - configs.Files.Add(new FileConfig(file)); - - file = Path.Combine(dir.FullName, FileName); - if (File.Exists(file) && - !GlobalLocation.Equals(file, StringComparison.OrdinalIgnoreCase) && - !SystemLocation.Equals(file, StringComparison.OrdinalIgnoreCase)) - configs.Files.Add(new FileConfig(file)); - - dir = dir.Parent; + // [config] root = true stops the directory walking + while (configs.GetBoolean("config", "root") != true && dir != null && dir.Exists) + { + var file = Path.Combine(dir.FullName, FileName + UserExtension); + if (File.Exists(file)) + configs.Files.Add(new FileConfig(file)); + + file = Path.Combine(dir.FullName, FileName); + // Never add global or system locations here, this loop is just for directory hierarchies + // NOTE: we did add the .user file at the global/system location, since it might be useful + // to allow local writing there too. + if (GlobalLocation.Equals(file, StringComparison.OrdinalIgnoreCase) || + SystemLocation.Equals(file, StringComparison.OrdinalIgnoreCase)) + break; + + if (File.Exists(file)) + configs.Files.Add(new FileConfig(file)); + + dir = dir.Parent; + } } // Don't read the global location if we're building the system location or it's been opted out explicitly diff --git a/src/Config/ConfigLevel.cs b/src/Config/ConfigLevel.cs index 730cc4a..8ba9aee 100644 --- a/src/Config/ConfigLevel.cs +++ b/src/Config/ConfigLevel.cs @@ -5,6 +5,11 @@ namespace DotNetConfig /// /// Specifies the level of configuration to use. /// + /// + /// If not provided, the default .netconfig location will be used, + /// which is typically the current directory unless building configuration + /// from a specific file. + /// public enum ConfigLevel { ///