feat: Named instances support

Use mapping-style instead of array-style for instances in configuration
YAML.
pull/151/head
Robert Dailey 2 years ago committed by Robert Dailey
parent 4ae54d8f54
commit 1d604b141b

@ -19,6 +19,12 @@ changes you need to make.
- Secrets support. You can now store sensitive information from your configuration YAML such as
`api_key` and `base_url` in a `secrets.yml` file. See [the secrets docs][secrets] for more info.
Huge thanks to @voltron4lyfe for this one. (#105, #139)
- Named instances are now supported in configuration YAML.
### Changed
- Deprecated array-style instances in configuration YAML. Read more about this in the v3.0 Upgrade
Guide.
### Removed

@ -5,131 +5,42 @@
"additionalProperties": false,
"properties": {
"sonarr": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["base_url", "api_key"],
"properties": {
"base_url": {
"$ref": "#/$defs/base_url",
"examples": [
"http://localhost:8989",
"https://sonarr.mydomain.com",
"https://mydomain.com/sonarr"
]
},
"api_key": {
"type": "string",
"minLength": 1,
"description": "The API key from Sonarr."
},
"quality_definition": {
"type": "string"
},
"delete_old_custom_formats": {
"$ref": "#/$defs/delete_old_custom_formats"
},
"custom_formats": {
"$ref": "#/$defs/custom_formats"
},
"release_profiles": {
"type": "array",
"minItems": 1,
"items": {
"additionalProperties": false,
"required": ["trash_ids"],
"properties": {
"trash_ids": {
"$ref": "#/$defs/trash_ids_list"
},
"strict_negative_scores": {
"type": "boolean",
"default": false,
"description": "Enables preferred term scores less than 0 to be instead treated as \"Must Not Contain\" (ignored) terms."
},
"tags": {
"type": "array",
"description": "A list of one or more strings representing tags that will be applied to this release profile.",
"items": {
"type": "string"
}
},
"filter": {
"type": "object",
"additionalProperties": false,
"description": "Defines various ways that release profile terms from the guide are synchronized with Sonarr.",
"oneOf": [
{
"required": ["include"]
},
{
"required": ["exclude"]
}
],
"properties": {
"include": {
"$ref": "#/$defs/trash_ids_list",
"description": "A list of trash_id values representing terms (Required, Ignored, or Preferred) that should be included in the created Release Profile in Sonarr."
},
"exclude": {
"$ref": "#/$defs/trash_ids_list",
"description": "A list of trash_id values representing terms (Required, Ignored, or Preferred) that should be excluded from the created Release Profile in Sonarr."
}
}
}
}
"oneOf": [
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/sonarr_instance"
}
},
{
"type": "object",
"patternProperties": {
"^.*$": {
"$ref": "#/$defs/sonarr_instance"
}
}
}
}
]
},
"radarr": {
"type": "array",
"minItems": 1,
"items": {
"type": "object",
"additionalProperties": false,
"required": ["base_url", "api_key"],
"properties": {
"base_url": {
"$ref": "#/$defs/base_url",
"examples": [
"http://localhost:7878",
"https://radarr.mydomain.com",
"https://mydomain.com/radarr"
]
},
"api_key": {
"type": "string",
"minLength": 1,
"description": "The API key from Radarr."
},
"quality_definition": {
"type": "object",
"additionalProperties": false,
"required": ["type"],
"properties": {
"type": {
"type": "string"
},
"preferred_ratio": {
"type": "number",
"default": 1.0,
"minimum": 0.0,
"maximum": 1.0
}
"oneOf": [
{
"type": "array",
"minItems": 1,
"items": {
"$ref": "#/$defs/radarr_instance"
}
},
{
"type": "object",
"patternProperties": {
"^.*$": {
"$ref": "#/$defs/radarr_instance"
}
},
"delete_old_custom_formats": {
"$ref": "#/$defs/delete_old_custom_formats"
},
"custom_formats": {
"$ref": "#/$defs/custom_formats"
}
}
}
]
}
},
"$defs": {
@ -193,6 +104,125 @@
}
}
}
},
"radarr_instance": {
"type": "object",
"additionalProperties": false,
"required": ["base_url", "api_key"],
"properties": {
"base_url": {
"$ref": "#/$defs/base_url",
"examples": [
"http://localhost:7878",
"https://radarr.mydomain.com",
"https://mydomain.com/radarr"
]
},
"api_key": {
"type": "string",
"minLength": 1,
"description": "The API key from Radarr."
},
"quality_definition": {
"type": "object",
"additionalProperties": false,
"required": ["type"],
"properties": {
"type": {
"type": "string"
},
"preferred_ratio": {
"type": "number",
"default": 1.0,
"minimum": 0.0,
"maximum": 1.0
}
}
},
"delete_old_custom_formats": {
"$ref": "#/$defs/delete_old_custom_formats"
},
"custom_formats": {
"$ref": "#/$defs/custom_formats"
}
}
},
"sonarr_instance": {
"type": "object",
"additionalProperties": false,
"required": ["base_url", "api_key"],
"properties": {
"base_url": {
"$ref": "#/$defs/base_url",
"examples": [
"http://localhost:8989",
"https://sonarr.mydomain.com",
"https://mydomain.com/sonarr"
]
},
"api_key": {
"type": "string",
"minLength": 1,
"description": "The API key from Sonarr."
},
"quality_definition": {
"type": "string"
},
"delete_old_custom_formats": {
"$ref": "#/$defs/delete_old_custom_formats"
},
"custom_formats": {
"$ref": "#/$defs/custom_formats"
},
"release_profiles": {
"type": "array",
"minItems": 1,
"items": {
"additionalProperties": false,
"required": ["trash_ids"],
"properties": {
"trash_ids": {
"$ref": "#/$defs/trash_ids_list"
},
"strict_negative_scores": {
"type": "boolean",
"default": false,
"description": "Enables preferred term scores less than 0 to be instead treated as \"Must Not Contain\" (ignored) terms."
},
"tags": {
"type": "array",
"description": "A list of one or more strings representing tags that will be applied to this release profile.",
"items": {
"type": "string"
}
},
"filter": {
"type": "object",
"additionalProperties": false,
"description": "Defines various ways that release profile terms from the guide are synchronized with Sonarr.",
"oneOf": [
{
"required": ["include"]
},
{
"required": ["exclude"]
}
],
"properties": {
"include": {
"$ref": "#/$defs/trash_ids_list",
"description": "A list of trash_id values representing terms (Required, Ignored, or Preferred) that should be included in the created Release Profile in Sonarr."
},
"exclude": {
"$ref": "#/$defs/trash_ids_list",
"description": "A list of trash_id values representing terms (Required, Ignored, or Preferred) that should be excluded from the created Release Profile in Sonarr."
}
}
}
}
}
}
}
}
}
}

