Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implement OneOfJsonConverter.Write #26

Merged
merged 3 commits into from
May 3, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 13 additions & 0 deletions src/Apple.AppStoreConnect/Converters/ITypedReader.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Reflection;
using System.Text.Json;

namespace Apple.AppStoreConnect.Converters;

public interface ITypedReader<TOneOf>
{
void Read(
TOneOf oneOf,
PropertyInfo propertyInfo,
ref Utf8JsonReader reader, JsonSerializerOptions options
);
}
13 changes: 13 additions & 0 deletions src/Apple.AppStoreConnect/Converters/ITypedWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
using System.Reflection;
using System.Text.Json;

namespace Apple.AppStoreConnect.Converters;

public interface ITypedWriter<TOneOf>
{
void Write(
TOneOf oneOf,
PropertyInfo propertyInfo,
Utf8JsonWriter writer, JsonSerializerOptions options
);
}
106 changes: 78 additions & 28 deletions src/Apple.AppStoreConnect/Converters/OneOfJsonConverter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,39 +14,26 @@ namespace Apple.AppStoreConnect.Converters;
public class OneOfJsonConverter<TOneOf> : JsonConverter<TOneOf>
where TOneOf : OneOf, new()
{
private readonly PropertyInfo? _oneOfDiscriminator;
private readonly PropertyInfo? _oneOfDiscriminator = typeof(TOneOf).GetProperty("OneOfType");

private readonly ILogger _logger;
private readonly IDictionary<string, PropertyInfo> _jsonTypeMap = new Dictionary<string, PropertyInfo>();
private readonly IDictionary<string, PropertyInfo> _jsonTypeMap;
private readonly IDictionary<string, Enum> _oneOfDiscriminators = new Dictionary<string, Enum>();
private readonly IDictionary<Enum, string> _reversedOneOfDiscriminators = new Dictionary<Enum, string>();

public OneOfJsonConverter(
ILogger logger
)
{
_logger = logger;
_oneOfDiscriminator = typeof(TOneOf).GetProperty("OneOfType");

BuildJsonTypeMap();
_jsonTypeMap = BuildJsonTypeMap();
BuildOneOfDiscriminatorsMap(_jsonTypeMap);
}

private void BuildJsonTypeMap()
private IDictionary<string, PropertyInfo> BuildJsonTypeMap()
{
if (_oneOfDiscriminator is null)
{
_logger.LogError("The {OneOfType} does not contain 'OneOfType' property", typeof(TOneOf));
return;
}

var oneOfDiscriminatorType = _oneOfDiscriminator.PropertyType.GetNotNullableType();

foreach (Enum enumValue in oneOfDiscriminatorType.GetEnumValues())
{
_oneOfDiscriminators.Add(
Enum.GetName(oneOfDiscriminatorType, enumValue)!,
enumValue
);
}
var jsonTypeMap = new Dictionary<string, PropertyInfo>();

foreach (var propertyInfo in typeof(TOneOf).GetProperties())
{
Expand Down Expand Up @@ -92,8 +79,52 @@ private void BuildJsonTypeMap()
continue;
}

_jsonTypeMap.Add(fieldTypeValues.ElementAt(0), propertyInfo);
jsonTypeMap.Add(fieldTypeValues.ElementAt(0), propertyInfo);
}

return jsonTypeMap;
}

private void BuildOneOfDiscriminatorsMap(IDictionary<string, PropertyInfo> jsonTypeMap)
{
if (_oneOfDiscriminator is null)
{
_logger.LogError("The {OneOfType} does not contain 'OneOfType' property", typeof(TOneOf));
return;
}

var oneOfDiscriminatorType = _oneOfDiscriminator.PropertyType.GetNotNullableType();

foreach (Enum enumValue in oneOfDiscriminatorType.GetEnumValues())
{
var enumName = Enum.GetName(oneOfDiscriminatorType, enumValue)!;

_oneOfDiscriminators.Add(enumName, enumValue);
}

foreach (var (typeName, propertyInfo) in jsonTypeMap)
{
_reversedOneOfDiscriminators.Add(_oneOfDiscriminators[propertyInfo.Name], typeName);
}
}


