From 8a124d12f9dc62199c87e731ef842b04937ad395 Mon Sep 17 00:00:00 2001 From: Robert Dailey Date: Sat, 10 Dec 2022 17:03:06 -0600 Subject: [PATCH] fix: Allow empty YAML files to be loaded - Better contextual logging for YAML files - When there's a syntax error in file parsing, skip that file. - When validation fails for instance config, skip just that instance. - If a file is empty, print a warning and skip it. - Print instance name (instead of URL) in more places. --- CHANGELOG.md | 8 + .../Command/Helpers/CacheStoragePathTest.cs | 6 +- .../Config/ConfigurationLoaderTest.cs | 111 ++++++++---- .../Command/Helpers/CacheStoragePath.cs | 2 +- src/Recyclarr/Command/RadarrCommand.cs | 3 +- src/Recyclarr/Command/SonarrCommand.cs | 3 +- src/Recyclarr/Config/ConfigurationFinder.cs | 1 - src/Recyclarr/Config/ConfigurationLoader.cs | 163 ++++++++++++------ src/Recyclarr/Config/IConfigurationLoader.cs | 6 +- src/Recyclarr/Logging/LogProperty.cs | 6 + src/Recyclarr/Logging/LoggerFactory.cs | 27 +-- .../Secrets/SecretNotFoundExceptionTest.cs | 18 ++ src/TrashLib/Config/IYamlSerializerFactory.cs | 2 +- .../Config/Secrets/SecretNotFoundException.cs | 14 ++ .../Config/Secrets/SecretsDeserializer.cs | 6 +- .../Config/Services/IServiceConfiguration.cs | 2 +- .../Config/Services/ServiceConfiguration.cs | 7 +- src/TrashLib/Config/YamlSerializerFactory.cs | 4 +- .../CustomFormat/Guide/CustomFormatLoader.cs | 1 - 19 files changed, 275 insertions(+), 115 deletions(-) create mode 100644 src/Recyclarr/Logging/LogProperty.cs create mode 100644 src/TrashLib.Tests/Config/Secrets/SecretNotFoundExceptionTest.cs create mode 100644 src/TrashLib/Config/Secrets/SecretNotFoundException.cs diff --git a/CHANGELOG.md b/CHANGELOG.md index 8b1bbcd1..3b6c2ad4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Changed - Improved logging: theme changes, better exception handling, more detail written to log files. +- Print instance name instead of URL in more places. +- Configuration parsing is more forgiving about errors: + - If there's a YAML syntax error, skip the file but continue. + - If there's a validation error, skip only that instance (not the whole file). + +### Fixed + +- Empty configuration files are skipped if they are empty (warning is printed). ## [3.0.0] - 2022-12-03 diff --git a/src/Recyclarr.Tests/Command/Helpers/CacheStoragePathTest.cs b/src/Recyclarr.Tests/Command/Helpers/CacheStoragePathTest.cs index c608124c..562745b7 100644 --- a/src/Recyclarr.Tests/Command/Helpers/CacheStoragePathTest.cs +++ b/src/Recyclarr.Tests/Command/Helpers/CacheStoragePathTest.cs @@ -13,11 +13,11 @@ namespace Recyclarr.Tests.Command.Helpers; public class CacheStoragePathTest : IntegrationFixture { [Test] - public void Use_guid_when_empty_name() + public void Use_guid_when_no_name() { var config = Substitute.ForPartsOf(); config.BaseUrl = "something"; - config.Name = ""; + config.Name = null; using var scope = Container.BeginLifetimeScope(builder => { @@ -31,7 +31,7 @@ public class CacheStoragePathTest : IntegrationFixture } [Test] - public void Use_name_when_not_empty() + public void Use_name_when_not_null() { var config = Substitute.ForPartsOf(); config.BaseUrl = "something"; diff --git a/src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs b/src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs index 99dbff10..dcd07165 100644 --- a/src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs +++ b/src/Recyclarr.Tests/Config/ConfigurationLoaderTest.cs @@ -2,7 +2,7 @@ using System.IO.Abstractions; using System.IO.Abstractions.Extensions; using System.IO.Abstractions.TestingHelpers; using System.Text; -using AutoFixture.NUnit3; +using Autofac; using Common; using Common.Extensions; using FluentAssertions; @@ -12,7 +12,10 @@ using NSubstitute; using NUnit.Framework; using Recyclarr.Config; using Recyclarr.TestLibrary; +using Serilog.Sinks.TestCorrelator; +using TestLibrary; using TestLibrary.AutoFixture; +using TrashLib.Config.Secrets; using TrashLib.Services.Sonarr.Config; using TrashLib.TestLibrary; using YamlDotNet.Core; @@ -29,6 +32,12 @@ public class ConfigurationLoaderTest : IntegrationFixture return new StringReader(testData.ReadData(file)); } + protected override void RegisterExtraTypes(ContainerBuilder builder) + { + base.RegisterExtraTypes(builder); + builder.RegisterMockFor>(); + } + [Test] public void Load_many_iterations_of_config() { @@ -44,7 +53,8 @@ public class ConfigurationLoaderTest : IntegrationFixture var fileData = new (string, string)[] { (baseDir.File("config1.yml").FullName, MockYaml(1, 2)), - (baseDir.File("config2.yml").FullName, MockYaml(3)) + (baseDir.File("config2.yml").FullName, MockYaml(3)), + (baseDir.File("config3.yml").FullName, "bad yaml") }; foreach (var (file, data) in fileData) @@ -101,32 +111,43 @@ public class ConfigurationLoaderTest : IntegrationFixture }); } - [Test, AutoMockData] - public void Throw_when_validation_fails( - [Frozen] IValidator validator, - ConfigurationLoader configLoader) + [Test] + public void Skip_when_validation_fails() + // [Frozen] IValidator validator, + // ConfigurationLoader configLoader) { + var validator = Resolve>(); + var sut = Resolve>(); + // force the validator to return a validation error validator.Validate(Arg.Any()).Returns(new ValidationResult { - Errors = {new ValidationFailure("PropertyName", "Test Validation Failure")} + Errors = + { + new ValidationFailure("PropertyName", "Test Validation Failure"), + new ValidationFailure("Another", "This is yet another failure") + } }); const string testYml = @" fubar: -- api_key: abc + instance1: + api_key: abc "; - Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); + var result = sut.LoadFromStream(new StringReader(testYml), "fubar"); - act.Should().Throw(); + result.Should().BeEmpty(); } - [Test, AutoMockData] - public void Validation_success_does_not_throw(ConfigurationLoader configLoader) + [Test] + public void Validation_success_does_not_throw() { + var configLoader = Resolve>(); + const string testYml = @" fubar: -- api_key: abc + instanceA: + api_key: abc "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "fubar"); act.Should().NotThrow(); @@ -139,9 +160,10 @@ fubar: const string testYml = @" sonarr: -- api_key: !secret api_key - base_url: !secret 123GARBAGE_ - release_profiles: + instance1: + api_key: !secret api_key + base_url: !secret 123GARBAGE_ + release_profiles: - trash_ids: - !secret secret_rp "; @@ -157,6 +179,7 @@ secret_rp: 1234567 { new() { + Name = "instance1", ApiKey = "95283e6b156c42f3af8a9b16173f876b", BaseUrl = "https://radarr:7878", ReleaseProfiles = new List @@ -176,48 +199,55 @@ secret_rp: 1234567 [Test] public void Throw_when_referencing_invalid_secret() { + using var logContext = TestCorrelator.CreateContext(); var configLoader = Resolve>(); const string testYml = @" sonarr: -- api_key: !secret api_key - base_url: fake_url + instance2: + api_key: !secret api_key + base_url: fake_url "; - const string secretsYml = @" -no_api_key: 95283e6b156c42f3af8a9b16173f876b -"; + const string secretsYml = "no_api_key: 95283e6b156c42f3af8a9b16173f876b"; Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml)); - Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); - act.Should().Throw().WithMessage("api_key is not defined in secrets.yml."); + var act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); + + act.Should().Throw() + .WithInnerException() + .WithMessage("*api_key is not defined in secrets.yml"); } [Test] public void Throw_when_referencing_secret_without_secrets_file() { - var configLoader = Resolve>(); + var configLoader = Resolve>(); const string testYml = @" sonarr: -- api_key: !secret api_key - base_url: fake_url + instance3: + api_key: !secret api_key + base_url: fake_url "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); - act.Should().Throw().WithMessage("api_key is not defined in secrets.yml."); + act.Should().Throw() + .WithInnerException() + .WithMessage("*api_key is not defined in secrets.yml"); } [Test] public void Throw_when_secret_value_is_not_scalar() { - var configLoader = Resolve>(); + var configLoader = Resolve>(); const string testYml = @" sonarr: -- api_key: !secret { property: value } - base_url: fake_url + instance4: + api_key: !secret { property: value } + base_url: fake_url "; Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); @@ -231,17 +261,26 @@ sonarr: const string testYml = @" sonarr: -- api_key: fake_key - base_url: fake_url - release_profiles: !secret bogus_profile + instance5: + api_key: fake_key + base_url: fake_url + release_profiles: !secret bogus_profile "; - const string secretsYml = @" -bogus_profile: 95283e6b156c42f3af8a9b16173f876b -"; + const string secretsYml = @"bogus_profile: 95283e6b156c42f3af8a9b16173f876b"; Fs.AddFile(Paths.SecretsPath.FullName, new MockFileData(secretsYml)); Action act = () => configLoader.LoadFromStream(new StringReader(testYml), "sonarr"); act.Should().Throw().WithMessage("Exception during deserialization"); } + + [Test, AutoMockData] + public void Yaml_file_with_only_comment_should_be_skipped(ConfigurationLoader sut) + { + const string testYml = "# YAML with nothing but this comment"; + + var result = sut.LoadFromStream(new StringReader(testYml), "fubar"); + + result.Should().BeEmpty(); + } } diff --git a/src/Recyclarr/Command/Helpers/CacheStoragePath.cs b/src/Recyclarr/Command/Helpers/CacheStoragePath.cs index 3ea04eb6..6db11023 100644 --- a/src/Recyclarr/Command/Helpers/CacheStoragePath.cs +++ b/src/Recyclarr/Command/Helpers/CacheStoragePath.cs @@ -34,7 +34,7 @@ public class CacheStoragePath : ICacheStoragePath { return _paths.CacheDirectory .SubDirectory(_serviceCommand.Name.ToLower()) - .SubDirectory(_config.Name.Any() ? _config.Name : BuildServiceGuid()) + .SubDirectory(_config.Name ?? BuildServiceGuid()) .File(cacheObjectName + ".json"); } } diff --git a/src/Recyclarr/Command/RadarrCommand.cs b/src/Recyclarr/Command/RadarrCommand.cs index 87d9ccd2..1302220c 100644 --- a/src/Recyclarr/Command/RadarrCommand.cs +++ b/src/Recyclarr/Command/RadarrCommand.cs @@ -60,7 +60,8 @@ internal class RadarrCommand : ServiceCommand builder.RegisterInstance(config).As(); }); - log.Information("Processing server {Url}", FlurlLogging.SanitizeUrl(config.BaseUrl)); + log.Information("Processing {Server} server {Name}", + Name, config.Name ?? FlurlLogging.SanitizeUrl(config.BaseUrl)); // There's no actual compatibility checks to perform yet. We directly access the RadarrCompatibility class, // as opposed to a IRadarrVersionEnforcement object (like Sonarr does), simply to force the API invocation diff --git a/src/Recyclarr/Command/SonarrCommand.cs b/src/Recyclarr/Command/SonarrCommand.cs index f3599c02..17f65fcd 100644 --- a/src/Recyclarr/Command/SonarrCommand.cs +++ b/src/Recyclarr/Command/SonarrCommand.cs @@ -94,7 +94,8 @@ public class SonarrCommand : ServiceCommand builder.RegisterInstance(config).As(); }); - log.Information("Processing server {Url}", FlurlLogging.SanitizeUrl(config.BaseUrl)); + log.Information("Processing {Server} server {Name}", + Name, config.Name ?? FlurlLogging.SanitizeUrl(config.BaseUrl)); var versionEnforcement = scope.Resolve(); await versionEnforcement.DoVersionEnforcement(config); diff --git a/src/Recyclarr/Config/ConfigurationFinder.cs b/src/Recyclarr/Config/ConfigurationFinder.cs index 7c92f290..395cb7bf 100644 --- a/src/Recyclarr/Config/ConfigurationFinder.cs +++ b/src/Recyclarr/Config/ConfigurationFinder.cs @@ -57,7 +57,6 @@ public class ConfigurationFinder : IConfigurationFinder throw new CommandException("No configuration YAML files found"); } - _log.Debug("Using config files: {ConfigFiles}", configs); return configs; } } diff --git a/src/Recyclarr/Config/ConfigurationLoader.cs b/src/Recyclarr/Config/ConfigurationLoader.cs index 0f79927f..d8ec7644 100644 --- a/src/Recyclarr/Config/ConfigurationLoader.cs +++ b/src/Recyclarr/Config/ConfigurationLoader.cs @@ -1,8 +1,11 @@ using System.IO.Abstractions; using FluentValidation; +using Recyclarr.Logging; using Serilog; +using Serilog.Context; using TrashLib.Config; using TrashLib.Config.Services; +using TrashLib.Http; using YamlDotNet.Core; using YamlDotNet.Core.Events; using YamlDotNet.Serialization; @@ -14,95 +17,159 @@ public class ConfigurationLoader : IConfigurationLoader { private readonly ILogger _log; private readonly IDeserializer _deserializer; - private readonly IFileSystem _fileSystem; + private readonly IFileSystem _fs; private readonly IValidator _validator; public ConfigurationLoader( ILogger log, - IFileSystem fileSystem, + IFileSystem fs, IYamlSerializerFactory yamlFactory, IValidator validator) { _log = log; - _fileSystem = fileSystem; + _fs = fs; _validator = validator; _deserializer = yamlFactory.CreateDeserializer(); } - public IEnumerable Load(string file, string configSection) + public ICollection LoadMany(IEnumerable configFiles, string configSection) { - using var stream = _fileSystem.File.OpenText(file); - return LoadFromStream(stream, configSection); + return configFiles.SelectMany(file => Load(file, configSection)).ToList(); } - public IEnumerable LoadFromStream(TextReader stream, string configSection) + public ICollection Load(string file, string configSection) { - var parser = new Parser(stream); - parser.Consume(); - parser.Consume(); - parser.Consume(); + _log.Debug("Loading config file: {File}", file); + using var logScope = LogContext.PushProperty(LogProperty.Scope, _fs.Path.GetFileName(file)); - var validConfigs = new List(); - while (parser.TryConsume(out var key)) + try { - if (key.Value != configSection) + using var stream = _fs.File.OpenText(file); + var configs = LoadFromStream(stream, configSection); + if (!configs.Any()) { - parser.SkipThisAndNestedEvents(); - continue; + _log.Warning("Configuration file yielded no usable configuration (is it empty?)"); } - List? configs; - switch (parser.Current) + return configs; + } + catch (YamlException e) + { + var line = e.Start.Line; + switch (e.InnerException) { - case MappingStart: - configs = _deserializer.Deserialize>(parser) - .Select(kvp => - { - kvp.Value.Name = kvp.Key; - return kvp.Value; - }) - .ToList(); - break; - - case SequenceStart: - _log.Warning( - "Found array-style list of instances instead of named-style. Array-style lists of Sonarr/Radarr " + - "instances are deprecated"); - configs = _deserializer.Deserialize>(parser); + case InvalidCastException: + _log.Error("Incompatible value assigned/used at line {Line}", line); break; default: - configs = null; + _log.Error("Exception at line {Line}: {Msg}", line, e.InnerException?.Message ?? e.Message); break; } + } - if (configs is not null) - { - ValidateConfigs(configSection, configs, validConfigs); - } + _log.Error("Due to previous exception, this file will be skipped: {File}", file); + return Array.Empty(); + } + + public ICollection LoadFromStream(TextReader stream, string requestedSection) + { + _log.Debug("Loading config section: {Section}", requestedSection); + var parser = new Parser(stream); - parser.SkipThisAndNestedEvents(); + parser.Consume(); + if (parser.Current is StreamEnd) + { + _log.Debug("Skipping this config due to StreamEnd"); + return Array.Empty(); } - return validConfigs; + parser.Consume(); + if (parser.Current is DocumentEnd) + { + _log.Debug("Skipping this config due to DocumentEnd"); + return Array.Empty(); + } + + return ParseAllSections(parser, requestedSection); } - private void ValidateConfigs(string configSection, IEnumerable configs, ICollection validConfigs) + private ICollection ParseAllSections(Parser parser, string requestedSection) { - foreach (var config in configs) + var configs = new List(); + + parser.Consume(); + while (parser.TryConsume(out var section)) { - var result = _validator.Validate(config); - if (result is {IsValid: false}) + if (section.Value == requestedSection) { - throw new ConfigurationException(configSection, typeof(T), result.Errors); + configs.AddRange(ParseSingleSection(parser)); } + else + { + _log.Debug("Skipping non-matching config section {Section} at line {Line}", + section.Value, section.Start.Line); + parser.SkipThisAndNestedEvents(); + } + } + + // If any config names are null, that means user specified array-style (deprecated) instances. + if (configs.Any(x => x.Name is null)) + { + _log.Warning( + "Found array-style list of instances instead of named-style. " + + "Array-style lists of Sonarr/Radarr instances are deprecated"); + } + + return configs; + } + + private ICollection ParseSingleSection(Parser parser) + { + var configs = new List(); + + switch (parser.Current) + { + case MappingStart: + ParseAndAdd(parser, configs); + break; - validConfigs.Add(config); + case SequenceStart: + ParseAndAdd(parser, configs); + break; } + + return configs; } - public IEnumerable LoadMany(IEnumerable configFiles, string configSection) + private void ParseAndAdd(Parser parser, ICollection configs) + where TStart : ParsingEvent + where TEnd : ParsingEvent { - return configFiles.SelectMany(file => Load(file, configSection)); + parser.Consume(); + while (!parser.TryConsume(out _)) + { + var lineNumber = parser.Current?.Start.Line; + + string? instanceName = null; + if (parser.TryConsume(out var key)) + { + instanceName = key.Value; + } + + var newConfig = _deserializer.Deserialize(parser); + newConfig.Name = instanceName; + + var result = _validator.Validate(newConfig); + if (result is {IsValid: false}) + { + var printableName = instanceName ?? FlurlLogging.SanitizeUrl(newConfig.BaseUrl); + _log.Error("Validation failed for instance config {Instance} at line {Line} with errors {Errors}", + printableName, lineNumber, result.Errors); + continue; + } + + configs.Add(newConfig); + } } } diff --git a/src/Recyclarr/Config/IConfigurationLoader.cs b/src/Recyclarr/Config/IConfigurationLoader.cs index 60d0b97f..59637c49 100644 --- a/src/Recyclarr/Config/IConfigurationLoader.cs +++ b/src/Recyclarr/Config/IConfigurationLoader.cs @@ -2,8 +2,10 @@ using TrashLib.Config.Services; namespace Recyclarr.Config; -public interface IConfigurationLoader +public interface IConfigurationLoader where T : IServiceConfiguration { - IEnumerable LoadMany(IEnumerable configFiles, string configSection); + ICollection LoadMany(IEnumerable configFiles, string configSection); + ICollection Load(string file, string configSection); + ICollection LoadFromStream(TextReader stream, string requestedSection); } diff --git a/src/Recyclarr/Logging/LogProperty.cs b/src/Recyclarr/Logging/LogProperty.cs new file mode 100644 index 00000000..e0363e55 --- /dev/null +++ b/src/Recyclarr/Logging/LogProperty.cs @@ -0,0 +1,6 @@ +namespace Recyclarr.Logging; + +public static class LogProperty +{ + public static string Scope => nameof(Scope); +} diff --git a/src/Recyclarr/Logging/LoggerFactory.cs b/src/Recyclarr/Logging/LoggerFactory.cs index f04fb4e6..0705f7aa 100644 --- a/src/Recyclarr/Logging/LoggerFactory.cs +++ b/src/Recyclarr/Logging/LoggerFactory.cs @@ -11,31 +11,31 @@ public class LoggerFactory { private readonly IAppPaths _paths; - private const string BaseTemplate = - "{@m}" + - "{@x}" + - "\n"; - public LoggerFactory(IAppPaths paths) { _paths = paths; } - private static ExpressionTemplate GetConsoleTemplate() + private static string GetBaseTemplateString() { - const string template = - "[{@l:u3}] " + - BaseTemplate; + var scope = LogProperty.Scope; + + return + $"{{#if {scope} is not null}}{{{scope}}}: {{#end}}" + + "{@m}" + + "{@x}" + + "\n"; + } + private static ExpressionTemplate GetConsoleTemplate() + { + var template = "[{@l:u3}] " + GetBaseTemplateString(); return new ExpressionTemplate(template, theme: TemplateTheme.Code); } private static ExpressionTemplate GetFileTemplate() { - const string template = - "[{@t:HH:mm:ss} {@l:u3}] " + - BaseTemplate; - + var template = "[{@t:HH:mm:ss} {@l:u3}] " + GetBaseTemplateString(); return new ExpressionTemplate(template); } @@ -47,6 +47,7 @@ public class LoggerFactory .MinimumLevel.Is(LogEventLevel.Debug) .WriteTo.Console(GetConsoleTemplate(), level) .WriteTo.File(GetFileTemplate(), logPath.FullName) + .Enrich.FromLogContext() .CreateLogger(); } } diff --git a/src/TrashLib.Tests/Config/Secrets/SecretNotFoundExceptionTest.cs b/src/TrashLib.Tests/Config/Secrets/SecretNotFoundExceptionTest.cs new file mode 100644 index 00000000..7258f5f5 --- /dev/null +++ b/src/TrashLib.Tests/Config/Secrets/SecretNotFoundExceptionTest.cs @@ -0,0 +1,18 @@ +using FluentAssertions; +using NUnit.Framework; +using TrashLib.Config.Secrets; + +namespace TrashLib.Tests.Config.Secrets; + +[TestFixture] +[Parallelizable(ParallelScope.All)] +public class SecretNotFoundExceptionTest +{ + [Test] + public void Properties_get_initialized() + { + var sut = new SecretNotFoundException(15, "key"); + sut.Line.Should().Be(15); + sut.SecretKey.Should().Be("key"); + } +} diff --git a/src/TrashLib/Config/IYamlSerializerFactory.cs b/src/TrashLib/Config/IYamlSerializerFactory.cs index 2ecc1fba..86735cba 100644 --- a/src/TrashLib/Config/IYamlSerializerFactory.cs +++ b/src/TrashLib/Config/IYamlSerializerFactory.cs @@ -4,6 +4,6 @@ namespace TrashLib.Config; public interface IYamlSerializerFactory { - IDeserializer CreateDeserializer(); + IDeserializer CreateDeserializer(Action? extraBuilder = null); ISerializer CreateSerializer(); } diff --git a/src/TrashLib/Config/Secrets/SecretNotFoundException.cs b/src/TrashLib/Config/Secrets/SecretNotFoundException.cs new file mode 100644 index 00000000..190b2239 --- /dev/null +++ b/src/TrashLib/Config/Secrets/SecretNotFoundException.cs @@ -0,0 +1,14 @@ +namespace TrashLib.Config.Secrets; + +public class SecretNotFoundException : Exception +{ + public int Line { get; } + public string SecretKey { get; } + + public SecretNotFoundException(int line, string secretKey) + : base($"Secret used on line {line} with key {secretKey} is not defined in secrets.yml") + { + Line = line; + SecretKey = secretKey; + } +} diff --git a/src/TrashLib/Config/Secrets/SecretsDeserializer.cs b/src/TrashLib/Config/Secrets/SecretsDeserializer.cs index d15dd87a..2927edd9 100644 --- a/src/TrashLib/Config/Secrets/SecretsDeserializer.cs +++ b/src/TrashLib/Config/Secrets/SecretsDeserializer.cs @@ -25,10 +25,10 @@ public class SecretsDeserializer : INodeDeserializer return false; } - var scalar = reader.Consume(); - if (!_secrets.Secrets.TryGetValue(scalar.Value, out var secretsValue)) + var secretKey = reader.Consume(); + if (!_secrets.Secrets.TryGetValue(secretKey.Value, out var secretsValue)) { - throw new YamlException(scalar.Start, scalar.End, $"{scalar.Value} is not defined in secrets.yml."); + throw new SecretNotFoundException(secretKey.Start.Line, secretKey.Value); } value = secretsValue; diff --git a/src/TrashLib/Config/Services/IServiceConfiguration.cs b/src/TrashLib/Config/Services/IServiceConfiguration.cs index 02bc6a2d..b863d43f 100644 --- a/src/TrashLib/Config/Services/IServiceConfiguration.cs +++ b/src/TrashLib/Config/Services/IServiceConfiguration.cs @@ -2,7 +2,7 @@ namespace TrashLib.Config.Services; public interface IServiceConfiguration { - string Name { get; } + string? Name { get; } string BaseUrl { get; } string ApiKey { get; } ICollection CustomFormats { get; } diff --git a/src/TrashLib/Config/Services/ServiceConfiguration.cs b/src/TrashLib/Config/Services/ServiceConfiguration.cs index 69ab8a87..49e0f664 100644 --- a/src/TrashLib/Config/Services/ServiceConfiguration.cs +++ b/src/TrashLib/Config/Services/ServiceConfiguration.cs @@ -1,11 +1,14 @@ using JetBrains.Annotations; +using YamlDotNet.Serialization; namespace TrashLib.Config.Services; public abstract class ServiceConfiguration : IServiceConfiguration { - // Name is set dynamically by - public string Name { get; set; } = ""; + // Name is set dynamically by ConfigurationLoader + [YamlIgnore] + public string? Name { get; set; } + public string BaseUrl { get; set; } = ""; public string ApiKey { get; set; } = ""; diff --git a/src/TrashLib/Config/YamlSerializerFactory.cs b/src/TrashLib/Config/YamlSerializerFactory.cs index 417475f8..e86867c3 100644 --- a/src/TrashLib/Config/YamlSerializerFactory.cs +++ b/src/TrashLib/Config/YamlSerializerFactory.cs @@ -16,7 +16,7 @@ public class YamlSerializerFactory : IYamlSerializerFactory _secretsProvider = secretsProvider; } - public IDeserializer CreateDeserializer() + public IDeserializer CreateDeserializer(Action? extraBuilder = null) { var builder = new DeserializerBuilder() .WithNamingConvention(UnderscoredNamingConvention.Instance) @@ -27,6 +27,8 @@ public class YamlSerializerFactory : IYamlSerializerFactory .WithNodeDeserializer(new ForceEmptySequences(_objectFactory)) .WithObjectFactory(_objectFactory); + extraBuilder?.Invoke(builder); + return builder.Build(); } diff --git a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs index 51e33fd4..3600d2b6 100644 --- a/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs +++ b/src/TrashLib/Services/CustomFormat/Guide/CustomFormatLoader.cs @@ -40,7 +40,6 @@ public class CustomFormatLoader : ICustomFormatLoader IReadOnlyCollection categories) { return Observable.Using(file.OpenText, x => x.ReadToEndAsync().ToObservable()) - .Do(_ => _log.Debug("Parsing CF Json: {Name}", file.Name)) .Select(x => { var cf = _parser.ParseCustomFormatData(x, file.Name);