diff --git a/README.md b/README.md index 84820c7..b168626 100644 --- a/README.md +++ b/README.md @@ -7,7 +7,7 @@ A smart CLI tool that automates the tedious task of configuring application services in the cloud. ## What it does? -Once executed, tool will hook up to host of `Microsoft.NET.Sdk.Web` application (NET6&7 only), collect all application settings and update your configuration JSON or YAML file. +Once executed, tool will hook up to host of `Microsoft.NET.Sdk.Web` application, collect all application settings and update your configuration JSON or YAML file. ## Getting started Tool can be installed using the `Nuget package manager` or the `dotnet` CLI. @@ -67,12 +67,13 @@ Tool is customisable, take a look at list of all arguments that can be passed. E | -a | --assembly | :x: | Startup assembly file path and name. Can be obtained by `$(OutputPath)\$(AssemblyName).dll` | | | -p | --providers | :heavy_check_mark: | A list of configuration providers from which all setting keys are taken. All types derived from `IConfigurationProvider` | `JsonConfigurationProvider` | | -i | --include | :heavy_check_mark: | A list of keys to include despite of providers list configuration. Example: `PROCESSOR_ARCHITECTURE` | | -| -x | --exclude | :heavy_check_mark: | A list of keys to exclude from all collected settings. Example: `Logging:LogLevel:Microsoft.AspNetCore` | | +| -x | --exclude | :heavy_check_mark: | A list of keys to exclude from all collected settings. Example: `Logging:LogLevel:Microsoft.AspNetCore` will remove exact key, `Logging:LogLevel` will remove also all nested keys | | | -s | --separator | :heavy_check_mark: | Setting nesting separator | `__` (double underscore) | | -v | ‑‑yaml‑variable‑name | :heavy_check_mark: | YAML variable name | `app_config` | | -y | --to-yaml | :heavy_check_mark: | Indicates whether configuration wrapped in YAML Azure DevOps variables file | `false` | | -f | --file-path-template | :heavy_check_mark: | File name template for output. Template may contain a placeholder for environment name. Example: `configuration.{0}.json` | `./configuration.json` | | -e | --environments | :heavy_check_mark: | A list of environment names. Separate configuration file will be created per each environment. Required when file name template contains placeholder | | +| -l | -eol | :heavy_check_mark: | Customize end-of-line characters, possible values: `Cr`, `CrLf` or `Lf`, when skipped, operating system default end-of-line characters will be used | | ### Configuration providers If you'd like to include all the settings collected by different configuration provider, add them all by `--providers` option. More information about [configuration providers](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration-providers). diff --git a/examples/src/ExampleWebAppMultipleEnvironments/.config/dotnet-tools.json b/examples/src/ExampleWebAppMultipleEnvironments/.config/dotnet-tools.json index d51d993..16b8af1 100644 --- a/examples/src/ExampleWebAppMultipleEnvironments/.config/dotnet-tools.json +++ b/examples/src/ExampleWebAppMultipleEnvironments/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "antyrama.tools.scribe.cli": { - "version": "0.0.3-rc", + "version": "0.0.5", "commands": [ "app-settings-to-file" ] diff --git a/examples/src/ExampleWebAppMultipleEnvironments/ExampleWebAppMultipleEnvironments.csproj b/examples/src/ExampleWebAppMultipleEnvironments/ExampleWebAppMultipleEnvironments.csproj index ff33445..7274a18 100644 --- a/examples/src/ExampleWebAppMultipleEnvironments/ExampleWebAppMultipleEnvironments.csproj +++ b/examples/src/ExampleWebAppMultipleEnvironments/ExampleWebAppMultipleEnvironments.csproj @@ -1,4 +1,4 @@ - + net6.0 @@ -7,7 +7,7 @@ - + diff --git a/examples/src/ExampleWebAppSimple/.config/dotnet-tools.json b/examples/src/ExampleWebAppSimple/.config/dotnet-tools.json index d51d993..16b8af1 100644 --- a/examples/src/ExampleWebAppSimple/.config/dotnet-tools.json +++ b/examples/src/ExampleWebAppSimple/.config/dotnet-tools.json @@ -3,7 +3,7 @@ "isRoot": true, "tools": { "antyrama.tools.scribe.cli": { - "version": "0.0.3-rc", + "version": "0.0.5", "commands": [ "app-settings-to-file" ] diff --git a/examples/src/ExampleWebAppSimple/ExampleWebAppSimple.csproj b/examples/src/ExampleWebAppSimple/ExampleWebAppSimple.csproj index 897ed7a..e79672e 100644 --- a/examples/src/ExampleWebAppSimple/ExampleWebAppSimple.csproj +++ b/examples/src/ExampleWebAppSimple/ExampleWebAppSimple.csproj @@ -7,7 +7,7 @@ - + diff --git a/src/Antyrama.Tools.Scribe.Cli/Antyrama.Tools.Scribe.Cli.csproj b/src/Antyrama.Tools.Scribe.Cli/Antyrama.Tools.Scribe.Cli.csproj index b29baad..2c22732 100644 --- a/src/Antyrama.Tools.Scribe.Cli/Antyrama.Tools.Scribe.Cli.csproj +++ b/src/Antyrama.Tools.Scribe.Cli/Antyrama.Tools.Scribe.Cli.csproj @@ -13,6 +13,7 @@ git true false + README.md @@ -26,4 +27,8 @@ + + + + diff --git a/src/Antyrama.Tools.Scribe.Core/Antyrama.Tools.Scribe.Core.csproj b/src/Antyrama.Tools.Scribe.Core/Antyrama.Tools.Scribe.Core.csproj index 615389b..2b3ee36 100644 --- a/src/Antyrama.Tools.Scribe.Core/Antyrama.Tools.Scribe.Core.csproj +++ b/src/Antyrama.Tools.Scribe.Core/Antyrama.Tools.Scribe.Core.csproj @@ -11,6 +11,7 @@ Core assembly to ensure all CLI tool dependencies are satisfied true configuration;appSettings;webapi;aspnet;aspnetcore + README.md @@ -24,8 +25,12 @@ - + + + + + <_Parameter1>UnitTests diff --git a/src/Antyrama.Tools.Scribe.Core/AppServiceConfigurationGenerator.cs b/src/Antyrama.Tools.Scribe.Core/AppServiceConfigurationGenerator.cs index 0003ff7..6743c30 100644 --- a/src/Antyrama.Tools.Scribe.Core/AppServiceConfigurationGenerator.cs +++ b/src/Antyrama.Tools.Scribe.Core/AppServiceConfigurationGenerator.cs @@ -21,6 +21,32 @@ public AppServiceConfigurationGenerator(IServiceProvider serviceProvider, public void Generate() { + var desiredSettings = CollectSettings(); + + var repository = CreateRepositoryInstance(); + + ProcessConfigurationFiles(repository, desiredSettings); + } + + private void ProcessConfigurationFiles(ConfigurationRepository repository, + Dictionary desiredSettings) + { + foreach (var filename in GetConfigurationFiles(_options)) + { + var currentSettings = Load(repository, filename); + + var newSettings = MatchSettings(desiredSettings, currentSettings); + + Save(repository, filename, newSettings); + } + } + + private Dictionary CollectSettings() + { + var excludeKeys = _options.ExcludeKeys + .Select(x => x.Replace(":", _options.PathSeparator)) + .ToArray(); + var configuration = (IConfiguration)_serviceProvider.GetService(typeof(IConfiguration)); var includeKeys = new IncludeKeysGenerator(configuration) @@ -31,24 +57,21 @@ public void Generate() var desiredSettings = allSettings .IntersectBy(includeKeys, setting => setting.Key) - .ExceptBy(_options.ExcludeKeys, setting => setting.Key) .ToDictionary(s => s.Key, s => s.Value); - var repository = _options.WrapInYaml - ? (ConfigurationRepository)new YamlConfigurationRepository(_options.YamlVariableName) - : new JsonConfigurationRepository(); + var toRemove = excludeKeys.Length > 0 + ? desiredSettings.Keys.Where(s => excludeKeys.Any(s.StartsWith)) + : Array.Empty(); - foreach (var filename in GetConfigurationFiles(_options)) + foreach (var key in toRemove) { - var currentSettings = Load(repository, filename); - - var newSettings = MatchSettings(desiredSettings, currentSettings); - - Save(repository, filename, newSettings); + desiredSettings.Remove(key); } + + return desiredSettings; } - private IReadOnlyDictionary> Load(IConfigurationRepository repository, string filename) + private static IReadOnlyDictionary> Load(IConfigurationRepository repository, string filename) { try { @@ -62,7 +85,7 @@ private IReadOnlyDictionary> Load(IC } } - private void Save(IConfigurationRepository repository, string filename, IEnumerable> settings) + private static void Save(IConfigurationRepository repository, string filename, IEnumerable> settings) { using var stream = new FileStream(filename, FileMode.Create, FileAccess.Write); @@ -119,6 +142,12 @@ private IEnumerable GetConfigurationFiles(ToolInternalOptions options) } throw new InvalidOperationException("File path template must contain '{0}' as environment placeholder when environments specified."); - } + + private ConfigurationRepository CreateRepositoryInstance() => + _options.WrapInYaml switch + { + true => new YamlConfigurationRepository(_options), + false => new JsonConfigurationRepository(_options) + }; } diff --git a/src/Antyrama.Tools.Scribe.Core/ConfigurationCollector.cs b/src/Antyrama.Tools.Scribe.Core/ConfigurationCollector.cs index c5fa388..2af8a1d 100644 --- a/src/Antyrama.Tools.Scribe.Core/ConfigurationCollector.cs +++ b/src/Antyrama.Tools.Scribe.Core/ConfigurationCollector.cs @@ -25,7 +25,7 @@ public IReadOnlyList> Collect(string pathSeparator) return settings; } - private void Collect(IConfigurationSection section, ICollection> settings, + private static void Collect(IConfigurationSection section, ICollection> settings, string pathSeparator) { if (section == null || string.IsNullOrEmpty(section.Key)) diff --git a/src/Antyrama.Tools.Scribe.Core/Repository/ConfigurationRepository.cs b/src/Antyrama.Tools.Scribe.Core/Repository/ConfigurationRepository.cs index 4fc1842..4266133 100644 --- a/src/Antyrama.Tools.Scribe.Core/Repository/ConfigurationRepository.cs +++ b/src/Antyrama.Tools.Scribe.Core/Repository/ConfigurationRepository.cs @@ -3,7 +3,7 @@ using System.IO; using System.Linq; using Antyrama.Tools.Scribe.Core.Extensions; -using Newtonsoft.Json; +using System.Text.Json; namespace Antyrama.Tools.Scribe.Core.Repository; @@ -15,26 +15,33 @@ internal interface IConfigurationRepository internal abstract class ConfigurationRepository : IConfigurationRepository { + protected readonly string Eol; + + protected ConfigurationRepository(ToolInternalOptions options) + { + Eol = ResolveEndOfLine(options); + } + public abstract IReadOnlyDictionary[] Load(Stream stream); public abstract void Save(Stream stream, IEnumerable> settings); - protected static string Serialize(IEnumerable> settings) + protected string Serialize(IEnumerable> settings) { - var serialized = settings.Select(setting => $" {JsonConvert.SerializeObject(setting)}"); + var serialized = settings.Select(setting => $" {JsonSerializer.Serialize(setting)}"); - var separator = $",{Environment.NewLine}"; + var separator = $",{Eol}"; var formatted = string.Join(separator, serialized) .BeautifyJson(); - return $"[{Environment.NewLine}{formatted}{Environment.NewLine}]"; + return $"[{Eol}{formatted}{Eol}]"; } protected static IReadOnlyDictionary[] Deserialize(string settings) { try { - var deserialized = JsonConvert.DeserializeObject[]>(settings); + var deserialized = JsonSerializer.Deserialize[]>(settings); return deserialized ?? Array.Empty>(); } @@ -43,4 +50,20 @@ protected static IReadOnlyDictionary[] Deserialize(string settin return Array.Empty>(); } } + + private static string ResolveEndOfLine(ToolInternalOptions options) + { + return options.Eol switch + { + EndOfLine.Cr => Cr, + EndOfLine.CrLf => CrLf, + EndOfLine.Lf => Lf, + null => Environment.NewLine, + _ => throw new ArgumentOutOfRangeException(nameof(options)) + }; + } + + private const string Cr = "\r"; + private const string CrLf = "\r\n"; + private const string Lf = "\n"; } diff --git a/src/Antyrama.Tools.Scribe.Core/Repository/JsonConfigurationRepository.cs b/src/Antyrama.Tools.Scribe.Core/Repository/JsonConfigurationRepository.cs index 1b13ce5..6d66940 100644 --- a/src/Antyrama.Tools.Scribe.Core/Repository/JsonConfigurationRepository.cs +++ b/src/Antyrama.Tools.Scribe.Core/Repository/JsonConfigurationRepository.cs @@ -1,24 +1,20 @@ -using System; -using System.Collections.Generic; +using System.Collections.Generic; using System.IO; namespace Antyrama.Tools.Scribe.Core.Repository; internal class JsonConfigurationRepository : ConfigurationRepository { + public JsonConfigurationRepository(ToolInternalOptions options) : base(options) + { + } + public override IReadOnlyDictionary[] Load(Stream stream) { - try - { - var reader = new StreamReader(stream); - var json = reader.ReadToEnd(); + var reader = new StreamReader(stream); + var json = reader.ReadToEnd(); - return Deserialize(json); - } - catch (FileNotFoundException) - { - return Array.Empty>(); - } + return Deserialize(json); } public override void Save(Stream stream, IEnumerable> settings) diff --git a/src/Antyrama.Tools.Scribe.Core/Repository/YamlConfigurationRepository.cs b/src/Antyrama.Tools.Scribe.Core/Repository/YamlConfigurationRepository.cs index 896b7db..be14ebf 100644 --- a/src/Antyrama.Tools.Scribe.Core/Repository/YamlConfigurationRepository.cs +++ b/src/Antyrama.Tools.Scribe.Core/Repository/YamlConfigurationRepository.cs @@ -12,24 +12,25 @@ internal class YamlConfigurationRepository : ConfigurationRepository private readonly Serializer _serializer; private readonly Deserializer _deserializer; - public YamlConfigurationRepository(string variableName) + public YamlConfigurationRepository(ToolInternalOptions options) + : base(options) { _serializer = new SerializerBuilder() .WithAttributeOverride(variables => variables.AppConfig, - new YamlMemberAttribute { Alias = variableName, ScalarStyle = ScalarStyle.Literal }) + new YamlMemberAttribute { Alias = options.YamlVariableName, ScalarStyle = ScalarStyle.Literal }) .Build(); _deserializer = new DeserializerBuilder() .WithAttributeOverride(variables => variables.AppConfig, - new YamlMemberAttribute { Alias = variableName, ScalarStyle = ScalarStyle.Literal }) + new YamlMemberAttribute { Alias = options.YamlVariableName, ScalarStyle = ScalarStyle.Literal }) .Build(); } public override IReadOnlyDictionary[] Load(Stream stream) { + var reader = new StreamReader(stream); + try { - var reader = new StreamReader(stream); - var variablesRoot = _deserializer.Deserialize(reader); if (variablesRoot?.Variables == null || string.IsNullOrWhiteSpace(variablesRoot.Variables.AppConfig)) @@ -39,7 +40,7 @@ public override IReadOnlyDictionary[] Load(Stream stream) return Deserialize(variablesRoot.Variables.AppConfig); } - catch (FileNotFoundException) + catch (YamlException) { return Array.Empty>(); } @@ -55,8 +56,15 @@ public override void Save(Stream stream, IEnumerable EnvironmentsList { get; set; } + + [Option('l', "eol", Required = false, + HelpText = "End-of-line delimiter, possible values: Cr, CrLf or Lf, by default operating system end-of-line will be used.")] + public EndOfLine? Eol { get; set; } } [Verb("_generate", Hidden = true)] @@ -75,4 +79,15 @@ public class ToolInternalOptions [Option('e', "environments", Required = false, Default = new string[0], HelpText = "A list of environment names. Separate configuration file will be created per each environment. Required when file name template contains placeholder.")] public IEnumerable Environments { get; set; } + + [Option('l', "eol", Required = false, + HelpText = "End-of-line delimiter, possible values: Cr, CrLf or Lf, by default operating system end-of-line will be used.")] + public EndOfLine? Eol { get; set; } +} + +public enum EndOfLine +{ + Cr, + CrLf, + Lf } diff --git a/tests/FunctionalTests/FunctionalTests.ShouldExcludeExactSetting.verified.txt b/tests/FunctionalTests/FunctionalTests.ShouldExcludeExactSetting.verified.txt new file mode 100644 index 0000000..5e867f5 --- /dev/null +++ b/tests/FunctionalTests/FunctionalTests.ShouldExcludeExactSetting.verified.txt @@ -0,0 +1,17 @@ +[ + { + name: Logging__ApplicationInsights__EnableAdaptiveSampling, + value: False, + slotSetting: false + }, + { + name: Logging__LogLevel__Default, + value: Information, + slotSetting: false + }, + { + name: Logging__LogLevel__Microsoft.AspNetCore, + value: Warning, + slotSetting: false + } +] \ No newline at end of file diff --git a/tests/FunctionalTests/FunctionalTests.ShouldExcludeWithAllNestedSettings.verified.txt b/tests/FunctionalTests/FunctionalTests.ShouldExcludeWithAllNestedSettings.verified.txt new file mode 100644 index 0000000..cb21ee0 --- /dev/null +++ b/tests/FunctionalTests/FunctionalTests.ShouldExcludeWithAllNestedSettings.verified.txt @@ -0,0 +1,12 @@ +[ + { + name: AllowedHosts, + value: *, + slotSetting: false + }, + { + name: Logging__ApplicationInsights__EnableAdaptiveSampling, + value: False, + slotSetting: false + } +] \ No newline at end of file diff --git a/tests/FunctionalTests/FunctionalTests.cs b/tests/FunctionalTests/FunctionalTests.cs index 183a640..3a1d46e 100644 --- a/tests/FunctionalTests/FunctionalTests.cs +++ b/tests/FunctionalTests/FunctionalTests.cs @@ -23,14 +23,20 @@ public FunctionalTests(WebApplicationFactory factory) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldCreateEntryWhenFoundInAppSettingsJson(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldCreateEntryWhenFoundInAppSettingsJson(bool wrapInYaml, EndOfLine? eol) { // assign var serviceProvider = _factory.Server.Services; - var options = CreateOptions(wrapInYaml, FileTemplate); + var options = CreateOptions(wrapInYaml, FileTemplate, eol); var sut = new AppServiceConfigurationGenerator(serviceProvider, options); @@ -51,14 +57,20 @@ public void ShouldCreateEntryWhenFoundInAppSettingsJson(bool wrapInYaml) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldExcludeEntryWhenSetInOptions(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldExcludeEntryWhenSetInOptions(bool wrapInYaml, EndOfLine? eol) { // assign var serviceProvider = _factory.Server.Services; - var options = CreateOptions(wrapInYaml, FileTemplate); + var options = CreateOptions(wrapInYaml, FileTemplate, eol); options.ExcludeKeys = new[] { "Logging__ApplicationInsights__EnableAdaptiveSampling" }; @@ -80,9 +92,15 @@ public void ShouldExcludeEntryWhenSetInOptions(bool wrapInYaml) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldIncludeEntryWhenSetInOptionsButNotSelectedByProvider(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldIncludeEntryWhenSetInOptionsButNotSelectedByProvider(bool wrapInYaml, EndOfLine? eol) { // assign var varName = "MAGIC_ENVIRONMENT_VARIABLE"; @@ -91,7 +109,7 @@ public void ShouldIncludeEntryWhenSetInOptionsButNotSelectedByProvider(bool wrap var serviceProvider = _factory.WithWebHostBuilder(_ => { }).Server.Services; - var options = CreateOptions(wrapInYaml, FileTemplate); + var options = CreateOptions(wrapInYaml, FileTemplate, eol); options.IncludeKeys = new[] { varName }; @@ -114,9 +132,15 @@ public void ShouldIncludeEntryWhenSetInOptionsButNotSelectedByProvider(bool wrap } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldIncludeAllEnvironmentVariablesWhenProviderSelected(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldIncludeAllEnvironmentVariablesWhenProviderSelected(bool wrapInYaml, EndOfLine? eol) { // assign var varName = "MAGIC_ENVIRONMENT_VARIABLE"; @@ -125,7 +149,7 @@ public void ShouldIncludeAllEnvironmentVariablesWhenProviderSelected(bool wrapIn var serviceProvider = _factory.Server.Services; - var options = CreateOptions(wrapInYaml, FileTemplate); + var options = CreateOptions(wrapInYaml, FileTemplate, eol); options.Providers = new[] { "EnvironmentVariablesConfigurationProvider" }; @@ -146,9 +170,15 @@ public void ShouldIncludeAllEnvironmentVariablesWhenProviderSelected(bool wrapIn } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldNotChangeAnExistingConfigurationEntryValueWhenSettingChanged(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldNotChangeAnExistingConfigurationEntryValueWhenSettingChanged(bool wrapInYaml, EndOfLine? eol) { // assign var settingKey = "Logging:ApplicationInsights:EnableAdaptiveSampling"; @@ -166,7 +196,7 @@ public void ShouldNotChangeAnExistingConfigurationEntryValueWhenSettingChanged(b var serviceProvider = factory.Server.Services; - var options = CreateOptions(wrapInYaml, FileTemplate); + var options = CreateOptions(wrapInYaml, FileTemplate, eol); var sut = new AppServiceConfigurationGenerator(serviceProvider, options); @@ -193,16 +223,22 @@ public void ShouldNotChangeAnExistingConfigurationEntryValueWhenSettingChanged(b } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldNotRemoveExtraPropertiesOnAnExistingConfigurationEntry(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldNotRemoveExtraPropertiesOnAnExistingConfigurationEntry(bool wrapInYaml, EndOfLine? eol) { // assign var settingKey = "Logging__ApplicationInsights__EnableAdaptiveSampling"; var serviceProvider = _factory.Server.Services; - var options = CreateOptions(wrapInYaml, FileTemplate); + var options = CreateOptions(wrapInYaml, FileTemplate, eol); var sut = new AppServiceConfigurationGenerator(serviceProvider, options); @@ -226,14 +262,20 @@ public void ShouldNotRemoveExtraPropertiesOnAnExistingConfigurationEntry(bool wr } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldGenerateFilePerEnvironmentGivenInOptions(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldGenerateFilePerEnvironmentGivenInOptions(bool wrapInYaml, EndOfLine? eol) { // assign var serviceProvider = _factory.Server.Services; - var options = CreateOptions(wrapInYaml, $"{FileTemplate}.{{0}}"); + var options = CreateOptions(wrapInYaml, $"{FileTemplate}.{{0}}", eol); options.Environments = new[] { "dev", "test", "prod" }; @@ -258,14 +300,20 @@ public void ShouldGenerateFilePerEnvironmentGivenInOptions(bool wrapInYaml) } [Theory] - [InlineData(false)] - [InlineData(true)] - public void ShouldHandleRelativePaths(bool wrapInYaml) + [InlineData(false, null)] + [InlineData(false, EndOfLine.Cr)] + [InlineData(false, EndOfLine.CrLf)] + [InlineData(false, EndOfLine.Lf)] + [InlineData(true, null)] + [InlineData(true, EndOfLine.Cr)] + [InlineData(true, EndOfLine.CrLf)] + [InlineData(true, EndOfLine.Lf)] + public void ShouldHandleRelativePaths(bool wrapInYaml, EndOfLine? eof) { // assign var serviceProvider = _factory.Server.Services; - var options = CreateOptions(wrapInYaml, $"..\\{FileTemplate}"); + var options = CreateOptions(wrapInYaml, $"..\\{FileTemplate}", eof); var sut = new AppServiceConfigurationGenerator(serviceProvider, options); @@ -277,6 +325,46 @@ public void ShouldHandleRelativePaths(bool wrapInYaml) File.Delete(options.FilePathTemplate); } + [Fact] + public async Task ShouldExcludeExactSetting() + { + // assign + var serviceProvider = _factory.Server.Services; + + var options = CreateOptions(false, $"..\\{FileTemplate}", null, new[] { "AllowedHosts" }); + + var sut = new AppServiceConfigurationGenerator(serviceProvider, options); + + // act + sut.Generate(); + + // assert + var json = await File.ReadAllTextAsync(options.FilePathTemplate); + await VerifyJson(json); + + File.Delete(options.FilePathTemplate); + } + + [Fact] + public async Task ShouldExcludeWithAllNestedSettings() + { + // assign + var serviceProvider = _factory.Server.Services; + + var options = CreateOptions(false, $"..\\{FileTemplate}", null, new[] { "Logging:LogLevel" }); + + var sut = new AppServiceConfigurationGenerator(serviceProvider, options); + + // act + sut.Generate(); + + // assert + var json = await File.ReadAllTextAsync(options.FilePathTemplate); + await VerifyJson(json); + + File.Delete(options.FilePathTemplate); + } + private static JArray? Deserialize(ToolInternalOptions options, TextReader reader, bool wrapInYaml) { string? content; @@ -301,8 +389,8 @@ public void ShouldHandleRelativePaths(bool wrapInYaml) return JsonConvert.DeserializeObject(content!); } - private static ToolInternalOptions CreateOptions(bool wrapInYaml, string fileTemplate, - [CallerMemberName] string prefix = "") + private static ToolInternalOptions CreateOptions(bool wrapInYaml, string fileTemplate, EndOfLine? eof, + string[]? exclude = null, [CallerMemberName] string prefix = "") { var options = new ToolInternalOptions { @@ -311,10 +399,11 @@ private static ToolInternalOptions CreateOptions(bool wrapInYaml, string fileTem Assembly = "assembly", FilePathTemplate = Path.Combine(Environment.CurrentDirectory, fileTemplate.Replace("[prefix]", prefix)), Environments = Array.Empty(), - ExcludeKeys = Array.Empty(), + ExcludeKeys = exclude ?? Array.Empty(), IncludeKeys = Array.Empty(), YamlVariableName = "some_name", - WrapInYaml = wrapInYaml + WrapInYaml = wrapInYaml, + Eol = eof }; return options; } diff --git a/tests/FunctionalTests/FunctionalTests.csproj b/tests/FunctionalTests/FunctionalTests.csproj index 1bde843..f868a7b 100644 --- a/tests/FunctionalTests/FunctionalTests.csproj +++ b/tests/FunctionalTests/FunctionalTests.csproj @@ -1,4 +1,4 @@ - + net7.0 @@ -23,16 +23,17 @@ - - - - - - + + + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all diff --git a/tests/UnitTests/Repository/JsonConfigurationRepositoryTests.cs b/tests/UnitTests/Repository/JsonConfigurationRepositoryTests.cs new file mode 100644 index 0000000..1de3056 --- /dev/null +++ b/tests/UnitTests/Repository/JsonConfigurationRepositoryTests.cs @@ -0,0 +1,73 @@ +using System.Text; +using Antyrama.Tools.Scribe.Core; +using Antyrama.Tools.Scribe.Core.Repository; +using FluentAssertions; + +namespace UnitTests.Repository; + +public sealed class JsonConfigurationRepositoryTests +{ + [Theory] + [InlineData(EndOfLine.CrLf, "\r\n")] + [InlineData(EndOfLine.Cr, "\r")] + [InlineData(EndOfLine.Lf, "\n")] + public void ShouldWriteAndReadConfigFileWithAllPossibleEndOfLines(EndOfLine? eol, string eolChars) + { + // arrange + var options = new ToolInternalOptions { Eol = eol }; + var sut = new JsonConfigurationRepository(options); + + using var stream = new MemoryStream(); + + // act + sut.Save(stream, + new IReadOnlyDictionary[] + { + new Dictionary { { "key1", "value1" }, { "key2", "value2" } } + }); + + // assert + stream.Seek(0, SeekOrigin.Begin); + + using var reader = new StreamReader(stream, Encoding.UTF8); + var result = reader.ReadToEnd(); + + result.Should().Contain(eolChars); + } + + [Theory] + [InlineData("")] + [InlineData("blah blah")] + public void ShouldReturnEmptyWhenConfigurationLineUnreadable(string json) + { + // arrange + var options = new ToolInternalOptions(); + var sut = new JsonConfigurationRepository(options); + + using var stream = new MemoryStream(); + + using var writer = new StreamWriter(stream, Encoding.UTF8); + writer.Write(json); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + // act + var result = sut.Load(stream); + + // assert + result.Should().BeEmpty(); + } + + [Fact] + public void ShouldThrowWhenEndOfLineUnhandled() + { + // arrange + var options = new ToolInternalOptions + { + Eol = (EndOfLine)33 + }; + + // act/assert + Assert.Throws(() => new JsonConfigurationRepository(options)); + } +} diff --git a/tests/UnitTests/Repository/YamlConfigurationRepositoryTests.cs b/tests/UnitTests/Repository/YamlConfigurationRepositoryTests.cs new file mode 100644 index 0000000..a5f1c55 --- /dev/null +++ b/tests/UnitTests/Repository/YamlConfigurationRepositoryTests.cs @@ -0,0 +1,68 @@ +using System.Text; +using Antyrama.Tools.Scribe.Core; +using Antyrama.Tools.Scribe.Core.Repository; +using FluentAssertions; + +namespace UnitTests.Repository; + +public sealed class YamlConfigurationRepositoryTests +{ + [Theory] + [InlineData(EndOfLine.CrLf, "\r\n")] + [InlineData(EndOfLine.Cr, "\r")] + [InlineData(EndOfLine.Lf, "\n")] + public void ShouldWriteAndReadConfigFileWithAllPossibleEndOfLines(EndOfLine? eol, string eolChars) + { + // arrange + var options = new ToolInternalOptions + { + Eol = eol, + YamlVariableName = "app_config" + }; + var sut = new YamlConfigurationRepository(options); + + using var stream = new MemoryStream(); + + // act + sut.Save(stream, + new IReadOnlyDictionary[] + { + new Dictionary { { "key1", "value1" }, { "key2", "value2" } } + }); + + // assert + stream.Seek(0, SeekOrigin.Begin); + + using var reader = new StreamReader(stream, Encoding.UTF8); + var result = reader.ReadToEnd(); + + result.Should().Contain(eolChars); + } + + [Theory] + [InlineData(" ")] + [InlineData("variables:")] + [InlineData("variables:\r\n app_config: -|")] + [InlineData("variables")] + public void ShouldEmptyResultWhenVariablesIsNullOrEmpty(string yaml) + { + // arrange + var options = new ToolInternalOptions + { + YamlVariableName = "app_config" + }; + var sut = new YamlConfigurationRepository(options); + + using var stream = new MemoryStream(); + using var writer = new StreamWriter(stream, Encoding.UTF8); + writer.Write(yaml); + writer.Flush(); + stream.Seek(0, SeekOrigin.Begin); + + // act + var result = sut.Load(stream); + + // assert + result.Should().BeEmpty(); + } +} diff --git a/tests/UnitTests/UnitTests.csproj b/tests/UnitTests/UnitTests.csproj index 90dcb99..897a17a 100644 --- a/tests/UnitTests/UnitTests.csproj +++ b/tests/UnitTests/UnitTests.csproj @@ -10,13 +10,13 @@ - - - + + + runtime; build; native; contentfiles; analyzers; buildtransitive all - + runtime; build; native; contentfiles; analyzers; buildtransitive all