Move implementation utility methods into their own namespace

- Preconditions is renamed to Validation
- Validation is public, allowing simplified validation in
  implementation classes
- CloudEvent.ValidateForConversion is now Validation.CheckCloudEventArgument
- All the MimeUtilities methods are non-extension methods (to avoid
  Intellisense suggesting them)
- BinaryDataUtilities is now public (it's much less of an issue once
  it's in a namespace that other users are somewhat discouraged from
  using)
- Documentation explains the purpose of the namespace, albeit briefly

Signed-off-by: Jon Skeet <jonskeet@google.com>
This commit is contained in:
Jon Skeet 2021-03-25 11:14:14 +00:00 committed by Jon Skeet
parent 00391089d2
commit 41f639d44b
26 changed files with 251 additions and 214 deletions

View File

@ -4,3 +4,15 @@ This directory contains documentation on:
- Using the SDK as a consumer - Using the SDK as a consumer
- Implementing new event formats and protocol bindings - Implementing new event formats and protocol bindings
## Implementation utility classes
The `CloudNative.CloudEvents.Core` namespace contains utility
classes which are generally helpful when implementing [protocol
bindings](bindings.md) or [event formatters](formatters.md) but are
not expected to be used by code which only creates or consumes
CloudEvents.
The classes in this namespace are static classes, but the methods
are deliberately not extension methods. This avoids the methods from
being suggested to non-implementation code.

View File

@ -95,7 +95,7 @@ following pseudo-code as structure:
- If the message contains content, call the - If the message contains content, call the
`formatter.DecodeBinaryModeEventData` method to populate the `formatter.DecodeBinaryModeEventData` method to populate the
`CloudEvent.Data` property appropriately. `CloudEvent.Data` property appropriately.
- Return the result of `CloudEvent.ValidateForConversion` which - Return the result of `Validation.CheckCloudEventArgument` which
will validate the event, and either return the original reference if will validate the event, and either return the original reference if
the event is valid, or throw an appropriate `ArgumentException` the event is valid, or throw an appropriate `ArgumentException`
otherwise. otherwise.
@ -155,9 +155,10 @@ The conversion should follow the following steps of pseudo-code:
- Parameter validation (which may be completed in any order): - Parameter validation (which may be completed in any order):
- `cloudEvent` and `formatter` should be non-null - `cloudEvent` and `formatter` should be non-null
(`CheckCloudEventArgument` will validate this for the `cloudEvent` parameter)
- In a `CopyTo...` method, `destination` should be non-null - In a `CopyTo...` method, `destination` should be non-null
- The `contentMode` should be a known, supported value - The `contentMode` should be a known, supported value
- Call `cloudEvent.ValidateForConversion(nameof(cloudEvent))` - Call `Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent))`
for validation of the original CloudEvent. for validation of the original CloudEvent.
- For structured mode encoding: - For structured mode encoding:
- Call `formatter.EncodeStructuredModeMessage` to encode - Call `formatter.EncodeStructuredModeMessage` to encode

View File

@ -73,10 +73,10 @@ being non-null, and additionally perform CloudEvent validation on:
- The `CloudEvent` accepted in `EncodeBinaryModeEventData` or - The `CloudEvent` accepted in `EncodeBinaryModeEventData` or
`EncodeStructuredModeMessage` `EncodeStructuredModeMessage`
Validation should be performed using the `ValidateForConversion` Validation should be performed using the `Validation.CheckCloudEventArgument`
method, so that an appropriate `ArgumentException` is thrown. method, so that an appropriate `ArgumentException` is thrown.
The formatter should *not* perform validation on the The formatter should *not* perform validation on the `CloudEvent`
`CloudEvent` accepted in `DecodeBinaryModeEventData`. This is accepted in `DecodeBinaryModeEventData`, beyond asserting that the
typically called by a protocol binding which should perform argument is not null. This is typically called by a protocol binding
validation itself later. which should perform validation itself later.

View File

@ -5,6 +5,7 @@
using Amqp; using Amqp;
using Amqp.Framing; using Amqp.Framing;
using Amqp.Types; using Amqp.Types;
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -50,8 +51,8 @@ namespace CloudNative.CloudEvents.Amqp
CloudEventFormatter formatter, CloudEventFormatter formatter,
IEnumerable<CloudEventAttribute> extensionAttributes) IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
message = message ?? throw new ArgumentNullException(nameof(message)); Validation.CheckNotNull(message, nameof(message));
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
if (HasCloudEventsContentType(message, out var contentType)) if (HasCloudEventsContentType(message, out var contentType))
{ {
@ -122,7 +123,7 @@ namespace CloudNative.CloudEvents.Amqp
throw new ArgumentException("Binary mode data in AMQP message must be in the application data section"); throw new ArgumentException("Binary mode data in AMQP message must be in the application data section");
} }
return cloudEvent.ValidateForConversion(nameof(message)); return Validation.CheckCloudEventArgument(cloudEvent, nameof(message));
} }
} }
@ -141,9 +142,8 @@ namespace CloudNative.CloudEvents.Amqp
/// <param name="formatter">The formatter to use within the conversion. Must not be null.</param> /// <param name="formatter">The formatter to use within the conversion. Must not be null.</param>
public static Message ToAmqpMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) public static Message ToAmqpMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent)); Validation.CheckNotNull(formatter, nameof(formatter));
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
var applicationProperties = MapHeaders(cloudEvent); var applicationProperties = MapHeaders(cloudEvent);
RestrictedDescribed bodySection; RestrictedDescribed bodySection;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers; using Microsoft.Net.Http.Headers;
using System; using System;
@ -30,15 +31,8 @@ namespace CloudNative.CloudEvents
public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) public override async Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding)
{ {
if (context == null) Validation.CheckNotNull(context, nameof(context));
{ Validation.CheckNotNull(encoding, nameof(encoding));
throw new ArgumentNullException(nameof(context));
}
if (encoding == null)
{
throw new ArgumentNullException(nameof(encoding));
}
var request = context.HttpContext.Request; var request = context.HttpContext.Request;

View File

@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Http; using CloudNative.CloudEvents.Http;
using CloudNative.CloudEvents.Core;
using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -79,13 +80,10 @@ namespace CloudNative.CloudEvents
cloudEvent.DataContentType = httpRequest.ContentType; cloudEvent.DataContentType = httpRequest.ContentType;
if (httpRequest.Body is Stream body) if (httpRequest.Body is Stream body)
{ {
// TODO: This is a bit ugly. We have code in BinaryDataUtilities to handle this, but byte[] data = await BinaryDataUtilities.ToByteArrayAsync(body).ConfigureAwait(false);
// we'd rather not expose it... formatter.DecodeBinaryModeEventData(data, cloudEvent);
var memoryStream = new MemoryStream();
await body.CopyToAsync(memoryStream).ConfigureAwait(false);
formatter.DecodeBinaryModeEventData(memoryStream.ToArray(), cloudEvent);
} }
return cloudEvent.ValidateForConversion(nameof(httpRequest)); return Validation.CheckCloudEventArgument(cloudEvent, nameof(httpRequest));
} }
} }

