Skip to content

Commit 2db0ce7

Browse files
authored
Merge pull request #235 from graphql-dotnet/fix-immutableconverter
Fix ImmutableConverter and ErrorPath
2 parents 47b4abf + 52dc9bd commit 2db0ce7

File tree

9 files changed

+288
-50
lines changed

9 files changed

+288
-50
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
using System;
2+
using System.Buffers;
3+
using System.Numerics;
4+
using System.Text;
5+
using System.Text.Json;
6+
7+
namespace GraphQL.Client.Serializer.SystemTextJson
8+
{
9+
public static class ConverterHelperExtensions
10+
{
11+
public static object ReadNumber(this ref Utf8JsonReader reader)
12+
{
13+
if (reader.TryGetInt32(out int i))
14+
return i;
15+
else if (reader.TryGetInt64(out long l))
16+
return l;
17+
else if (reader.TryGetDouble(out double d))
18+
return reader.TryGetBigInteger(out var bi) && bi != new BigInteger(d)
19+
? bi
20+
: (object)d;
21+
else if (reader.TryGetDecimal(out decimal dd))
22+
return reader.TryGetBigInteger(out var bi) && bi != new BigInteger(dd)
23+
? bi
24+
: (object)dd;
25+
26+
throw new NotImplementedException($"Unexpected Number value. Raw text was: {reader.GetRawString()}");
27+
}
28+
29+
public static bool TryGetBigInteger(this ref Utf8JsonReader reader, out BigInteger bi) => BigInteger.TryParse(reader.GetRawString(), out bi);
30+
31+
public static string GetRawString(this ref Utf8JsonReader reader)
32+
{
33+
var byteArray = reader.HasValueSequence ? reader.ValueSequence.ToArray() : reader.ValueSpan.ToArray();
34+
return Encoding.UTF8.GetString(byteArray);
35+
}
36+
}
37+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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+
new ErrorPath(ReadArray(ref reader));
14+
15+
public override void Write(Utf8JsonWriter writer, ErrorPath value, JsonSerializerOptions options)
16+
=> throw new NotImplementedException(
17+
"This converter currently is only intended to be used to read a JSON object into a strongly-typed representation.");
18+
19+
private IEnumerable<object?> ReadArray(ref Utf8JsonReader reader)
20+
{
21+
if (reader.TokenType != JsonTokenType.StartArray)
22+
{
23+
throw new JsonException("This converter can only parse when the root element is a JSON Array.");
24+
}
25+
26+
var array = new List<object?>();
27+
28+
while (reader.Read())
29+
{
30+
if (reader.TokenType == JsonTokenType.EndArray)
31+
break;
32+
33+
array.Add(ReadValue(ref reader));
34+
}
35+
36+
return array;
37+
}
38+
39+
private object? ReadValue(ref Utf8JsonReader reader)
40+
=> reader.TokenType switch
41+
{
42+
JsonTokenType.None => null,
43+
JsonTokenType.String => reader.GetString(),
44+
JsonTokenType.Number => reader.ReadNumber(),
45+
JsonTokenType.True => true,
46+
JsonTokenType.False => false,
47+
JsonTokenType.Null => null,
48+
_ => throw new InvalidOperationException($"Unexpected token type: {reader.TokenType}")
49+
};
50+
}
51+
}

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: 47 additions & 45 deletions
Original file line numberDiff line numberDiff line change
@@ -15,68 +15,70 @@ namespace GraphQL.Client.Serializer.SystemTextJson
1515
/// </remarks>
1616
public class MapConverter : JsonConverter<Map>
1717
{
18-
public override Map Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options)
19-
{
20-
using var doc = JsonDocument.ParseValue(ref reader);
21-
22-
if (doc?.RootElement == null || doc?.RootElement.ValueKind != JsonValueKind.Object)
23-
{
24-
throw new ArgumentException("This converter can only parse when the root element is a JSON Object.");
25-
}
26-
27-
return ReadDictionary<Map>(doc.RootElement);
28-
}
18+
public override Map Read(ref Utf8JsonReader reader, Type typeToConvert, JsonSerializerOptions options) => ReadDictionary(ref reader, new Map());
2919

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

34-
private TDictionary ReadDictionary<TDictionary>(JsonElement element) where TDictionary : Dictionary<string, object>
24+
private static TDictionary ReadDictionary<TDictionary>(ref Utf8JsonReader reader, TDictionary result)
25+
where TDictionary : Dictionary<string, object>
3526
{
36-
var result = Activator.CreateInstance<TDictionary>();
37-
foreach (var property in element.EnumerateObject())
27+
if (reader.TokenType != JsonTokenType.StartObject)
28+
throw new JsonException();
29+
30+
while (reader.Read())
3831
{
39-
result[property.Name] = ReadValue(property.Value);
32+
if (reader.TokenType == JsonTokenType.EndObject)
33+
break;
34+
35+
if (reader.TokenType != JsonTokenType.PropertyName)
36+
throw new JsonException();
37+
38+
string key = reader.GetString();
39+
40+
// move to property value
41+
if (!reader.Read())
42+
throw new JsonException();
43+
44+
result.Add(key, ReadValue(ref reader));
4045
}
46+
4147
return result;
4248
}
4349