private static ITypedReader<TOneOf> CreateTypedReader(PropertyInfo propertyInfo)
{
var typedReaderType = typeof(TypedReader<,>).MakeGenericType(
typeof(TOneOf), propertyInfo.PropertyType
);

return (ITypedReader<TOneOf>) Activator.CreateInstance(typedReaderType)!;
}

private static ITypedWriter<TOneOf> CreateTypedWriter(PropertyInfo propertyInfo)
{
var typedWriterType = typeof(TypedWriter<,>).MakeGenericType(
typeof(TOneOf), propertyInfo.PropertyType
);

return (ITypedWriter<TOneOf>) Activator.CreateInstance(typedWriterType)!;
}

public override TOneOf? Read(
Expand Down Expand Up @@ -137,12 +168,7 @@ private void BuildJsonTypeMap()
{
var oneOfEnvelope = new TOneOf();

var typedReaderType = typeof(TypedReader<,>).MakeGenericType(
typeof(TOneOf), propertyInfo.PropertyType
);
var typedReader = (TypedReader<TOneOf>) Activator.CreateInstance(typedReaderType)!;

typedReader.Read(
CreateTypedReader(propertyInfo).Read(
oneOfEnvelope,
propertyInfo,
ref reader, options
Expand Down Expand Up @@ -180,6 +206,30 @@ out var oneOfDiscriminator

public override void Write(Utf8JsonWriter writer, TOneOf value, JsonSerializerOptions options)
{
throw new NotImplementedException();
if (_oneOfDiscriminator is null)
{
_logger.LogError("The {OneOfType} does not contain 'OneOfType' property", typeof(TOneOf));
return;
}

var oneOfTypeValue = (Enum?) _oneOfDiscriminator.GetValue(value);

if (oneOfTypeValue is null)
{
_logger.LogError("The 'OneOfType' property in the {OneOfType} bust be set", typeof(TOneOf));
return;
}

if (
_reversedOneOfDiscriminators.TryGetValue(oneOfTypeValue, out var typeName)
&& _jsonTypeMap.TryGetValue(typeName, out var propertyInfo)
)
{
CreateTypedWriter(propertyInfo).Write(
value,
propertyInfo,
writer, options
);
}
}
}
13 changes: 2 additions & 11 deletions src/Apple.AppStoreConnect/Converters/TypedReader.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,11 @@

namespace Apple.AppStoreConnect.Converters;

public abstract class TypedReader<TOneOf>
{
public abstract void Read(
TOneOf oneOf,
PropertyInfo propertyInfo,
ref Utf8JsonReader reader, JsonSerializerOptions options
);
}

public sealed class TypedReader<TOneOf, TTargetType> : TypedReader<TOneOf>
public readonly struct TypedReader<TOneOf, TTargetType> : ITypedReader<TOneOf>
where TOneOf : OneOf
where TTargetType : class
{
public override void Read(
public void Read(
TOneOf oneOf,
PropertyInfo propertyInfo,
ref Utf8JsonReader reader, JsonSerializerOptions options
Expand Down
29 changes: 29 additions & 0 deletions src/Apple.AppStoreConnect/Converters/TypedWriter.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Serialization;

namespace Apple.AppStoreConnect.Converters;

public readonly struct TypedWriter<TOneOf, TTargetType> : ITypedWriter<TOneOf>
where TOneOf : OneOf
where TTargetType : class
{
public void Write(
TOneOf oneOf,
PropertyInfo propertyInfo,
Utf8JsonWriter writer, JsonSerializerOptions options
)
{
var innerValue = (TTargetType?) propertyInfo.GetValue(oneOf);

if (innerValue is null)
{
writer.WriteNullValue();
return;
}

var targetConverter = (JsonConverter<TTargetType>) options.GetConverter(propertyInfo.PropertyType);

targetConverter.Write(writer, innerValue, options);
}
}
105 changes: 93 additions & 12 deletions src/Test.Apple.AppStoreConnect/Converters/OneOfJsonConverterTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,14 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
using System.Text.Json;
using System.Text.Json.Serialization;
using Xunit;

namespace Test.Apple.AppStoreConnect.Converters;

public class OneOfJsonConverterTests
{
[Theory]
[MemberData(nameof(OneOfData))]
[MemberData(nameof(OneOfJsonData))]
public void DeserializeOneOfWorks(string json)
{
using var loggerFactory = new LoggerFactory();
Expand Down Expand Up @@ -57,7 +56,60 @@ public void DeserializeOneOfWorks(string json)
}
}

public static IEnumerable<object[]> OneOfData()
[Theory]
[MemberData(nameof(OneOfObjectData))]
public void SerializeOneOfWorks(TestingAppAvailabilityResponseIncluded oneOf)
{
using var loggerFactory = new LoggerFactory();

var json = JsonSerializer.Serialize(
oneOf,
new JsonSerializerOptions
{
WriteIndented = true,
Converters =
{
new JsonStringEnumConverterFactory(),
new OneOfJsonConverterFactory(loggerFactory),
}
}
);

Assert.NotNull(json);

switch (oneOf.OneOfType)
{
case AppAvailabilityResponseIncludedEnum.App:
Assert.Equal("""
{
"type": "apps",
"id": "app-id",
"links": {
"self": "self-url"
}
}
""".Replace("\r\n", Environment.NewLine), json);
break;
case AppAvailabilityResponseIncludedEnum.Territory:
Assert.Equal("""
{
"type": "territories",
"id": "territory-id",
"links": {
"self": "self-url"
}
}
""".Replace("\r\n", Environment.NewLine), json);
break;
default:
throw new ArgumentOutOfRangeException(
nameof(oneOf.OneOfType), oneOf.OneOfType,
$"Unexpected {nameof(oneOf.OneOfType)} '{oneOf.OneOfType}'"
);
}
}

public static IEnumerable<object[]> OneOfJsonData()
{
yield return new object[]
{
Expand All @@ -79,7 +131,43 @@ public static IEnumerable<object[]> OneOfData()
};
}

private enum AppAvailabilityResponseIncludedEnum
public static IEnumerable<object[]> OneOfObjectData()
{
yield return new object[]
{
new TestingAppAvailabilityResponseIncluded
{
OneOfType = AppAvailabilityResponseIncludedEnum.App,
App = new App
{
Type = AppType.Apps,
Id = "app-id",
Links = new ResourceLinks
{
Self = "self-url",
},
}
},
};
yield return new object[]
{
new TestingAppAvailabilityResponseIncluded
{
OneOfType = AppAvailabilityResponseIncludedEnum.Territory,
Territory = new Territory
{
Type = TerritoryType.Territories,
Id = "territory-id",
Links = new ResourceLinks
{
Self = "self-url",
},
}
},
};
}

public enum AppAvailabilityResponseIncludedEnum
{
[EnumMember(Value = @"App")]
App = 0,
Expand All @@ -88,19 +176,12 @@ private enum AppAvailabilityResponseIncludedEnum
Territory = 1,
}

private record TestingAppAvailabilityResponseIncluded : OneOf
public record TestingAppAvailabilityResponseIncluded : OneOf
{
[JsonPropertyName("OneOfType")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
[JsonConverter(typeof(JsonStringEnumConverter))]
public AppAvailabilityResponseIncludedEnum? OneOfType { get; set; }

[JsonPropertyName("App")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public App? App { get; set; }

[JsonPropertyName("Territory")]
[JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
public Territory? Territory { get; set; }
}
}