Skip to content
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
91 changes: 45 additions & 46 deletions src/Framework/Framework/Utils/SystemTextJsonUtils.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,56 +14,55 @@ static class SystemTextJsonUtils
/// <summary> Returns the property path to an unfinished JSON value </summary>
public static string[] GetFailurePath(ReadOnlySpan<byte> data)
{
// TODO: tests
var reader = new Utf8JsonReader(data, false, default);
var path = new Stack<(string? name, int index)>();
var isArray = false;
int arrayIndex = 0;
while (reader.Read())
// configure higher max depth than default (64), to correctly display path of
// the "max depth exceeded" error
var options = new JsonReaderOptions { MaxDepth = 196 };
var reader = new Utf8JsonReader(data, false, new JsonReaderState(options));
if (!reader.Read())
return [];
return GetFailurePathInternal(ref reader) ?? throw new Exception("No error in specified JSON");
}

private static string[]? GetFailurePathInternal(ref Utf8JsonReader reader)
{
if (reader.TokenType == JsonTokenType.None)
return [];
else if (reader.TokenType == JsonTokenType.StartObject)
{
if (!reader.Read()) return [];
string? lastProperty = null;
while (reader.TokenType == JsonTokenType.PropertyName)
{
lastProperty = reader.GetString().NotNull();
if (!reader.Read())
return [lastProperty];
if (GetFailurePathInternal(ref reader) is {} nestedError)
return [lastProperty, ..nestedError];
if (!reader.Read())
return [];
}
if (reader.TokenType != JsonTokenType.EndObject)
return lastProperty is null ? [] : [lastProperty];
return null;
}
else if (reader.TokenType == JsonTokenType.StartArray)
{
switch (reader.TokenType)
if (!reader.Read()) return ["0"];
int index = 0;
while (reader.TokenType != JsonTokenType.EndArray)
{
case JsonTokenType.StartObject:
if (isArray) {
isArray = false;
path.Push((null, arrayIndex));
}
break;
case JsonTokenType.Comment:
break;
case JsonTokenType.StartArray:
isArray = true;
arrayIndex = 0;
break;
case JsonTokenType.EndArray:
isArray = false;
break;
case JsonTokenType.True:
case JsonTokenType.False:
case JsonTokenType.Number:
case JsonTokenType.String:
case JsonTokenType.Null:
case JsonTokenType.EndObject:
if (!isArray) {
var old = path.Pop();
if (old.name is null) {
isArray = true;
arrayIndex = old.index + 1;
}
}
else {
arrayIndex++;
}
break;
case JsonTokenType.PropertyName:
path.Push((reader.GetString()!, -1));
break;
case JsonTokenType.None:
goto Done;
if (GetFailurePathInternal(ref reader) is {} nestedError)
return [$"{index}", ..nestedError];
index++;
if (!reader.Read() || reader.TokenType == JsonTokenType.None)
return [$"{index}"];
}
return null;
}
else
{
return null;
}
Done:
return path.Reverse().Select(n => n.name ?? n.index.ToString()).ToArray();
}

public static JsonElement? GetPropertyOrNull(this in JsonElement jsonObj, ReadOnlySpan<byte> name) =>
Expand Down
110 changes: 110 additions & 0 deletions src/Tests/ViewModel/JsonUtilsTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using DotVVM.Framework.ViewModel.Serialization;
using DotVVM.Framework.Utils;

namespace DotVVM.Framework.Tests.ViewModel
{
[TestClass]
public class JsonUtilsTests
{
const string IrrelevantObjectStart = """
"notthis": { "alsonotthis": "lala", "another": 1 },
"alsonot": [ 1, 2, 3, { "irrelevant": 1, "nope": null, "neither_this": true, "alsonot": false }, [], [[[[], []]]] ],
""";

[DataTestMethod]
[DataRow("{\"viewModel\":", "viewModel")]
[DataRow(
$$"""
{
"test": [
{ "notthis": 1, "myprop": 5},
{
"notthis": 1,
"myprop":
""",
"test/1/myprop"
)]
[DataRow(
$$"""
{ {{IrrelevantObjectStart}}
"test": [
{ "notthis": 1, "myprop": 5},
{
"notthis": 1,
"myprop":
""",
"test/1/myprop"
)]
[DataRow(
$$"""
{ {{IrrelevantObjectStart}}
"test": [ { {{IrrelevantObjectStart}} "myprop":{
""",
"test/0/myprop"
)]
[DataRow(
$$"""
{ {{IrrelevantObjectStart}}
"test2":
""",
"test2"
)]
[DataRow(
$$"""
{ {{IrrelevantObjectStart}}
"test3": { "ok": [1, 2, 3], "alsofine": {}
""",
"test3"
)]
[DataRow(
$$"""
{
"items": [
{ "id": 1 },
""",
"items/1"
)]
[DataRow(
$$"""
{
"items": [
{
"nested": [
1,
2,
""",
"items/0/nested/2"
)]
[DataRow(
$$"""
{
"items": [
""",
"items/0"
)]
[DataRow(
"""
{ "arr": [[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[[1, 2], []]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]]],
"prop": 1
""",
"prop"
)]
[DataRow(
"""
[
1, 2,
{ "ok": true },
{ "not_ok":
""",
"3/not_ok"
)]
[DataRow(" ", "")]
public void GetInvalidJsonErrorPath(string json, string expectedPath)
{
var utf8 = StringUtils.Utf8.GetBytes(json);
var path = SystemTextJsonUtils.GetFailurePath(utf8);
Assert.AreEqual(expectedPath, string.Join("/", path));
}
}
}