From 8d7f8e0e7c69406513adaf312bc363e0b5332612 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Wed, 17 Aug 2022 12:31:11 +0100 Subject: [PATCH] Implement public JsonElement conversions in JsonEventFormatter The method names are consistent with the ones in ProtobufEventFormatter and the Json.NET event formatter. Currently batch conversion is not supported, but we can add that later if requested. The ConvertToJsonElement method is horribly inefficient at the moment (serialize then parse); we can consider how to make it more efficient later if necessary. Signed-off-by: Jon Skeet --- .../JsonEventFormatter.cs | 31 +++++++++++ .../SystemTextJson/JsonEventFormatterTest.cs | 51 ++++++++++++++++++- 2 files changed, 80 insertions(+), 2 deletions(-) diff --git a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs index 1b612a0..decdb77 100644 --- a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs +++ b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs @@ -10,6 +10,7 @@ using System.Text; using System.Text.Json; using System.Threading.Tasks; +using System.Xml.Linq; namespace CloudNative.CloudEvents.SystemTextJson { @@ -157,6 +158,15 @@ public override IReadOnlyList DecodeBatchModeMessage(Stream body, Co public override IReadOnlyList DecodeBatchModeMessage(ReadOnlyMemory body, ContentType? contentType, IEnumerable? extensionAttributes) => DecodeBatchModeMessageImpl(BinaryDataUtilities.AsStream(body), contentType, extensionAttributes, false).GetAwaiter().GetResult(); + /// + /// Converts the given into a . + /// + /// The JSON representation of a CloudEvent. Must have a of . + /// The extension attributes to use when populating the CloudEvent. May be null. + /// The SDK representation of the CloudEvent. + public CloudEvent ConvertFromJsonElement(JsonElement element, IEnumerable? extensionAttributes) => + DecodeJsonElement(element, extensionAttributes, nameof(element)); + private async Task> DecodeBatchModeMessageImpl(Stream data, ContentType? contentType, IEnumerable? extensionAttributes, bool async) { Validation.CheckNotNull(data, nameof(data)); @@ -428,6 +438,27 @@ public override ReadOnlyMemory EncodeStructuredModeMessage(CloudEvent clou return stream.ToArray(); } + /// + /// Converts the given to a containing the structured mode JSON format representation + /// of the event. + /// + /// The current implementation of this method is inefficient. Care should be taken before + /// using this in performance-sensitive scenarios. The efficiency may well be improved in the future. + /// The event to convert. Must not be null. + /// A containing the structured mode JSON format representation of the event. + public JsonElement ConvertToJsonElement(CloudEvent cloudEvent) + { + // Unfortunately System.Text.Json doesn't have an equivalent of JTokenWriter, + // so we serialize all the data then parse it. That's horrible, but at least + // it's contained in this one place (rather than in user code everywhere else). + // We can optimize it later by duplicating the logic of WriteCloudEventForBatchOrStructuredMode + // to use System.Text.Json.Nodes. + var data = EncodeStructuredModeMessage(cloudEvent, out _); + using var document = JsonDocument.Parse(data); + // We have to clone the data so that we can dispose of the JsonDocument. + return document.RootElement.Clone(); + } + private Utf8JsonWriter CreateUtf8JsonWriter(Stream stream) { var options = new JsonWriterOptions diff --git a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs index 3e456c9..dd48c50 100644 --- a/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/SystemTextJson/JsonEventFormatterTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using CloudNative.CloudEvents.UnitTests; using System; using System.Collections.Generic; @@ -15,10 +16,9 @@ using System.Threading.Tasks; using Xunit; using static CloudNative.CloudEvents.UnitTests.TestHelpers; +using JArray = Newtonsoft.Json.Linq.JArray; // JObject is a really handy way of creating JSON which we can then parse with System.Text.Json using JObject = Newtonsoft.Json.Linq.JObject; -using JArray = Newtonsoft.Json.Linq.JArray; -using CloudNative.CloudEvents.Core; namespace CloudNative.CloudEvents.SystemTextJson.UnitTests { @@ -1102,6 +1102,53 @@ public void EncodeStructured_IndentationSettings() Assert.Equal(expected, json); } + [Fact] + public void ConvertToJsonElement() + { + var cloudEvent = new CloudEvent + { + Data = SampleBinaryData + }.PopulateRequiredAttributes(); + + JsonElement element = new JsonEventFormatter().ConvertToJsonElement(cloudEvent); + var asserter = new JsonElementAsserter + { + { "data_base64", JsonValueKind.String, SampleBinaryDataBase64 }, + { "id", JsonValueKind.String, "test-id" }, + { "source", JsonValueKind.String, "//test" }, + { "specversion", JsonValueKind.String, "1.0" }, + { "type", JsonValueKind.String, "test-type" } + }; + asserter.AssertProperties(element, assertCount: true); + } + + [Fact] + public void ConvertFromJsonElement() + { + var obj = new JObject + { + ["specversion"] = "1.0", + ["type"] = "test-type", + ["id"] = "test-id", + ["data"] = "text", // Just so that it's reasonable to have a DataContentType, + ["datacontenttype"] = "text/plain", + ["dataschema"] = "https://data-schema", + ["subject"] = "event-subject", + ["source"] = "//event-source", + ["time"] = SampleTimestampText + }; + using var document = JsonDocument.Parse(obj.ToString()); + var cloudEvent = new JsonEventFormatter().ConvertFromJsonElement(document.RootElement, extensionAttributes: null); + Assert.Equal(CloudEventsSpecVersion.V1_0, cloudEvent.SpecVersion); + Assert.Equal("test-type", cloudEvent.Type); + Assert.Equal("test-id", cloudEvent.Id); + Assert.Equal("text/plain", cloudEvent.DataContentType); + Assert.Equal(new Uri("https://data-schema"), cloudEvent.DataSchema); + Assert.Equal("event-subject", cloudEvent.Subject); + Assert.Equal(new Uri("//event-source", UriKind.RelativeOrAbsolute), cloudEvent.Source); + AssertTimestampsEqual(SampleTimestamp, cloudEvent.Time); + } + // Utility methods private static object DecodeBinaryModeEventData(byte[] bytes, string contentType) {