Skip to content
Merged
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
179 changes: 179 additions & 0 deletions src/Docfx.Build/OneOfJsonConverterFactory.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Diagnostics.Contracts;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Text.Json;
using System.Text.Json.Serialization;
using Docfx.Build.ApiPage;
using OneOf;

#nullable enable
Expand Down Expand Up @@ -38,6 +42,9 @@ private class OneOfJsonConverter<T> : JsonConverter<T> where T : IOneOf
// It also depends on marking discriminator properties as required.
foreach (var (type, cast) in s_types)
{
if (!IsDeserializableType(ref reader, type, typeToConvert))
continue;

try
{
Utf8JsonReader readerCopy = reader;
Expand Down Expand Up @@ -77,5 +84,177 @@ private static (Type type, MethodInfo cast)[] GetOneOfTypes()
}
throw new InvalidOperationException($"{typeof(T)} isn't OneOf or OneOfBase");
}

/// <summary>
/// Helper method to check it can deserialize to specified type.
/// </summary>
/// <param name="reader">Current reader.</param>
/// <param name="type">The type that to be deserialized by JsonSerializer.</param>
/// <param name="typeToConvert">The type that to be converted by JsonConverter.</param>
private static bool IsDeserializableType(ref Utf8JsonReader reader, Type type, Type typeToConvert)
{
var tokenType = reader.TokenType;
switch (tokenType)
{
case JsonTokenType.String:
if (type == typeof(bool) && typeToConvert == typeof(OneOf<bool, string>))
return false;

Assert(type, [typeof(string), typeof(Span)]);
Assert(typeToConvert, [typeof(Span), typeof(Inline), typeof(OneOf<string, string[]>), typeof(OneOf<bool, string>)]);

return true;

case JsonTokenType.StartArray:
Assert(type, [typeof(string), typeof(string[]), typeof(Span), typeof(Span[])]);
Assert(typeToConvert, [typeof(Inline), typeof(OneOf<string, string[]>)]);

return type.IsArray;

case JsonTokenType.StartObject:
if (!TryGetFirstPropertyName(ref reader, out var propertyName))
return false;

var key = (typeToConvert, type, propertyName);

if (KnownTypes.Contains(key))
return true;

if (KnownTypesToSkip.Contains(key))
return false;

// Unknown type/name combinations found.
// Fallback to default behavior.
return true;

default:
return true;
}
}

private static bool TryGetFirstPropertyName(ref Utf8JsonReader reader, [NotNullWhen(true)] out string? propertyName)
{
Contract.Assert(reader.TokenType == JsonTokenType.StartObject);

var readerCopy = reader;
if (readerCopy.Read() && readerCopy.TokenType == JsonTokenType.PropertyName)
{
propertyName = readerCopy.GetString()!;
return true;
}

propertyName = null;
return false;
}

[Conditional("DEBUG")]
private static void Assert(
Type type,
Type[] expectedTypes,
[CallerArgumentExpression(nameof(expectedTypes))] string? message = null)
{
if (!expectedTypes.Contains(type))
throw new InvalidOperationException($"{type.Name} is not expected. Expected: {message}");
}

