refactor: Config loader no longer reuses state

pull/201/head
Robert Dailey 1 year ago
parent f5386dfeda
commit ce481e0d1f

@ -1,8 +1,10 @@
using System.IO.Abstractions;
using JetBrains.Annotations;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Config.Services.Radarr;
using Recyclarr.TrashLib.Config.Services.Sonarr;
using Recyclarr.TrashLib.Config.Yaml;
using Serilog.Context;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
using YamlDotNet.Serialization;
@ -12,6 +14,7 @@ namespace Recyclarr.TrashLib.Config.Parsing;
[UsedImplicitly]
public class ConfigParser
{
private readonly ILogger _log;
private readonly ConfigValidationExecutor _validator;
private readonly IDeserializer _deserializer;
private readonly ConfigRegistry _configs = new();
@ -26,14 +29,140 @@ public class ConfigParser
public IConfigRegistry Configs => _configs;
public ConfigParser(
ILogger log,
IYamlSerializerFactory yamlFactory,
ConfigValidationExecutor validator)
{
_log = log;
_validator = validator;
_deserializer = yamlFactory.CreateDeserializer();
}
public bool SetCurrentSection(string name)
public void Load(IFileInfo file, string? desiredSection = null)
{
_log.Debug("Loading config file: {File}", file);
using var logScope = LogContext.PushProperty(LogProperty.Scope, file.Name);
try
{
using var stream = file.OpenText();
LoadFromStream(stream, desiredSection);
return;
}
catch (EmptyYamlException)
{
_log.Warning("Configuration file yielded no usable configuration (is it empty?)");
return;
}
catch (YamlException e)
{
var line = e.Start.Line;
switch (e.InnerException)
{
case InvalidCastException:
_log.Error("Incompatible value assigned/used at line {Line}: {Msg}", line,
e.InnerException.Message);
break;
default:
_log.Error("Exception at line {Line}: {Msg}", line, e.InnerException?.Message ?? e.Message);
break;
}
}
_log.Error("Due to previous exception, this file will be skipped: {File}", file);
}
public void LoadFromStream(TextReader stream, string? desiredSection)
{
var parser = new Parser(stream);
parser.Consume<StreamStart>();
if (parser.Current is StreamEnd)
{
_log.Debug("Skipping this config due to StreamEnd");
throw new EmptyYamlException();
}
parser.Consume<DocumentStart>();
if (parser.Current is DocumentEnd)
{
_log.Debug("Skipping this config due to DocumentEnd");
throw new EmptyYamlException();
}
ParseAllSections(parser, desiredSection);
if (Configs.Count == 0)
{
_log.Debug("Document isn't empty, but still yielded no configs");
}
}
private void ParseAllSections(Parser parser, string? desiredSection)
{
parser.Consume<MappingStart>();
while (parser.TryConsume<Scalar>(out var section))
{
if (desiredSection is not null && desiredSection != section.Value)
{
_log.Debug("Skipping section {Section} because it doesn't match {DesiredSection}",
section.Value, desiredSection);
continue;
}
if (!SetCurrentSection(section.Value))
{
_log.Warning("Unknown service type {Type} at line {Line}; skipping",
section.Value, section.Start.Line);
parser.SkipThisAndNestedEvents();
continue;
}
if (!ParseSingleSection(parser))
{
parser.SkipThisAndNestedEvents();
}
}
}
private bool ParseSingleSection(Parser parser)
{
switch (parser.Current)
{
case MappingStart:
ParseAndAdd<MappingStart, MappingEnd>(parser);
break;
case SequenceStart:
ParseAndAdd<SequenceStart, SequenceEnd>(parser);
break;
case Scalar:
_log.Debug("End of section");
return false;
default:
_log.Warning("Unexpected YAML type at line {Line}; skipping this section", parser.Current?.Start.Line);
return false;
}
return true;
}
private void ParseAndAdd<TStart, TEnd>(Parser parser)
where TStart : ParsingEvent
where TEnd : ParsingEvent
{
parser.Consume<TStart>();
while (!parser.TryConsume<TEnd>(out _))
{
ParseAndAddConfig(parser);
}
}
private bool SetCurrentSection(string name)
{
if (!Enum.TryParse(name, true, out SupportedServices key) || !_configTypes.ContainsKey(key))
{
@ -44,7 +173,7 @@ public class ConfigParser
return true;
}
public void ParseAndAddConfig(Parser parser)
private void ParseAndAddConfig(Parser parser)
{
var lineNumber = parser.Current?.Start.Line;

@ -1,155 +1,39 @@
using System.IO.Abstractions;
using Recyclarr.TrashLib.Config.Yaml;
using Serilog.Context;
using YamlDotNet.Core;
using YamlDotNet.Core.Events;
namespace Recyclarr.TrashLib.Config.Parsing;
public class ConfigurationLoader : IConfigurationLoader
{
private readonly ILogger _log;
private readonly ConfigParser _parser;
private readonly Func<ConfigParser> _parserFactory;
public ConfigurationLoader(ILogger log, ConfigParser parser)
public ConfigurationLoader(Func<ConfigParser> parserFactory)
{
_log = log;
_parser = parser;
_parserFactory = parserFactory;
}
public IConfigRegistry LoadMany(IEnumerable<IFileInfo> configFiles, string? desiredSection = null)
{
var parser = _parserFactory();
foreach (var file in configFiles)
{
Load(file, desiredSection);
parser.Load(file, desiredSection);
}
return _parser.Configs;
return parser.Configs;
}
public IConfigRegistry Load(IFileInfo file, string? desiredSection = null)
{
_log.Debug("Loading config file: {File}", file);
using var logScope = LogContext.PushProperty(LogProperty.Scope, file.Name);
try
{
using var stream = file.OpenText();
return LoadFromStream(stream, desiredSection);
}
catch (EmptyYamlException)
{
_log.Warning("Configuration file yielded no usable configuration (is it empty?)");
return _parser.Configs;
}
catch (YamlException e)
{
var line = e.Start.Line;
switch (e.InnerException)
{
case InvalidCastException:
_log.Error("Incompatible value assigned/used at line {Line}: {Msg}", line,
e.InnerException.Message);
break;
default:
_log.Error("Exception at line {Line}: {Msg}", line, e.InnerException?.Message ?? e.Message);
break;
}
}
_log.Error("Due to previous exception, this file will be skipped: {File}", file);
return _parser.Configs;
var parser = _parserFactory();
parser.Load(file, desiredSection);
return parser.Configs;
}
public IConfigRegistry LoadFromStream(TextReader stream, string? desiredSection = null)
{
var parser = new Parser(stream);
parser.Consume<StreamStart>();
if (parser.Current is StreamEnd)
{
_log.Debug("Skipping this config due to StreamEnd");
throw new EmptyYamlException();
}
parser.Consume<DocumentStart>();
if (parser.Current is DocumentEnd)
{
_log.Debug("Skipping this config due to DocumentEnd");
throw new EmptyYamlException();
}
ParseAllSections(parser, desiredSection);
if (_parser.Configs.Count == 0)
{
_log.Debug("Document isn't empty, but still yielded no configs");
}
return _parser.Configs;
}
private void ParseAllSections(Parser parser, string? desiredSection)
{
parser.Consume<MappingStart>();
while (parser.TryConsume<Scalar>(out var section))
{
if (desiredSection is not null && desiredSection != section.Value)
{
_log.Debug("Skipping section {Section} because it doesn't match {DesiredSection}",
section.Value, desiredSection);
continue;
}
if (!_parser.SetCurrentSection(section.Value))
{
_log.Warning("Unknown service type {Type} at line {Line}; skipping",
section.Value, section.Start.Line);
parser.SkipThisAndNestedEvents();
continue;
}
if (!ParseSingleSection(parser))
{
parser.SkipThisAndNestedEvents();
}
}
}
private bool ParseSingleSection(Parser parser)
{
switch (parser.Current)
{
case MappingStart:
ParseAndAdd<MappingStart, MappingEnd>(parser);
break;
case SequenceStart:
ParseAndAdd<SequenceStart, SequenceEnd>(parser);
break;
case Scalar:
_log.Debug("End of section");
return false;
default:
_log.Warning("Unexpected YAML type at line {Line}; skipping this section", parser.Current?.Start.Line);
return false;
}
return true;
}
private void ParseAndAdd<TStart, TEnd>(Parser parser)
where TStart : ParsingEvent
where TEnd : ParsingEvent
{
parser.Consume<TStart>();
while (!parser.TryConsume<TEnd>(out _))
{
_parser.ParseAndAddConfig(parser);
}
var parser = _parserFactory();
parser.LoadFromStream(stream, desiredSection);
return parser.Configs;
}
}

Loading…
Cancel
Save