View File

@ -5,6 +5,7 @@
using Avro; using Avro;
using Avro.Generic; using Avro.Generic;
using Avro.IO; using Avro.IO;
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -53,7 +54,7 @@ namespace CloudNative.CloudEvents
public override CloudEvent DecodeStructuredModeMessage(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes) public override CloudEvent DecodeStructuredModeMessage(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
data = data ?? throw new ArgumentNullException(nameof(data)); Validation.CheckNotNull(data, nameof(data));
var decoder = new BinaryDecoder(data); var decoder = new BinaryDecoder(data);
var rawEvent = avroReader.Read<GenericRecord>(null, decoder); var rawEvent = avroReader.Read<GenericRecord>(null, decoder);
@ -62,7 +63,7 @@ namespace CloudNative.CloudEvents
public override CloudEvent DecodeStructuredModeMessage(byte[] data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes) public override CloudEvent DecodeStructuredModeMessage(byte[] data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
data = data ?? throw new ArgumentNullException(nameof(data)); Validation.CheckNotNull(data, nameof(data));
return DecodeStructuredModeMessage(new MemoryStream(data), contentType, extensionAttributes); return DecodeStructuredModeMessage(new MemoryStream(data), contentType, extensionAttributes);
} }
@ -119,13 +120,12 @@ namespace CloudNative.CloudEvents
} }
} }
return cloudEvent.ValidateForConversion(nameof(record)); return Validation.CheckCloudEventArgument(cloudEvent, nameof(record));
} }
public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent));
contentType = new ContentType(CloudEvent.MediaType + MediaTypeSuffix); contentType = new ContentType(CloudEvent.MediaType + MediaTypeSuffix);

View File

