Skip to content

Commit 191e139

Browse files
committed
test example response from spec, fix conversion
1 parent d1b8d22 commit 191e139

File tree

9 files changed

+210
-25
lines changed

9 files changed

+210
-25
lines changed
Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
using System;
2+
using System.Numerics;
3+
using System.Text.Json;
4+
5+
namespace GraphQL.Client.Serializer.SystemTextJson
6+
{
7+
public static class ConverterHelperExtensions
8+
{
9+
public static object ReadNumber(this JsonElement value)
10+
{
11+
if (value.TryGetInt32(out int i))
12+
return i;
13+
else if (value.TryGetInt64(out long l))
14+
return l;
15+
else if (BigInteger.TryParse(value.GetRawText(), out var bi))
16+
return bi;
17+
else if (value.TryGetDouble(out double d))
18+
return d;
19+
else if (value.TryGetDecimal(out decimal dd))
20+
return dd;
21+
22+
throw new NotImplementedException($"Unexpected Number value. Raw text was: {value.GetRawText()}");
23+
}
24+
}
25+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text.Json;
5+
using System.Text.Json.Serialization;
6+
7+
namespace GraphQL.Client.Serializer.SystemTextJson
8+
{
9+
public class ErrorPathConverter : JsonConverter<ErrorPath>
10+
{
11+
12+
public override ErrorPath Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
13+
{
14+
using var doc = JsonDocument.ParseValue(ref reader);
15+
16+
if (doc?.RootElement == null || doc?.RootElement.ValueKind != JsonValueKind.Array)
17+
{
18+
throw new ArgumentException("This converter can only parse when the root element is a JSON Object.");
19+
}
20+
21+
return new ErrorPath(ReadArray(doc.RootElement));
22+
}
23+
24+
public override void Write(Utf8JsonWriter writer, ErrorPath value, JsonSerializerOptions options)
25+
=> throw new NotImplementedException(
26+
"This converter currently is only intended to be used to read a JSON object into a strongly-typed representation.");
27+
28+
private IEnumerable<object?> ReadArray(JsonElement value)
29+
{
30+
foreach (var item in value.EnumerateArray())
31+
{
32+
yield return ReadValue(item);
33+
}
34+
}
35+
36+
private object? ReadValue(JsonElement value)
37+
=> value.ValueKind switch
38+
{
39+
JsonValueKind.Number => value.ReadNumber(),
40+
JsonValueKind.True => true,
41+
JsonValueKind.False => false,
42+
JsonValueKind.String => value.GetString(),
43+
JsonValueKind.Null => null,
44+
JsonValueKind.Undefined => null,
45+
_ => throw new InvalidOperationException($"Unexpected value kind: {value.ValueKind}")
46+
};
47+
}
48+
}

src/GraphQL.Client.Serializer.SystemTextJson/ImmutableConverter.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,13 @@ public class ImmutableConverter : JsonConverter<object>
1515
{
1616
public override bool CanConvert(Type typeToConvert)
1717
{
18+
if (typeToConvert.IsPrimitive)
19+
return false;
20+
21+
var nullableUnderlyingType = Nullable.GetUnderlyingType(typeToConvert);
22+
if (nullableUnderlyingType != null && nullableUnderlyingType.IsPrimitive)
23+
return false;
24+
1825
bool result;
1926
var constructors = typeToConvert.GetConstructors(BindingFlags.Public | BindingFlags.Instance);
2027
if (constructors.Length != 1)

src/GraphQL.Client.Serializer.SystemTextJson/MapConverter.cs

Lines changed: 6 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -24,16 +24,16 @@ public override Map Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSeri
2424
throw new ArgumentException("This converter can only parse when the root element is a JSON Object.");
2525
}
2626

27-
return ReadDictionary<Map>(doc.RootElement);
27+
return ReadDictionary(doc.RootElement, new Map());
2828
}
2929

3030
public override void Write(Utf8JsonWriter writer, Map value, JsonSerializerOptions options)
3131
=> throw new NotImplementedException(
3232
"This converter currently is only intended to be used to read a JSON object into a strongly-typed representation.");
3333

34-
private TDictionary ReadDictionary<TDictionary>(JsonElement element) where TDictionary : Dictionary<string, object>
34+
private TDictionary ReadDictionary<TDictionary>(JsonElement element, TDictionary result)
35+
where TDictionary : Dictionary<string, object>
3536
{
36-
var result = Activator.CreateInstance<TDictionary>();
3737
foreach (var property in element.EnumerateObject())
3838
{
3939
result[property.Name] = ReadValue(property.Value);
@@ -53,8 +53,8 @@ private TDictionary ReadDictionary<TDictionary>(JsonElement element) where TDict
5353
=> value.ValueKind switch
5454
{
5555
JsonValueKind.Array => ReadArray(value).ToList(),
56-
JsonValueKind.Object => ReadDictionary<Dictionary<string, object>>(value),
57-
JsonValueKind.Number => ReadNumber(value),
56+
JsonValueKind.Object => ReadDictionary(value, new Dictionary<string, object>()),
57+
JsonValueKind.Number => value.ReadNumber(),
5858
JsonValueKind.True => true,
5959
JsonValueKind.False => false,
6060
JsonValueKind.String => value.GetString(),
@@ -63,20 +63,6 @@ private TDictionary ReadDictionary<TDictionary>(JsonElement element) where TDict
6363
_ => throw new InvalidOperationException($"Unexpected value kind: {value.ValueKind}")
6464
};
6565

66-
private object ReadNumber(JsonElement value)
67-
{
68-
if (value.TryGetInt32(out int i))
69-
return i;
70-
else if (value.TryGetInt64(out long l))
71-
return l;
72-
else if (BigInteger.TryParse(value.GetRawText(), out var bi))
73-
return bi;
74-
else if (value.TryGetDouble(out double d))
75-
return d;
76-
else if (value.TryGetDecimal(out decimal dd))
77-
return dd;
78-
79-
throw new NotImplementedException($"Unexpected Number value. Raw text was: {value.GetRawText()}");
80-
}
66+
8167
}
8268
}

src/GraphQL.Client.Serializer.SystemTextJson/SystemTextJsonSerializer.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ public SystemTextJsonSerializer(JsonSerializerOptions options)
3030
private void ConfigureMandatorySerializerOptions()
3131
{
3232
// deserialize extensions to Dictionary<string, object>
33+
Options.Converters.Insert(0, new ErrorPathConverter());
3334
Options.Converters.Insert(0, new MapConverter());
3435
// allow the JSON field "data" to match the property "Data" even without JsonNamingPolicy.CamelCase
3536
Options.PropertyNameCaseInsensitive = true;
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
using System.Collections.Generic;
2+
3+
namespace GraphQL
4+
{
5+
public class ErrorPath : List<object>
6+
{
7+
public ErrorPath()
8+
{
9+
}
10+
11+
public ErrorPath(IEnumerable<object> collection) : base(collection)
12+
{
13+
}
14+
}
15+
}

src/GraphQL.Primitives/GraphQLError.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ public class GraphQLError : IEquatable<GraphQLError?>
2626
/// The Path of the error
2727
/// </summary>
2828
[DataMember(Name = "path")]
29-
public object[]? Path { get; set; }
29+
public ErrorPath? Path { get; set; }
3030

3131
/// <summary>
3232
/// The extensions of the error

tests/GraphQL.Client.Serializer.Tests/BaseSerializerTest.cs

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
using System;
22
using System.IO;
3+
using System.Linq;
4+
using System.Reflection;
35
using System.Text;
46
using System.Threading;
7+
using System.Threading.Tasks;
58
using FluentAssertions;
9+
using FluentAssertions.Execution;
610
using GraphQL.Client.Abstractions;
711
using GraphQL.Client.Abstractions.Websocket;
812
using GraphQL.Client.LocalExecution;
@@ -49,14 +53,31 @@ public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest re
4953

5054
[Theory]
5155
[ClassData(typeof(DeserializeResponseTestData))]
52-
public async void DeserializeFromUtf8StreamTest(string json, GraphQLResponse<object> expectedResponse)
56+
public async void DeserializeFromUtf8StreamTest(string json, IGraphQLResponse expectedResponse)
5357
{
5458
var jsonBytes = Encoding.UTF8.GetBytes(json);
5559
await using var ms = new MemoryStream(jsonBytes);
56-
var response = await Serializer.DeserializeFromUtf8StreamAsync<GraphQLResponse<object>>(ms, CancellationToken.None);
60+
var response = await DeserializeToUnknownType(expectedResponse.Data?.GetType() ?? typeof(object), ms);
61+
//var response = await Serializer.DeserializeFromUtf8StreamAsync<object>(ms, CancellationToken.None);
5762

58-
response.Data.Should().BeEquivalentTo(expectedResponse.Data);
59-
response.Errors.Should().Equal(expectedResponse.Errors);
63+
response.Data.Should().BeEquivalentTo(expectedResponse.Data, options => options.WithAutoConversion());
64+
65+
if (expectedResponse.Errors is null)
66+
response.Errors.Should().BeNull();
67+
else {
68+
using (new AssertionScope())
69+
{
70+
response.Errors.Should().NotBeNull();
71+
response.Errors.Should().HaveSameCount(expectedResponse.Errors);
72+
for (int i = 0; i < expectedResponse.Errors.Length; i++)
73+
{
74+
response.Errors[i].Message.Should().BeEquivalentTo(expectedResponse.Errors[i].Message);
75+
response.Errors[i].Locations.Should().BeEquivalentTo(expectedResponse.Errors[i].Locations?.ToList());
76+
response.Errors[i].Path.Should().BeEquivalentTo(expectedResponse.Errors[i].Path);
77+
response.Errors[i].Extensions.Should().BeEquivalentTo(expectedResponse.Errors[i].Extensions);
78+
}
79+
}
80+
}
6081

6182
if (expectedResponse.Extensions == null)
6283
response.Extensions.Should().BeNull();
@@ -70,6 +91,17 @@ public async void DeserializeFromUtf8StreamTest(string json, GraphQLResponse<obj
7091
}
7192
}
7293

94+
public async Task<IGraphQLResponse> DeserializeToUnknownType(Type dataType, Stream stream)
95+
{
96+
MethodInfo mi = Serializer.GetType().GetMethod("DeserializeFromUtf8StreamAsync", BindingFlags.Instance | BindingFlags.Public);
97+
MethodInfo mi2 = mi.MakeGenericMethod(dataType);
98+
var task = (Task) mi2.Invoke(Serializer, new object[] { stream, CancellationToken.None });
99+
await task;
100+
var resultProperty = task.GetType().GetProperty("Result", BindingFlags.Public | BindingFlags.Instance);
101+
var result = resultProperty.GetValue(task);
102+
return (IGraphQLResponse)result;
103+
}
104+
73105
[Fact]
74106
public async void CanDeserializeExtensions()
75107
{

tests/GraphQL.Client.Serializer.Tests/TestData/DeserializeResponseTestData.cs

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,8 +37,79 @@ public IEnumerator<object[]> GetEnumerator()
3737
}
3838
}
3939
};
40+
41+
yield return new object[]
42+
{
43+
@"{
44+
""errors"": [
45+
{
46+
""message"": ""Name for character with ID 1002 could not be fetched."",
47+
""locations"": [
48+
{
49+
""line"": 6,
50+
""column"": 7
51+
}
52+
],
53+
""path"": [
54+
""hero"",
55+
""heroFriends"",
56+
1,
57+
""name""
58+
]
59+
}
60+
],
61+
""data"": {
62+
""hero"": {
63+
""name"": ""R2-D2"",
64+
""heroFriends"": [
65+
{
66+
""id"": ""1000"",
67+
""name"": ""Luke Skywalker""
68+
},
69+
{
70+
""id"": ""1002"",
71+
""name"": null
72+
},
73+
{
74+
""id"": ""1003"",
75+
""name"": ""Leia Organa""
76+
}
77+
]
78+
}
79+
}
80+
}",
81+
NewAnonymouslyTypedGraphQLResponse(new
82+
{
83+
hero = new
84+
{
85+
name = "R2-D2",
86+
heroFriends = new List<Friend>
87+
{
88+
new Friend {Id = "1000", Name = "Luke Skywalker"},
89+
new Friend {Id = "1002", Name = null},
90+
new Friend {Id = "1003", Name = "Leia Organa"}
91+
}
92+
}
93+
},
94+
new[] {
95+
new GraphQLError {
96+
Message = "Name for character with ID 1002 could not be fetched.",
97+
Locations = new [] { new GraphQLLocation{Line = 6, Column = 7 }},
98+
Path = new ErrorPath{"hero", "heroFriends", 1, "name"}
99+
}
100+
})
101+
};
40102
}
41103

42104
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
105+
106+
private GraphQLResponse<T> NewAnonymouslyTypedGraphQLResponse<T>(T data, GraphQLError[]? errors = null, Map? extensions = null)
107+
=> new GraphQLResponse<T> {Data = data, Errors = errors, Extensions = extensions};
108+
}
109+
110+
public class Friend
111+
{
112+
public string Id { get; set; }
113+
public string? Name { get; set; }
43114
}
44115
}

0 commit comments

Comments
 (0)