Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

JSON formatter container conversions #234

Merged
merged 2 commits into from
Aug 18, 2022
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
34 changes: 27 additions & 7 deletions src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -136,7 +136,7 @@ public override async Task<CloudEvent> DecodeStructuredModeMessageAsync(Stream b

var jsonReader = CreateJsonReader(body, MimeUtilities.GetEncoding(contentType));
var jObject = await JObject.LoadAsync(jsonReader).ConfigureAwait(false);
return DecodeJObject(jObject, extensionAttributes);
return DecodeJObject(jObject, extensionAttributes, nameof(body));
}

/// <inheritdoc />
Expand All @@ -146,7 +146,7 @@ public override CloudEvent DecodeStructuredModeMessage(Stream body, ContentType?

var jsonReader = CreateJsonReader(body, MimeUtilities.GetEncoding(contentType));
var jObject = JObject.Load(jsonReader);
return DecodeJObject(jObject, extensionAttributes);
return DecodeJObject(jObject, extensionAttributes, nameof(body));
}

/// <inheritdoc />
Expand Down Expand Up @@ -177,14 +177,23 @@ public override IReadOnlyList<CloudEvent> DecodeBatchModeMessage(Stream body, Co
public override IReadOnlyList<CloudEvent> DecodeBatchModeMessage(ReadOnlyMemory<byte> body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
DecodeBatchModeMessage(BinaryDataUtilities.AsStream(body), contentType, extensionAttributes);

/// <summary>
/// Converts the given <see cref="JObject"/> into a <see cref="CloudEvent"/>.
/// </summary>
/// <param name="jObject">The JSON representation of a CloudEvent. Must not be null.</param>
/// <param name="extensionAttributes">The extension attributes to use when populating the CloudEvent. May be null.</param>
/// <returns>The SDK representation of the CloudEvent.</returns>
public CloudEvent ConvertFromJObject(JObject jObject, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
DecodeJObject(Validation.CheckNotNull(jObject, nameof(jObject)), extensionAttributes, nameof(jObject));

private IReadOnlyList<CloudEvent> DecodeJArray(JArray jArray, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName)
{
List<CloudEvent> events = new List<CloudEvent>(jArray.Count);
foreach (var token in jArray)
{
if (token is JObject obj)
{
events.Add(DecodeJObject(obj, extensionAttributes));
events.Add(DecodeJObject(obj, extensionAttributes, paramName));
}
else
{
Expand All @@ -194,7 +203,7 @@ private IReadOnlyList<CloudEvent> DecodeJArray(JArray jArray, IEnumerable<CloudE
return events;
}

private CloudEvent DecodeJObject(JObject jObject, IEnumerable<CloudEventAttribute>? extensionAttributes)
private CloudEvent DecodeJObject(JObject jObject, IEnumerable<CloudEventAttribute>? extensionAttributes, string paramName)
{
if (!jObject.TryGetValue(CloudEventsSpecVersion.SpecVersionAttribute.Name, out var specVersionToken)
|| specVersionToken.Type != JTokenType.String)
Expand All @@ -207,9 +216,7 @@ private CloudEvent DecodeJObject(JObject jObject, IEnumerable<CloudEventAttribut
var cloudEvent = new CloudEvent(specVersion, extensionAttributes);
PopulateAttributesFromStructuredEvent(cloudEvent, jObject);
PopulateDataFromStructuredEvent(cloudEvent, jObject);
// "data" is always the parameter from the public method. It's annoying not to be able to use
// nameof here, but this will give the appropriate result.
return Validation.CheckCloudEventArgument(cloudEvent, "data");
return Validation.CheckCloudEventArgument(cloudEvent, paramName);
}

private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JObject jObject)
Expand Down Expand Up @@ -395,6 +402,19 @@ public override ReadOnlyMemory<byte> EncodeStructuredModeMessage(CloudEvent clou
return stream.ToArray();
}

/// <summary>
/// Converts the given <see cref="CloudEvent"/> to a <see cref="JObject"/> containing the structured mode JSON format representation
/// of the event.
/// </summary>
/// <param name="cloudEvent">The event to convert. Must not be null.</param>
/// <returns>A <see cref="JObject"/> containing the structured mode JSON format representation of the event.</returns>
public JObject ConvertToJObject(CloudEvent cloudEvent)
{
var writer = new JTokenWriter();
WriteCloudEventForBatchOrStructuredMode(writer, cloudEvent);
return (JObject) writer.Token!;
}

/// <inheritdoc />
public override ReadOnlyMemory<byte> EncodeBatchModeMessage(IEnumerable<CloudEvent> cloudEvents, out ContentType contentType)
{
Expand Down
31 changes: 31 additions & 0 deletions src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
using System.Xml.Linq;

namespace CloudNative.CloudEvents.SystemTextJson
{
Expand Down Expand Up @@ -157,6 +158,15 @@ public override IReadOnlyList<CloudEvent> DecodeBatchModeMessage(Stream body, Co
public override IReadOnlyList<CloudEvent> DecodeBatchModeMessage(ReadOnlyMemory<byte> body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
DecodeBatchModeMessageImpl(BinaryDataUtilities.AsStream(body), contentType, extensionAttributes, false).GetAwaiter().GetResult();

/// <summary>
/// Converts the given <see cref="JsonElement"/> into a <see cref="CloudEvent"/>.
/// </summary>
/// <param name="element">The JSON representation of a CloudEvent. Must have a <see cref="JsonElement.ValueKind"/> of <see cref="JsonValueKind.Object"/>.</param>
/// <param name="extensionAttributes">The extension attributes to use when populating the CloudEvent. May be null.</param>
/// <returns>The SDK representation of the CloudEvent.</returns>
public CloudEvent ConvertFromJsonElement(JsonElement element, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
DecodeJsonElement(element, extensionAttributes, nameof(element));

private async Task<IReadOnlyList<CloudEvent>> DecodeBatchModeMessageImpl(Stream data, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes, bool async)
{
Validation.CheckNotNull(data, nameof(data));
Expand Down Expand Up @@ -428,6 +438,27 @@ public override ReadOnlyMemory<byte> EncodeStructuredModeMessage(CloudEvent clou
return stream.ToArray();
}

/// <summary>
/// Converts the given <see cref="CloudEvent"/> to a <see cref="JsonElement"/> containing the structured mode JSON format representation
/// of the event.
/// </summary>
/// <remarks>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.</remarks>
/// <param name="cloudEvent">The event to convert. Must not be null.</param>
/// <returns>A <see cref="JsonElement"/> containing the structured mode JSON format representation of the event.</returns>
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1092,6 +1092,55 @@ public void EncodeStructured_IndentationSettings()
Assert.Equal(expected, json);
}

// Effectively smoke tests for LINQ to JSON conversions; these piggy-back on the same implementation
// as the rest of the code, so we don't need to test exhaustively.

[Fact]
public void ConvertToJObject()
{
var cloudEvent = new CloudEvent
{
Data = SampleBinaryData
}.PopulateRequiredAttributes();

JObject obj = new JsonEventFormatter().ConvertToJObject(cloudEvent);
var asserter = new JTokenAsserter
{
{ "data_base64", JTokenType.String, SampleBinaryDataBase64 },
{ "id", JTokenType.String, "test-id" },
{ "source", JTokenType.String, "//test" },
{ "specversion", JTokenType.String, "1.0" },
{ "type", JTokenType.String, "test-type" },
};
asserter.AssertProperties(obj, assertCount: true);
}

[Fact]
public void ConvertFromJObject()
{
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
};
var cloudEvent = new JsonEventFormatter().ConvertFromJObject(obj, 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)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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
{
Expand Down Expand Up @@ -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)
{
Expand Down