/// <summary>
/// Known type/name combinations that can be deserialize.
/// </summary>
private static readonly HashSet<(Type, Type, string)> KnownTypes =
[
// Block : OneOfBase<Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code>
(typeof(Block), typeof(Heading), "h1"),
(typeof(Block), typeof(Heading), "h2"),
(typeof(Block), typeof(Heading), "h3"),
(typeof(Block), typeof(Heading), "h4"),
(typeof(Block), typeof(Heading), "h5"),
(typeof(Block), typeof(Heading), "h6"),
(typeof(Block), typeof(Api), "api1"),
(typeof(Block), typeof(Api), "api2"),
(typeof(Block), typeof(Api), "api3"),
(typeof(Block), typeof(Api), "api4"),
(typeof(Block), typeof(Markdown), "markdown"),
(typeof(Block), typeof(Facts), "facts"),
(typeof(Block), typeof(Parameters), "parameters"),
(typeof(Block), typeof(List), "list"),
(typeof(Block), typeof(Inheritance), "inheritance"),
(typeof(Block), typeof(Code), "code"),

// Heading : OneOfBase<H1, H2, H3, H4, H5, H6>
(typeof(Heading), typeof(H1), "h1"),
(typeof(Heading), typeof(H2), "h2"),
(typeof(Heading), typeof(H3), "h3"),
(typeof(Heading), typeof(H4), "h4"),
(typeof(Heading), typeof(H5), "h5"),
(typeof(Heading), typeof(H6), "h6"),

// Api : OneOfBase<Api1, Api2, Api3, Api4>
(typeof(Api), typeof(Api1), "api1"),
(typeof(Api), typeof(Api2), "api2"),
(typeof(Api), typeof(Api3), "api3"),
(typeof(Api), typeof(Api4), "api4"),

// Span : OneOfBase<string, LinkSpan>
(typeof(Span), typeof(LinkSpan), "text"),

// Inline : OneOfBase<Span, Span[]>
(typeof(Inline), typeof(Span), "text"),
];

/// <summary>
/// Known type/name combinations that can not be deserialize.
/// </summary>
private static readonly HashSet<(Type, Type, string)> KnownTypesToSkip =
[
// Block : OneOfBase<Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code>
(typeof(Heading), typeof(H1), "h2"),
(typeof(Heading), typeof(H1), "h3"),
(typeof(Heading), typeof(H2), "h3"),
(typeof(Heading), typeof(H1), "h4"),
(typeof(Heading), typeof(H2), "h4"),
(typeof(Heading), typeof(H3), "h4"),
(typeof(Block), typeof(Heading), "api1"),
(typeof(Block), typeof(Heading), "api2"),
(typeof(Block), typeof(Heading), "api3"),
(typeof(Block), typeof(Heading), "api4"),
(typeof(Block), typeof(Heading), "markdown"),
(typeof(Block), typeof(Api), "markdown"),
(typeof(Block), typeof(Heading), "facts"),
(typeof(Block), typeof(Api), "facts"),
(typeof(Block), typeof(Markdown), "facts"),
(typeof(Block), typeof(Heading), "parameters"),
(typeof(Block), typeof(Api), "parameters"),
(typeof(Block), typeof(Markdown), "parameters"),
(typeof(Block), typeof(Facts), "parameters"),
(typeof(Block), typeof(Heading), "list"),
(typeof(Block), typeof(Api), "list"),
(typeof(Block), typeof(Markdown), "list"),
(typeof(Block), typeof(Facts), "list"),
(typeof(Block), typeof(Parameters), "list"),
(typeof(Block), typeof(Heading), "inheritance"),
(typeof(Block), typeof(Api), "inheritance"),
(typeof(Block), typeof(Markdown), "inheritance"),
(typeof(Block), typeof(Facts), "inheritance"),
(typeof(Block), typeof(Parameters), "inheritance"),
(typeof(Block), typeof(List), "inheritance"),
(typeof(Block), typeof(Heading), "code"),
(typeof(Block), typeof(Api), "code"),
(typeof(Block), typeof(Markdown), "code"),
(typeof(Block), typeof(Facts), "code"),
(typeof(Block), typeof(Parameters), "code"),
(typeof(Block), typeof(List), "code"),
(typeof(Block), typeof(Inheritance), "code"),

// OneOfBase<Api1, Api2, Api3, Api4>
(typeof(Api), typeof(Api1), "api2"),
(typeof(Api), typeof(Api1), "api3"),
(typeof(Api), typeof(Api2), "api3"),
(typeof(Api), typeof(Api1), "api4"),
(typeof(Api), typeof(Api2), "api4"),
(typeof(Api), typeof(Api3), "api4"),

// OneOfBase<string, LinkSpan>
(typeof(Span), typeof(String), "text"),
];
}
}