Skip to content

Commit

Permalink
When saving to an aggregate config, use fallback locations
Browse files Browse the repository at this point in the history
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
  • Loading branch information
kzu committed Apr 26, 2021
1 parent 0db91f4 commit d99d1d7
Show file tree
Hide file tree
Showing 4 changed files with 73 additions and 29 deletions.
20 changes: 20 additions & 0 deletions src/Config.Tests/ConfigTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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"));
}
}
}
40 changes: 24 additions & 16 deletions src/Config/AggregateConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -13,16 +13,16 @@ public AggregateConfig(params Config[] configs)
public List<Config> 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<ConfigEntry> GetAll(string section, string? subsection, string variable, string? valueRegex)
=> Files.SelectMany(x => x.GetAll(section, subsection, variable, valueRegex));
Expand All @@ -34,34 +34,34 @@ public override IEnumerable<ConfigEntry> 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)
{
Expand Down Expand Up @@ -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<ConfigEntry> GetEntries() => Files.SelectMany(x => x);

Expand Down
37 changes: 24 additions & 13 deletions src/Config/Config.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
5 changes: 5 additions & 0 deletions src/Config/ConfigLevel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@ namespace DotNetConfig
/// <summary>
/// Specifies the level of configuration to use.
/// </summary>
/// <remarks>
/// If not provided, the default <c>.netconfig</c> location will be used,
/// which is typically the current directory unless building configuration
/// from a specific file.
/// </remarks>
public enum ConfigLevel
{
/// <summary>
Expand Down

0 comments on commit d99d1d7

Please sign in to comment.