44-
private IEnumerable<object?> ReadArray(JsonElement value)
50+
private static List<object> ReadArray(ref Utf8JsonReader reader)
4551
{
46-
foreach (var item in value.EnumerateArray())
52+
if (reader.TokenType != JsonTokenType.StartArray)
53+
throw new JsonException();
54+
55+
var result = new List<object>();
56+
57+
while (reader.Read())
4758
{
48-
yield return ReadValue(item);
59+
if (reader.TokenType == JsonTokenType.EndArray)
60+
break;
61+
62+
result.Add(ReadValue(ref reader));
4963
}
50-
}
5164

52-
private object? ReadValue(JsonElement value)
53-
=> value.ValueKind switch
65+
return result;
66+
}
67+
68+
private static object? ReadValue(ref Utf8JsonReader reader)
69+
=> reader.TokenType switch
5470
{
55-
JsonValueKind.Array => ReadArray(value).ToList(),
56-
JsonValueKind.Object => ReadDictionary<Dictionary<string, object>>(value),
57-
JsonValueKind.Number => ReadNumber(value),
58-
JsonValueKind.True => true,
59-
JsonValueKind.False => false,
60-
JsonValueKind.String => value.GetString(),
61-
JsonValueKind.Null => null,
62-
JsonValueKind.Undefined => null,
63-
_ => throw new InvalidOperationException($"Unexpected value kind: {value.ValueKind}")
71+
JsonTokenType.StartArray => ReadArray(ref reader).ToList(),
72+
JsonTokenType.StartObject => ReadDictionary(ref reader, new Dictionary<string, object>()),
73+
JsonTokenType.Number => reader.ReadNumber(),
74+
JsonTokenType.True => true,
75+
JsonTokenType.False => false,
76+
JsonTokenType.String => reader.GetString(),
77+
JsonTokenType.Null => null,
78+
JsonTokenType.None => null,
79+
_ => throw new InvalidOperationException($"Unexpected value kind: {reader.TokenType}")
6480
};
6581

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-
}
82+
8183
}
8284
}

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: 58 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,12 @@
1+
using System;
12
using System.IO;
3+
using System.Linq;
4+
using System.Reflection;
25
using System.Text;
36
using System.Threading;
7+
using System.Threading.Tasks;
48
using FluentAssertions;
9+
using FluentAssertions.Execution;
510
using GraphQL.Client.Abstractions;
611
using GraphQL.Client.Abstractions.Websocket;
712
using GraphQL.Client.LocalExecution;
@@ -48,14 +53,31 @@ public void SerializeToBytesTest(string expectedJson, GraphQLWebSocketRequest re
4853

4954
[Theory]
5055
[ClassData(typeof(DeserializeResponseTestData))]
51-
public async void DeserializeFromUtf8StreamTest(string json, GraphQLResponse<object> expectedResponse)
56+
public async void DeserializeFromUtf8StreamTest(string json, IGraphQLResponse expectedResponse)
5257
{
5358
var jsonBytes = Encoding.UTF8.GetBytes(json);
5459
await using var ms = new MemoryStream(jsonBytes);
55-
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);
5662

57-
response.Data.Should().BeEquivalentTo(expectedResponse.Data);
58-
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+
}
5981

6082
if (expectedResponse.Extensions == null)
6183
response.Extensions.Should().BeNull();
@@ -69,6 +91,17 @@ public async void DeserializeFromUtf8StreamTest(string json, GraphQLResponse<obj
6991
}
7092
}
7193

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+
72105
[Fact]
73106
public async void CanDeserializeExtensions()
74107
{
@@ -118,5 +151,26 @@ public async void CanDoSerializationWithPredefinedTypes()
118151

119152
Assert.Equal(message, response.Data.AddMessage.Content);
120153
}
154+
155+
156+
public class WithNullable
157+
{
158+
public int? NullableInt { get; set; }
159+
}
160+
161+
[Fact]
162+
public void CanSerializeNullableInt()
163+
{
164+
Action action = () => Serializer.SerializeToString(new GraphQLRequest
165+
{
166+
Query = "{}",
167+
Variables = new WithNullable
168+
{
169+
NullableInt = 2
170+
}
171+
});
172+
173+
action.Should().NotThrow();
174+
}
121175
}
122176
}

0 commit comments

Comments
 (0)