From 41f639d44b24811ea151886b9b0bd4da93870ade Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Thu, 25 Mar 2021 11:14:14 +0000 Subject: [PATCH] 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 --- docs/README.md | 12 +++ docs/bindings.md | 5 +- docs/formatters.md | 10 +- .../AmqpClientExtensions.cs | 12 +-- .../CloudEventJsonInputFormatter.cs | 12 +-- .../HttpRequestExtensions.cs | 10 +- .../AvroEventFormatter.cs | 10 +- .../KafkaClientExtensions.cs | 17 ++-- .../MqttClientExtensions.cs | 10 +- .../JsonEventFormatter.cs | 29 +++--- .../JsonEventFormatter.cs | 26 ++--- src/CloudNative.CloudEvents/CloudEvent.cs | 63 ++++-------- .../CloudEventAttribute.cs | 9 +- .../CloudEventAttributeType.cs | 7 +- .../CloudEventFormatter.cs | 1 + .../Http/HttpClientExtensions.cs | 15 +-- .../Http/HttpContentExtensions.cs | 8 +- .../Http/HttpListenerExtensions.cs | 14 +-- .../Http/HttpWebExtensions.cs | 8 +- .../{ => Impl}/BinaryDataUtilities.cs | 8 +- .../{ => Impl}/MimeUtilities.cs | 10 +- .../Impl/Validation.cs | 98 +++++++++++++++++++ src/CloudNative.CloudEvents/Preconditions.cs | 43 -------- src/CloudNative.CloudEvents/Timestamps.cs | 7 +- .../CloudEventTest.cs | 5 +- .../{ => Impl}/MimeUtilitiesTest.cs | 16 +-- 26 files changed, 251 insertions(+), 214 deletions(-) rename src/CloudNative.CloudEvents/{ => Impl}/BinaryDataUtilities.cs (77%) rename src/CloudNative.CloudEvents/{ => Impl}/MimeUtilities.cs (91%) create mode 100644 src/CloudNative.CloudEvents/Impl/Validation.cs delete mode 100644 src/CloudNative.CloudEvents/Preconditions.cs rename test/CloudNative.CloudEvents.UnitTests/{ => Impl}/MimeUtilitiesTest.cs (81%) diff --git a/docs/README.md b/docs/README.md index 4fed203..de9a525 100644 --- a/docs/README.md +++ b/docs/README.md @@ -4,3 +4,15 @@ This directory contains documentation on: - Using the SDK as a consumer - 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. diff --git a/docs/bindings.md b/docs/bindings.md index 11674ea..30a8ca0 100644 --- a/docs/bindings.md +++ b/docs/bindings.md @@ -95,7 +95,7 @@ following pseudo-code as structure: - If the message contains content, call the `formatter.DecodeBinaryModeEventData` method to populate the `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 the event is valid, or throw an appropriate `ArgumentException` otherwise. @@ -155,9 +155,10 @@ The conversion should follow the following steps of pseudo-code: - Parameter validation (which may be completed in any order): - `cloudEvent` and `formatter` should be non-null + (`CheckCloudEventArgument` will validate this for the `cloudEvent` parameter) - In a `CopyTo...` method, `destination` should be non-null - 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 structured mode encoding: - Call `formatter.EncodeStructuredModeMessage` to encode diff --git a/docs/formatters.md b/docs/formatters.md index 5decac3..b075713 100644 --- a/docs/formatters.md +++ b/docs/formatters.md @@ -73,10 +73,10 @@ being non-null, and additionally perform CloudEvent validation on: - The `CloudEvent` accepted in `EncodeBinaryModeEventData` or `EncodeStructuredModeMessage` -Validation should be performed using the `ValidateForConversion` +Validation should be performed using the `Validation.CheckCloudEventArgument` method, so that an appropriate `ArgumentException` is thrown. -The formatter should *not* perform validation on the -`CloudEvent` accepted in `DecodeBinaryModeEventData`. This is -typically called by a protocol binding which should perform -validation itself later. +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. \ No newline at end of file diff --git a/src/CloudNative.CloudEvents.Amqp/AmqpClientExtensions.cs b/src/CloudNative.CloudEvents.Amqp/AmqpClientExtensions.cs index 6c595de..321a627 100644 --- a/src/CloudNative.CloudEvents.Amqp/AmqpClientExtensions.cs +++ b/src/CloudNative.CloudEvents.Amqp/AmqpClientExtensions.cs @@ -5,6 +5,7 @@ using Amqp; using Amqp.Framing; using Amqp.Types; +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.IO; @@ -50,8 +51,8 @@ namespace CloudNative.CloudEvents.Amqp CloudEventFormatter formatter, IEnumerable extensionAttributes) { - message = message ?? throw new ArgumentNullException(nameof(message)); - formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + Validation.CheckNotNull(message, nameof(message)); + Validation.CheckNotNull(formatter, nameof(formatter)); 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"); } - return cloudEvent.ValidateForConversion(nameof(message)); + return Validation.CheckCloudEventArgument(cloudEvent, nameof(message)); } } @@ -141,9 +142,8 @@ namespace CloudNative.CloudEvents.Amqp /// The formatter to use within the conversion. Must not be null. public static Message ToAmqpMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); - formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); + Validation.CheckNotNull(formatter, nameof(formatter)); var applicationProperties = MapHeaders(cloudEvent); RestrictedDescribed bodySection; diff --git a/src/CloudNative.CloudEvents.AspNetCore/CloudEventJsonInputFormatter.cs b/src/CloudNative.CloudEvents.AspNetCore/CloudEventJsonInputFormatter.cs index dd5615b..cdf43be 100644 --- a/src/CloudNative.CloudEvents.AspNetCore/CloudEventJsonInputFormatter.cs +++ b/src/CloudNative.CloudEvents.AspNetCore/CloudEventJsonInputFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using Microsoft.AspNetCore.Mvc.Formatters; using Microsoft.Net.Http.Headers; using System; @@ -30,15 +31,8 @@ namespace CloudNative.CloudEvents public override async Task ReadRequestBodyAsync(InputFormatterContext context, Encoding encoding) { - if (context == null) - { - throw new ArgumentNullException(nameof(context)); - } - - if (encoding == null) - { - throw new ArgumentNullException(nameof(encoding)); - } + Validation.CheckNotNull(context, nameof(context)); + Validation.CheckNotNull(encoding, nameof(encoding)); var request = context.HttpContext.Request; diff --git a/src/CloudNative.CloudEvents.AspNetCore/HttpRequestExtensions.cs b/src/CloudNative.CloudEvents.AspNetCore/HttpRequestExtensions.cs index 04ac1ac..57afd15 100644 --- a/src/CloudNative.CloudEvents.AspNetCore/HttpRequestExtensions.cs +++ b/src/CloudNative.CloudEvents.AspNetCore/HttpRequestExtensions.cs @@ -3,6 +3,7 @@ // See LICENSE file in the project root for full license information. using CloudNative.CloudEvents.Http; +using CloudNative.CloudEvents.Core; using Microsoft.AspNetCore.Http; using System; using System.Collections.Generic; @@ -79,13 +80,10 @@ namespace CloudNative.CloudEvents cloudEvent.DataContentType = httpRequest.ContentType; if (httpRequest.Body is Stream body) { - // TODO: This is a bit ugly. We have code in BinaryDataUtilities to handle this, but - // we'd rather not expose it... - var memoryStream = new MemoryStream(); - await body.CopyToAsync(memoryStream).ConfigureAwait(false); - formatter.DecodeBinaryModeEventData(memoryStream.ToArray(), cloudEvent); + byte[] data = await BinaryDataUtilities.ToByteArrayAsync(body).ConfigureAwait(false); + formatter.DecodeBinaryModeEventData(data, cloudEvent); } - return cloudEvent.ValidateForConversion(nameof(httpRequest)); + return Validation.CheckCloudEventArgument(cloudEvent, nameof(httpRequest)); } } diff --git a/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs b/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs index f3c900f..c521d02 100644 --- a/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs +++ b/src/CloudNative.CloudEvents.Avro/AvroEventFormatter.cs @@ -5,6 +5,7 @@ using Avro; using Avro.Generic; using Avro.IO; +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.IO; @@ -53,7 +54,7 @@ namespace CloudNative.CloudEvents public override CloudEvent DecodeStructuredModeMessage(Stream data, ContentType contentType, IEnumerable extensionAttributes) { - data = data ?? throw new ArgumentNullException(nameof(data)); + Validation.CheckNotNull(data, nameof(data)); var decoder = new BinaryDecoder(data); var rawEvent = avroReader.Read(null, decoder); @@ -62,7 +63,7 @@ namespace CloudNative.CloudEvents public override CloudEvent DecodeStructuredModeMessage(byte[] data, ContentType contentType, IEnumerable extensionAttributes) { - data = data ?? throw new ArgumentNullException(nameof(data)); + Validation.CheckNotNull(data, nameof(data)); 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) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); contentType = new ContentType(CloudEvent.MediaType + MediaTypeSuffix); diff --git a/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs b/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs index 1b97b75..3b2b0ea 100644 --- a/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs +++ b/src/CloudNative.CloudEvents.Kafka/KafkaClientExtensions.cs @@ -3,6 +3,7 @@ // See LICENSE file in the project root for full license information. using CloudNative.CloudEvents.Extensions; +using CloudNative.CloudEvents.Core; using Confluent.Kafka; using System; using System.Collections.Generic; @@ -50,8 +51,8 @@ namespace CloudNative.CloudEvents.Kafka public static CloudEvent ToCloudEvent(this Message message, CloudEventFormatter formatter, IEnumerable extensionAttributes) { - message = message ?? throw new ArgumentNullException(nameof(message)); - formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + Validation.CheckNotNull(message, nameof(message)); + Validation.CheckNotNull(formatter, nameof(formatter)); if (!IsCloudEvent(message)) { @@ -104,7 +105,7 @@ namespace CloudNative.CloudEvents.Kafka } InitPartitioningKey(message, cloudEvent); - return cloudEvent.ValidateForConversion(nameof(message)); + return Validation.CheckCloudEventArgument(cloudEvent, nameof(message)); } private static string ExtractContentType(Message message) @@ -133,15 +134,11 @@ namespace CloudNative.CloudEvents.Kafka /// The formatter to use within the conversion. Must not be null. public static Message ToKafkaMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); - formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); + Validation.CheckNotNull(formatter, nameof(formatter)); // TODO: Is this appropriate? Why can't we transport a CloudEvent without data in Kafka? - if (cloudEvent.Data == null) - { - throw new ArgumentNullException(nameof(cloudEvent.Data)); - } + Validation.CheckArgument(cloudEvent.Data is object, nameof(cloudEvent), "Only CloudEvents with data can be converted to Kafka messages"); var headers = MapHeaders(cloudEvent, formatter); string key = (string) cloudEvent[Partitioning.PartitionKeyAttribute]; byte[] value; diff --git a/src/CloudNative.CloudEvents.Mqtt/MqttClientExtensions.cs b/src/CloudNative.CloudEvents.Mqtt/MqttClientExtensions.cs index cf9b143..5bcff07 100644 --- a/src/CloudNative.CloudEvents.Mqtt/MqttClientExtensions.cs +++ b/src/CloudNative.CloudEvents.Mqtt/MqttClientExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using MQTTnet; using System; using System.Collections.Generic; @@ -34,8 +35,8 @@ namespace CloudNative.CloudEvents.Mqtt public static CloudEvent ToCloudEvent(this MqttApplicationMessage message, CloudEventFormatter formatter, IEnumerable extensionAttributes) { - message = message ?? throw new ArgumentNullException(nameof(message)); - formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + Validation.CheckNotNull(formatter, nameof(formatter)); + Validation.CheckNotNull(message, nameof(message)); // TODO: Determine if there's a sensible content type we should apply. return formatter.DecodeStructuredModeMessage(message.Payload, contentType: null, extensionAttributes); @@ -51,9 +52,8 @@ namespace CloudNative.CloudEvents.Mqtt /// The MQTT topic for the message. May be null. public static MqttApplicationMessage ToMqttApplicationMessage(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter, string topic) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); - formatter = formatter ?? throw new ArgumentNullException(nameof(formatter)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); + Validation.CheckNotNull(formatter, nameof(formatter)); switch (contentMode) { diff --git a/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs index 216b51b..e495777 100644 --- a/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs +++ b/src/CloudNative.CloudEvents.NewtonsoftJson/JsonEventFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using System; @@ -112,23 +113,23 @@ namespace CloudNative.CloudEvents.NewtonsoftJson /// public JsonEventFormatter(JsonSerializer serializer) { - Serializer = serializer ?? throw new ArgumentNullException(nameof(serializer)); + Serializer = Validation.CheckNotNull(serializer, nameof(serializer)); } public override async Task DecodeStructuredModeMessageAsync(Stream data, ContentType contentType, IEnumerable 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); return DecodeJObject(jObject, extensionAttributes); } public override CloudEvent DecodeStructuredModeMessage(Stream data, ContentType contentType, IEnumerable 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); return DecodeJObject(jObject, extensionAttributes); } @@ -151,7 +152,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson PopulateDataFromStructuredEvent(cloudEvent, jObject); // "data" is always the parameter from the public method. It's annoying not to be able to use // nameof here, but this will give the appropriate result. - return cloudEvent.ValidateForConversion("data"); + return Validation.CheckCloudEventArgument(cloudEvent, "data"); } private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JObject jObject) @@ -306,8 +307,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); contentType = new ContentType("application/cloudevents+json") { @@ -389,8 +389,7 @@ namespace CloudNative.CloudEvents.NewtonsoftJson public override byte[] EncodeBinaryModeEventData(CloudEvent cloudEvent) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); if (cloudEvent.Data is null) { @@ -405,11 +404,11 @@ namespace CloudNative.CloudEvents.NewtonsoftJson // without a preamble (or rewrite StreamWriter...) var stringWriter = new StringWriter(); 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) { - return contentType.GetEncoding().GetBytes(text); + return MimeUtilities.GetEncoding(contentType).GetBytes(text); } if (cloudEvent.Data is byte[] bytes) { @@ -420,12 +419,12 @@ namespace CloudNative.CloudEvents.NewtonsoftJson public override void DecodeBinaryModeEventData(byte[] value, CloudEvent cloudEvent) { - value = value ?? throw new ArgumentNullException(nameof(value)); - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); + Validation.CheckNotNull(value, nameof(value)); + Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType); - Encoding encoding = contentType.GetEncoding(); + Encoding encoding = MimeUtilities.GetEncoding(contentType); if (contentType.MediaType == JsonMediaType) { diff --git a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs index bdeec5c..d70aa26 100644 --- a/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs +++ b/src/CloudNative.CloudEvents.SystemTextJson/JsonEventFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.IO; @@ -119,8 +120,9 @@ namespace CloudNative.CloudEvents.SystemTextJson private async Task DecodeStructuredModeMessageImpl(Stream data, ContentType contentType, IEnumerable extensionAttributes, bool async) { - data = data ?? throw new ArgumentNullException(nameof(data)); - var encoding = contentType.GetEncoding(); + Validation.CheckNotNull(data, nameof(data)); + + var encoding = MimeUtilities.GetEncoding(contentType); JsonDocument document; if (encoding is UTF8Encoding) { @@ -157,7 +159,7 @@ namespace CloudNative.CloudEvents.SystemTextJson PopulateDataFromStructuredEvent(cloudEvent, document); // "data" is always the parameter from the public method. It's annoying not to be able to use // nameof here, but this will give the appropriate result. - return cloudEvent.ValidateForConversion("data"); + return Validation.CheckCloudEventArgument(cloudEvent, "data"); } private void PopulateAttributesFromStructuredEvent(CloudEvent cloudEvent, JsonDocument document) @@ -321,8 +323,7 @@ namespace CloudNative.CloudEvents.SystemTextJson public override byte[] EncodeStructuredModeMessage(CloudEvent cloudEvent, out ContentType contentType) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); contentType = new ContentType("application/cloudevents+json") { @@ -404,8 +405,7 @@ namespace CloudNative.CloudEvents.SystemTextJson public override byte[] EncodeBinaryModeEventData(CloudEvent cloudEvent) { - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); if (cloudEvent.Data is null) { @@ -414,19 +414,19 @@ namespace CloudNative.CloudEvents.SystemTextJson ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType); if (contentType.MediaType == JsonMediaType) { - var encoding = contentType.GetEncoding(); + var encoding = MimeUtilities.GetEncoding(contentType); if (encoding is UTF8Encoding) { return JsonSerializer.SerializeToUtf8Bytes(cloudEvent.Data, SerializerOptions); } 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) { - return contentType.GetEncoding().GetBytes(text); + return MimeUtilities.GetEncoding(contentType).GetBytes(text); } if (cloudEvent.Data is byte[] bytes) { @@ -437,12 +437,12 @@ namespace CloudNative.CloudEvents.SystemTextJson public override void DecodeBinaryModeEventData(byte[] value, CloudEvent cloudEvent) { - value = value ?? throw new ArgumentNullException(nameof(value)); - cloudEvent = cloudEvent ?? throw new ArgumentNullException(nameof(cloudEvent)); + Validation.CheckNotNull(value, nameof(value)); + Validation.CheckNotNull(cloudEvent, nameof(cloudEvent)); ContentType contentType = new ContentType(cloudEvent.DataContentType ?? JsonMediaType); - Encoding encoding = contentType.GetEncoding(); + Encoding encoding = MimeUtilities.GetEncoding(contentType); if (contentType.MediaType == JsonMediaType) { diff --git a/src/CloudNative.CloudEvents/CloudEvent.cs b/src/CloudNative.CloudEvents/CloudEvent.cs index efb6c6a..1d8d881 100644 --- a/src/CloudNative.CloudEvents/CloudEvent.cs +++ b/src/CloudNative.CloudEvents/CloudEvent.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.Linq; @@ -62,33 +63,32 @@ namespace CloudNative.CloudEvents /// to an empty sequence. public CloudEvent(CloudEventsSpecVersion specVersion, IEnumerable 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 // extension attributes. - SpecVersion = Preconditions.CheckNotNull(specVersion, nameof(specVersion)); + SpecVersion = Validation.CheckNotNull(specVersion, nameof(specVersion)); if (extensionAttributes is object) { foreach (var extension in extensionAttributes) { - Preconditions.CheckArgument( + Validation.CheckArgument( extension is object, nameof(extensionAttributes), "Extension attribute collection cannot contain null elements"); - Preconditions.CheckArgument( + Validation.CheckArgument( extension.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(extensionAttributes), "The 'specversion' attribute cannot be specified as an extension attribute"); - Preconditions.CheckArgument( + Validation.CheckArgument( SpecVersion.GetAttributeByName(extension.Name) is null, nameof(extensionAttributes), "'{0}' cannot be specified as the name of an extension attribute; it is already a context attribute", extension.Name); - Preconditions.CheckArgument( + Validation.CheckArgument( extension.IsExtension, nameof(extensionAttributes), "'{0}' is not an extension attribute", extension.Name); - Preconditions.CheckArgument( + Validation.CheckArgument( !this.extensionAttributes.ContainsKey(extension.Name), nameof(extensionAttributes), "'{0}' cannot be specified more than once as an extension attribute"); @@ -123,8 +123,8 @@ namespace CloudNative.CloudEvents { get { - Preconditions.CheckNotNull(attribute, nameof(attribute)); - Preconditions.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute); + Validation.CheckNotNull(attribute, nameof(attribute)); + Validation.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute); // TODO: Is this validation definitely useful? It does mean we never return something // that's invalid for the attribute, which is potentially good... @@ -137,14 +137,14 @@ namespace CloudNative.CloudEvents } set { - Preconditions.CheckNotNull(attribute, nameof(attribute)); - Preconditions.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute); + Validation.CheckNotNull(attribute, nameof(attribute)); + Validation.CheckArgument(attribute.Name != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attribute), Strings.ErrorCannotIndexBySpecVersionAttribute); string name = attribute.Name; var knownAttribute = GetAttribute(name); // 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), "Cannot add an unknown non-extension attribute to an event."); @@ -178,14 +178,14 @@ namespace CloudNative.CloudEvents get { // TODO: Validate the attribute name is valid (e.g. not upper case)? Seems overkill. - Preconditions.CheckNotNull(attributeName, nameof(attributeName)); - Preconditions.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute); - return attributeValues.GetValueOrDefault(Preconditions.CheckNotNull(attributeName, nameof(attributeName))); + Validation.CheckNotNull(attributeName, nameof(attributeName)); + Validation.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute); + return attributeValues.GetValueOrDefault(Validation.CheckNotNull(attributeName, nameof(attributeName))); } set { - Preconditions.CheckNotNull(attributeName, nameof(attributeName)); - Preconditions.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute); + Validation.CheckNotNull(attributeName, nameof(attributeName)); + Validation.CheckArgument(attributeName != CloudEventsSpecVersion.SpecVersionAttributeName, nameof(attributeName), Strings.ErrorCannotIndexBySpecVersionAttribute); var knownAttribute = GetAttribute(attributeName); @@ -193,7 +193,7 @@ namespace CloudNative.CloudEvents // (It's a simple way of populating extensions after the fact...) 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}'", value.GetType(), attributeName); knownAttribute = CloudEventAttribute.CreateExtension(attributeName, CloudEventAttributeType.String); @@ -352,8 +352,8 @@ namespace CloudNative.CloudEvents /// The value of the attribute to set. Must not be null. public void SetAttributeFromString(string name, string value) { - Preconditions.CheckNotNull(name, nameof(name)); - Preconditions.CheckNotNull(value, nameof(value)); + Validation.CheckNotNull(name, nameof(name)); + Validation.CheckNotNull(value, nameof(value)); var attribute = GetAttribute(name); if (attribute is null) @@ -385,29 +385,6 @@ namespace CloudNative.CloudEvents throw new InvalidOperationException($"Missing required attributes: {joinedMissing}"); } - // TODO: consider moving this to an extension method in an implementation helper namespace? - - /// - /// Validates that this CloudEvent is valid in the same way as , - /// but throwing an 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. - /// - /// The parameter name to use in the exception if the event is invalid. - /// May be null. - /// The event is invalid. - /// A reference to the same object, for simplicity of method chaining. - 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); - } - /// /// Returns whether this CloudEvent is valid, i.e. whether all required attributes have /// values. diff --git a/src/CloudNative.CloudEvents/CloudEventAttribute.cs b/src/CloudNative.CloudEvents/CloudEventAttribute.cs index 2a1e67f..5b5d807 100644 --- a/src/CloudNative.CloudEvents/CloudEventAttribute.cs +++ b/src/CloudNative.CloudEvents/CloudEventAttribute.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; namespace CloudNative.CloudEvents @@ -44,7 +45,7 @@ namespace CloudNative.CloudEvents // TODO: Have a "mode" of Required/Optional/Extension? private CloudEventAttribute(string name, CloudEventAttributeType type, bool required, bool extension, Action 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 validator) => new CloudEventAttribute(name, type, required: true, extension: false, validator: validator); @@ -87,7 +88,7 @@ namespace CloudNative.CloudEvents /// , for convenience. internal static string ValidateName(string name) { - Preconditions.CheckNotNull(name, nameof(name)); + Validation.CheckNotNull(name, nameof(name)); if (name.Length == 0) { throw new ArgumentException("Attribute names must be non-empty", nameof(name)); @@ -106,7 +107,7 @@ namespace CloudNative.CloudEvents public object Parse(string text) { - Preconditions.CheckNotNull(text, nameof(text)); + Validation.CheckNotNull(text, nameof(text)); object value; // By wrapping every exception here, we always get an // ArgumentException (other than the ArgumentNullException above) and have the name in the message. @@ -132,7 +133,7 @@ namespace CloudNative.CloudEvents /// The value, for simple method chaining. 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 // ArgumentException (other than the ArgumentNullException above) and have the name in the message. try diff --git a/src/CloudNative.CloudEvents/CloudEventAttributeType.cs b/src/CloudNative.CloudEvents/CloudEventAttributeType.cs index ee151e6..5fa3e1c 100644 --- a/src/CloudNative.CloudEvents/CloudEventAttributeType.cs +++ b/src/CloudNative.CloudEvents/CloudEventAttributeType.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; 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) { @@ -109,13 +110,13 @@ namespace CloudNative.CloudEvents public override sealed void Validate(object value) { - Preconditions.CheckNotNull(value, nameof(value)); + Validation.CheckNotNull(value, nameof(value)); if (!ClrType.IsInstanceOfType(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); diff --git a/src/CloudNative.CloudEvents/CloudEventFormatter.cs b/src/CloudNative.CloudEvents/CloudEventFormatter.cs index 448bf15..43dabdc 100644 --- a/src/CloudNative.CloudEvents/CloudEventFormatter.cs +++ b/src/CloudNative.CloudEvents/CloudEventFormatter.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.IO; diff --git a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs index e27456f..0dc4e0b 100644 --- a/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpClientExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.Linq; @@ -60,7 +61,7 @@ namespace CloudNative.CloudEvents.Http /// true, if the response is a CloudEvent public static bool IsCloudEvent(this HttpRequestMessage httpRequestMessage) { - Preconditions.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); + Validation.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); return HasCloudEventsContentType(httpRequestMessage.Content) || httpRequestMessage.Headers.Contains(HttpUtilities.SpecVersionHttpHeader); } @@ -72,7 +73,7 @@ namespace CloudNative.CloudEvents.Http /// true, if the response is a CloudEvent public static bool IsCloudEvent(this HttpResponseMessage httpResponseMessage) { - Preconditions.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage)); + Validation.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage)); return HasCloudEventsContentType(httpResponseMessage.Content) || httpResponseMessage.Headers.Contains(HttpUtilities.SpecVersionHttpHeader); } @@ -109,7 +110,7 @@ namespace CloudNative.CloudEvents.Http CloudEventFormatter formatter, IEnumerable extensionAttributes) { - Preconditions.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage)); + Validation.CheckNotNull(httpResponseMessage, nameof(httpResponseMessage)); return ToCloudEventInternalAsync(httpResponseMessage.Headers, httpResponseMessage.Content, formatter, extensionAttributes, nameof(httpResponseMessage)); } @@ -138,19 +139,19 @@ namespace CloudNative.CloudEvents.Http CloudEventFormatter formatter, IEnumerable extensionAttributes) { - Preconditions.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); + Validation.CheckNotNull(httpRequestMessage, nameof(httpRequestMessage)); return ToCloudEventInternalAsync(httpRequestMessage.Headers, httpRequestMessage.Content, formatter, extensionAttributes, nameof(httpRequestMessage)); } private static async Task ToCloudEventInternalAsync(HttpHeaders headers, HttpContent content, CloudEventFormatter formatter, IEnumerable extensionAttributes, string paramName) { - Preconditions.CheckNotNull(formatter, nameof(formatter)); + Validation.CheckNotNull(formatter, nameof(formatter)); if (HasCloudEventsContentType(content)) { 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 { @@ -179,7 +180,7 @@ namespace CloudNative.CloudEvents.Http var data = await content.ReadAsByteArrayAsync().ConfigureAwait(false); formatter.DecodeBinaryModeEventData(data, cloudEvent); } - return cloudEvent.ValidateForConversion(paramName); + return Validation.CheckCloudEventArgument(cloudEvent, paramName); } } diff --git a/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs index b5180cb..1996891 100644 --- a/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpContentExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Net.Http; using System.Net.Mime; @@ -21,9 +22,8 @@ namespace CloudNative.CloudEvents.Http /// The formatter to use within the conversion. Must not be null. public static HttpContent ToHttpContent(this CloudEvent cloudEvent, ContentMode contentMode, CloudEventFormatter formatter) { - Preconditions.CheckNotNull(cloudEvent, nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); - Preconditions.CheckNotNull(formatter, nameof(formatter)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); + Validation.CheckNotNull(formatter, nameof(formatter)); byte[] content; // 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); if (contentType is object) { - ret.Headers.ContentType = contentType.ToMediaTypeHeaderValue(); + ret.Headers.ContentType = MimeUtilities.ToMediaTypeHeaderValue(contentType); } else if (content.Length != 0) { diff --git a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs index b5cceb8..b7ec263 100644 --- a/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpListenerExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Collections.Generic; using System.Net; @@ -27,10 +28,9 @@ namespace CloudNative.CloudEvents.Http public static Task CopyToHttpListenerResponseAsync(this CloudEvent cloudEvent, HttpListenerResponse destination, ContentMode contentMode, CloudEventFormatter formatter) { - Preconditions.CheckNotNull(cloudEvent, nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); - Preconditions.CheckNotNull(destination, nameof(destination)); - Preconditions.CheckNotNull(formatter, nameof(formatter)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); + Validation.CheckNotNull(destination, nameof(destination)); + Validation.CheckNotNull(formatter, nameof(formatter)); byte[] content; ContentType contentType; @@ -142,8 +142,8 @@ namespace CloudNative.CloudEvents.Http public static CloudEvent ToCloudEvent(this HttpListenerRequest httpListenerRequest, CloudEventFormatter formatter, IEnumerable extensionAttributes) { - Preconditions.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest)); - Preconditions.CheckNotNull(formatter, nameof(formatter)); + Validation.CheckNotNull(httpListenerRequest, nameof(httpListenerRequest)); + Validation.CheckNotNull(formatter, nameof(formatter)); if (HasCloudEventsContentType(httpListenerRequest)) { @@ -176,7 +176,7 @@ namespace CloudNative.CloudEvents.Http cloudEvent.DataContentType = httpListenerRequest.ContentType; formatter.DecodeBinaryModeEventData(BinaryDataUtilities.ToByteArray(httpListenerRequest.InputStream), cloudEvent); - return cloudEvent.ValidateForConversion(nameof(httpListenerRequest)); + return Validation.CheckCloudEventArgument(cloudEvent, nameof(httpListenerRequest)); } } diff --git a/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs b/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs index ef58c25..0d5e84f 100644 --- a/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs +++ b/src/CloudNative.CloudEvents/Http/HttpWebExtensions.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Net; using System.Net.Mime; @@ -28,10 +29,9 @@ namespace CloudNative.CloudEvents.Http public static async Task CopyToHttpWebRequestAsync(this CloudEvent cloudEvent, HttpWebRequest destination, ContentMode contentMode, CloudEventFormatter formatter) { - Preconditions.CheckNotNull(cloudEvent, nameof(cloudEvent)); - cloudEvent.ValidateForConversion(nameof(cloudEvent)); - Preconditions.CheckNotNull(destination, nameof(destination)); - Preconditions.CheckNotNull(formatter, nameof(formatter)); + Validation.CheckCloudEventArgument(cloudEvent, nameof(cloudEvent)); + Validation.CheckNotNull(destination, nameof(destination)); + Validation.CheckNotNull(formatter, nameof(formatter)); byte[] content; // The content type to include in the ContentType header - may be the data content type, or the formatter's content type. diff --git a/src/CloudNative.CloudEvents/BinaryDataUtilities.cs b/src/CloudNative.CloudEvents/Impl/BinaryDataUtilities.cs similarity index 77% rename from src/CloudNative.CloudEvents/BinaryDataUtilities.cs rename to src/CloudNative.CloudEvents/Impl/BinaryDataUtilities.cs index 70017df..311b788 100644 --- a/src/CloudNative.CloudEvents/BinaryDataUtilities.cs +++ b/src/CloudNative.CloudEvents/Impl/BinaryDataUtilities.cs @@ -5,15 +5,15 @@ using System.IO; using System.Threading.Tasks; -namespace CloudNative.CloudEvents +namespace CloudNative.CloudEvents.Core { /// /// Utilities methods for dealing with binary data, converting between /// streams, arrays, Memory{T} etc. /// - internal static class BinaryDataUtilities + public static class BinaryDataUtilities { - internal async static Task ToByteArrayAsync(Stream stream) + public async static Task ToByteArrayAsync(Stream stream) { // TODO: Optimize if it's already a MemoryStream? var memory = new MemoryStream(); @@ -21,7 +21,7 @@ namespace CloudNative.CloudEvents return memory.ToArray(); } - internal static byte[] ToByteArray(Stream stream) + public static byte[] ToByteArray(Stream stream) { var memory = new MemoryStream(); stream.CopyTo(memory); diff --git a/src/CloudNative.CloudEvents/MimeUtilities.cs b/src/CloudNative.CloudEvents/Impl/MimeUtilities.cs similarity index 91% rename from src/CloudNative.CloudEvents/MimeUtilities.cs rename to src/CloudNative.CloudEvents/Impl/MimeUtilities.cs index 3b5fec1..95ccc86 100644 --- a/src/CloudNative.CloudEvents/MimeUtilities.cs +++ b/src/CloudNative.CloudEvents/Impl/MimeUtilities.cs @@ -6,13 +6,13 @@ using System.Net.Http.Headers; using System.Net.Mime; 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. // But it's not ideal... /// - /// Utility and extension methods around MIME. + /// Utility methods around MIME. /// public static class MimeUtilities { @@ -23,7 +23,7 @@ namespace CloudNative.CloudEvents /// The content type, or null if no content type is known. /// An encoding suitable for the charset specified in , /// or UTF-8 if no charset has been specified. - public static Encoding GetEncoding(this ContentType contentType) => + public static Encoding GetEncoding(ContentType contentType) => contentType?.CharSet is string charSet ? Encoding.GetEncoding(charSet) : Encoding.UTF8; /// @@ -31,7 +31,7 @@ namespace CloudNative.CloudEvents /// /// The header value to convert. May be null. /// The converted content type, or null if is null. - public static ContentType ToContentType(this MediaTypeHeaderValue headerValue) => + public static ContentType ToContentType(MediaTypeHeaderValue headerValue) => headerValue is null ? null : new ContentType(headerValue.ToString()); /// @@ -39,7 +39,7 @@ namespace CloudNative.CloudEvents /// /// The content type to convert. May be null. /// The converted media type header value, or null if is null. - public static MediaTypeHeaderValue ToMediaTypeHeaderValue(this ContentType contentType) + public static MediaTypeHeaderValue ToMediaTypeHeaderValue(ContentType contentType) { if (contentType is null) { diff --git a/src/CloudNative.CloudEvents/Impl/Validation.cs b/src/CloudNative.CloudEvents/Impl/Validation.cs new file mode 100644 index 0000000..a8555ab --- /dev/null +++ b/src/CloudNative.CloudEvents/Impl/Validation.cs @@ -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 +{ + /// + /// Validation methods which are typically convenient for implementers of CloudEvent formatters + /// and protocol bindings. + /// + public static class Validation + { + /// + /// Validates that the given reference is non-null. + /// + /// Type of the value to check + /// The reference to check for nullity + /// The parameter name to use in the exception if is null. + /// May be null. + /// The value of , for convenient method chaining or assignment. + public static T CheckNotNull(T value, string paramName) where T : class => + value ?? throw new ArgumentNullException(paramName); + + /// + /// Validates an argument-dependent condition, throwing an exception if the check fails. + /// + /// The condition to validate; this method will throw an if this is false. + /// The name of the parameter being validated. May be null. + /// The message to use in the exception, if one is thrown. + public static void CheckArgument(bool condition, string paramName, string message) + { + if (!condition) + { + throw new ArgumentException(message, paramName); + } + } + + /// + /// Validates an argument-dependent condition, throwing an exception if the check fails. + /// + /// The condition to validate; this method will throw an if this is false. + /// The name of the parameter being validated. May be null. + /// The string format to use in the exception message, if one is thrown. + /// The first argument in the string format. + public static void CheckArgument(bool condition, string paramName, string messageFormat, + object arg1) + { + if (!condition) + { + throw new ArgumentException(string.Format(messageFormat, arg1), paramName); + } + } + + /// + /// Validates an argument-dependent condition, throwing an exception if the check fails. + /// + /// The condition to validate; this method will throw an if this is false. + /// The name of the parameter being validated. May be null. + /// The string format to use in the exception message, if one is thrown. + /// The first argument in the string format. + /// The first argument in the string format. + 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); + } + } + + /// + /// Validates that the specified CloudEvent is valid in the same way as , + /// but throwing an 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. + /// The event to validate. + /// The parameter name to use in the exception if is null or invalid. + /// May be null. + /// is null. + /// The event is invalid. + /// A reference to the same object, for simplicity of method chaining. + 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); + } + } +} diff --git a/src/CloudNative.CloudEvents/Preconditions.cs b/src/CloudNative.CloudEvents/Preconditions.cs deleted file mode 100644 index c1e7483..0000000 --- a/src/CloudNative.CloudEvents/Preconditions.cs +++ /dev/null @@ -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 -{ - /// - /// Convenient precondition methods. - /// - internal static class Preconditions - { - internal static T CheckNotNull(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); - } - } - } -} diff --git a/src/CloudNative.CloudEvents/Timestamps.cs b/src/CloudNative.CloudEvents/Timestamps.cs index 782d9d1..d8d1cdb 100644 --- a/src/CloudNative.CloudEvents/Timestamps.cs +++ b/src/CloudNative.CloudEvents/Timestamps.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Globalization; @@ -49,10 +50,8 @@ namespace CloudNative.CloudEvents public static bool TryParse(string input, out DateTimeOffset result) { // TODO: Check this and add a test - if (input is null) - { - throw new ArgumentNullException(nameof(input)); - } + Validation.CheckNotNull(input, nameof(input)); + if (input.Length < MinLength) // "yyyy-MM-ddTHH:mm:ssZ" is the shortest possible value. { result = default; diff --git a/test/CloudNative.CloudEvents.UnitTests/CloudEventTest.cs b/test/CloudNative.CloudEvents.UnitTests/CloudEventTest.cs index fb31c4c..0d65d38 100644 --- a/test/CloudNative.CloudEvents.UnitTests/CloudEventTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/CloudEventTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache 2.0 license. // See LICENSE file in the project root for full license information. +using CloudNative.CloudEvents.Core; using System; using System.Linq; using System.Net.Mime; @@ -112,7 +113,7 @@ namespace CloudNative.CloudEvents.UnitTests Assert.Contains(CloudEventsSpecVersion.Default.SourceAttribute.Name, exception1.Message); Assert.DoesNotContain(CloudEventsSpecVersion.Default.TypeAttribute.Name, exception1.Message); - var exception2 = Assert.Throws(() => cloudEvent.ValidateForConversion("param")); + var exception2 = Assert.Throws(() => Validation.CheckCloudEventArgument(cloudEvent, "param")); Assert.Equal("param", exception2.ParamName); Assert.Contains(CloudEventsSpecVersion.Default.IdAttribute.Name, exception1.Message); Assert.Contains(CloudEventsSpecVersion.Default.SourceAttribute.Name, exception1.Message); @@ -130,7 +131,7 @@ namespace CloudNative.CloudEvents.UnitTests }; Assert.True(cloudEvent.IsValid); Assert.Same(cloudEvent, cloudEvent.Validate()); - Assert.Same(cloudEvent, cloudEvent.ValidateForConversion("param")); + Assert.Same(cloudEvent, Validation.CheckCloudEventArgument(cloudEvent, "param")); } [Fact] diff --git a/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs b/test/CloudNative.CloudEvents.UnitTests/Impl/MimeUtilitiesTest.cs similarity index 81% rename from test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs rename to test/CloudNative.CloudEvents.UnitTests/Impl/MimeUtilitiesTest.cs index 1819a89..7ab4258 100644 --- a/test/CloudNative.CloudEvents.UnitTests/MimeUtilitiesTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/Impl/MimeUtilitiesTest.cs @@ -8,7 +8,7 @@ using System.Net.Mime; using System.Text; using Xunit; -namespace CloudNative.CloudEvents.UnitTests.Http +namespace CloudNative.CloudEvents.Core.UnitTests { public class MimeUtilitiesTest { @@ -21,9 +21,9 @@ namespace CloudNative.CloudEvents.UnitTests.Http public void ContentTypeConversions(string text) { var originalContentType = new ContentType(text); - var header = originalContentType.ToMediaTypeHeaderValue(); + var header = MimeUtilities.ToMediaTypeHeaderValue(originalContentType); AssertEqualParts(text, header.ToString()); - var convertedContentType = header.ToContentType(); + var convertedContentType = MimeUtilities.ToContentType(header); AssertEqualParts(originalContentType.ToString(), convertedContentType.ToString()); // Conversions can end up reordering the parameters. In reality we're only @@ -40,8 +40,8 @@ namespace CloudNative.CloudEvents.UnitTests.Http [Fact] public void ContentTypeConversions_Null() { - Assert.Null(default(ContentType).ToMediaTypeHeaderValue()); - Assert.Null(default(MediaTypeHeaderValue).ToContentType()); + Assert.Null(MimeUtilities.ToMediaTypeHeaderValue(default(ContentType))); + Assert.Null(MimeUtilities.ToContentType(default(MediaTypeHeaderValue))); } [Theory] @@ -50,7 +50,7 @@ namespace CloudNative.CloudEvents.UnitTests.Http public void ContentTypeGetEncoding(string charSet) { var contentType = new ContentType($"text/plain; charset={charSet}"); - Encoding encoding = contentType.GetEncoding(); + Encoding encoding = MimeUtilities.GetEncoding(contentType); Assert.Equal(charSet, encoding.WebName); } @@ -58,7 +58,7 @@ namespace CloudNative.CloudEvents.UnitTests.Http public void ContentTypeGetEncoding_NoContentType() { ContentType contentType = null; - Encoding encoding = contentType.GetEncoding(); + Encoding encoding = MimeUtilities.GetEncoding(contentType); Assert.Equal(Encoding.UTF8, encoding); } @@ -66,7 +66,7 @@ namespace CloudNative.CloudEvents.UnitTests.Http public void ContentTypeGetEncoding_NoCharSet() { ContentType contentType = new ContentType("text/plain"); - Encoding encoding = contentType.GetEncoding(); + Encoding encoding = MimeUtilities.GetEncoding(contentType); Assert.Equal(Encoding.UTF8, encoding); }