diff --git a/src/StrawberryShake/Client/src/Core/Json/JsonExtensionParser.cs b/src/StrawberryShake/Client/src/Core/Json/JsonExtensionParser.cs new file mode 100644 index 00000000000..cdb98b31872 --- /dev/null +++ b/src/StrawberryShake/Client/src/Core/Json/JsonExtensionParser.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Text.Json; + +namespace StrawberryShake.Json; + +public static class JsonExtensionParser +{ + public static IReadOnlyDictionary? ParseExtensions(JsonElement result) + { + if (result is { ValueKind: JsonValueKind.Object }) + { + var extensions = JsonSerializationHelper.ReadValue(result); + return (IReadOnlyDictionary?)extensions; + } + + return null; + } +} diff --git a/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs b/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs index 5cb0a4fe4ba..b6c00ef3df3 100644 --- a/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs +++ b/src/StrawberryShake/Client/src/Core/OperationResultBuilder.cs @@ -25,6 +25,7 @@ public IOperationResult Build( TResultData? data = null; IOperationResultDataInfo? dataInfo = null; IReadOnlyList? errors = null; + IReadOnlyDictionary? extensions = null; try { @@ -42,6 +43,12 @@ public IOperationResult Build( { errors = JsonErrorParser.ParseErrors(errorsProp); } + + if (body.RootElement.TryGetProperty(ResultFields.Extensions, out var extensionsProp) && + extensionsProp.ValueKind is JsonValueKind.Object) + { + extensions = JsonExtensionParser.ParseExtensions(extensionsProp); + } } } catch (Exception ex) @@ -71,7 +78,7 @@ public IOperationResult Build( dataInfo, ResultDataFactory, errors, - response.Extensions, + extensions, response.ContextData); } diff --git a/src/StrawberryShake/Client/src/Core/ResultFields.cs b/src/StrawberryShake/Client/src/Core/ResultFields.cs index dad183da44d..ea35098e4a8 100644 --- a/src/StrawberryShake/Client/src/Core/ResultFields.cs +++ b/src/StrawberryShake/Client/src/Core/ResultFields.cs @@ -7,4 +7,5 @@ public static class ResultFields public const string Path = "path"; public const string Label = "label"; public const string HasNext = "hasNext"; + public const string Extensions = "extensions"; } diff --git a/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs b/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs new file mode 100644 index 00000000000..7c32904b9e0 --- /dev/null +++ b/src/StrawberryShake/Client/test/Core.Tests/OperationResultBuilderTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Collections.Generic; +using System.Text.Json; + +namespace StrawberryShake; + +public class OperationResultBuilderTests +{ + [Fact] + public void Build_With_Extensions() + { + // arrange + var factory = new DocumentDataFactory(); + var builder = new DocumentOperationResultBuilder(factory); + + // According to the current design, all implementations of IConnection operate + // on JsonDocuments which are deserialized straight from response streams and + // very few properties in the Response object are in fact ever initialized, + // including Response.Extensions. It is therefore safe to assume that, at + // least for now, OperationResultBuilder is the best place to actually parse + // and extract "extensions". + var body = JsonDocument.Parse(@"{""data"": { }, ""extensions"": { ""a"": 1, ""b"": { ""c"": ""Strawberry"" }, ""d"": 3.14 } }"); + var response = new Response(body, null); + + // act + + var result = builder.Build(response); + + // assert + Assert.NotEmpty(result.Extensions); + Assert.Equal(1L, result.Extensions["a"]); + + var b = (IReadOnlyDictionary?)result.Extensions["b"]; + + Assert.NotNull(b); + Assert.Equal("Strawberry", b["c"]); + + Assert.Equal(3.14, result.Extensions["d"]); + } + + internal class Document + { + } + + internal class DocumentDataInfo : IOperationResultDataInfo + { + public IReadOnlyCollection EntityIds { get; } = ArraySegment.Empty; + + public ulong Version { get; } = 0; + + public IOperationResultDataInfo WithVersion(ulong version) + { + throw new NotImplementedException(); + } + } + + internal class DocumentDataFactory : IOperationResultDataFactory + { + public Type ResultType { get => typeof(Document); } + + public Document Create(IOperationResultDataInfo dataInfo, IEntityStoreSnapshot? snapshot = null) + { + return new Document(); + } + + object IOperationResultDataFactory.Create(IOperationResultDataInfo dataInfo, IEntityStoreSnapshot? snapshot) + { + return Create(dataInfo, snapshot); + } + } + + internal class DocumentOperationResultBuilder : OperationResultBuilder + { + public DocumentOperationResultBuilder(IOperationResultDataFactory resultDataFactory) + { + ResultDataFactory = resultDataFactory; + } + + protected override IOperationResultDataFactory ResultDataFactory { get; } + + protected override IOperationResultDataInfo BuildData(JsonElement obj) + { + return new DocumentDataInfo(); + } + } +}