|
2 | 2 | // The .NET Foundation licenses this file to you under the MIT license.
|
3 | 3 |
|
4 | 4 | using System.Diagnostics;
|
| 5 | +using System.Diagnostics.CodeAnalysis; |
| 6 | +using System.Diagnostics.Contracts; |
5 | 7 | using System.Reflection;
|
| 8 | +using System.Runtime.CompilerServices; |
6 | 9 | using System.Text.Json;
|
7 | 10 | using System.Text.Json.Serialization;
|
| 11 | +using Docfx.Build.ApiPage; |
8 | 12 | using OneOf;
|
9 | 13 |
|
10 | 14 | #nullable enable
|
@@ -38,6 +42,9 @@ private class OneOfJsonConverter<T> : JsonConverter<T> where T : IOneOf
|
38 | 42 | // It also depends on marking discriminator properties as required.
|
39 | 43 | foreach (var (type, cast) in s_types)
|
40 | 44 | {
|
| 45 | + if (!IsDeserializableType(ref reader, type, typeToConvert)) |
| 46 | + continue; |
| 47 | + |
41 | 48 | try
|
42 | 49 | {
|
43 | 50 | Utf8JsonReader readerCopy = reader;
|
@@ -77,5 +84,177 @@ private static (Type type, MethodInfo cast)[] GetOneOfTypes()
|
77 | 84 | }
|
78 | 85 | throw new InvalidOperationException($"{typeof(T)} isn't OneOf or OneOfBase");
|
79 | 86 | }
|
| 87 | + |
| 88 | + /// <summary> |
| 89 | + /// Helper method to check it can deserialize to specified type. |
| 90 | + /// </summary> |
| 91 | + /// <param name="reader">Current reader.</param> |
| 92 | + /// <param name="type">The type that to be deserialized by JsonSerializer.</param> |
| 93 | + /// <param name="typeToConvert">The type that to be converted by JsonConverter.</param> |
| 94 | + private static bool IsDeserializableType(ref Utf8JsonReader reader, Type type, Type typeToConvert) |
| 95 | + { |
| 96 | + var tokenType = reader.TokenType; |
| 97 | + switch (tokenType) |
| 98 | + { |
| 99 | + case JsonTokenType.String: |
| 100 | + if (type == typeof(bool) && typeToConvert == typeof(OneOf<bool, string>)) |
| 101 | + return false; |
| 102 | + |
| 103 | + Assert(type, [typeof(string), typeof(Span)]); |
| 104 | + Assert(typeToConvert, [typeof(Span), typeof(Inline), typeof(OneOf<string, string[]>), typeof(OneOf<bool, string>)]); |
| 105 | + |
| 106 | + return true; |
| 107 | + |
| 108 | + case JsonTokenType.StartArray: |
| 109 | + Assert(type, [typeof(string), typeof(string[]), typeof(Span), typeof(Span[])]); |
| 110 | + Assert(typeToConvert, [typeof(Inline), typeof(OneOf<string, string[]>)]); |
| 111 | + |
| 112 | + return type.IsArray; |
| 113 | + |
| 114 | + case JsonTokenType.StartObject: |
| 115 | + if (!TryGetFirstPropertyName(ref reader, out var propertyName)) |
| 116 | + return false; |
| 117 | + |
| 118 | + var key = (typeToConvert, type, propertyName); |
| 119 | + |
| 120 | + if (KnownTypes.Contains(key)) |
| 121 | + return true; |
| 122 | + |
| 123 | + if (KnownTypesToSkip.Contains(key)) |
| 124 | + return false; |
| 125 | + |
| 126 | + // Unknown type/name combinations found. |
| 127 | + // Fallback to default behavior. |
| 128 | + return true; |
| 129 | + |
| 130 | + default: |
| 131 | + return true; |
| 132 | + } |
| 133 | + } |
| 134 | + |
| 135 | + private static bool TryGetFirstPropertyName(ref Utf8JsonReader reader, [NotNullWhen(true)] out string? propertyName) |
| 136 | + { |
| 137 | + Contract.Assert(reader.TokenType == JsonTokenType.StartObject); |
| 138 | + |
| 139 | + var readerCopy = reader; |
| 140 | + if (readerCopy.Read() && readerCopy.TokenType == JsonTokenType.PropertyName) |
| 141 | + { |
| 142 | + propertyName = readerCopy.GetString()!; |
| 143 | + return true; |
| 144 | + } |
| 145 | + |
| 146 | + propertyName = null; |
| 147 | + return false; |
| 148 | + } |
| 149 | + |
| 150 | + [Conditional("DEBUG")] |
| 151 | + private static void Assert( |
| 152 | + Type type, |
| 153 | + Type[] expectedTypes, |
| 154 | + [CallerArgumentExpression(nameof(expectedTypes))] string? message = null) |
| 155 | + { |
| 156 | + if (!expectedTypes.Contains(type)) |
| 157 | + throw new InvalidOperationException($"{type.Name} is not expected. Expected: {message}"); |
| 158 | + } |
| 159 | + |
| 160 | + /// <summary> |
| 161 | + /// Known type/name combinations that can be deserialize. |
| 162 | + /// </summary> |
| 163 | + private static readonly HashSet<(Type, Type, string)> KnownTypes = |
| 164 | + [ |
| 165 | + // Block : OneOfBase<Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code> |
| 166 | + (typeof(Block), typeof(Heading), "h1"), |
| 167 | + (typeof(Block), typeof(Heading), "h2"), |
| 168 | + (typeof(Block), typeof(Heading), "h3"), |
| 169 | + (typeof(Block), typeof(Heading), "h4"), |
| 170 | + (typeof(Block), typeof(Heading), "h5"), |
| 171 | + (typeof(Block), typeof(Heading), "h6"), |
| 172 | + (typeof(Block), typeof(Api), "api1"), |
| 173 | + (typeof(Block), typeof(Api), "api2"), |
| 174 | + (typeof(Block), typeof(Api), "api3"), |
| 175 | + (typeof(Block), typeof(Api), "api4"), |
| 176 | + (typeof(Block), typeof(Markdown), "markdown"), |
| 177 | + (typeof(Block), typeof(Facts), "facts"), |
| 178 | + (typeof(Block), typeof(Parameters), "parameters"), |
| 179 | + (typeof(Block), typeof(List), "list"), |
| 180 | + (typeof(Block), typeof(Inheritance), "inheritance"), |
| 181 | + (typeof(Block), typeof(Code), "code"), |
| 182 | + |
| 183 | + // Heading : OneOfBase<H1, H2, H3, H4, H5, H6> |
| 184 | + (typeof(Heading), typeof(H1), "h1"), |
| 185 | + (typeof(Heading), typeof(H2), "h2"), |
| 186 | + (typeof(Heading), typeof(H3), "h3"), |
| 187 | + (typeof(Heading), typeof(H4), "h4"), |
| 188 | + (typeof(Heading), typeof(H5), "h5"), |
| 189 | + (typeof(Heading), typeof(H6), "h6"), |
| 190 | + |
| 191 | + // Api : OneOfBase<Api1, Api2, Api3, Api4> |
| 192 | + (typeof(Api), typeof(Api1), "api1"), |
| 193 | + (typeof(Api), typeof(Api2), "api2"), |
| 194 | + (typeof(Api), typeof(Api3), "api3"), |
| 195 | + (typeof(Api), typeof(Api4), "api4"), |
| 196 | + |
| 197 | + // Span : OneOfBase<string, LinkSpan> |
| 198 | + (typeof(Span), typeof(LinkSpan), "text"), |
| 199 | + |
| 200 | + // Inline : OneOfBase<Span, Span[]> |
| 201 | + (typeof(Inline), typeof(Span), "text"), |
| 202 | + ]; |
| 203 | + |
| 204 | + /// <summary> |
| 205 | + /// Known type/name combinations that can not be deserialize. |
| 206 | + /// </summary> |
| 207 | + private static readonly HashSet<(Type, Type, string)> KnownTypesToSkip = |
| 208 | + [ |
| 209 | + // Block : OneOfBase<Heading, Api, Markdown, Facts, Parameters, List, Inheritance, Code> |
| 210 | + (typeof(Heading), typeof(H1), "h2"), |
| 211 | + (typeof(Heading), typeof(H1), "h3"), |
| 212 | + (typeof(Heading), typeof(H2), "h3"), |
| 213 | + (typeof(Heading), typeof(H1), "h4"), |
| 214 | + (typeof(Heading), typeof(H2), "h4"), |
| 215 | + (typeof(Heading), typeof(H3), "h4"), |
| 216 | + (typeof(Block), typeof(Heading), "api1"), |
| 217 | + (typeof(Block), typeof(Heading), "api2"), |
| 218 | + (typeof(Block), typeof(Heading), "api3"), |
| 219 | + (typeof(Block), typeof(Heading), "api4"), |
| 220 | + (typeof(Block), typeof(Heading), "markdown"), |
| 221 | + (typeof(Block), typeof(Api), "markdown"), |
| 222 | + (typeof(Block), typeof(Heading), "facts"), |
| 223 | + (typeof(Block), typeof(Api), "facts"), |
| 224 | + (typeof(Block), typeof(Markdown), "facts"), |
| 225 | + (typeof(Block), typeof(Heading), "parameters"), |
| 226 | + (typeof(Block), typeof(Api), "parameters"), |
| 227 | + (typeof(Block), typeof(Markdown), "parameters"), |
| 228 | + (typeof(Block), typeof(Facts), "parameters"), |
| 229 | + (typeof(Block), typeof(Heading), "list"), |
| 230 | + (typeof(Block), typeof(Api), "list"), |
| 231 | + (typeof(Block), typeof(Markdown), "list"), |
| 232 | + (typeof(Block), typeof(Facts), "list"), |
| 233 | + (typeof(Block), typeof(Parameters), "list"), |
| 234 | + (typeof(Block), typeof(Heading), "inheritance"), |
| 235 | + (typeof(Block), typeof(Api), "inheritance"), |
| 236 | + (typeof(Block), typeof(Markdown), "inheritance"), |
| 237 | + (typeof(Block), typeof(Facts), "inheritance"), |
| 238 | + (typeof(Block), typeof(Parameters), "inheritance"), |
| 239 | + (typeof(Block), typeof(List), "inheritance"), |
| 240 | + (typeof(Block), typeof(Heading), "code"), |
| 241 | + (typeof(Block), typeof(Api), "code"), |
| 242 | + (typeof(Block), typeof(Markdown), "code"), |
| 243 | + (typeof(Block), typeof(Facts), "code"), |
| 244 | + (typeof(Block), typeof(Parameters), "code"), |
| 245 | + (typeof(Block), typeof(List), "code"), |
| 246 | + (typeof(Block), typeof(Inheritance), "code"), |
| 247 | + |
| 248 | + // OneOfBase<Api1, Api2, Api3, Api4> |
| 249 | + (typeof(Api), typeof(Api1), "api2"), |
| 250 | + (typeof(Api), typeof(Api1), "api3"), |
| 251 | + (typeof(Api), typeof(Api2), "api3"), |
| 252 | + (typeof(Api), typeof(Api1), "api4"), |
| 253 | + (typeof(Api), typeof(Api2), "api4"), |
| 254 | + (typeof(Api), typeof(Api3), "api4"), |
| 255 | + |
| 256 | + // OneOfBase<string, LinkSpan> |
| 257 | + (typeof(Span), typeof(String), "text"), |
| 258 | + ]; |
80 | 259 | }
|
81 | 260 | }
|
0 commit comments