refactor: Better settings support in DI

Added ISettings interface to allow injection of a specific subset of
settings into areas of code. This replaced the old ISettingsProvider
method of accessing settings.
pull/351/head
Robert Dailey 6 months ago
parent a1358014ad
commit 84a5651655

@ -4,7 +4,7 @@ using Recyclarr.Settings;
namespace Recyclarr.Cli.Console.Setup;
public class JanitorCleanupTask(LogJanitor janitor, ILogger log, ISettingsProvider settingsProvider)
public class JanitorCleanupTask(LogJanitor janitor, ILogger log, ISettings<LogJanitorSettings> settings)
: IGlobalSetupTask
{
public void OnStart(BaseCommandSettings cmd)
@ -13,7 +13,7 @@ public class JanitorCleanupTask(LogJanitor janitor, ILogger log, ISettingsProvid
public void OnFinish()
{
var maxFiles = settingsProvider.Settings.LogJanitor.MaxFiles;
var maxFiles = settings.Value.MaxFiles;
log.Debug("Cleaning up logs using max files of {MaxFiles}", maxFiles);
janitor.DeleteOldestLogFiles(maxFiles);
}

@ -6,7 +6,10 @@ using Serilog.Context;
namespace Recyclarr.Repo;
public class ConfigTemplatesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISettingsProvider settings)
public class ConfigTemplatesRepo(
IRepoUpdater repoUpdater,
IAppPaths paths,
ISettings<ConfigTemplateRepository> settings)
: IConfigTemplatesRepo, IUpdateableRepo
{
public IDirectoryInfo Path { get; } = paths.ReposDirectory.SubDirectory("config-templates");
@ -14,6 +17,6 @@ public class ConfigTemplatesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISet
public Task Update(CancellationToken token)
{
using var logScope = LogContext.PushProperty(LogProperty.Scope, "Config Templates Repo");
return repoUpdater.UpdateRepo(Path, settings.Settings.Repositories.ConfigTemplates, token);
return repoUpdater.UpdateRepo(Path, settings.Value, token);
}
}

@ -3,8 +3,8 @@ using Recyclarr.VersionControl;
namespace Recyclarr.Repo;
public class GitPath(ISettingsProvider settings) : IGitPath
public class GitPath(ISettings<RecyclarrSettings> settings) : IGitPath
{
public static string Default => "git";
public string Path => settings.Settings.GitPath ?? Default;
public string Path => settings.Value.GitPath ?? Default;
}

@ -6,7 +6,7 @@ using Serilog.Context;
namespace Recyclarr.Repo;
public class TrashGuidesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISettingsProvider settings)
public class TrashGuidesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISettings<TrashRepository> settings)
: ITrashGuidesRepo, IUpdateableRepo
{
public IDirectoryInfo Path { get; } = paths.ReposDirectory.SubDirectory("trash-guides");
@ -14,6 +14,6 @@ public class TrashGuidesRepo(IRepoUpdater repoUpdater, IAppPaths paths, ISetting
public Task Update(CancellationToken token)
{
using var logScope = LogContext.PushProperty(LogProperty.Scope, "Trash Guides Repo");
return repoUpdater.UpdateRepo(Path, settings.Settings.Repositories.TrashGuides, token);
return repoUpdater.UpdateRepo(Path, settings.Value, token);
}
}

@ -13,7 +13,7 @@ namespace Recyclarr.ServarrApi;
public class ServarrRequestBuilder(
ILogger log,
IFlurlClientCache clientCache,
ISettingsProvider settingsProvider,
ISettings<RecyclarrSettings> settings,
IEnumerable<FlurlSpecificEventHandler> eventHandlers,
IServiceConfiguration config)
: IServarrRequestBuilder
@ -38,12 +38,12 @@ public class ServarrRequestBuilder(
builder.EventHandlers.Add(handler);
}
builder.WithSettings(settings =>
builder.WithSettings(httpSettings =>
{
settings.JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services);
httpSettings.JsonSerializer = new DefaultJsonSerializer(GlobalJsonSerializerSettings.Services);
});
if (!settingsProvider.Settings.EnableSslCertificateValidation)
if (!settings.Value.EnableSslCertificateValidation)
{
builder.ConfigureInnerHandler(handler =>
{

@ -0,0 +1,6 @@
namespace Recyclarr.Settings;
public interface ISettings<out T>
{
T Value { get; }
}

@ -1,6 +0,0 @@
namespace Recyclarr.Settings;
public interface ISettingsProvider
{
SettingsValues Settings { get; }
}

@ -25,7 +25,7 @@ public record Repositories
public ConfigTemplateRepository ConfigTemplates { get; [UsedImplicitly] init; } = new();
}
public record SettingsValues
public record RecyclarrSettings
{
public Repositories Repositories { get; [UsedImplicitly] init; } = new();
public bool EnableSslCertificateValidation { get; [UsedImplicitly] init; } = true;

@ -0,0 +1,3 @@
namespace Recyclarr.Settings;
internal record Settings<T>(T Value) : ISettings<T>;

@ -7,6 +7,12 @@ public class SettingsAutofacModule : Module
protected override void Load(ContainerBuilder builder)
{
base.Load(builder);
builder.RegisterType<SettingsProvider>().As<ISettingsProvider>().SingleInstance();
builder.RegisterType<SettingsLoader>();
builder.RegisterType<SettingsProvider>().SingleInstance();
builder.RegisterSettings(x => x);
builder.RegisterSettings(x => x.LogJanitor);
builder.RegisterSettings(x => x.Repositories.ConfigTemplates);
builder.RegisterSettings(x => x.Repositories.TrashGuides);
}
}

@ -18,7 +18,7 @@ public static class SettingsContextualMessages
if (msg.Contains(
"Property 'repository' not found on type " +
$"'{typeof(SettingsValues).FullName}'"))
$"'{typeof(RecyclarrSettings).FullName}'"))
{
return
"Usage of 'repository' setting is no " +

@ -0,0 +1,20 @@
using Autofac;
namespace Recyclarr.Settings;
internal static class SettingsExtensions
{
public static void RegisterSettings<TSettings>(
this ContainerBuilder builder,
Func<RecyclarrSettings, TSettings> settingsSelector)
{
builder.Register(c =>
{
var provider = c.Resolve<SettingsProvider>();
var settings = settingsSelector(provider.Settings);
return new Settings<TSettings>(settings);
})
.As<ISettings<TSettings>>()
.SingleInstance();
}
}

@ -0,0 +1,45 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.Platform;
using Recyclarr.Yaml;
using YamlDotNet.Core;
namespace Recyclarr.Settings;
public class SettingsLoader(IAppPaths paths, IYamlSerializerFactory serializerFactory)
{
public RecyclarrSettings LoadAndOptionallyCreate()
{
var yamlPath = paths.AppDataDirectory.YamlFile("settings") ?? CreateDefaultSettingsFile();
try
{
using var stream = yamlPath.OpenText();
var deserializer = serializerFactory.CreateDeserializer();
return deserializer.Deserialize<RecyclarrSettings?>(stream.ReadToEnd()) ?? new RecyclarrSettings();
}
catch (YamlException e)
{
e.Data["ContextualMessage"] = SettingsContextualMessages.GetContextualErrorFromException(e);
throw;
}
}
private IFileInfo CreateDefaultSettingsFile()
{
const string fileData =
"""
# yaml-language-server: $schema=https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/settings-schema.json
# Edit this file to customize the behavior of Recyclarr beyond its defaults
# For the settings file reference guide, visit the link to the wiki below:
# https://recyclarr.dev/wiki/yaml/settings-reference/
""";
var settingsFile = paths.AppDataDirectory.File("settings.yml");
settingsFile.CreateParentDirectory();
using var stream = settingsFile.CreateText();
stream.Write(fileData);
return settingsFile;
}
}

@ -1,56 +1,7 @@
using System.IO.Abstractions;
using Recyclarr.Common.Extensions;
using Recyclarr.Platform;
using Recyclarr.Yaml;
using YamlDotNet.Core;
namespace Recyclarr.Settings;
public class SettingsProvider : ISettingsProvider
internal class SettingsProvider(SettingsLoader loader)
{
private readonly IAppPaths _paths;
private readonly Lazy<SettingsValues> _settings;
public SettingsValues Settings => _settings.Value;
public SettingsProvider(IAppPaths paths, IYamlSerializerFactory serializerFactory)
{
_paths = paths;
_settings = new Lazy<SettingsValues>(() => LoadOrCreateSettingsFile(serializerFactory));
}
private SettingsValues LoadOrCreateSettingsFile(IYamlSerializerFactory serializerFactory)
{
var yamlPath = _paths.AppDataDirectory.YamlFile("settings") ?? CreateDefaultSettingsFile();
try
{
using var stream = yamlPath.OpenText();
var deserializer = serializerFactory.CreateDeserializer();
return deserializer.Deserialize<SettingsValues?>(stream.ReadToEnd()) ?? new SettingsValues();
}
catch (YamlException e)
{
e.Data["ContextualMessage"] = SettingsContextualMessages.GetContextualErrorFromException(e);
throw;
}
}
private IFileInfo CreateDefaultSettingsFile()
{
const string fileData =
"""
# yaml-language-server: $schema=https://raw.githubusercontent.com/recyclarr/recyclarr/master/schemas/settings-schema.json
# Edit this file to customize the behavior of Recyclarr beyond its defaults
# For the settings file reference guide, visit the link to the wiki below:
# https://recyclarr.dev/wiki/yaml/settings-reference/
""";
var settingsFile = _paths.AppDataDirectory.File("settings.yml");
settingsFile.CreateParentDirectory();
using var stream = settingsFile.CreateText();
stream.Write(fileData);
return settingsFile;
}
private readonly Lazy<RecyclarrSettings> _settings = new(loader.LoadAndOptionallyCreate);
public RecyclarrSettings Settings => _settings.Value;
}

@ -33,8 +33,7 @@ internal class BaseCommandSetupIntegrationTest : CliIntegrationFixture
[Test]
public void Log_janitor_cleans_up_default_max_files()
{
var settingsProvider = Resolve<ISettingsProvider>();
var maxFiles = settingsProvider.Settings.LogJanitor.MaxFiles;
var maxFiles = Resolve<ISettings<LogJanitorSettings>>().Value.MaxFiles;
for (var i = 0; i < maxFiles + 20; ++i)
{

@ -9,9 +9,6 @@ internal class ServiceCompatibilityIntegrationTest : CliIntegrationFixture
[Test]
public void Load_settings_yml_correctly_when_file_exists()
{
var sut = Resolve<SettingsProvider>();
// For this test, it doesn't really matter if the YAML data matches what SettingsValue expects.
// This test only ensures that the data deserialized is from the actual correct file.
const string yamlData =
"""
repositories:
@ -21,8 +18,8 @@ internal class ServiceCompatibilityIntegrationTest : CliIntegrationFixture
Fs.AddFile(Paths.AppDataDirectory.File("settings.yml"), new MockFileData(yamlData));
var settings = sut.Settings;
var settings = Resolve<ISettings<TrashRepository>>();
settings.Repositories.TrashGuides.CloneUrl.Should().Be("http://the_url.com");
settings.Value.CloneUrl.Should().Be("http://the_url.com");
}
}

@ -6,15 +6,15 @@ using Recyclarr.Yaml;
namespace Recyclarr.Tests.Config.Settings;
[TestFixture]
public class SettingsPersisterTest
public class SettingsLoaderTest
{
[Test, AutoMockData]
public void Load_should_create_settings_file_if_not_exists(
[Frozen] MockFileSystem fileSystem,
[Frozen] IAppPaths paths,
SettingsProvider sut)
SettingsLoader sut)
{
_ = sut.Settings;
sut.LoadAndOptionallyCreate();
fileSystem.AllFiles.Should().ContainSingle(paths.AppDataDirectory.File("settings.yml").FullName);
}
@ -23,11 +23,10 @@ public class SettingsPersisterTest
public void Load_defaults_when_file_does_not_exist(
[Frozen(Matching.ImplementedInterfaces)] YamlSerializerFactory serializerFactory,
[Frozen] IAppPaths paths,
SettingsProvider sut)
SettingsLoader sut)
{
var expectedSettings = new SettingsValues();
var settings = sut.Settings;
var expectedSettings = new RecyclarrSettings();
var settings = sut.LoadAndOptionallyCreate();
settings.Should().BeEquivalentTo(expectedSettings);
}

@ -8,10 +8,10 @@ public class GitPathTest
{
[Test, AutoMockData]
public void Default_path_used_when_setting_is_null(
[Frozen] ISettingsProvider settings,
[Frozen] ISettings<RecyclarrSettings> settings,
GitPath sut)
{
settings.Settings.Returns(new SettingsValues
settings.Value.Returns(new RecyclarrSettings
{
GitPath = null
});
@ -23,11 +23,11 @@ public class GitPathTest
[Test, AutoMockData]
public void User_specified_path_used_instead_of_default(
[Frozen] ISettingsProvider settings,
[Frozen] ISettings<RecyclarrSettings> settings,
GitPath sut)
{
var expectedPath = "/usr/local/bin/git";
settings.Settings.Returns(new SettingsValues
settings.Value.Returns(new RecyclarrSettings
{
GitPath = expectedPath
});

Loading…
Cancel
Save