fix: Do not assume only one field element in CF specifications

For most CF specifications, there is only one element in the `fields`
array, which has a `value` property inside of each of its objects. One
particular specification, however, deviates from this assumption. The
"SizeSpecification" has been observed with *two* field objects.

Logic for parsing custom format CFs no longer assumes that the fields
property may only have one element in it.

Fixes #178
pull/201/head
Robert Dailey 1 year ago
parent bebd28bc11
commit 9d53fd0152

@ -8,6 +8,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
### Fixed
- Fixed JSON parsing issue that sometimes occurs when pulling custom formats from Radarr (#178)
## [4.4.0] - 2023-04-06
### Added

@ -0,0 +1,27 @@
using Flurl.Http.Testing;
using Recyclarr.Cli.TestLibrary;
using Recyclarr.Common;
using Recyclarr.TrashLib.Config.Services;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Api;
namespace Recyclarr.TrashLib.Tests.Pipelines.CustomFormat.Api;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class CustomFormatServiceTest : IntegrationFixture
{
[Test, AutoMockData]
public async Task Get_can_parse_json(IServiceConfiguration config)
{
var resourceData = new ResourceDataReader(typeof(CustomFormatServiceTest), "Data");
var jsonBody = resourceData.ReadData("issue_178.json");
using var http = new HttpTest();
http.RespondWith(jsonBody);
var sut = Resolve<CustomFormatService>();
var result = await sut.GetCustomFormats(config);
result.Should().HaveCountGreaterThan(5);
}
}

@ -66,9 +66,12 @@ public class CustomFormatParserTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -77,9 +80,12 @@ public class CustomFormatParserTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 7
new CustomFormatFieldData
{
Value = 7
}
}
},
new CustomFormatSpecificationData
@ -88,9 +94,12 @@ public class CustomFormatParserTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}

@ -21,9 +21,12 @@ public class CustomFormatDataComparerTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -32,9 +35,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 7
new CustomFormatFieldData
{
Value = 7
}
}
},
new CustomFormatSpecificationData
@ -43,9 +49,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}
@ -63,9 +72,12 @@ public class CustomFormatDataComparerTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -74,9 +86,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 7
new CustomFormatFieldData
{
Value = 7
}
}
},
new CustomFormatSpecificationData
@ -85,17 +100,18 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}
};
var result = CustomFormatData.Comparer.Equals(a, b);
result.Should().BeTrue();
a.Should().BeEquivalentTo(b, o => o.Using(CustomFormatData.Comparer));
}
[Test]
@ -113,9 +129,12 @@ public class CustomFormatDataComparerTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -124,9 +143,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 7
new CustomFormatFieldData
{
Value = 7
}
}
},
new CustomFormatSpecificationData
@ -135,9 +157,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}
@ -155,9 +180,12 @@ public class CustomFormatDataComparerTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -166,9 +194,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 10 // this is different
new CustomFormatFieldData
{
Value = 10 // this is different
}
}
},
new CustomFormatSpecificationData
@ -177,9 +208,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}
@ -292,9 +326,12 @@ public class CustomFormatDataComparerTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -303,9 +340,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 7
new CustomFormatFieldData
{
Value = 7
}
}
},
new CustomFormatSpecificationData
@ -314,9 +354,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}
@ -334,9 +377,12 @@ public class CustomFormatDataComparerTest
Implementation = "ReleaseTitleSpecification",
Negate = false,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = "\\bEVO(TGX)?\\b"
new CustomFormatFieldData
{
Value = "\\bEVO(TGX)?\\b"
}
}
},
new CustomFormatSpecificationData
@ -345,9 +391,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 7
new CustomFormatFieldData
{
Value = 7
}
}
},
new CustomFormatSpecificationData
@ -356,9 +405,12 @@ public class CustomFormatDataComparerTest
Implementation = "SourceSpecification",
Negate = true,
Required = true,
Fields = new CustomFormatFieldData
Fields = new[]
{
Value = 8
new CustomFormatFieldData
{
Value = 8
}
}
}
}

