Support data content type inference
- New methods in CloudEventFormatter to support inference - JsonEventFormatter infers a content type of application/json for non-binary data - All transports use the inferred content type when formatting in binary mode - Documentation for both formatters and bindings has been updated Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
parent
8d3da7d1e8
commit
b1f29cf25b
|
@ -169,8 +169,11 @@ The conversion should follow the following steps of pseudo-code:
|
|||
- For binary mode encoding:
|
||||
- Call `formatter.EncodeBinaryModeEventData` to encode
|
||||
the data within the CloudEvent
|
||||
- Call `formatter.GetOrInferDataContentType` to obtain the
|
||||
appropriate content type, which may be inferred from the
|
||||
data if the CloudEvent itself does not specify the data content type.
|
||||
- Populate metadata in the message from the attributes in the
|
||||
CloudEvent.
|
||||
CloudEvent and the content type.
|
||||
- For `To...` methods, return the resulting protocol message.
|
||||
This must not be null. (`CopyTo...` messages do not return
|
||||
anything.)
|
||||
|
|
|
@ -105,3 +105,18 @@ The formatter should *not* perform validation on the `CloudEvent`
|
|||
accepted in `DecodeBinaryModeEventData`, beyond asserting that the
|
||||
argument is not null. This is typically called by a protocol binding
|
||||
which should perform validation itself later.
|
||||
|
||||
## Data content type inference
|
||||
|
||||
Some event formats (e.g. JSON) infer the data content type from the
|
||||
actual data provided. In the C# SDK, this is implemented via the
|
||||
`CloudEventFormatter` methods `GetOrInferDataContentType` and
|
||||
`InferDataContentType`. The first of these is primarily a
|
||||
convenience method to be called by bindings; the second may be
|
||||
overridden by any formatter implementation that wishes to infer
|
||||
a data content type when one is not specified. Implementations *can*
|
||||
override `GetOrInferDataContentType` if they have unusual
|
||||
requirements, but the default implementation is usually sufficient.
|
||||
|
||||
The base implementation of `InferDataContentType` always returns
|
||||
null; this means that no content type is inferred by default.
|
||||
|
|
|
@ -168,7 +168,7 @@ namespace CloudNative.CloudEvents.Amqp
|
|||
break;
|
||||
case ContentMode.Binary:
|
||||
bodySection = new Data { Binary = BinaryDataUtilities.AsArray(formatter.EncodeBinaryModeEventData(cloudEvent)) };
|
||||
properties = new Properties { ContentType = cloudEvent.DataContentType };
|
||||
properties = new Properties { ContentType = formatter.GetOrInferDataContentType(cloudEvent) };
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace CloudNative.CloudEvents.AspNetCore
|
|||
break;
|
||||
case ContentMode.Binary:
|
||||
content = formatter.EncodeBinaryModeEventData(cloudEvent);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
|
||||
|
|
|
@ -30,6 +30,9 @@ namespace CloudNative.CloudEvents
|
|||
/// Avro record, so the value will have the natural Avro deserialization type for that data (which may
|
||||
/// not be exactly the same as the type that was serialized).
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This event formatter does not infer any data content type.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class AvroEventFormatter : CloudEventFormatter
|
||||
{
|
||||
|
|
|
@ -0,0 +1,12 @@
|
|||
// Copyright 2022 Cloud Native Foundation.
|
||||
// Licensed under the Apache 2.0 license.
|
||||
// See LICENSE file in the project root for full license information.
|
||||
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
[assembly: InternalsVisibleTo("CloudNative.CloudEvents.UnitTests,PublicKey="
|
||||
+ "0024000004800000940000000602000000240000525341310004000001000100e945e99352d0b8"
|
||||
+ "90ddb645995bc05ef5a22497d97e78196b9f6148ea33b0c1b219f0c28df523878d1d8c9d042a02"
|
||||
+ "f005777461dffe455b348f82b39fcbc64985ef091295c0ad2dcb265c23589e9ce8e48dbe84c8e1"
|
||||
+ "7fc37555938b2669aea7575cee288809065aa9dc04dff67ce1dfc5a3167770323c1a2c632f0eb2"
|
||||
+ "f8c64acf")]
|
|
@ -20,7 +20,8 @@ namespace CloudNative.CloudEvents.Kafka
|
|||
{
|
||||
private const string KafkaHeaderPrefix = "ce_";
|
||||
|
||||
private const string KafkaContentTypeAttributeName = "content-type";
|
||||
// Visible for testing
|
||||
internal const string KafkaContentTypeAttributeName = "content-type";
|
||||
private const string SpecVersionKafkaHeader = KafkaHeaderPrefix + "specversion";
|
||||
|
||||
/// <summary>
|
||||
|
@ -155,7 +156,7 @@ namespace CloudNative.CloudEvents.Kafka
|
|||
break;
|
||||
case ContentMode.Binary:
|
||||
value = BinaryDataUtilities.AsArray(formatter.EncodeBinaryModeEventData(cloudEvent));
|
||||
contentTypeHeaderValue = cloudEvent.DataContentType;
|
||||
contentTypeHeaderValue = formatter.GetOrInferDataContentType(cloudEvent);
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
|
||||
|
|
|
@ -446,23 +446,33 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
|
|||
|
||||
if (cloudEvent.Data is object)
|
||||
{
|
||||
if (cloudEvent.DataContentType is null)
|
||||
if (cloudEvent.DataContentType is null && GetOrInferDataContentType(cloudEvent) is string inferredDataContentType)
|
||||
{
|
||||
cloudEvent.SpecVersion.DataContentTypeAttribute.Validate(inferredDataContentType);
|
||||
writer.WritePropertyName(cloudEvent.SpecVersion.DataContentTypeAttribute.Name);
|
||||
writer.WriteValue(JsonMediaType);
|
||||
writer.WriteValue(inferredDataContentType);
|
||||
}
|
||||
EncodeStructuredModeData(cloudEvent, writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infers the data content type of a CloudEvent based on its data. This implementation
|
||||
/// infers a data content type of "application/json" for any non-binary data, and performs
|
||||
/// no inference for binary data.
|
||||
/// </summary>
|
||||
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
|
||||
/// <returns>The inferred data content type, or null if no inference is performed.</returns>
|
||||
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
|
||||
|
||||
/// <summary>
|
||||
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="JsonWriter"/>.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This implementation follows the rules listed in the class remarks. Override this method
|
||||
/// to provide more specialized behavior, writing only <see cref="DataPropertyName"/> or
|
||||
/// to provide more specialized behavior, usually writing only <see cref="DataPropertyName"/> or
|
||||
/// <see cref="DataBase64PropertyName"/> properties.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
|
@ -480,7 +490,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
|
|||
}
|
||||
else
|
||||
{
|
||||
ContentType dataContentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType);
|
||||
ContentType dataContentType = new ContentType(GetOrInferDataContentType(cloudEvent));
|
||||
if (IsJsonMediaType(dataContentType.MediaType))
|
||||
{
|
||||
writer.WritePropertyName(DataPropertyName);
|
||||
|
|
|
@ -59,6 +59,9 @@ namespace CloudNative.CloudEvents.Protobuf
|
|||
/// a string, otherwise it is left as a byte array. Derived classes can specialize this behavior by overriding
|
||||
/// <see cref="DecodeBinaryModeEventData(ReadOnlyMemory{byte}, CloudEvent)"/>.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// This event formatter does not infer any data content type.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
public class ProtobufEventFormatter : CloudEventFormatter
|
||||
{
|
||||
|
|
|
@ -457,16 +457,26 @@ namespace CloudNative.CloudEvents.SystemTextJson
|
|||
|
||||
if (cloudEvent.Data is object)
|
||||
{
|
||||
if (cloudEvent.DataContentType is null)
|
||||
if (cloudEvent.DataContentType is null && GetOrInferDataContentType(cloudEvent) is string inferredDataContentType)
|
||||
{
|
||||
cloudEvent.SpecVersion.DataContentTypeAttribute.Validate(inferredDataContentType);
|
||||
writer.WritePropertyName(cloudEvent.SpecVersion.DataContentTypeAttribute.Name);
|
||||
writer.WriteStringValue(JsonMediaType);
|
||||
writer.WriteStringValue(inferredDataContentType);
|
||||
}
|
||||
EncodeStructuredModeData(cloudEvent, writer);
|
||||
}
|
||||
writer.WriteEndObject();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infers the data content type of a CloudEvent based on its data. This implementation
|
||||
/// infers a data content type of "application/json" for any non-binary data, and performs
|
||||
/// no inference for binary data.
|
||||
/// </summary>
|
||||
/// <param name="data">The CloudEvent to infer the data content from. Must not be null.</param>
|
||||
/// <returns>The inferred data content type, or null if no inference is performed.</returns>
|
||||
protected override string? InferDataContentType(object data) => data is byte[]? null : JsonMediaType;
|
||||
|
||||
/// <summary>
|
||||
/// Encodes structured mode data within a CloudEvent, writing it to the specified <see cref="Utf8JsonWriter"/>.
|
||||
/// </summary>
|
||||
|
|
|
@ -164,5 +164,41 @@ namespace CloudNative.CloudEvents
|
|||
/// Must not be null (on return).</param>
|
||||
/// <returns>The batch representation of the CloudEvent.</returns>
|
||||
public abstract ReadOnlyMemory<byte> EncodeBatchModeMessage(IEnumerable<CloudEvent> cloudEvents, out ContentType contentType);
|
||||
|
||||
/// <summary>
|
||||
/// Determines the effective data content type of the given CloudEvent.
|
||||
/// </summary>
|
||||
/// <remarks>
|
||||
/// <para>
|
||||
/// This implementation validates that <paramref name="cloudEvent"/> is not null,
|
||||
/// returns the existing <see cref="CloudEvent.DataContentType"/> if that's not null,
|
||||
/// and otherwise returns null if <see cref="CloudEvent.Data"/> is null or
|
||||
/// delegates to <see cref="InferDataContentType(object)"/> to infer the data content type
|
||||
/// from the actual data.
|
||||
/// </para>
|
||||
/// <para>
|
||||
/// Derived classes may override this if additional information is needed from the CloudEvent
|
||||
/// in order to determine the effective data content type, but most cases can be handled by
|
||||
/// simply overriding <see cref="InferDataContentType(object)"/>.
|
||||
/// </para>
|
||||
/// </remarks>
|
||||
/// <param name="cloudEvent">The CloudEvent to get or infer the data content type from. Must not be null.</param>
|
||||
/// <returns>The data content type of the CloudEvent, or null for no data content type.</returns>
|
||||
public virtual string? GetOrInferDataContentType(CloudEvent cloudEvent)
|
||||
{
|
||||
Validation.CheckNotNull(cloudEvent, nameof(cloudEvent));
|
||||
return cloudEvent.DataContentType is string dataContentType ? dataContentType
|
||||
: cloudEvent.Data is not object data ? null
|
||||
: InferDataContentType(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Infers the effective data content type based on the actual data. This base implementation
|
||||
/// always returns null, but derived classes may override this method to effectively provide
|
||||
/// a default data content type based on the in-memory data type.
|
||||
/// </summary>
|
||||
/// <param name="data">The data within a CloudEvent. Should not be null.</param>
|
||||
/// <returns>The inferred content type, or null if no content type is inferred.</returns>
|
||||
protected virtual string? InferDataContentType(object data) => null;
|
||||
}
|
||||
}
|
|
@ -3,7 +3,7 @@
|
|||
<PropertyGroup>
|
||||
<TargetFrameworks>netstandard2.0;netstandard2.1</TargetFrameworks>
|
||||
<Description>CNCF CloudEvents SDK</Description>
|
||||
<LangVersion>8.0</LangVersion>
|
||||
<LangVersion>latest</LangVersion>
|
||||
<Nullable>enable</Nullable>
|
||||
<PackageTags>cloudnative;cloudevents;events</PackageTags>
|
||||
</PropertyGroup>
|
||||
|
|
|
@ -273,7 +273,7 @@ namespace CloudNative.CloudEvents.Http
|
|||
break;
|
||||
case ContentMode.Binary:
|
||||
content = formatter.EncodeBinaryModeEventData(cloudEvent);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
|
||||
|
|
|
@ -41,7 +41,7 @@ namespace CloudNative.CloudEvents.Http
|
|||
break;
|
||||
case ContentMode.Binary:
|
||||
content = formatter.EncodeBinaryModeEventData(cloudEvent);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
|
||||
|
|
|
@ -43,7 +43,7 @@ namespace CloudNative.CloudEvents.Http
|
|||
break;
|
||||
case ContentMode.Binary:
|
||||
content = formatter.EncodeBinaryModeEventData(cloudEvent);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(cloudEvent.DataContentType);
|
||||
contentType = MimeUtilities.CreateContentTypeOrNull(formatter.GetOrInferDataContentType(cloudEvent));
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentOutOfRangeException(nameof(contentMode), $"Unsupported content mode: {contentMode}");
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
using Amqp;
|
||||
using Amqp.Framing;
|
||||
using CloudNative.CloudEvents.NewtonsoftJson;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using System;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
|
@ -18,8 +19,7 @@ namespace CloudNative.CloudEvents.Amqp.UnitTests
|
|||
[Fact]
|
||||
public void AmqpStructuredMessageTest()
|
||||
{
|
||||
// the AMQPNetLite library is factored such
|
||||
// that we don't need to do a wire test here
|
||||
// The AMQPNetLite library is factored such that we don't need to do a wire test here.
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Type = "com.github.pull.create",
|
||||
|
@ -55,9 +55,7 @@ namespace CloudNative.CloudEvents.Amqp.UnitTests
|
|||
[Fact]
|
||||
public void AmqpBinaryMessageTest()
|
||||
{
|
||||
// the AMQPNetLite library is factored such
|
||||
// that we don't need to do a wire test here
|
||||
|
||||
// The AMQPNetLite library is factored such that we don't need to do a wire test here.
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Type = "com.github.pull.create",
|
||||
|
@ -89,6 +87,18 @@ namespace CloudNative.CloudEvents.Amqp.UnitTests
|
|||
Assert.Equal("value", (string?)receivedCloudEvent["comexampleextension1"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void BinaryMode_ContentTypeCanBeInferredByFormatter()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = "plain text"
|
||||
}.PopulateRequiredAttributes();
|
||||
|
||||
var message = cloudEvent.ToAmqpMessage(ContentMode.Binary, new JsonEventFormatter());
|
||||
Assert.Equal("application/json", message.Properties.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AmqpNormalizesTimestampsToUtc()
|
||||
{
|
||||
|
|
|
@ -40,9 +40,22 @@ namespace CloudNative.CloudEvents.AspNetCore.UnitTests
|
|||
// There's no data content type header; the content type itself is used for that.
|
||||
Assert.False(response.Headers.ContainsKey("ce-datacontenttype"));
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToHttpResponseAsync_ContentButNoContentType()
|
||||
public async Task CopyToHttpResponseAsync_BinaryDataButNoDataContentType()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = new byte[10],
|
||||
}.PopulateRequiredAttributes();
|
||||
var formatter = new JsonEventFormatter();
|
||||
var response = CreateResponse();
|
||||
// The formatter doesn't infer the data content type for binary data.
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => cloudEvent.CopyToHttpResponseAsync(response, ContentMode.Binary, formatter));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToHttpResponseAsync_NonBinaryDataButNoDataContentType_ContentTypeIsInferred()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
|
@ -50,7 +63,11 @@ namespace CloudNative.CloudEvents.AspNetCore.UnitTests
|
|||
}.PopulateRequiredAttributes();
|
||||
var formatter = new JsonEventFormatter();
|
||||
var response = CreateResponse();
|
||||
await Assert.ThrowsAsync<ArgumentException>(() => cloudEvent.CopyToHttpResponseAsync(response, ContentMode.Binary, formatter));
|
||||
await cloudEvent.CopyToHttpResponseAsync(response, ContentMode.Binary, formatter);
|
||||
var content = GetContent(response);
|
||||
// The formatter infers that it should encode the string as a JSON value (so it includes the double quotes)
|
||||
Assert.Equal("application/json", response.ContentType);
|
||||
Assert.Equal("\"plain text\"", Encoding.UTF8.GetString(content.Span));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -0,0 +1,87 @@
|
|||
// Copyright 2022 Cloud Native Foundation.
|
||||
// Licensed under the Apache 2.0 license.
|
||||
// See LICENSE file in the project root for full license information.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
using Xunit;
|
||||
|
||||
namespace CloudNative.CloudEvents.UnitTests
|
||||
{
|
||||
public class CloudEventFormatterTest
|
||||
{
|
||||
[Fact]
|
||||
public void GetOrInferDataContentType_NullCloudEvent()
|
||||
{
|
||||
var formatter = new ContentTypeInferringFormatter();
|
||||
Assert.Throws<ArgumentNullException>(() => formatter.GetOrInferDataContentType(null!));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrInferDataContentType_NoDataOrDataContentType()
|
||||
{
|
||||
var formatter = new ContentTypeInferringFormatter();
|
||||
var cloudEvent = new CloudEvent();
|
||||
Assert.Null(formatter.GetOrInferDataContentType(cloudEvent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrInferDataContentType_HasDataContentType()
|
||||
{
|
||||
var formatter = new ContentTypeInferringFormatter();
|
||||
var cloudEvent = new CloudEvent { DataContentType = "test/pass" };
|
||||
Assert.Equal(cloudEvent.DataContentType, formatter.GetOrInferDataContentType(cloudEvent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrInferDataContentType_HasDataButNoContentType_OverriddenInferDataContentType()
|
||||
{
|
||||
var formatter = new ContentTypeInferringFormatter();
|
||||
var cloudEvent = new CloudEvent { Data = "some-data" };
|
||||
Assert.Equal("test/some-data", formatter.GetOrInferDataContentType(cloudEvent));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void GetOrInferDataContentType_DataButNoContentType_DefaultInferDataContentType()
|
||||
{
|
||||
var formatter = new ThrowingEventFormatter();
|
||||
var cloudEvent = new CloudEvent { Data = "some-data" };
|
||||
Assert.Null(formatter.GetOrInferDataContentType(cloudEvent));
|
||||
}
|
||||
|
||||
private class ContentTypeInferringFormatter : ThrowingEventFormatter
|
||||
{
|
||||
protected override string? InferDataContentType(object data) => $"test/{data}";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Event formatter that overrides every abstract method to throw NotImplementedException.
|
||||
/// This can be derived from (and further overridden) to easily test concrete methods
|
||||
/// in CloudEventFormatter itself.
|
||||
/// </summary>
|
||||
private class ThrowingEventFormatter : CloudEventFormatter
|
||||
{
|
||||
public override IReadOnlyList<CloudEvent> DecodeBatchModeMessage(ReadOnlyMemory<byte> body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override void DecodeBinaryModeEventData(ReadOnlyMemory<byte> body, CloudEvent cloudEvent) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override CloudEvent DecodeStructuredModeMessage(ReadOnlyMemory<byte> body, ContentType? contentType, IEnumerable<CloudEventAttribute>? extensionAttributes) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override ReadOnlyMemory<byte> EncodeBatchModeMessage(IEnumerable<CloudEvent> cloudEvents, out ContentType contentType) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override ReadOnlyMemory<byte> EncodeBinaryModeEventData(CloudEvent cloudEvent) =>
|
||||
throw new NotImplementedException();
|
||||
|
||||
public override ReadOnlyMemory<byte> EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) =>
|
||||
throw new NotImplementedException();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -397,7 +397,7 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
// It should be okay to not set a DataContentType if there's no data...
|
||||
// but what if there's a data value which is an empty string, empty byte array or empty stream?
|
||||
[Fact]
|
||||
public void NoContentType_NoContent()
|
||||
public void NoDataContentType_NoData()
|
||||
{
|
||||
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
||||
var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
|
||||
|
@ -405,10 +405,22 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void NoContentType_WithContent()
|
||||
public void NoDataContentType_ContentTypeInferredFromFormatter()
|
||||
{
|
||||
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
||||
cloudEvent.Data = "Some text";
|
||||
// The JSON event format infers application/json for non-binary data
|
||||
cloudEvent.Data = new { Name = "xyz" };
|
||||
var content = cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter());
|
||||
var expectedContentType = new MediaTypeHeaderValue("application/json");
|
||||
Assert.Equal(expectedContentType, content.Headers.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void NoDataContentType_NoContentTypeInferredFromFormatter()
|
||||
{
|
||||
var cloudEvent = new CloudEvent().PopulateRequiredAttributes();
|
||||
// The JSON event format does not infer a data content type for binary data
|
||||
cloudEvent.Data = new byte[10];
|
||||
var exception = Assert.Throws<ArgumentException>(() => cloudEvent.ToHttpContent(ContentMode.Binary, new JsonEventFormatter()));
|
||||
Assert.StartsWith(Strings.ErrorContentTypeUnspecified, exception.Message);
|
||||
}
|
||||
|
|
|
@ -207,15 +207,30 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToListenerResponseAsync_ContentButNoContentType()
|
||||
public async Task CopyToListenerResponseAsync_BinaryDataButNoDataContentType()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = new byte[10],
|
||||
}.PopulateRequiredAttributes();
|
||||
var formatter = new JsonEventFormatter();
|
||||
await GetResponseAsync(
|
||||
async context => await Assert.ThrowsAsync<ArgumentException>(() => cloudEvent.CopyToHttpListenerResponseAsync(context.Response, ContentMode.Binary, formatter)));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToListenerResponseAsync_NonBinaryDataButNoDataContentType_ContentTypeIsInferred()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = "plain text",
|
||||
}.PopulateRequiredAttributes();
|
||||
var formatter = new JsonEventFormatter();
|
||||
await GetResponseAsync(
|
||||
async context => await Assert.ThrowsAsync<ArgumentException>(() => cloudEvent.CopyToHttpListenerResponseAsync(context.Response, ContentMode.Binary, formatter)));
|
||||
var response = await GetResponseAsync(
|
||||
async context => await cloudEvent.CopyToHttpListenerResponseAsync(context.Response, ContentMode.Binary, formatter));
|
||||
var content = response.Content;
|
||||
Assert.Equal("application/json", content.Headers.ContentType.MediaType);
|
||||
Assert.Equal("\"plain text\"", await content.ReadAsStringAsync());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
|
|
@ -108,6 +108,32 @@ namespace CloudNative.CloudEvents.Http.UnitTests
|
|||
Assert.True(result.StatusCode == HttpStatusCode.NoContent, content);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToHttpWebRequestAsync_BinaryDataButNoDataContentType()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = new byte[10],
|
||||
}.PopulateRequiredAttributes();
|
||||
HttpWebRequest httpWebRequest = WebRequest.CreateHttp(ListenerAddress + "ep");
|
||||
httpWebRequest.Method = "POST";
|
||||
await Assert.ThrowsAsync<ArgumentException>(
|
||||
async () => await cloudEvent.CopyToHttpWebRequestAsync(httpWebRequest, ContentMode.Binary, new JsonEventFormatter()));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToHttpWebRequestAsync_NonBinaryDataButNoDataContentType_ContentTypeIsInferred()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = "plain text",
|
||||
}.PopulateRequiredAttributes();
|
||||
HttpWebRequest httpWebRequest = WebRequest.CreateHttp(ListenerAddress + "ep");
|
||||
httpWebRequest.Method = "POST";
|
||||
await cloudEvent.CopyToHttpWebRequestAsync(httpWebRequest, ContentMode.Binary, new JsonEventFormatter());
|
||||
Assert.Equal("application/json", httpWebRequest.ContentType);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CopyToHttpWebRequestAsync_Batch()
|
||||
{
|
||||
|
|
|
@ -8,6 +8,7 @@ using Confluent.Kafka;
|
|||
using Newtonsoft.Json;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Net.Mime;
|
||||
using System.Text;
|
||||
using Xunit;
|
||||
|
@ -61,8 +62,8 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests
|
|||
|
||||
Assert.True(message.IsCloudEvent());
|
||||
|
||||
// using serialization to create fully independent copy thus simulating message transport
|
||||
// real transport will work in a similar way
|
||||
// Using serialization to create fully independent copy thus simulating message transport.
|
||||
// The real transport will work in a similar way.
|
||||
var serialized = JsonConvert.SerializeObject(message, new HeaderConverter());
|
||||
var messageCopy = JsonConvert.DeserializeObject<Message<string?, byte[]>>(serialized, new HeadersConverter(), new HeaderConverter())!;
|
||||
|
||||
|
@ -104,8 +105,8 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests
|
|||
var message = cloudEvent.ToKafkaMessage(ContentMode.Binary, new JsonEventFormatter());
|
||||
Assert.True(message.IsCloudEvent());
|
||||
|
||||
// using serialization to create fully independent copy thus simulating message transport
|
||||
// real transport will work in a similar way
|
||||
// Using serialization to create fully independent copy thus simulating message transport.
|
||||
// The real transport will work in a similar way.
|
||||
var serialized = JsonConvert.SerializeObject(message, new HeaderConverter());
|
||||
var settings = new JsonSerializerSettings
|
||||
{
|
||||
|
@ -128,6 +129,20 @@ namespace CloudNative.CloudEvents.Kafka.UnitTests
|
|||
Assert.Equal("value", (string?)receivedCloudEvent["comexampleextension1"]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ContentTypeCanBeInferredByFormatter()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = "plain text"
|
||||
}.PopulateRequiredAttributes();
|
||||
|
||||
var message = cloudEvent.ToKafkaMessage(ContentMode.Binary, new JsonEventFormatter());
|
||||
var contentTypeHeader = message.Headers.Single(h => h.Key == KafkaExtensions.KafkaContentTypeAttributeName);
|
||||
var contentTypeValue = Encoding.UTF8.GetString(contentTypeHeader.GetValueBytes());
|
||||
Assert.Equal("application/json", contentTypeValue);
|
||||
}
|
||||
|
||||
private class HeadersConverter : JsonConverter
|
||||
{
|
||||
public override bool CanConvert(Type objectType)
|
||||
|
|
|
@ -1036,23 +1036,22 @@ namespace CloudNative.CloudEvents.NewtonsoftJson.UnitTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeStructured_BinaryData_DefaultContentTypeToApplicationJson()
|
||||
public void EncodeStructured_BinaryData_DefaultContentTypeIsNotImplied()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = SampleBinaryData
|
||||
}.PopulateRequiredAttributes();
|
||||
|
||||
// While it's odd for a CloudEvent to have binary data but no data content type,
|
||||
// the spec says the data should be placed in data_base64, and the content type should
|
||||
// default to application/json. (Checking in https://github.com/cloudevents/spec/issues/933)
|
||||
// If a CloudEvent to have binary data but no data content type,
|
||||
// the spec says the data should be placed in data_base64, but the content type
|
||||
// should *not* be defaulted to application/json, as clarified in https://github.com/cloudevents/spec/issues/933
|
||||
var encoded = new JsonEventFormatter().EncodeStructuredModeMessage(cloudEvent, out var contentType);
|
||||
Assert.Equal("application/cloudevents+json; charset=utf-8", contentType.ToString());
|
||||
JObject obj = ParseJson(encoded);
|
||||
var asserter = new JTokenAsserter
|
||||
{
|
||||
{ "data_base64", JTokenType.String, SampleBinaryDataBase64 },
|
||||
{ "datacontenttype", JTokenType.String, "application/json" },
|
||||
{ "id", JTokenType.String, "test-id" },
|
||||
{ "source", JTokenType.String, "//test" },
|
||||
{ "specversion", JTokenType.String, "1.0" },
|
||||
|
|
|
@ -1043,23 +1043,22 @@ namespace CloudNative.CloudEvents.SystemTextJson.UnitTests
|
|||
}
|
||||
|
||||
[Fact]
|
||||
public void EncodeStructured_BinaryData_DefaultContentTypeToApplicationJson()
|
||||
public void EncodeStructured_BinaryData_DefaultContentTypeIsNotImplied()
|
||||
{
|
||||
var cloudEvent = new CloudEvent
|
||||
{
|
||||
Data = SampleBinaryData
|
||||
}.PopulateRequiredAttributes();
|
||||
|
||||
// While it's odd for a CloudEvent to have binary data but no data content type,
|
||||
// the spec says the data should be placed in data_base64, and the content type should
|
||||
// default to application/json. (Checking in https://github.com/cloudevents/spec/issues/933)
|
||||
// If a CloudEvent to have binary data but no data content type,
|
||||
// the spec says the data should be placed in data_base64, but the content type
|
||||
// should *not* be defaulted to application/json, as clarified in https://github.com/cloudevents/spec/issues/933
|
||||
var encoded = new JsonEventFormatter().EncodeStructuredModeMessage(cloudEvent, out var contentType);
|
||||
Assert.Equal("application/cloudevents+json; charset=utf-8", contentType.ToString());
|
||||
JsonElement obj = ParseJson(encoded);
|
||||
var asserter = new JsonElementAsserter
|
||||
{
|
||||
{ "data_base64", JsonValueKind.String, SampleBinaryDataBase64 },
|
||||
{ "datacontenttype", JsonValueKind.String, "application/json" },
|
||||
{ "id", JsonValueKind.String, "test-id" },
|
||||
{ "source", JsonValueKind.String, "//test" },
|
||||
{ "specversion", JsonValueKind.String, "1.0" },
|
||||
|
|
Loading…
Reference in New Issue