@ -0,0 +1,50 @@
using Autofac;
using FluentAssertions;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Command.Helpers;
using Recyclarr.TestLibrary;
using TrashLib.Config.Services;
namespace Recyclarr.Tests.Command.Helpers;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CacheStoragePathTest : IntegrationFixture
{
[Test]
public void Use_guid_when_empty_name()
{
var config = Substitute.ForPartsOf<ServiceConfiguration>();
config.BaseUrl = "something";
config.Name = "";
using var scope = Container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).AsImplementedInterfaces();
});
var sut = scope.Resolve<CacheStoragePath>();
var result = sut.CalculatePath("obj");
result.FullName.Should().MatchRegex(@".*[/\\][a-f0-9]+[/\\]obj\.json$");
}
[Test]
public void Use_name_when_not_empty()
{
var config = Substitute.ForPartsOf<ServiceConfiguration>();
config.BaseUrl = "something";
config.Name = "thename";
using var scope = Container.BeginLifetimeScope(builder =>
{
builder.RegisterInstance(config).AsImplementedInterfaces();
});
var sut = scope.Resolve<CacheStoragePath>();
var result = sut.CalculatePath("obj");
result.FullName.Should().MatchRegex(@".*[/\\]thename[/\\]obj\.json$");
}
}

@ -1,4 +1,3 @@
using System.Diagnostics.CodeAnalysis;
using System.IO.Abstractions;
using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers;
@ -9,14 +8,13 @@ using Common.Extensions;
using FluentAssertions;
using FluentValidation;
using FluentValidation.Results;
using JetBrains.Annotations;
using NSubstitute;
using NUnit.Framework;
using Recyclarr.Config;
using Recyclarr.TestLibrary;
using TestLibrary.AutoFixture;
using TrashLib.Config.Services;
using TrashLib.Services.Sonarr.Config;
using TrashLib.TestLibrary;
using YamlDotNet.Core;
namespace Recyclarr.Tests.Config;
@ -31,19 +29,6 @@ public class ConfigurationLoaderTest : IntegrationFixture
return new StringReader(testData.ReadData(file));
}
[UsedImplicitly]
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
[SuppressMessage("Microsoft.Design", "CA1034",
Justification = "YamlDotNet requires this type to be public so it may access it")]
[SuppressMessage("Performance", "CA1822", MessageId = "Mark members as static")]
public class TestConfig : IServiceConfiguration
{
public string BaseUrl => "";
public string ApiKey => "";
public ICollection<CustomFormatConfig> CustomFormats => new List<CustomFormatConfig>();
public bool DeleteOldCustomFormats => false;
}
[Test]
public void Load_many_iterations_of_config()
{
@ -92,6 +77,7 @@ public class ConfigurationLoaderTest : IntegrationFixture
{
ApiKey = "95283e6b156c42f3af8a9b16173f876b",
BaseUrl = "http://localhost:8989",
Name = "name",
ReleaseProfiles = new List<ReleaseProfileConfig>
{
new()

@ -1,5 +1,6 @@
sonarr:
- base_url: http://localhost:8989
name:
base_url: http://localhost:8989
api_key: 95283e6b156c42f3af8a9b16173f876b
release_profiles:
- trash_ids: [123]

@ -34,7 +34,7 @@ public class CacheStoragePath : ICacheStoragePath
{
return _paths.CacheDirectory
.SubDirectory(_serviceCommand.Name.ToLower())
.SubDirectory(BuildServiceGuid())
.SubDirectory(_config.Name.Any() ? _config.Name : BuildServiceGuid())
.File(cacheObjectName + ".json");
}
}

@ -1,5 +1,6 @@
using System.IO.Abstractions;
using System.IO.Abstractions;
using FluentValidation;
using Serilog;
using TrashLib.Config;
using TrashLib.Config.Services;
using YamlDotNet.Core;
@ -9,17 +10,20 @@ using YamlDotNet.Serialization;
namespace Recyclarr.Config;
public class ConfigurationLoader<T> : IConfigurationLoader<T>
where T : IServiceConfiguration
where T : ServiceConfiguration
{
private readonly ILogger _log;
private readonly IDeserializer _deserializer;
private readonly IFileSystem _fileSystem;
private readonly IValidator<T> _validator;
public ConfigurationLoader(
ILogger log,
IFileSystem fileSystem,
IYamlSerializerFactory yamlFactory,
IValidator<T> validator)
{
_log = log;
_fileSystem = fileSystem;
_validator = validator;
_deserializer = yamlFactory.CreateDeserializer();
@ -47,14 +51,36 @@ public class ConfigurationLoader<T> : IConfigurationLoader<T>
continue;
}
var configs = _deserializer.Deserialize<List<T>?>(parser);
if (configs == null)
List<T>? configs;
switch (parser.Current)
{
parser.SkipThisAndNestedEvents();
continue;
case MappingStart:
configs = _deserializer.Deserialize<Dictionary<string, T>>(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<List<T>>(parser);
break;
default:
configs = null;
break;
}
if (configs is not null)
{
ValidateConfigs(configSection, configs, validConfigs);
}
ValidateConfigs(configSection, configs, validConfigs);
parser.SkipThisAndNestedEvents();
}

@ -0,0 +1,13 @@
using JetBrains.Annotations;
using TrashLib.Config.Services;
namespace TrashLib.TestLibrary;
[UsedImplicitly]
public class TestConfig : ServiceConfiguration
{
public TestConfig()
{
Name = "Test";
}
}

@ -4,6 +4,7 @@ using FluentValidation;
using NUnit.Framework;
using Recyclarr.TestLibrary;
using TrashLib.Config.Services;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Config.Services;
@ -15,7 +16,7 @@ public class ServiceConfigurationTest : IntegrationFixture
public void Validation_fails_for_all_missing_required_properties()
{
// default construct which should yield default values (invalid) for all required properties
var config = new ServiceConfiguration();
var config = new TestConfig();
var validator = Container.Resolve<IValidator<ServiceConfiguration>>();
@ -36,7 +37,7 @@ public class ServiceConfigurationTest : IntegrationFixture
[Test]
public void Fail_when_trash_ids_missing()
{
var config = new ServiceConfiguration
var config = new TestConfig
{
BaseUrl = "valid",
ApiKey = "valid",

@ -2,6 +2,7 @@ namespace TrashLib.Config.Services;
public interface IServiceConfiguration
{
string Name { get; }
string BaseUrl { get; }
string ApiKey { get; }
ICollection<CustomFormatConfig> CustomFormats { get; }

@ -2,10 +2,12 @@ using JetBrains.Annotations;
namespace TrashLib.Config.Services;
public class ServiceConfiguration : IServiceConfiguration
public abstract class ServiceConfiguration : IServiceConfiguration
{
public string BaseUrl { get; init; } = "";
public string ApiKey { get; init; } = "";
// Name is set dynamically by
public string Name { get; set; } = "";
public string BaseUrl { get; set; } = "";
public string ApiKey { get; set; } = "";
public ICollection<CustomFormatConfig> CustomFormats { get; init; } =
new List<CustomFormatConfig>();

Loading…
Cancel
Save