@ -0,0 +1,101 @@
using Flurl.Http.Configuration;
using Recyclarr.TrashLib.Json;
using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.Tests.Pipelines.CustomFormat.Models;
[TestFixture]
[Parallelizable(ParallelScope.All)]
public class FieldsArrayJsonConverterTest
{
[Test]
public void Read_multiple_as_array()
{
var serializer = new NewtonsoftJsonSerializer(ServiceJsonSerializerFactory.Settings);
const string json = @"
{
'fields': [
{
'order': 0,
'name': 'min',
'label': 'Minimum Size',
'unit': 'GB',
'helpText': 'Release must be greater than this size',
'value': 25,
'type': 'number',
'advanced': false
},
{
'order': 1,
'name': 'max',
'label': 'Maximum Size',
'unit': 'GB',
'helpText': 'Release must be less than or equal to this size',
'value': 40,
'type': 'number',
'advanced': false
}
]
}
";
var result = serializer.Deserialize<CustomFormatSpecificationData>(json);
result.Fields.Should().BeEquivalentTo(new[]
{
new CustomFormatFieldData
{
Value = 25
},
new CustomFormatFieldData
{
Value = 40
}
});
}
[Test]
public void Read_single_as_array()
{
var serializer = new NewtonsoftJsonSerializer(ServiceJsonSerializerFactory.Settings);
const string json = @"
{
'fields': {
'order': 0,
'name': 'min',
'label': 'Minimum Size',
'unit': 'GB',
'helpText': 'Release must be greater than this size',
'value': 25,
'type': 'number',
'advanced': false
}
}
";
var result = serializer.Deserialize<CustomFormatSpecificationData>(json);
result.Fields.Should().BeEquivalentTo(new[]
{
new CustomFormatFieldData
{
Value = 25
}
});
}
[Test]
public void Read_throws_on_unsupported_token_type()
{
var serializer = new NewtonsoftJsonSerializer(ServiceJsonSerializerFactory.Settings);
const string json = @"
{
'fields': 0
}
";
var act = () => serializer.Deserialize<CustomFormatSpecificationData>(json);
act.Should().Throw<InvalidOperationException>();
}
}

@ -5,7 +5,7 @@ using Recyclarr.TrashLib.Pipelines.CustomFormat.Models;
namespace Recyclarr.TrashLib.Pipelines.CustomFormat.Api;
internal class CustomFormatService : ICustomFormatService
public class CustomFormatService : ICustomFormatService
{
private readonly IServiceRequestBuilder _service;

@ -19,7 +19,7 @@ public record CustomFormatSpecificationData
public bool Required { get; init; }
[JsonConverter(typeof(FieldsArrayJsonConverter))]
public CustomFormatFieldData Fields { get; init; } = new();
public IReadOnlyCollection<CustomFormatFieldData> Fields { get; init; } = Array.Empty<CustomFormatFieldData>();
}
public record CustomFormatData

@ -39,10 +39,29 @@ public sealed class CustomFormatDataEqualityComparer : IEqualityComparer<CustomF
private static bool SpecificationEqual(CustomFormatSpecificationData a, CustomFormatSpecificationData b)
{
return a.Name.Equals(b.Name, StringComparison.Ordinal) &&
Equals(a.Fields.Value, b.Fields.Value) &&
a.Implementation.Equals(b.Implementation, StringComparison.Ordinal) &&
a.Negate.Equals(b.Negate) &&
a.Required.Equals(b.Required);
a.Required.Equals(b.Required) &&
AllFieldsEqual(a.Fields, b.Fields);
}
private static bool AllFieldsEqual(
IReadOnlyCollection<CustomFormatFieldData> first,
IReadOnlyCollection<CustomFormatFieldData> second)
{
if (first.Count != second.Count)
{
return false;
}
return first
.FullJoin(second, x => x.Name, _ => false, _ => false, FieldEqual)
.All(x => x);
}
private static bool FieldEqual(CustomFormatFieldData a, CustomFormatFieldData b)
{
return a.Value?.Equals(b.Value) ?? false;
}
public int GetHashCode(CustomFormatData obj)

@ -7,22 +7,7 @@ public class FieldsArrayJsonConverter : JsonConverter
{
public override void WriteJson(JsonWriter writer, object? value, JsonSerializer serializer)
{
if (value is not CustomFormatFieldData)
{
serializer.Serialize(writer, value);
return;
}
var token = JToken.FromObject(value);
if (token.Type == JTokenType.Object)
{
var array = new JArray(token);
serializer.Serialize(writer, array);
}
else
{
serializer.Serialize(writer, token);
}
serializer.Serialize(writer, value);
}
public override object? ReadJson(
@ -31,17 +16,14 @@ public class FieldsArrayJsonConverter : JsonConverter
object? existingValue,
JsonSerializer serializer)
{
if (existingValue is not CustomFormatFieldData)
{
return new CustomFormatFieldData();
}
var token = JToken.Load(reader);
// ReSharper disable once SwitchExpressionHandlesSomeKnownEnumValuesWithExceptionInDefault
return token.Type switch
{
JTokenType.Object => token.ToObject<CustomFormatFieldData>(),
JTokenType.Array => token.ToObject<CustomFormatFieldData[]>()?.SingleOrDefault(),
_ => null
JTokenType.Object => new[] {token.ToObject<CustomFormatFieldData>()},
JTokenType.Array => token.ToObject<CustomFormatFieldData[]>(),
_ => throw new InvalidOperationException("Unsupported token type for CustomFormatFieldData")
};
}

Loading…
Cancel
Save