diff --git a/CHANGELOG.md b/CHANGELOG.md index 5057f529..b125a76b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,14 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Changed + +- The node `quality_profiles` under `custom_formats` was renamed to `assign_scores_to` to + disambiguate it from the top-level `quality_profiles`. The old name is deprecated until the next + major release. See [here][qp_rename] for details. + +[qp_rename]: https://recyclarr.dev/wiki/upgrade-guide/v8.0/#assign-scores-to + ### Fixed - Incorrect URLs were fixed in the local starter config template. diff --git a/schemas/config/custom-formats.json b/schemas/config/custom-formats.json index 6164fd42..bc25ae14 100644 --- a/schemas/config/custom-formats.json +++ b/schemas/config/custom-formats.json @@ -17,7 +17,7 @@ "type": "string" } }, - "quality_profiles": { + "assign_scores_to": { "type": ["null", "array"], "description": "One or more quality profiles to update with the scores from the specified custom formats.", "items": { diff --git a/src/Recyclarr.Cli/Pipelines/CustomFormat/Models/ProcessedConfigData.cs b/src/Recyclarr.Cli/Pipelines/CustomFormat/Models/ProcessedConfigData.cs index 00fa3b63..e2564a0e 100644 --- a/src/Recyclarr.Cli/Pipelines/CustomFormat/Models/ProcessedConfigData.cs +++ b/src/Recyclarr.Cli/Pipelines/CustomFormat/Models/ProcessedConfigData.cs @@ -8,6 +8,6 @@ public class ProcessedConfigData public ICollection CustomFormats { get; init; } = new List(); - public ICollection QualityProfiles { get; init; } - = new List(); + public ICollection QualityProfiles { get; init; } + = new List(); } diff --git a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs index d9d42bd3..00424e42 100644 --- a/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs +++ b/src/Recyclarr.Cli/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhase.cs @@ -16,7 +16,7 @@ public class QualityProfileConfigPhase(ILogger log, ProcessedCustomFormatCache c // 2. For each quality profile score config in that CF group // 3. For each CF in the group above, match it to a Guide CF object and pair it with the quality profile config var profileAndCfs = config.CustomFormats - .SelectMany(x => x.QualityProfiles + .SelectMany(x => x.AssignScoresTo .Select(y => (Profile: y, x.TrashIds))) .SelectMany(x => x.TrashIds .Select(cache.LookupByTrashId) @@ -74,7 +74,7 @@ public class QualityProfileConfigPhase(ILogger log, ProcessedCustomFormatCache c private void AddCustomFormatScoreData( ProcessedQualityProfileData profile, - QualityProfileScoreConfig scoreConfig, + AssignScoresToConfig scoreConfig, CustomFormatData cf) { var existingScoreData = profile.CfScores; @@ -109,7 +109,7 @@ public class QualityProfileConfigPhase(ILogger log, ProcessedCustomFormatCache c private int? DetermineScore( QualityProfileConfig profile, - QualityProfileScoreConfig scoreConfig, + AssignScoresToConfig scoreConfig, CustomFormatData cf) { if (scoreConfig.Score is not null) diff --git a/src/Recyclarr.Config/ConfigAutofacModule.cs b/src/Recyclarr.Config/ConfigAutofacModule.cs index 25f45899..2ab04a25 100644 --- a/src/Recyclarr.Config/ConfigAutofacModule.cs +++ b/src/Recyclarr.Config/ConfigAutofacModule.cs @@ -1,9 +1,11 @@ using Autofac; +using Autofac.Extras.Ordering; using AutoMapper.Contrib.Autofac.DependencyInjection; using FluentValidation; using Recyclarr.Config.Parsing; using Recyclarr.Config.Parsing.PostProcessing; using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; +using Recyclarr.Config.Parsing.PostProcessing.Deprecations; using Recyclarr.Config.Secrets; using Recyclarr.Yaml; @@ -21,8 +23,6 @@ public class ConfigAutofacModule : Module builder.RegisterType().As().SingleInstance(); builder.RegisterType().As(); - builder.RegisterType().As(); - builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); builder.RegisterType().As(); @@ -31,9 +31,26 @@ public class ConfigAutofacModule : Module builder.RegisterType(); builder.RegisterType(); + // Keyed include processors + builder.RegisterType().Keyed(typeof(ConfigYamlInclude)); + builder.RegisterType().Keyed(typeof(TemplateYamlInclude)); + // Config Post Processors - builder.RegisterType().As(); - builder.RegisterType().As(); + builder.RegisterTypes( + // Order-sensitive! + typeof(ConfigDeprecationPostProcessor), + typeof(ImplicitUrlAndKeyPostProcessor), + typeof(IncludePostProcessor)) + .As() + .OrderByRegistration(); + + // Config Deprecations + builder.RegisterType(); + builder.RegisterTypes( + // Order-sensitive! + typeof(CfQualityProfilesDeprecationCheck)) + .As() + .OrderByRegistration(); RegisterValidators(builder); } diff --git a/src/Recyclarr.Config/Models/ServiceConfiguration.cs b/src/Recyclarr.Config/Models/ServiceConfiguration.cs index b2dbd2f6..d8b3e0b0 100644 --- a/src/Recyclarr.Config/Models/ServiceConfiguration.cs +++ b/src/Recyclarr.Config/Models/ServiceConfiguration.cs @@ -28,12 +28,12 @@ public record CustomFormatConfig { public ICollection TrashIds { get; init; } = new List(); - public ICollection QualityProfiles { get; init; } = - new List(); + public ICollection AssignScoresTo { get; init; } = + new List(); } [UsedImplicitly(ImplicitUseTargetFlags.WithMembers)] -public record QualityProfileScoreConfig +public record AssignScoresToConfig { public string Name { get; init; } = ""; public int? Score { get; init; } diff --git a/src/Recyclarr.Config/Parsing/ConfigYamlDataObjects.cs b/src/Recyclarr.Config/Parsing/ConfigYamlDataObjects.cs index 94485385..105f6b07 100644 --- a/src/Recyclarr.Config/Parsing/ConfigYamlDataObjects.cs +++ b/src/Recyclarr.Config/Parsing/ConfigYamlDataObjects.cs @@ -17,6 +17,7 @@ public record QualityScoreConfigYaml public record CustomFormatConfigYaml { public IReadOnlyCollection? TrashIds { get; init; } + public IReadOnlyCollection? AssignScoresTo { get; init; } public IReadOnlyCollection? QualityProfiles { get; init; } } diff --git a/src/Recyclarr.Config/Parsing/ConfigYamlDataObjectsValidation.cs b/src/Recyclarr.Config/Parsing/ConfigYamlDataObjectsValidation.cs index f832486d..df8ccf1f 100644 --- a/src/Recyclarr.Config/Parsing/ConfigYamlDataObjectsValidation.cs +++ b/src/Recyclarr.Config/Parsing/ConfigYamlDataObjectsValidation.cs @@ -42,7 +42,7 @@ public class CustomFormatConfigYamlValidator : AbstractValidator x.QualityProfiles) + RuleForEach(x => x.AssignScoresTo) .SetValidator(new QualityScoreConfigYamlValidator()); } } diff --git a/src/Recyclarr.Config/Parsing/ConfigYamlMapperProfile.cs b/src/Recyclarr.Config/Parsing/ConfigYamlMapperProfile.cs index d463f3c1..9bee360e 100644 --- a/src/Recyclarr.Config/Parsing/ConfigYamlMapperProfile.cs +++ b/src/Recyclarr.Config/Parsing/ConfigYamlMapperProfile.cs @@ -9,7 +9,7 @@ public class ConfigYamlMapperProfile : Profile { public ConfigYamlMapperProfile() { - CreateMap(); + CreateMap(); CreateMap(); CreateMap(); CreateMap(); diff --git a/src/Recyclarr.Config/Parsing/ConfigurationLoader.cs b/src/Recyclarr.Config/Parsing/ConfigurationLoader.cs index bcbb6d5a..122d8d11 100644 --- a/src/Recyclarr.Config/Parsing/ConfigurationLoader.cs +++ b/src/Recyclarr.Config/Parsing/ConfigurationLoader.cs @@ -12,7 +12,7 @@ public class ConfigurationLoader( ConfigParser parser, IMapper mapper, ConfigValidationExecutor validator, - IEnumerable postProcessors) + IOrderedEnumerable postProcessors) : IConfigurationLoader { public IReadOnlyCollection Load(IFileInfo file) diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigDeprecationPostProcessor.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigDeprecationPostProcessor.cs new file mode 100644 index 00000000..37e9a862 --- /dev/null +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigDeprecationPostProcessor.cs @@ -0,0 +1,17 @@ +using System.Diagnostics.CodeAnalysis; +using Recyclarr.Config.Parsing.PostProcessing.Deprecations; + +namespace Recyclarr.Config.Parsing.PostProcessing; + +public class ConfigDeprecationPostProcessor(ConfigDeprecations deprecations) : IConfigPostProcessor +{ + [SuppressMessage("ReSharper", "WithExpressionModifiesAllMembers")] + public RootConfigYaml Process(RootConfigYaml config) + { + return config with + { + Radarr = config.Radarr?.ToDictionary(x => x.Key, x => deprecations.CheckAndTransform(x.Value)), + Sonarr = config.Sonarr?.ToDictionary(x => x.Key, x => deprecations.CheckAndTransform(x.Value)) + }; + } +} diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessor.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessor.cs index 4fecc65e..57716803 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessor.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessor.cs @@ -6,9 +6,22 @@ namespace Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; public class ConfigIncludeProcessor(IFileSystem fs, IAppPaths paths, ILogger log) : IIncludeProcessor { - public bool CanProcess(IYamlInclude includeDirective) + public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType) { - return includeDirective is ConfigYamlInclude; + var include = (ConfigYamlInclude) includeDirective; + + if (include.Config is null) + { + throw new YamlIncludeException("`config` property is required."); + } + + var configFile = ConvertToAbsolute(include.Config); + if (configFile?.Exists != true) + { + throw new YamlIncludeException($"Include path could not be resolved: {include.Config}"); + } + + return configFile; } private IFileInfo? ConvertToAbsolute(string path) @@ -40,22 +53,4 @@ public class ConfigIncludeProcessor(IFileSystem fs, IAppPaths paths, ILogger log return null; } - - public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType) - { - var include = (ConfigYamlInclude) includeDirective; - - if (include.Config is null) - { - throw new YamlIncludeException("`config` property is required."); - } - - var configFile = ConvertToAbsolute(include.Config); - if (configFile?.Exists != true) - { - throw new YamlIncludeException($"Include path could not be resolved: {include.Config}"); - } - - return configFile; - } } diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IIncludeProcessor.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IIncludeProcessor.cs index a2d13888..4cbcfbee 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IIncludeProcessor.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IIncludeProcessor.cs @@ -5,6 +5,5 @@ namespace Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; public interface IIncludeProcessor { - bool CanProcess(IYamlInclude includeDirective); IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType); } diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IYamlIncludeResolver.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IYamlIncludeResolver.cs index e2e3a48c..4e795d54 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IYamlIncludeResolver.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/IYamlIncludeResolver.cs @@ -5,5 +5,5 @@ namespace Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; public interface IYamlIncludeResolver { - IFileInfo GetIncludePath(IYamlInclude includeType, SupportedServices serviceType); + IFileInfo GetIncludePath(IYamlInclude includeDirective, SupportedServices serviceType); } diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs index cd72ffde..bec7f66a 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/ServiceConfigMerger.cs @@ -64,7 +64,7 @@ public abstract class ServiceConfigMerger where T : ServiceConfigYaml TrashIds = x.A.TrashIds .Except(x.B, StringComparer.InvariantCultureIgnoreCase) .ToList(), - QualityProfiles = x.A.ProfileName is not null + AssignScoresTo = x.A.ProfileName is not null ? new[] {new QualityScoreConfigYaml {Name = x.A.ProfileName, Score = x.A.Score}} : null }) @@ -75,8 +75,8 @@ public abstract class ServiceConfigMerger where T : ServiceConfigYaml { return cfs .Where(x => x.TrashIds is not null) - .SelectMany(x => x is {QualityProfiles.Count: > 0} - ? x.QualityProfiles.Select(y => new FlattenedCfs(y.Name, y.Score, x.TrashIds!)) + .SelectMany(x => x is {AssignScoresTo.Count: > 0} + ? x.AssignScoresTo.Select(y => new FlattenedCfs(y.Name, y.Score, x.TrashIds!)) : new[] {new FlattenedCfs(null, null, x.TrashIds!)}) .GroupBy(x => (Name: x.ProfileName, x.Score)) .Select(x => new FlattenedCfs(x.Key.Name, x.Key.Score, x.SelectMany(y => y.TrashIds).ToList())) diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessor.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessor.cs index dec51f3f..d5129c8a 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessor.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessor.cs @@ -6,11 +6,6 @@ namespace Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; public class TemplateIncludeProcessor(IConfigTemplateGuideService templates) : IIncludeProcessor { - public bool CanProcess(IYamlInclude includeDirective) - { - return includeDirective is TemplateYamlInclude; - } - public IFileInfo GetPathToConfig(IYamlInclude includeDirective, SupportedServices serviceType) { var include = (TemplateYamlInclude) includeDirective; diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolver.cs b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolver.cs index 5e20ac80..f16b3511 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolver.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolver.cs @@ -1,19 +1,19 @@ using System.IO.Abstractions; +using Autofac.Features.Indexed; using Recyclarr.TrashGuide; namespace Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; -public class YamlIncludeResolver(IReadOnlyCollection includeProcessors) : IYamlIncludeResolver +public class YamlIncludeResolver(IIndex processorFactory) : IYamlIncludeResolver { - public IFileInfo GetIncludePath(IYamlInclude includeType, SupportedServices serviceType) + public IFileInfo GetIncludePath(IYamlInclude includeDirective, SupportedServices serviceType) { - var processor = includeProcessors.FirstOrDefault(x => x.CanProcess(includeType)); - if (processor is null) + if (!processorFactory.TryGetValue(includeDirective.GetType(), out var processor)) { throw new YamlIncludeException("Include type is not supported"); } - var yamlFile = processor.GetPathToConfig(includeType, serviceType); + var yamlFile = processor.GetPathToConfig(includeDirective, serviceType); if (!yamlFile.Exists) { throw new YamlIncludeException($"Included YAML file does not exist: {yamlFile.FullName}"); diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/CfQualityProfilesDeprecationCheck.cs b/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/CfQualityProfilesDeprecationCheck.cs new file mode 100644 index 00000000..24ee7fb3 --- /dev/null +++ b/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/CfQualityProfilesDeprecationCheck.cs @@ -0,0 +1,31 @@ +namespace Recyclarr.Config.Parsing.PostProcessing.Deprecations; + +public class CfQualityProfilesDeprecationCheck(ILogger log) : IConfigDeprecationCheck +{ + public bool CheckIfNeeded(ServiceConfigYaml include) + { + return + include.CustomFormats is not null && + include.CustomFormats.Any(x => x.QualityProfiles is {Count: > 0}); + } + + public ServiceConfigYaml Transform(ServiceConfigYaml include) + { + log.Warning( + "DEPRECATED: The `quality_profiles` element under `custom_formats` nodes was " + + "detected in your config. This has been renamed to `assign_scores_to`. " + + "See: https://recyclarr.dev/wiki/upgrade-guide/v8.0/#assign-scores-to"); + + // CustomFormats is checked for null in the CheckIfNeeded() method, which is called first. + var cfs = include.CustomFormats!.Select(x => x with + { + AssignScoresTo = [..x.AssignScoresTo ?? [], ..x.QualityProfiles ?? []], + QualityProfiles = null + }); + + return include with + { + CustomFormats = cfs.ToList() + }; + } +} diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/ConfigDeprecations.cs b/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/ConfigDeprecations.cs new file mode 100644 index 00000000..a19faf60 --- /dev/null +++ b/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/ConfigDeprecations.cs @@ -0,0 +1,21 @@ +using System.Diagnostics.CodeAnalysis; + +namespace Recyclarr.Config.Parsing.PostProcessing.Deprecations; + +public class ConfigDeprecations(IOrderedEnumerable deprecationChecks) +{ + [SuppressMessage("SonarLint", "S3267: Loops should be simplified with LINQ expressions", Justification = + "The 'Where' condition must happen after each Transform() call instead of all at once")] + public T CheckAndTransform(T include) where T : ServiceConfigYaml + { + foreach (var check in deprecationChecks) + { + if (check.CheckIfNeeded(include)) + { + include = (T) check.Transform(include); + } + } + + return include; + } +} diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/IConfigDeprecationCheck.cs b/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/IConfigDeprecationCheck.cs new file mode 100644 index 00000000..c8013633 --- /dev/null +++ b/src/Recyclarr.Config/Parsing/PostProcessing/Deprecations/IConfigDeprecationCheck.cs @@ -0,0 +1,7 @@ +namespace Recyclarr.Config.Parsing.PostProcessing.Deprecations; + +public interface IConfigDeprecationCheck +{ + ServiceConfigYaml Transform(ServiceConfigYaml include); + bool CheckIfNeeded(ServiceConfigYaml include); +} diff --git a/src/Recyclarr.Config/Parsing/PostProcessing/IncludePostProcessor.cs b/src/Recyclarr.Config/Parsing/PostProcessing/IncludePostProcessor.cs index c44d5ae8..77a9569b 100644 --- a/src/Recyclarr.Config/Parsing/PostProcessing/IncludePostProcessor.cs +++ b/src/Recyclarr.Config/Parsing/PostProcessing/IncludePostProcessor.cs @@ -1,24 +1,42 @@ using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; +using Recyclarr.Config.Parsing.PostProcessing.Deprecations; using Recyclarr.Platform; using Recyclarr.TrashGuide; using Serilog.Context; namespace Recyclarr.Config.Parsing.PostProcessing; -public class IncludePostProcessor( +public sealed class IncludePostProcessor( ILogger log, ConfigParser parser, ConfigValidationExecutor validator, - IYamlIncludeResolver includeResolver) - : IConfigPostProcessor + IYamlIncludeResolver includeResolver, + ConfigDeprecations deprecations) + : IConfigPostProcessor, IDisposable { + private IDisposable? _logScope; + + public void Dispose() + { + _logScope?.Dispose(); + } + public RootConfigYaml Process(RootConfigYaml config) { - return new RootConfigYaml + try { - Radarr = ProcessIncludes(config.Radarr, new RadarrConfigMerger(), SupportedServices.Radarr), - Sonarr = ProcessIncludes(config.Sonarr, new SonarrConfigMerger(), SupportedServices.Sonarr) - }; + config = config with + { + Radarr = ProcessIncludes(config.Radarr, new RadarrConfigMerger(), SupportedServices.Radarr), + Sonarr = ProcessIncludes(config.Sonarr, new SonarrConfigMerger(), SupportedServices.Sonarr) + }; + } + finally + { + _logScope?.Dispose(); + } + + return config; } private Dictionary? ProcessIncludes( @@ -44,7 +62,11 @@ public class IncludePostProcessor( // Combine all includes together first var aggregateInclude = config.Include - .Select(x => LoadYamlInclude(x, serviceType)) + .Select(x => + { + var include = LoadYamlInclude(x, serviceType); + return deprecations.CheckAndTransform(include); + }) .Aggregate(new T(), merger.Merge); // Merge the config into the aggregated includes so that root config values overwrite included values. @@ -61,11 +83,11 @@ public class IncludePostProcessor( return mergedConfigs; } - private T LoadYamlInclude(IYamlInclude includeType, SupportedServices serviceType) + private T LoadYamlInclude(IYamlInclude includeDirective, SupportedServices serviceType) where T : ServiceConfigYaml { - var yamlFile = includeResolver.GetIncludePath(includeType, serviceType); - using var logScope = LogContext.PushProperty(LogProperty.Scope, $"Include {yamlFile.Name}"); + var yamlFile = includeResolver.GetIncludePath(includeDirective, serviceType); + _logScope = LogContext.PushProperty(LogProperty.Scope, $"Include {yamlFile.Name}"); var configToMerge = parser.Load(yamlFile); if (configToMerge is null) diff --git a/src/Recyclarr.Config/Recyclarr.Config.csproj b/src/Recyclarr.Config/Recyclarr.Config.csproj index 8626ed4d..191db802 100644 --- a/src/Recyclarr.Config/Recyclarr.Config.csproj +++ b/src/Recyclarr.Config/Recyclarr.Config.csproj @@ -1,6 +1,7 @@ + diff --git a/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs b/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs index f1b6d1a1..81c0950c 100644 --- a/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs +++ b/tests/Recyclarr.Cli.Tests/Pipelines/QualityProfile/PipelinePhases/QualityProfileConfigPhaseTest.cs @@ -33,7 +33,7 @@ public class QualityProfileConfigPhaseTest fixture.Inject(SetupCfs(new CustomFormatConfig { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -70,7 +70,7 @@ public class QualityProfileConfigPhaseTest fixture.Inject(SetupCfs(new CustomFormatConfig { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -105,7 +105,7 @@ public class QualityProfileConfigPhaseTest fixture.Inject(SetupCfs(new CustomFormatConfig { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -144,7 +144,7 @@ public class QualityProfileConfigPhaseTest new CustomFormatConfig { TrashIds = new[] {"id1"}, - QualityProfiles = new List + AssignScoresTo = new List { new() {Name = "test_profile1", Score = 100} } @@ -152,7 +152,7 @@ public class QualityProfileConfigPhaseTest new CustomFormatConfig { TrashIds = new[] {"id1"}, - QualityProfiles = new List + AssignScoresTo = new List { new() {Name = "test_profile1", Score = 200} } @@ -160,7 +160,7 @@ public class QualityProfileConfigPhaseTest new CustomFormatConfig { TrashIds = new[] {"id1"}, - QualityProfiles = new List + AssignScoresTo = new List { new() {Name = "test_profile2", Score = 200} } @@ -168,7 +168,7 @@ public class QualityProfileConfigPhaseTest new CustomFormatConfig { TrashIds = new[] {"id1"}, - QualityProfiles = new List + AssignScoresTo = new List { new() {Name = "test_profile2", Score = 100} } @@ -206,9 +206,9 @@ public class QualityProfileConfigPhaseTest new CustomFormatConfig { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { - new QualityProfileScoreConfig {Name = "test_profile"} + new AssignScoresToConfig {Name = "test_profile"} } } }, @@ -245,7 +245,7 @@ public class QualityProfileConfigPhaseTest fixture.Inject(SetupCfs(new CustomFormatConfig { TrashIds = Array.Empty(), - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -277,7 +277,7 @@ public class QualityProfileConfigPhaseTest fixture.Inject(SetupCfs(new CustomFormatConfig { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = Array.Empty() + AssignScoresTo = Array.Empty() })); var context = new QualityProfilePipelineContext(); diff --git a/tests/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs b/tests/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs index d3253376..b9b11855 100644 --- a/tests/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs +++ b/tests/Recyclarr.TestLibrary/Autofac/StubAutofacIndex.cs @@ -6,17 +6,33 @@ namespace Recyclarr.TestLibrary.Autofac; public class StubAutofacIndex : IIndex where TKey : notnull { - private readonly Dictionary _values = new(); + private Dictionary _values = new(); + + public StubAutofacIndex() + { + } + + public StubAutofacIndex(Dictionary values) + { + _values = values; + } public void Add(TKey key, TValue value) { _values.Add(key, value); } + public void AddRange(IEnumerable<(TKey, TValue)> pairs) + { + _values = _values.Union(pairs.ToDictionary(x => x.Item1, x => x.Item2)).ToDictionary(); + } + public bool TryGetValue(TKey key, [UnscopedRef] out TValue value) { return _values.TryGetValue(key, out value!); } public TValue this[TKey key] => _values[key]; + public IReadOnlyCollection Keys => _values.Keys; + public IReadOnlyCollection Values => _values.Values; } diff --git a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessorTest.cs b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessorTest.cs index 62935892..ce4eae88 100644 --- a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessorTest.cs +++ b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/ConfigIncludeProcessorTest.cs @@ -7,14 +7,6 @@ namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging; [TestFixture] public class ConfigIncludeProcessorTest { - [Test, AutoMockData] - public void Can_process_expected_type( - ConfigIncludeProcessor sut) - { - var result = sut.CanProcess(new ConfigYamlInclude()); - result.Should().BeTrue(); - } - [Test, AutoMockData] public void Throw_when_null_include_path( ConfigIncludeProcessor sut) diff --git a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/MergeCustomFormatsTest.cs b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/MergeCustomFormatsTest.cs index edff8fdc..fe268158 100644 --- a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/MergeCustomFormatsTest.cs +++ b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/MergeCustomFormatsTest.cs @@ -16,7 +16,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "c", Score = 100} } @@ -45,7 +45,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "c", Score = 100} } @@ -70,7 +70,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "c", Score = 100}, new QualityScoreConfigYaml {Name = "d", Score = 101}, @@ -80,7 +80,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "f", Score = 100} } @@ -95,7 +95,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id3", "id4"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "d", Score = 200} } @@ -103,7 +103,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id5", "id6"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "e", Score = 300} } @@ -111,7 +111,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "c", Score = 50} } @@ -130,7 +130,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "c", Score = 100} } @@ -138,7 +138,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "d", Score = 101} } @@ -146,7 +146,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1", "id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "e", Score = 102} } @@ -154,7 +154,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id2"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "f", Score = 100} } @@ -162,7 +162,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id3", "id4"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "d", Score = 200} } @@ -170,7 +170,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id5", "id6"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "e", Score = 300} } @@ -178,7 +178,7 @@ public class MergeCustomFormatsTest new CustomFormatConfigYaml { TrashIds = new[] {"id1"}, - QualityProfiles = new[] + AssignScoresTo = new[] { new QualityScoreConfigYaml {Name = "c", Score = 50} } diff --git a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessorTest.cs b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessorTest.cs index 9eaabd36..070c7102 100644 --- a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessorTest.cs +++ b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/TemplateIncludeProcessorTest.cs @@ -7,14 +7,6 @@ namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging; [TestFixture] public class TemplateIncludeProcessorTest { - [Test, AutoMockData] - public void Can_process_expected_type( - TemplateIncludeProcessor sut) - { - var result = sut.CanProcess(new TemplateYamlInclude()); - result.Should().BeTrue(); - } - [Test, AutoMockData] public void Obtain_path_from_template( [Frozen(Matching.ImplementedInterfaces)] MockFileSystem fs, diff --git a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolverTest.cs b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolverTest.cs index f4e2514e..1df252d5 100644 --- a/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolverTest.cs +++ b/tests/Recyclarr.Tests/Config/Parsing/PostProcessing/ConfigMerging/YamlIncludeResolverTest.cs @@ -1,23 +1,35 @@ +using System.Diagnostics.CodeAnalysis; using System.IO.Abstractions; using Recyclarr.Config.Parsing.PostProcessing.ConfigMerging; +using Recyclarr.TestLibrary.Autofac; using Recyclarr.TrashGuide; namespace Recyclarr.Tests.Config.Parsing.PostProcessing.ConfigMerging; [TestFixture] +[SuppressMessage("Design", "CA1034:Nested types should not be visible")] +[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")] public class YamlIncludeResolverTest { - [Test] - public void Find_and_return_processor() + public abstract class TestYamlInclude1 : IYamlInclude; + + public abstract class TestYamlInclude2 : IYamlInclude; + + public abstract class TestYamlInclude3 : IYamlInclude; + + [Test, AutoMockData] + public void Find_and_return_processor( + [Frozen(Matching.ImplementedInterfaces)] StubAutofacIndex index, + YamlIncludeResolver sut) { - var processors = new[] + var processors = new List<(IYamlInclude Directive, IIncludeProcessor Value)> { - Substitute.For(), - Substitute.For() + (Substitute.ForPartsOf(), Substitute.For()), + (Substitute.ForPartsOf(), Substitute.For()) }; - processors[1].CanProcess(default!).ReturnsForAnyArgs(true); - processors[1].GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ => + index.AddRange(processors.Select(x => (x.Directive.GetType(), x.Value))); + processors[1].Value.GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ => { var fileInfo = Substitute.For(); fileInfo.Exists.Returns(true); @@ -25,46 +37,50 @@ public class YamlIncludeResolverTest return fileInfo; }); - var sut = new YamlIncludeResolver(processors); - var result = sut.GetIncludePath(Substitute.For(), SupportedServices.Radarr); + var result = sut.GetIncludePath(processors[1].Directive, SupportedServices.Radarr); result.FullName.Should().Be("the_path"); } - [Test] - public void Throw_when_no_matching_processor() + [Test, AutoMockData] + public void Throw_when_no_matching_processor( + [Frozen(Matching.ImplementedInterfaces)] StubAutofacIndex index, + YamlIncludeResolver sut) { - var processors = new[] + var processors = new List<(IYamlInclude Directive, IIncludeProcessor Value)> { - Substitute.For(), - Substitute.For() + (Substitute.ForPartsOf(), Substitute.For()), + (Substitute.ForPartsOf(), Substitute.For()) }; - var sut = new YamlIncludeResolver(processors); - var act = () => sut.GetIncludePath(Substitute.For(), SupportedServices.Radarr); + index.AddRange(processors.Select(x => (x.Directive.GetType(), x.Value))); + + var act = () => sut.GetIncludePath(Substitute.ForPartsOf(), SupportedServices.Radarr); act.Should().Throw().WithMessage("*type is not supported*"); } - [Test] - public void Throw_when_path_does_not_exist() + [Test, AutoMockData] + public void Throw_when_path_does_not_exist( + [Frozen(Matching.ImplementedInterfaces)] StubAutofacIndex index, + YamlIncludeResolver sut) { - var processors = new[] + var processors = new List<(IYamlInclude Directive, IIncludeProcessor Value)> { - Substitute.For(), - Substitute.For() + (Substitute.ForPartsOf(), Substitute.For()), + (Substitute.ForPartsOf(), Substitute.For()) }; - processors[1].CanProcess(default!).ReturnsForAnyArgs(true); - processors[1].GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ => + index.AddRange(processors.Select(x => (x.Directive.GetType(), x.Value))); + + processors[1].Value.GetPathToConfig(default!, default!).ReturnsForAnyArgs(_ => { var fileInfo = Substitute.For(); fileInfo.Exists.Returns(false); return fileInfo; }); - var sut = new YamlIncludeResolver(processors); - var act = () => sut.GetIncludePath(Substitute.For(), SupportedServices.Radarr); + var act = () => sut.GetIncludePath(processors[1].Directive, SupportedServices.Radarr); act.Should().Throw().WithMessage("*does not exist*"); } diff --git a/tests/Recyclarr.Tests/Config/Parsing/YamlConfigValidatorTest.cs b/tests/Recyclarr.Tests/Config/Parsing/YamlConfigValidatorTest.cs index 7623a692..9a397893 100644 --- a/tests/Recyclarr.Tests/Config/Parsing/YamlConfigValidatorTest.cs +++ b/tests/Recyclarr.Tests/Config/Parsing/YamlConfigValidatorTest.cs @@ -19,7 +19,7 @@ public class YamlConfigValidatorTest new() { TrashIds = new List {"01234567890123456789012345678901"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -52,7 +52,7 @@ public class YamlConfigValidatorTest new() { TrashIds = new[] {"valid"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -85,7 +85,7 @@ public class YamlConfigValidatorTest new() { TrashIds = new[] {"valid"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -119,7 +119,7 @@ public class YamlConfigValidatorTest new() { TrashIds = new[] {"valid"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -155,7 +155,7 @@ public class YamlConfigValidatorTest new() { TrashIds = new List {"valid"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -188,7 +188,7 @@ public class YamlConfigValidatorTest new() { TrashIds = new List {"valid"}, - QualityProfiles = new List + AssignScoresTo = new List { new() { @@ -207,7 +207,7 @@ public class YamlConfigValidatorTest var result = validator.TestValidate(config); result.ShouldHaveValidationErrorFor(FirstCf + - $"{nameof(CustomFormatConfig.QualityProfiles)}[0].{nameof(QualityProfileScoreConfig.Name)}"); + $"{nameof(CustomFormatConfig.AssignScoresTo)}[0].{nameof(AssignScoresToConfig.Name)}"); } [Test]