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 - 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. `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) 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 ### Removed

@ -5,131 +5,42 @@
"additionalProperties": false, "additionalProperties": false,
"properties": { "properties": {
"sonarr": { "sonarr": {
"type": "array", "oneOf": [
"minItems": 1, {
"items": { "type": "array",
"type": "object", "minItems": 1,
"additionalProperties": false, "items": {
"required": ["base_url", "api_key"], "$ref": "#/$defs/sonarr_instance"
"properties": { }
"base_url": { },
"$ref": "#/$defs/base_url", {
"examples": [ "type": "object",
"http://localhost:8989", "patternProperties": {
"https://sonarr.mydomain.com", "^.*$": {
"https://mydomain.com/sonarr" "$ref": "#/$defs/sonarr_instance"
]
},
"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."
}
}
}
}
} }
} }
} }
} ]
}, },
"radarr": { "radarr": {
"type": "array", "oneOf": [
"minItems": 1, {
"items": { "type": "array",
"type": "object", "minItems": 1,
"additionalProperties": false, "items": {
"required": ["base_url", "api_key"], "$ref": "#/$defs/radarr_instance"
"properties": { }
"base_url": { },
"$ref": "#/$defs/base_url", {
"examples": [ "type": "object",
"http://localhost:7878", "patternProperties": {
"https://radarr.mydomain.com", "^.*$": {
"https://mydomain.com/radarr" "$ref": "#/$defs/radarr_instance"
]
},
"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"
} }
} }
} ]
} }
}, },
"$defs": { "$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;
using System.IO.Abstractions.Extensions; using System.IO.Abstractions.Extensions;
using System.IO.Abstractions.TestingHelpers; using System.IO.Abstractions.TestingHelpers;
@ -9,14 +8,13 @@ using Common.Extensions;
using FluentAssertions; using FluentAssertions;
using FluentValidation; using FluentValidation;
using FluentValidation.Results; using FluentValidation.Results;
using JetBrains.Annotations;
using NSubstitute; using NSubstitute;
using NUnit.Framework; using NUnit.Framework;
using Recyclarr.Config; using Recyclarr.Config;
using Recyclarr.TestLibrary; using Recyclarr.TestLibrary;
using TestLibrary.AutoFixture; using TestLibrary.AutoFixture;
using TrashLib.Config.Services;
using TrashLib.Services.Sonarr.Config; using TrashLib.Services.Sonarr.Config;
using TrashLib.TestLibrary;
using YamlDotNet.Core; using YamlDotNet.Core;
namespace Recyclarr.Tests.Config; namespace Recyclarr.Tests.Config;
@ -31,19 +29,6 @@ public class ConfigurationLoaderTest : IntegrationFixture
return new StringReader(testData.ReadData(file)); 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] [Test]
public void Load_many_iterations_of_config() public void Load_many_iterations_of_config()
{ {
@ -92,6 +77,7 @@ public class ConfigurationLoaderTest : IntegrationFixture
{ {
ApiKey = "95283e6b156c42f3af8a9b16173f876b", ApiKey = "95283e6b156c42f3af8a9b16173f876b",
BaseUrl = "http://localhost:8989", BaseUrl = "http://localhost:8989",
Name = "name",
ReleaseProfiles = new List<ReleaseProfileConfig> ReleaseProfiles = new List<ReleaseProfileConfig>
{ {
new() new()

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

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

@ -1,5 +1,6 @@
using System.IO.Abstractions; using System.IO.Abstractions;
using FluentValidation; using FluentValidation;
using Serilog;
using TrashLib.Config; using TrashLib.Config;
using TrashLib.Config.Services; using TrashLib.Config.Services;
using YamlDotNet.Core; using YamlDotNet.Core;
@ -9,17 +10,20 @@ using YamlDotNet.Serialization;
namespace Recyclarr.Config; namespace Recyclarr.Config;
public class ConfigurationLoader<T> : IConfigurationLoader<T> public class ConfigurationLoader<T> : IConfigurationLoader<T>
where T : IServiceConfiguration where T : ServiceConfiguration
{ {
private readonly ILogger _log;
private readonly IDeserializer _deserializer; private readonly IDeserializer _deserializer;
private readonly IFileSystem _fileSystem; private readonly IFileSystem _fileSystem;
private readonly IValidator<T> _validator; private readonly IValidator<T> _validator;
public ConfigurationLoader( public ConfigurationLoader(
ILogger log,
IFileSystem fileSystem, IFileSystem fileSystem,
IYamlSerializerFactory yamlFactory, IYamlSerializerFactory yamlFactory,
IValidator<T> validator) IValidator<T> validator)
{ {
_log = log;
_fileSystem = fileSystem; _fileSystem = fileSystem;
_validator = validator; _validator = validator;
_deserializer = yamlFactory.CreateDeserializer(); _deserializer = yamlFactory.CreateDeserializer();
@ -47,14 +51,36 @@ public class ConfigurationLoader<T> : IConfigurationLoader<T>
continue; continue;
} }
var configs = _deserializer.Deserialize<List<T>?>(parser); List<T>? configs;
if (configs == null) switch (parser.Current)
{ {
parser.SkipThisAndNestedEvents(); case MappingStart:
continue; 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(); 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 NUnit.Framework;
using Recyclarr.TestLibrary; using Recyclarr.TestLibrary;
using TrashLib.Config.Services; using TrashLib.Config.Services;
using TrashLib.TestLibrary;
namespace TrashLib.Tests.Config.Services; namespace TrashLib.Tests.Config.Services;
@ -15,7 +16,7 @@ public class ServiceConfigurationTest : IntegrationFixture
public void Validation_fails_for_all_missing_required_properties() public void Validation_fails_for_all_missing_required_properties()
{ {
// default construct which should yield default values (invalid) for all 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>>(); var validator = Container.Resolve<IValidator<ServiceConfiguration>>();
@ -36,7 +37,7 @@ public class ServiceConfigurationTest : IntegrationFixture
[Test] [Test]
public void Fail_when_trash_ids_missing() public void Fail_when_trash_ids_missing()
{ {
var config = new ServiceConfiguration var config = new TestConfig
{ {
BaseUrl = "valid", BaseUrl = "valid",
ApiKey = "valid", ApiKey = "valid",

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

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

Loading…
Cancel
Save