@ -3,6 +3,7 @@
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Extensions; using CloudNative.CloudEvents.Extensions;
using CloudNative.CloudEvents.Core;
using Confluent.Kafka; using Confluent.Kafka;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -50,8 +51,8 @@ namespace CloudNative.CloudEvents.Kafka
public static CloudEvent ToCloudEvent(this Message<string, byte[]> message, public static CloudEvent ToCloudEvent(this Message<string, byte[]> message,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
message = message ?? throw new ArgumentNullException(nameof(message)); Validation.CheckNotNull(message, nameof(message));
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
if (!IsCloudEvent(message)) if (!IsCloudEvent(message))
{ {
@ -104,7 +105,7 @@ namespace CloudNative.CloudEvents.Kafka
} }
InitPartitioningKey(message, cloudEvent); InitPartitioningKey(message, cloudEvent);
return cloudEvent.ValidateForConversion(nameof(message)); return Validation.CheckCloudEventArgument(cloudEvent, nameof(message));
} }
private static string ExtractContentType(Message<string, byte[]> message) private static string ExtractContentType(Message<string, byte[]> message)
@ -133,15 +134,11 @@ namespace CloudNative.CloudEvents.Kafka
/// <param name="formatter">The formatter to use within the conversion. Must not be null.</param> /// <param name="formatter">The formatter to use within the conversion. Must not be null.</param>
public static Message<string, byte[]> ToKafkaMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) public static Message<string, byte[]> ToKafkaMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent)); Validation.CheckNotNull(formatter, nameof(formatter));
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
// TODO: Is this appropriate? Why can't we transport a CloudEvent without data in Kafka? // TODO: Is this appropriate? Why can't we transport a CloudEvent without data in Kafka?
if (cloudEvent.Data == null) Validation.CheckArgument(cloudEvent.Data is object, nameof(cloudEvent), "Only CloudEvents with data can be converted to Kafka messages");
{
throw new ArgumentNullException(nameof(cloudEvent.Data));
}
var headers = MapHeaders(cloudEvent, formatter); var headers = MapHeaders(cloudEvent, formatter);
string key = (string) cloudEvent[Partitioning.PartitionKeyAttribute]; string key = (string) cloudEvent[Partitioning.PartitionKeyAttribute];
byte[] value; byte[] value;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using MQTTnet; using MQTTnet;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
@ -34,8 +35,8 @@ namespace CloudNative.CloudEvents.Mqtt
public static CloudEvent ToCloudEvent(this MqttApplicationMessage message, public static CloudEvent ToCloudEvent(this MqttApplicationMessage message,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
message = message ?? throw new ArgumentNullException(nameof(message)); Validation.CheckNotNull(formatter, nameof(formatter));
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); Validation.CheckNotNull(message, nameof(message));
// TODO: Determine if there's a sensible content type we should apply. // TODO: Determine if there's a sensible content type we should apply.
return formatter.DecodeStructuredModeMessage(message.Payload, contentType: null, extensionAttributes); return formatter.DecodeStructuredModeMessage(message.Payload, contentType: null, extensionAttributes);
@ -51,9 +52,8 @@ namespace CloudNative.CloudEvents.Mqtt
/// <param name="topic">The MQTT topic for the message. May be null.</param> /// <param name="topic">The MQTT topic for the message. May be null.</param>
public static MqttApplicationMessage ToMqttApplicationMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter, string topic) public static MqttApplicationMessage ToMqttApplicationMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter, string topic)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent)); Validation.CheckNotNull(formatter, nameof(formatter));
formatter = formatter ?? throw new ArgumentNullException(nameof(formatter));
switch (contentMode) switch (contentMode)
{ {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using Newtonsoft.Json; using Newtonsoft.Json;
using Newtonsoft.Json.Linq; using Newtonsoft.Json.Linq;
using System; using System;
@ -112,23 +113,23 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
/// </summary> /// </summary>
public JsonEventFormatter(JsonSerializer serializer) public JsonEventFormatter(JsonSerializer serializer)
{ {
Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); Serializer = Validation.CheckNotNull(serializer, nameof(serializer));
} }
public override async Task<CloudEvent> DecodeStructuredModeMessageAsync(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes) public override async Task<CloudEvent> DecodeStructuredModeMessageAsync(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
data = data ?? throw new ArgumentNullException(nameof(data)); Validation.CheckNotNull(data, nameof(data));
var jsonReader = CreateJsonReader(data, contentType.GetEncoding()); var jsonReader = CreateJsonReader(data, MimeUtilities.GetEncoding(contentType));
var jObject = await JObject.LoadAsync(jsonReader).ConfigureAwait(false); var jObject = await JObject.LoadAsync(jsonReader).ConfigureAwait(false);
return DecodeJObject(jObject, extensionAttributes); return DecodeJObject(jObject, extensionAttributes);
} }
public override CloudEvent DecodeStructuredModeMessage(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes) public override CloudEvent DecodeStructuredModeMessage(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
data = data ?? throw new ArgumentNullException(nameof(data)); Validation.CheckNotNull(data, nameof(data));
var jsonReader = CreateJsonReader(data, contentType.GetEncoding()); var jsonReader = CreateJsonReader(data, MimeUtilities.GetEncoding(contentType));
var jObject = JObject.Load(jsonReader); var jObject = JObject.Load(jsonReader);
return DecodeJObject(jObject, extensionAttributes); return DecodeJObject(jObject, extensionAttributes);
} }
@ -151,7 +152,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
PopulateDataFromStructuredEvent(cloudEvent, jObject); PopulateDataFromStructuredEvent(cloudEvent, jObject);
// "data" is always the parameter from the public method. It's annoying not to be able to use // "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. // nameof here, but this will give the appropriate result.
return cloudEvent.ValidateForConversion("data"); return Validation.CheckCloudEventArgument(cloudEvent, "data");
} }
private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JObject jObject) private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JObject jObject)
@ -306,8 +307,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent));
contentType = new ContentType("application/cloudevents+json") contentType = new ContentType("application/cloudevents+json")
{ {
@ -389,8 +389,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
public override byte[] EncodeBinaryModeEventData(CloudEvent cloudEvent) public override byte[] EncodeBinaryModeEventData(CloudEvent cloudEvent)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent));
if (cloudEvent.Data is null) if (cloudEvent.Data is null)
{ {
@ -405,11 +404,11 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
// without a preamble (or rewrite StreamWriter...) // without a preamble (or rewrite StreamWriter...)
var stringWriter = new StringWriter(); var stringWriter = new StringWriter();
Serializer.Serialize(stringWriter, cloudEvent.Data); Serializer.Serialize(stringWriter, cloudEvent.Data);
return contentType.GetEncoding().GetBytes(stringWriter.ToString()); return MimeUtilities.GetEncoding(contentType).GetBytes(stringWriter.ToString());
} }
if (contentType.MediaType.StartsWith("text/") && cloudEvent.Data is string text) if (contentType.MediaType.StartsWith("text/") && cloudEvent.Data is string text)
{ {
return contentType.GetEncoding().GetBytes(text); return MimeUtilities.GetEncoding(contentType).GetBytes(text);
} }
if (cloudEvent.Data is byte[] bytes) if (cloudEvent.Data is byte[] bytes)
{ {
@ -420,12 +419,12 @@ namespace CloudNative.CloudEvents.NewtonsoftJson
public override void DecodeBinaryModeEventData(byte[] value, CloudEvent cloudEvent) public override void DecodeBinaryModeEventData(byte[] value, CloudEvent cloudEvent)
{ {
value = value ?? throw new ArgumentNullException(nameof(value)); Validation.CheckNotNull(value, nameof(value));
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckNotNull(cloudEvent, nameof(cloudEvent));
ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType); ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType);
Encoding encoding = contentType.GetEncoding(); Encoding encoding = MimeUtilities.GetEncoding(contentType);
if (contentType.MediaType == JsonMediaType) if (contentType.MediaType == JsonMediaType)
{ {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;
@ -119,8 +120,9 @@ namespace CloudNative.CloudEvents.SystemTextJson
private async Task<CloudEvent> DecodeStructuredModeMessageImpl(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes, bool async) private async Task<CloudEvent> DecodeStructuredModeMessageImpl(Stream data, ContentType contentType, IEnumerable<CloudEventAttribute> extensionAttributes, bool async)
{ {
data = data ?? throw new ArgumentNullException(nameof(data)); Validation.CheckNotNull(data, nameof(data));
var encoding = contentType.GetEncoding();
var encoding = MimeUtilities.GetEncoding(contentType);
JsonDocument document; JsonDocument document;
if (encoding is UTF8Encoding) if (encoding is UTF8Encoding)
{ {
@ -157,7 +159,7 @@ namespace CloudNative.CloudEvents.SystemTextJson
PopulateDataFromStructuredEvent(cloudEvent, document); PopulateDataFromStructuredEvent(cloudEvent, document);
// "data" is always the parameter from the public method. It's annoying not to be able to use // "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. // nameof here, but this will give the appropriate result.
return cloudEvent.ValidateForConversion("data"); return Validation.CheckCloudEventArgument(cloudEvent, "data");
} }
private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JsonDocument document) private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JsonDocument document)
@ -321,8 +323,7 @@ namespace CloudNative.CloudEvents.SystemTextJson
public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent));
contentType = new ContentType("application/cloudevents+json") contentType = new ContentType("application/cloudevents+json")
{ {
@ -404,8 +405,7 @@ namespace CloudNative.CloudEvents.SystemTextJson
public override byte[] EncodeBinaryModeEventData(CloudEvent cloudEvent) public override byte[] EncodeBinaryModeEventData(CloudEvent cloudEvent)
{ {
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent));
if (cloudEvent.Data is null) if (cloudEvent.Data is null)
{ {
@ -414,19 +414,19 @@ namespace CloudNative.CloudEvents.SystemTextJson
ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType); ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType);
if (contentType.MediaType == JsonMediaType) if (contentType.MediaType == JsonMediaType)
{ {
var encoding = contentType.GetEncoding(); var encoding = MimeUtilities.GetEncoding(contentType);
if (encoding is UTF8Encoding) if (encoding is UTF8Encoding)
{ {
return JsonSerializer.SerializeToUtf8Bytes(cloudEvent.Data, SerializerOptions); return JsonSerializer.SerializeToUtf8Bytes(cloudEvent.Data, SerializerOptions);
} }
else else
{ {
return contentType.GetEncoding().GetBytes(JsonSerializer.Serialize(cloudEvent.Data, SerializerOptions)); return MimeUtilities.GetEncoding(contentType).GetBytes(JsonSerializer.Serialize(cloudEvent.Data, SerializerOptions));
} }
} }
if (contentType.MediaType.StartsWith("text/") && cloudEvent.Data is string text) if (contentType.MediaType.StartsWith("text/") && cloudEvent.Data is string text)
{ {
return contentType.GetEncoding().GetBytes(text); return MimeUtilities.GetEncoding(contentType).GetBytes(text);
} }
if (cloudEvent.Data is byte[] bytes) if (cloudEvent.Data is byte[] bytes)
{ {
@ -437,12 +437,12 @@ namespace CloudNative.CloudEvents.SystemTextJson
public override void DecodeBinaryModeEventData(byte[] value, CloudEvent cloudEvent) public override void DecodeBinaryModeEventData(byte[] value, CloudEvent cloudEvent)
{ {
value = value ?? throw new ArgumentNullException(nameof(value)); Validation.CheckNotNull(value, nameof(value));
cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); Validation.CheckNotNull(cloudEvent, nameof(cloudEvent));
ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType); ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType);
Encoding encoding = contentType.GetEncoding(); Encoding encoding = MimeUtilities.GetEncoding(contentType);
if (contentType.MediaType == JsonMediaType) if (contentType.MediaType == JsonMediaType)
{ {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -62,33 +63,32 @@ namespace CloudNative.CloudEvents
/// to an empty sequence.</param> /// to an empty sequence.</param>
public CloudEvent(CloudEventsSpecVersion specVersion, IEnumerable<CloudEventAttribute> extensionAttributes) public CloudEvent(CloudEventsSpecVersion specVersion, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
// TODO: Validate that all attributes are extension attributes.
// TODO: Work out how to be more efficient, e.g. not creating a dictionary at all if there are no // TODO: Work out how to be more efficient, e.g. not creating a dictionary at all if there are no
// extension attributes. // extension attributes.
SpecVersion = Preconditions.CheckNotNull(specVersion, nameof(specVersion)); SpecVersion = Validation.CheckNotNull(specVersion, nameof(specVersion));
if (extensionAttributes is object) if (extensionAttributes is object)
{ {
foreach (var extension in extensionAttributes) foreach (var extension in extensionAttributes)
{ {
Preconditions.CheckArgument( Validation.CheckArgument(
extension is object, extension is object,
nameof(extensionAttributes), nameof(extensionAttributes),
"Extension attribute collection cannot contain null elements"); "Extension attribute collection cannot contain null elements");
Preconditions.CheckArgument( Validation.CheckArgument(
extension.Name != CloudEventsSpecVersion.SpecVersionAttributeName, extension.Name != CloudEventsSpecVersion.SpecVersionAttributeName,
nameof(extensionAttributes), nameof(extensionAttributes),
"The 'specversion' attribute cannot be specified as an extension attribute"); "The 'specversion' attribute cannot be specified as an extension attribute");
Preconditions.CheckArgument( Validation.CheckArgument(
SpecVersion.GetAttributeByName(extension.Name) is null, SpecVersion.GetAttributeByName(extension.Name) is null,
nameof(extensionAttributes), nameof(extensionAttributes),
"'{0}' cannot be specified as the name of an extension attribute; it is already a context attribute", "'{0}' cannot be specified as the name of an extension attribute; it is already a context attribute",
extension.Name); extension.Name);
Preconditions.CheckArgument( Validation.CheckArgument(
extension.IsExtension, extension.IsExtension,
nameof(extensionAttributes), nameof(extensionAttributes),
"'{0}' is not an extension attribute", "'{0}' is not an extension attribute",
extension.Name); extension.Name);
Preconditions.CheckArgument( Validation.CheckArgument(
!this.extensionAttributes.ContainsKey(extension.Name), !this.extensionAttributes.ContainsKey(extension.Name),
nameof(extensionAttributes), nameof(extensionAttributes),
"'{0}' cannot be specified more than once as an extension attribute"); "'{0}' cannot be specified more than once as an extension attribute");
@ -123,8 +123,8 @@ namespace CloudNative.CloudEvents
{ {
get get
{ {
Preconditions.CheckNotNull(attribute, nameof(attribute)); Validation.CheckNotNull(attribute, nameof(attribute));
Preconditions.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute); Validation.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute);
// TODO: Is this validation definitely useful? It does mean we never return something // TODO: Is this validation definitely useful? It does mean we never return something
// that's invalid for the attribute, which is potentially good... // that's invalid for the attribute, which is potentially good...
@ -137,14 +137,14 @@ namespace CloudNative.CloudEvents
} }
set set
{ {
Preconditions.CheckNotNull(attribute, nameof(attribute)); Validation.CheckNotNull(attribute, nameof(attribute));
Preconditions.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute); Validation.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute);
string name = attribute.Name; string name = attribute.Name;
var knownAttribute = GetAttribute(name); var knownAttribute = GetAttribute(name);
// TODO: Are we happy to add the extension in even if the value is null? // TODO: Are we happy to add the extension in even if the value is null?
Preconditions.CheckArgument(knownAttribute is object || attribute.IsExtension, Validation.CheckArgument(knownAttribute is object || attribute.IsExtension,
nameof(attribute), nameof(attribute),
"Cannot add an unknown non-extension attribute to an event."); "Cannot add an unknown non-extension attribute to an event.");
@ -178,14 +178,14 @@ namespace CloudNative.CloudEvents
get get
{ {
// TODO: Validate the attribute name is valid (e.g. not upper case)? Seems overkill. // TODO: Validate the attribute name is valid (e.g. not upper case)? Seems overkill.
Preconditions.CheckNotNull(attributeName, nameof(attributeName)); Validation.CheckNotNull(attributeName, nameof(attributeName));
Preconditions.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute); Validation.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute);
return attributeValues.GetValueOrDefault(Preconditions.CheckNotNull(attributeName, nameof(attributeName))); return attributeValues.GetValueOrDefault(Validation.CheckNotNull(attributeName, nameof(attributeName)));
} }
set set
{ {
Preconditions.CheckNotNull(attributeName, nameof(attributeName)); Validation.CheckNotNull(attributeName, nameof(attributeName));
Preconditions.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute); Validation.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute);
var knownAttribute = GetAttribute(attributeName); var knownAttribute = GetAttribute(attributeName);
@ -193,7 +193,7 @@ namespace CloudNative.CloudEvents
// (It's a simple way of populating extensions after the fact...) // (It's a simple way of populating extensions after the fact...)
if (knownAttribute is null) if (knownAttribute is null)
{ {
Preconditions.CheckArgument(value is null || value is string, Validation.CheckArgument(value is null || value is string,
nameof(value), "Cannot assign value of type {0} to unknown attribute '{1}'", nameof(value), "Cannot assign value of type {0} to unknown attribute '{1}'",
value.GetType(), attributeName); value.GetType(), attributeName);
knownAttribute = CloudEventAttribute.CreateExtension(attributeName, CloudEventAttributeType.String); knownAttribute = CloudEventAttribute.CreateExtension(attributeName, CloudEventAttributeType.String);
@ -352,8 +352,8 @@ namespace CloudNative.CloudEvents
/// <param name="value">The value of the attribute to set. Must not be null.</param> /// <param name="value">The value of the attribute to set. Must not be null.</param>
public void SetAttributeFromString(string name, string value) public void SetAttributeFromString(string name, string value)
{ {
Preconditions.CheckNotNull(name, nameof(name)); Validation.CheckNotNull(name, nameof(name));
Preconditions.CheckNotNull(value, nameof(value)); Validation.CheckNotNull(value, nameof(value));
var attribute = GetAttribute(name); var attribute = GetAttribute(name);
if (attribute is null) if (attribute is null)
@ -385,29 +385,6 @@ namespace CloudNative.CloudEvents
throw new InvalidOperationException($"Missing required attributes: {joinedMissing}"); throw new InvalidOperationException($"Missing required attributes: {joinedMissing}");
} }
// TODO: consider moving this to an extension method in an implementation helper namespace?
/// <summary>
/// Validates that this CloudEvent is valid in the same way as <see cref="IsValid"/>,
/// but throwing an <see cref="ArgumentException"/> using the given parameter name
/// if the event is invalid. This is typically used within protocol bindings or event formatters
/// as the last step in decoding an event, or as the first step when encoding an event.
/// </summary>
/// <param name="paramName">The parameter name to use in the exception if the event is invalid.
/// May be null.</param>
/// <exception cref="ArgumentException">The event is invalid.</exception>
/// <returns>A reference to the same object, for simplicity of method chaining.</returns>
public CloudEvent ValidateForConversion(string paramName)
{
if (IsValid)
{
return this;
}
var missing = SpecVersion.RequiredAttributes.Where(attr => this[attr] is null).ToList();
string joinedMissing = string.Join(", ", missing);
throw new ArgumentException($"CloudEvent is missing required attributes: {joinedMissing}", paramName);
}
/// <summary> /// <summary>
/// Returns whether this CloudEvent is valid, i.e. whether all required attributes have /// Returns whether this CloudEvent is valid, i.e. whether all required attributes have
/// values. /// values.

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
namespace CloudNative.CloudEvents namespace CloudNative.CloudEvents
@ -44,7 +45,7 @@ namespace CloudNative.CloudEvents
// TODO: Have a "mode" of Required/Optional/Extension? // TODO: Have a "mode" of Required/Optional/Extension?
private CloudEventAttribute(string name, CloudEventAttributeType type, bool required, bool extension, Action<object> validator) => private CloudEventAttribute(string name, CloudEventAttributeType type, bool required, bool extension, Action<object> validator) =>
(Name, Type, IsRequired, IsExtension, this.validator) = (ValidateName(name), Preconditions.CheckNotNull(type, nameof(type)), required, extension, validator); (Name, Type, IsRequired, IsExtension, this.validator) = (ValidateName(name), Validation.CheckNotNull(type, nameof(type)), required, extension, validator);
internal static CloudEventAttribute CreateRequired(string name, CloudEventAttributeType type, Action<object> validator) => internal static CloudEventAttribute CreateRequired(string name, CloudEventAttributeType type, Action<object> validator) =>
new CloudEventAttribute(name, type, required: true, extension: false, validator: validator); new CloudEventAttribute(name, type, required: true, extension: false, validator: validator);
@ -87,7 +88,7 @@ namespace CloudNative.CloudEvents
/// <returns><paramref name="name"/>, for convenience.</returns> /// <returns><paramref name="name"/>, for convenience.</returns>
internal static string ValidateName(string name) internal static string ValidateName(string name)
{ {
Preconditions.CheckNotNull(name, nameof(name)); Validation.CheckNotNull(name, nameof(name));
if (name.Length == 0) if (name.Length == 0)
{ {
throw new ArgumentException("Attribute names must be non-empty", nameof(name)); throw new ArgumentException("Attribute names must be non-empty", nameof(name));
@ -106,7 +107,7 @@ namespace CloudNative.CloudEvents
public object Parse(string text) public object Parse(string text)
{ {
Preconditions.CheckNotNull(text, nameof(text)); Validation.CheckNotNull(text, nameof(text));
object value; object value;
// By wrapping every exception here, we always get an // By wrapping every exception here, we always get an
// ArgumentException (other than the ArgumentNullException above) and have the name in the message. // ArgumentException (other than the ArgumentNullException above) and have the name in the message.
@ -132,7 +133,7 @@ namespace CloudNative.CloudEvents
/// <returns>The value, for simple method chaining.</returns> /// <returns>The value, for simple method chaining.</returns>
public object Validate(object value) public object Validate(object value)
{ {
Preconditions.CheckNotNull(value, nameof(value)); Validation.CheckNotNull(value, nameof(value));
// By wrapping every exception, whether from the type or the custom validator, we always get an // By wrapping every exception, whether from the type or the custom validator, we always get an
// ArgumentException (other than the ArgumentNullException above) and have the name in the message. // ArgumentException (other than the ArgumentNullException above) and have the name in the message.
try try

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Globalization; using System.Globalization;
@ -98,7 +99,7 @@ namespace CloudNative.CloudEvents
{ {
} }
public override sealed object Parse(string value) => ParseImpl(Preconditions.CheckNotNull(value, nameof(value))); public override sealed object Parse(string value) => ParseImpl(Validation.CheckNotNull(value, nameof(value)));
public override sealed string Format(object value) public override sealed string Format(object value)
{ {
@ -109,13 +110,13 @@ namespace CloudNative.CloudEvents
public override sealed void Validate(object value) public override sealed void Validate(object value)
{ {
Preconditions.CheckNotNull(value, nameof(value)); Validation.CheckNotNull(value, nameof(value));
if (!ClrType.IsInstanceOfType(value)) if (!ClrType.IsInstanceOfType(value))
{ {
throw new ArgumentException($"Value of type {value.GetType()} is incompatible with expected type {ClrType}", nameof(value)); throw new ArgumentException($"Value of type {value.GetType()} is incompatible with expected type {ClrType}", nameof(value));
} }
ValidateImpl((T)Preconditions.CheckNotNull(value, nameof(value))); ValidateImpl((T)Validation.CheckNotNull(value, nameof(value)));
} }
protected abstract T ParseImpl(string value); protected abstract T ParseImpl(string value);

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.IO; using System.IO;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@ -60,7 +61,7 @@ namespace CloudNative.CloudEvents.Http
/// <returns>true, if the response is a CloudEvent</returns> /// <returns>true, if the response is a CloudEvent</returns>
public static bool IsCloudEvent(this HttpRequestMessage httpRequestMessage) public static bool IsCloudEvent(this HttpRequestMessage httpRequestMessage)
{ {
Preconditions.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); Validation.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage));
return HasCloudEventsContentType(httpRequestMessage.Content) || return HasCloudEventsContentType(httpRequestMessage.Content) ||
httpRequestMessage.Headers.Contains(HttpUtilities.SpecVersionHttpHeader); httpRequestMessage.Headers.Contains(HttpUtilities.SpecVersionHttpHeader);
} }
@ -72,7 +73,7 @@ namespace CloudNative.CloudEvents.Http
/// <returns>true, if the response is a CloudEvent</returns> /// <returns>true, if the response is a CloudEvent</returns>
public static bool IsCloudEvent(this HttpResponseMessage httpResponseMessage) public static bool IsCloudEvent(this HttpResponseMessage httpResponseMessage)
{ {
Preconditions.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage)); Validation.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage));
return HasCloudEventsContentType(httpResponseMessage.Content) || return HasCloudEventsContentType(httpResponseMessage.Content) ||
httpResponseMessage.Headers.Contains(HttpUtilities.SpecVersionHttpHeader); httpResponseMessage.Headers.Contains(HttpUtilities.SpecVersionHttpHeader);
} }
@ -109,7 +110,7 @@ namespace CloudNative.CloudEvents.Http
CloudEventFormatter formatter, CloudEventFormatter formatter,
IEnumerable<CloudEventAttribute> extensionAttributes) IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
Preconditions.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage)); Validation.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage));
return ToCloudEventInternalAsync(httpResponseMessage.Headers, httpResponseMessage.Content, formatter, extensionAttributes, nameof(httpResponseMessage)); return ToCloudEventInternalAsync(httpResponseMessage.Headers, httpResponseMessage.Content, formatter, extensionAttributes, nameof(httpResponseMessage));
} }
@ -138,19 +139,19 @@ namespace CloudNative.CloudEvents.Http
CloudEventFormatter formatter, CloudEventFormatter formatter,
IEnumerable<CloudEventAttribute> extensionAttributes) IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
Preconditions.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); Validation.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage));
return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage));
} }
private static async Task<CloudEvent> ToCloudEventInternalAsync(HttpHeaders headers, HttpContent content, private static async Task<CloudEvent> ToCloudEventInternalAsync(HttpHeaders headers, HttpContent content,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes, string paramName) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes, string paramName)
{ {
Preconditions.CheckNotNull(formatter, nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
if (HasCloudEventsContentType(content)) if (HasCloudEventsContentType(content))
{ {
var stream = await content.ReadAsStreamAsync().ConfigureAwait(false); var stream = await content.ReadAsStreamAsync().ConfigureAwait(false);
return await formatter.DecodeStructuredModeMessageAsync(stream, content.Headers.ContentType.ToContentType(), extensionAttributes).ConfigureAwait(false); return await formatter.DecodeStructuredModeMessageAsync(stream, MimeUtilities.ToContentType(content.Headers.ContentType), extensionAttributes).ConfigureAwait(false);
} }
else else
{ {
@ -179,7 +180,7 @@ namespace CloudNative.CloudEvents.Http
var data = await content.ReadAsByteArrayAsync().ConfigureAwait(false); var data = await content.ReadAsByteArrayAsync().ConfigureAwait(false);
formatter.DecodeBinaryModeEventData(data, cloudEvent); formatter.DecodeBinaryModeEventData(data, cloudEvent);
} }
return cloudEvent.ValidateForConversion(paramName); return Validation.CheckCloudEventArgument(cloudEvent, paramName);
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Net.Http; using System.Net.Http;
using System.Net.Mime; using System.Net.Mime;
@ -21,9 +22,8 @@ namespace CloudNative.CloudEvents.Http
/// <param name="formatter">The formatter to use within the conversion. Must not be null.</param> /// <param name="formatter">The formatter to use within the conversion. Must not be null.</param>
public static HttpContent ToHttpContent(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) public static HttpContent ToHttpContent(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter)
{ {
Preconditions.CheckNotNull(cloudEvent, nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent)); Validation.CheckNotNull(formatter, nameof(formatter));
Preconditions.CheckNotNull(formatter, nameof(formatter));
byte[] content; byte[] content;
// The content type to include in the ContentType header - may be the data content type, or the formatter's content type. // The content type to include in the ContentType header - may be the data content type, or the formatter's content type.
@ -43,7 +43,7 @@ namespace CloudNative.CloudEvents.Http
var ret = new ByteArrayContent(content); var ret = new ByteArrayContent(content);
if (contentType is object) if (contentType is object)
{ {
ret.Headers.ContentType = contentType.ToMediaTypeHeaderValue(); ret.Headers.ContentType = MimeUtilities.ToMediaTypeHeaderValue(contentType);
} }
else if (content.Length != 0) else if (content.Length != 0)
{ {

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Net; using System.Net;
@ -27,10 +28,9 @@ namespace CloudNative.CloudEvents.Http
public static Task CopyToHttpListenerResponseAsync(this CloudEvent cloudEvent, HttpListenerResponse destination, public static Task CopyToHttpListenerResponseAsync(this CloudEvent cloudEvent, HttpListenerResponse destination,
ContentMode contentMode, CloudEventFormatter formatter) ContentMode contentMode, CloudEventFormatter formatter)
{ {
Preconditions.CheckNotNull(cloudEvent, nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent)); Validation.CheckNotNull(destination, nameof(destination));
Preconditions.CheckNotNull(destination, nameof(destination)); Validation.CheckNotNull(formatter, nameof(formatter));
Preconditions.CheckNotNull(formatter, nameof(formatter));
byte[] content; byte[] content;
ContentType contentType; ContentType contentType;
@ -142,8 +142,8 @@ namespace CloudNative.CloudEvents.Http
public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest, public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest,
CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes) CloudEventFormatter formatter, IEnumerable<CloudEventAttribute> extensionAttributes)
{ {
Preconditions.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest)); Validation.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest));
Preconditions.CheckNotNull(formatter, nameof(formatter)); Validation.CheckNotNull(formatter, nameof(formatter));
if (HasCloudEventsContentType(httpListenerRequest)) if (HasCloudEventsContentType(httpListenerRequest))
{ {
@ -176,7 +176,7 @@ namespace CloudNative.CloudEvents.Http
cloudEvent.DataContentType = httpListenerRequest.ContentType; cloudEvent.DataContentType = httpListenerRequest.ContentType;
formatter.DecodeBinaryModeEventData(BinaryDataUtilities.ToByteArray(httpListenerRequest.InputStream), cloudEvent); formatter.DecodeBinaryModeEventData(BinaryDataUtilities.ToByteArray(httpListenerRequest.InputStream), cloudEvent);
return cloudEvent.ValidateForConversion(nameof(httpListenerRequest)); return Validation.CheckCloudEventArgument(cloudEvent, nameof(httpListenerRequest));
} }
} }

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Net; using System.Net;
using System.Net.Mime; using System.Net.Mime;
@ -28,10 +29,9 @@ namespace CloudNative.CloudEvents.Http
public static async Task CopyToHttpWebRequestAsync(this CloudEvent cloudEvent, HttpWebRequest destination, public static async Task CopyToHttpWebRequestAsync(this CloudEvent cloudEvent, HttpWebRequest destination,
ContentMode contentMode, CloudEventFormatter formatter) ContentMode contentMode, CloudEventFormatter formatter)
{ {
Preconditions.CheckNotNull(cloudEvent, nameof(cloudEvent)); Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent));
cloudEvent.ValidateForConversion(nameof(cloudEvent)); Validation.CheckNotNull(destination, nameof(destination));
Preconditions.CheckNotNull(destination, nameof(destination)); Validation.CheckNotNull(formatter, nameof(formatter));
Preconditions.CheckNotNull(formatter, nameof(formatter));
byte[] content; byte[] content;
// The content type to include in the ContentType header - may be the data content type, or the formatter's content type. // The content type to include in the ContentType header - may be the data content type, or the formatter's content type.

View File

@ -5,15 +5,15 @@
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
namespace CloudNative.CloudEvents namespace CloudNative.CloudEvents.Core
{ {
/// <summary> /// <summary>
/// Utilities methods for dealing with binary data, converting between /// Utilities methods for dealing with binary data, converting between
/// streams, arrays, Memory{T} etc. /// streams, arrays, Memory{T} etc.
/// </summary> /// </summary>
internal static class BinaryDataUtilities public static class BinaryDataUtilities
{ {
internal async static Task<byte[]> ToByteArrayAsync(Stream stream) public async static Task<byte[]> ToByteArrayAsync(Stream stream)
{ {
// TODO: Optimize if it's already a MemoryStream? // TODO: Optimize if it's already a MemoryStream?
var memory = new MemoryStream(); var memory = new MemoryStream();
@ -21,7 +21,7 @@ namespace CloudNative.CloudEvents
return memory.ToArray(); return memory.ToArray();
} }
internal static byte[] ToByteArray(Stream stream) public static byte[] ToByteArray(Stream stream)
{ {
var memory = new MemoryStream(); var memory = new MemoryStream();
stream.CopyTo(memory); stream.CopyTo(memory);

View File

@ -6,13 +6,13 @@ using System.Net.Http.Headers;
using System.Net.Mime; using System.Net.Mime;
using System.Text; using System.Text;
namespace CloudNative.CloudEvents namespace CloudNative.CloudEvents.Core
{ {
// TODO: Consider this name and namespace carefully. It really does need to be public, as all the event formatters are elsewhere. // TODO: Consider this name and namespace carefully. It really does need to be public, as all the event formatters are elsewhere.
// But it's not ideal... // But it's not ideal...
/// <summary> /// <summary>
/// Utility and extension methods around MIME. /// Utility methods around MIME.
/// </summary> /// </summary>
public static class MimeUtilities public static class MimeUtilities
{ {
@ -23,7 +23,7 @@ namespace CloudNative.CloudEvents
/// <param name="contentType">The content type, or null if no content type is known.</param> /// <param name="contentType">The content type, or null if no content type is known.</param>
/// <returns>An encoding suitable for the charset specified in <paramref name="contentType"/>, /// <returns>An encoding suitable for the charset specified in <paramref name="contentType"/>,
/// or UTF-8 if no charset has been specified.</returns> /// or UTF-8 if no charset has been specified.</returns>
public static Encoding GetEncoding(this ContentType contentType) => public static Encoding GetEncoding(ContentType contentType) =>
contentType?.CharSet is string charSet ? Encoding.GetEncoding(charSet) : Encoding.UTF8; contentType?.CharSet is string charSet ? Encoding.GetEncoding(charSet) : Encoding.UTF8;
/// <summary> /// <summary>
@ -31,7 +31,7 @@ namespace CloudNative.CloudEvents
/// </summary> /// </summary>
/// <param name="headerValue">The header value to convert. May be null.</param> /// <param name="headerValue">The header value to convert. May be null.</param>
/// <returns>The converted content type, or null if <paramref name="headerValue"/> is null.</returns> /// <returns>The converted content type, or null if <paramref name="headerValue"/> is null.</returns>
public static ContentType ToContentType(this MediaTypeHeaderValue headerValue) => public static ContentType ToContentType(MediaTypeHeaderValue headerValue) =>
headerValue is null ? null : new ContentType(headerValue.ToString()); headerValue is null ? null : new ContentType(headerValue.ToString());
/// <summary> /// <summary>
@ -39,7 +39,7 @@ namespace CloudNative.CloudEvents
/// </summary> /// </summary>
/// <param name="contentType">The content type to convert. May be null.</param> /// <param name="contentType">The content type to convert. May be null.</param>
/// <returns>The converted media type header value, or null if <paramref name="contentType"/> is null.</returns> /// <returns>The converted media type header value, or null if <paramref name="contentType"/> is null.</returns>
public static MediaTypeHeaderValue ToMediaTypeHeaderValue(this ContentType contentType) public static MediaTypeHeaderValue ToMediaTypeHeaderValue(ContentType contentType)
{ {
if (contentType is null) if (contentType is null)
{ {

View File

@ -0,0 +1,98 @@
// Copyright 2021 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.Linq;
namespace CloudNative.CloudEvents.Core
{
/// <summary>
/// Validation methods which are typically convenient for implementers of CloudEvent formatters
/// and protocol bindings.
/// </summary>
public static class Validation
{
/// <summary>
/// Validates that the given reference is non-null.
/// </summary>
/// <typeparam name="T">Type of the value to check</typeparam>
/// <param name="value">The reference to check for nullity</param>
/// <param name="paramName">The parameter name to use in the exception if <paramref name="value"/> is null.
/// May be null.</param>
/// <returns>The value of <paramref name="value"/>, for convenient method chaining or assignment.</returns>
public static T CheckNotNull<T>(T value, string paramName) where T : class =>
value ?? throw new ArgumentNullException(paramName);
/// <summary>
/// Validates an argument-dependent condition, throwing an exception if the check fails.
/// </summary>
/// <param name="condition">The condition to validate; this method will throw an <see cref="ArgumentException"/> if this is false.</param>
/// <param name="paramName">The name of the parameter being validated. May be null.</param>
/// <param name="message">The message to use in the exception, if one is thrown.</param>
public static void CheckArgument(bool condition, string paramName, string message)
{
if (!condition)
{
throw new ArgumentException(message, paramName);
}
}
/// <summary>
/// Validates an argument-dependent condition, throwing an exception if the check fails.
/// </summary>
/// <param name="condition">The condition to validate; this method will throw an <see cref="ArgumentException"/> if this is false.</param>
/// <param name="paramName">The name of the parameter being validated. May be null.</param>
/// <param name="messageFormat">The string format to use in the exception message, if one is thrown.</param>
/// <param name="arg1">The first argument in the string format.</param>
public static void CheckArgument(bool condition, string paramName, string messageFormat,
object arg1)
{
if (!condition)
{
throw new ArgumentException(string.Format(messageFormat, arg1), paramName);
}
}
/// <summary>
/// Validates an argument-dependent condition, throwing an exception if the check fails.
/// </summary>
/// <param name="condition">The condition to validate; this method will throw an <see cref="ArgumentException"/> if this is false.</param>
/// <param name="paramName">The name of the parameter being validated. May be null.</param>
/// <param name="messageFormat">The string format to use in the exception message, if one is thrown.</param>
/// <param name="arg1">The first argument in the string format.</param>
/// <param name="arg2">The first argument in the string format.</param>
public static void CheckArgument(bool condition, string paramName, string messageFormat,
object arg1, object arg2)
{
if (!condition)
{
throw new ArgumentException(string.Format(messageFormat, arg1, arg2), paramName);
}
}
/// <summary>
/// Validates that the specified CloudEvent is valid in the same way as <see cref="CloudEvent.IsValid"/>,
/// but throwing an <see cref="ArgumentException"/> using the given parameter name
/// if the event is invalid. This is typically used within protocol bindings or event formatters
/// as the last step in decoding an event, or as the first step when encoding an event.
/// </summary
/// <param name="cloudEvent">The event to validate.
/// <param name="paramName">The parameter name to use in the exception if <paramref name="cloudEvent"/> is null or invalid.
/// May be null.</param>
/// <exception cref="ArgumentNullException"><paramref name="cloudEvent"/> is null.</exception>
/// <exception cref="ArgumentException">The event is invalid.</exception>
/// <returns>A reference to the same object, for simplicity of method chaining.</returns>
public static CloudEvent CheckCloudEventArgument(CloudEvent cloudEvent, string paramName)
{
CheckNotNull(cloudEvent, paramName);
if (cloudEvent.IsValid)
{
return cloudEvent;
}
var missing = cloudEvent.SpecVersion.RequiredAttributes.Where(attr => cloudEvent[attr] is null).ToList();
string joinedMissing = string.Join(", ", missing);
throw new ArgumentException($"CloudEvent is missing required attributes: {joinedMissing}", paramName);
}
}
}

View File

@ -1,43 +0,0 @@
// Copyright 2021 Cloud Native Foundation.
// Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information.
using System;
namespace CloudNative.CloudEvents
{
/// <summary>
/// Convenient precondition methods.
/// </summary>
internal static class Preconditions
{
internal static T CheckNotNull<T>(T value, string paramName) where T : class =>
value ?? throw new ArgumentNullException(paramName);
internal static void CheckArgument(bool condition, string paramName, string message)
{
if (!condition)
{
throw new ArgumentException(message, paramName);
}
}
internal static void CheckArgument(bool condition, string paramName, string messageFormat,
object arg1)
{
if (!condition)
{
throw new ArgumentException(string.Format(messageFormat, arg1), paramName);
}
}
internal static void CheckArgument(bool condition, string paramName, string messageFormat,
object arg1, object arg2)
{
if (!condition)
{
throw new ArgumentException(string.Format(messageFormat, arg1, arg2), paramName);
}
}
}
}

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Globalization; using System.Globalization;
@ -49,10 +50,8 @@ namespace CloudNative.CloudEvents
public static bool TryParse(string input, out DateTimeOffset result) public static bool TryParse(string input, out DateTimeOffset result)
{ {
// TODO: Check this and add a test // TODO: Check this and add a test
if (input is null) Validation.CheckNotNull(input, nameof(input));
{
throw new ArgumentNullException(nameof(input));
}
if (input.Length < MinLength) // "yyyy-MM-ddTHH:mm:ssZ" is the shortest possible value. if (input.Length < MinLength) // "yyyy-MM-ddTHH:mm:ssZ" is the shortest possible value.
{ {
result = default; result = default;

View File

@ -2,6 +2,7 @@
// Licensed under the Apache 2.0 license. // Licensed under the Apache 2.0 license.
// See LICENSE file in the project root for full license information. // See LICENSE file in the project root for full license information.
using CloudNative.CloudEvents.Core;
using System; using System;
using System.Linq; using System.Linq;
using System.Net.Mime; using System.Net.Mime;
@ -112,7 +113,7 @@ namespace CloudNative.CloudEvents.UnitTests
Assert.Contains(CloudEventsSpecVersion.Default.SourceAttribute.Name, exception1.Message); Assert.Contains(CloudEventsSpecVersion.Default.SourceAttribute.Name, exception1.Message);
Assert.DoesNotContain(CloudEventsSpecVersion.Default.TypeAttribute.Name, exception1.Message); Assert.DoesNotContain(CloudEventsSpecVersion.Default.TypeAttribute.Name, exception1.Message);
var exception2 = Assert.Throws<ArgumentException>(() => cloudEvent.ValidateForConversion("param")); var exception2 = Assert.Throws<ArgumentException>(() => Validation.CheckCloudEventArgument(cloudEvent, "param"));
Assert.Equal("param", exception2.ParamName); Assert.Equal("param", exception2.ParamName);
Assert.Contains(CloudEventsSpecVersion.Default.IdAttribute.Name, exception1.Message); Assert.Contains(CloudEventsSpecVersion.Default.IdAttribute.Name, exception1.Message);
Assert.Contains(CloudEventsSpecVersion.Default.SourceAttribute.Name, exception1.Message); Assert.Contains(CloudEventsSpecVersion.Default.SourceAttribute.Name, exception1.Message);
@ -130,7 +131,7 @@ namespace CloudNative.CloudEvents.UnitTests
}; };
Assert.True(cloudEvent.IsValid); Assert.True(cloudEvent.IsValid);
Assert.Same(cloudEvent, cloudEvent.Validate()); Assert.Same(cloudEvent, cloudEvent.Validate());
Assert.Same(cloudEvent, cloudEvent.ValidateForConversion("param")); Assert.Same(cloudEvent, Validation.CheckCloudEventArgument(cloudEvent, "param"));
} }
[Fact] [Fact]

View File

@ -8,7 +8,7 @@ using System.Net.Mime;
using System.Text; using System.Text;
using Xunit; using Xunit;
namespace CloudNative.CloudEvents.UnitTests.Http namespace CloudNative.CloudEvents.Core.UnitTests
{ {
public class MimeUtilitiesTest public class MimeUtilitiesTest
{ {
@ -21,9 +21,9 @@ namespace CloudNative.CloudEvents.UnitTests.Http
public void ContentTypeConversions(string text) public void ContentTypeConversions(string text)
{ {
var originalContentType = new ContentType(text); var originalContentType = new ContentType(text);
var header = originalContentType.ToMediaTypeHeaderValue(); var header = MimeUtilities.ToMediaTypeHeaderValue(originalContentType);
AssertEqualParts(text, header.ToString()); AssertEqualParts(text, header.ToString());
var convertedContentType = header.ToContentType(); var convertedContentType = MimeUtilities.ToContentType(header);
AssertEqualParts(originalContentType.ToString(), convertedContentType.ToString()); AssertEqualParts(originalContentType.ToString(), convertedContentType.ToString());
// Conversions can end up reordering the parameters. In reality we're only // Conversions can end up reordering the parameters. In reality we're only
@ -40,8 +40,8 @@ namespace CloudNative.CloudEvents.UnitTests.Http
[Fact] [Fact]
public void ContentTypeConversions_Null() public void ContentTypeConversions_Null()
{ {
Assert.Null(default(ContentType).ToMediaTypeHeaderValue()); Assert.Null(MimeUtilities.ToMediaTypeHeaderValue(default(ContentType)));
Assert.Null(default(MediaTypeHeaderValue).ToContentType()); Assert.Null(MimeUtilities.ToContentType(default(MediaTypeHeaderValue)));
} }
[Theory] [Theory]
@ -50,7 +50,7 @@ namespace CloudNative.CloudEvents.UnitTests.Http
public void ContentTypeGetEncoding(string charSet) public void ContentTypeGetEncoding(string charSet)
{ {
var contentType = new ContentType($"text/plain; charset={charSet}"); var contentType = new ContentType($"text/plain; charset={charSet}");
Encoding encoding = contentType.GetEncoding(); Encoding encoding = MimeUtilities.GetEncoding(contentType);
Assert.Equal(charSet, encoding.WebName); Assert.Equal(charSet, encoding.WebName);
} }
@ -58,7 +58,7 @@ namespace CloudNative.CloudEvents.UnitTests.Http
public void ContentTypeGetEncoding_NoContentType() public void ContentTypeGetEncoding_NoContentType()
{ {
ContentType contentType = null; ContentType contentType = null;
Encoding encoding = contentType.GetEncoding(); Encoding encoding = MimeUtilities.GetEncoding(contentType);
Assert.Equal(Encoding.UTF8, encoding); Assert.Equal(Encoding.UTF8, encoding);
} }
@ -66,7 +66,7 @@ namespace CloudNative.CloudEvents.UnitTests.Http
public void ContentTypeGetEncoding_NoCharSet() public void ContentTypeGetEncoding_NoCharSet()
{ {
ContentType contentType = new ContentType("text/plain"); ContentType contentType = new ContentType("text/plain");
Encoding encoding = contentType.GetEncoding(); Encoding encoding = MimeUtilities.GetEncoding(contentType);
Assert.Equal(Encoding.UTF8, encoding); Assert.Equal(Encoding.UTF8, encoding);
} }