Merge pull request #26 from zihotki/feature/kafka-transport
added Kafka transport support as well as Partitioning extension
This commit is contained in:
		
						commit
						86effe7f68
					
				|  | @ -21,6 +21,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.Mqt | |||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.Amqp", "src\CloudNative.CloudEvents.Amqp\CloudNative.CloudEvents.Amqp.csproj", "{39EF4DB0-9890-4CAD-A36E-F7E25D2E72EF}" | ||||
| EndProject | ||||
| Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "CloudNative.CloudEvents.Kafka", "src\CloudNative.CloudEvents.Kafka\CloudNative.CloudEvents.Kafka.csproj", "{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}" | ||||
| EndProject | ||||
| Global | ||||
| 	GlobalSection(SolutionConfigurationPlatforms) = preSolution | ||||
| 		Debug|Any CPU = Debug|Any CPU | ||||
|  | @ -91,6 +93,18 @@ Global | |||
| 		{39EF4DB0-9890-4CAD-A36E-F7E25D2E72EF}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{39EF4DB0-9890-4CAD-A36E-F7E25D2E72EF}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{39EF4DB0-9890-4CAD-A36E-F7E25D2E72EF}.Release|x86.Build.0 = Release|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Debug|Any CPU.ActiveCfg = Debug|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Debug|Any CPU.Build.0 = Debug|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Debug|x64.ActiveCfg = Debug|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Debug|x64.Build.0 = Debug|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Debug|x86.ActiveCfg = Debug|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Debug|x86.Build.0 = Debug|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Release|Any CPU.ActiveCfg = Release|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Release|Any CPU.Build.0 = Release|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Release|x64.ActiveCfg = Release|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Release|x64.Build.0 = Release|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Release|x86.ActiveCfg = Release|Any CPU | ||||
| 		{193D6D9D-C1A0-459E-86CF-F207CDF0FC73}.Release|x86.Build.0 = Release|Any CPU | ||||
| 	EndGlobalSection | ||||
| 	GlobalSection(SolutionProperties) = preSolution | ||||
| 		HideSolutionNode = FALSE | ||||
|  |  | |||
|  | @ -63,8 +63,7 @@ namespace CloudNative.CloudEvents.Amqp | |||
|             } | ||||
|             else | ||||
|             { | ||||
|                 var cloudEvent = new CloudEvent( | ||||
|                     message.ApplicationProperties.Map.ContainsKey(SpecVersionAmqpHeader1) | ||||
|                 var specVersion = message.ApplicationProperties.Map.ContainsKey(SpecVersionAmqpHeader1) | ||||
|                         ? CloudEventsSpecVersion.V0_1 | ||||
|                         : message.ApplicationProperties.Map.ContainsKey(SpecVersionAmqpHeader2) | ||||
|                             ? (message.ApplicationProperties.Map[SpecVersionAmqpHeader2] as string == "0.2" | ||||
|  | @ -72,7 +71,9 @@ namespace CloudNative.CloudEvents.Amqp | |||
|                                 (message.ApplicationProperties.Map[SpecVersionAmqpHeader2] as string == "0.3" | ||||
|                                     ? CloudEventsSpecVersion.V0_3 | ||||
|                                 : CloudEventsSpecVersion.Default)) | ||||
|                             : CloudEventsSpecVersion.Default, extensions); | ||||
|                             : CloudEventsSpecVersion.Default; | ||||
| 
 | ||||
|                 var cloudEvent = new CloudEvent(specVersion , extensions); | ||||
|                 var attributes = cloudEvent.GetAttributes(); | ||||
|                 foreach (var prop in message.ApplicationProperties.Map) | ||||
|                 { | ||||
|  |  | |||
|  | @ -0,0 +1,20 @@ | |||
| <Project Sdk="Microsoft.NET.Sdk"> | ||||
| 
 | ||||
|   <PropertyGroup> | ||||
|     <TargetFramework>netstandard2.0</TargetFramework> | ||||
|     <PackageVersion>0.1</PackageVersion> | ||||
|     <Description>Kafka extensions for CloudNative.CloudEvents</Description> | ||||
|     <Copyright>Copyright Cloud Native Foundation</Copyright> | ||||
|     <RepositoryUrl>https://github.com/cloudevents/sdk-csharp</RepositoryUrl> | ||||
|     <PackageProjectUrl>https://cloudevents.io</PackageProjectUrl> | ||||
|   </PropertyGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <PackageReference Include="Confluent.Kafka" Version="1.1.0" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" /> | ||||
|   </ItemGroup> | ||||
| 
 | ||||
| </Project> | ||||
|  | @ -0,0 +1,159 @@ | |||
| // Copyright (c) Cloud Native Foundation.  | ||||
| // Licensed under the Apache 2.0 license. | ||||
| // See LICENSE file in the project root for full license information. | ||||
| 
 | ||||
| 
 | ||||
| namespace CloudNative.CloudEvents.Kafka | ||||
| { | ||||
|     using CloudNative.CloudEvents.Extensions; | ||||
|     using Confluent.Kafka; | ||||
|     using System; | ||||
|     using System.Linq; | ||||
|     using System.Net.Mime; | ||||
|     using System.Text; | ||||
| 
 | ||||
|     public static class KafkaClientExtensions | ||||
|     { | ||||
|         private static string StructuredContentTypePrefix = "application/cloudevents"; | ||||
|         private const string SpecVersionKafkaHeader1 = KafkaCloudEventMessage.KafkaHeaderPerfix + "cloudEventsVersion"; | ||||
| 
 | ||||
|         private const string SpecVersionKafkaHeader2 = KafkaCloudEventMessage.KafkaHeaderPerfix + "specversion"; | ||||
| 
 | ||||
|         private static JsonEventFormatter _jsonFormatter = new JsonEventFormatter(); | ||||
| 
 | ||||
|         public static bool IsCloudEvent(this Message<string, byte[]> message) | ||||
|         { | ||||
|             return message.Headers.Any(x => | ||||
|             string.Equals(x.Key, SpecVersionKafkaHeader1, StringComparison.InvariantCultureIgnoreCase) | ||||
|                 || string.Equals(x.Key, SpecVersionKafkaHeader2, StringComparison.InvariantCultureIgnoreCase) | ||||
|                 || (string.Equals(x.Key, KafkaCloudEventMessage.KafkaContentTypeAttributeName, StringComparison.InvariantCultureIgnoreCase) | ||||
|                     && Encoding.UTF8.GetString(x.GetValueBytes() ?? Array.Empty<byte>()).StartsWith(StructuredContentTypePrefix)));                 | ||||
|         }        | ||||
| 
 | ||||
|         public static CloudEvent ToCloudEvent(this Message<string, byte[]> message, | ||||
|             ICloudEventFormatter eventFormatter = null, params ICloudEventExtension[] extensions) | ||||
|         { | ||||
|             if (!IsCloudEvent(message)) | ||||
|             { | ||||
|                 throw new InvalidOperationException(); | ||||
|             } | ||||
| 
 | ||||
|             var contentType = ExtractContentType(message); | ||||
| 
 | ||||
|             CloudEvent cloudEvent; | ||||
| 
 | ||||
|             if (!string.IsNullOrEmpty(contentType) | ||||
|                 && contentType.StartsWith(CloudEvent.MediaType, StringComparison.InvariantCultureIgnoreCase)) | ||||
|             { | ||||
|                 // structured mode | ||||
|                 if (eventFormatter == null) | ||||
|                 { | ||||
|                     if (contentType.EndsWith(JsonEventFormatter.MediaTypeSuffix, StringComparison.InvariantCultureIgnoreCase)) | ||||
|                     { | ||||
|                         eventFormatter = _jsonFormatter; | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         throw new InvalidOperationException("Not supported CloudEvents media formatter."); | ||||
|                     } | ||||
|                 } | ||||
| 
 | ||||
|                 cloudEvent = _jsonFormatter.DecodeStructuredEvent(message.Value, extensions); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // binary mode | ||||
|                 var specVersion = ExtractVersion(message); | ||||
| 
 | ||||
|                 cloudEvent = new CloudEvent(specVersion, extensions); | ||||
|                 var attributes = cloudEvent.GetAttributes(); | ||||
|                 var cloudEventHeaders = message.Headers.Where(h => h.Key.StartsWith(KafkaCloudEventMessage.KafkaHeaderPerfix)); | ||||
| 
 | ||||
|                 foreach (var header in cloudEventHeaders) | ||||
|                 { | ||||
|                     if (string.Equals(header.Key, SpecVersionKafkaHeader1, StringComparison.InvariantCultureIgnoreCase) | ||||
|                         || string.Equals(header.Key, SpecVersionKafkaHeader2, StringComparison.InvariantCultureIgnoreCase)) | ||||
|                     { | ||||
|                         continue; | ||||
|                     } | ||||
| 
 | ||||
|                     var attributeName = header.Key.Substring(KafkaCloudEventMessage.KafkaHeaderPerfix.Length); | ||||
|                     attributes.Add(attributeName, | ||||
|                         eventFormatter.DecodeAttribute(specVersion, attributeName, header.GetValueBytes(), extensions)); | ||||
|                 } | ||||
| 
 | ||||
|                 cloudEvent.DataContentType = contentType != null ? new ContentType(contentType) : null; | ||||
|                 cloudEvent.Data = message.Value; | ||||
|             } | ||||
| 
 | ||||
|             InitPartitioningKey(message, cloudEvent); | ||||
| 
 | ||||
|             return cloudEvent; | ||||
|         } | ||||
| 
 | ||||
|         private static string ExtractContentType(Message<string, byte[]> message) | ||||
|         { | ||||
|             var contentTypeHeader = message.Headers.FirstOrDefault(x => string.Equals(x.Key, KafkaCloudEventMessage.KafkaContentTypeAttributeName, | ||||
|                 StringComparison.InvariantCultureIgnoreCase)); | ||||
|             string contentType = null; | ||||
|             if (contentTypeHeader != null) | ||||
|             { | ||||
|                 var bytes = contentTypeHeader.GetValueBytes();                 | ||||
|                 contentType = Encoding.UTF8.GetString(bytes ?? Array.Empty<byte>()); | ||||
|             } | ||||
| 
 | ||||
|             return contentType; | ||||
|         } | ||||
| 
 | ||||
|         private static void InitPartitioningKey(Message<string, byte[]> message, CloudEvent cloudEvent) | ||||
|         { | ||||
|             if (!string.IsNullOrEmpty(message.Key)) | ||||
|             { | ||||
|                 var extension = cloudEvent.Extension<PartitioningExtension>(); | ||||
|                 extension.PartitioningKeyValue = message.Key; | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private static CloudEventsSpecVersion ExtractVersion(Message<string, byte[]> message) | ||||
|         { | ||||
|             var specVersionHeaders = message.Headers.Where(x => string.Equals(x.Key, SpecVersionKafkaHeader1, StringComparison.InvariantCultureIgnoreCase) | ||||
|                                 || string.Equals(x.Key, SpecVersionKafkaHeader2, StringComparison.InvariantCultureIgnoreCase)) | ||||
|                              .ToDictionary(x => x.Key, x => x, StringComparer.InvariantCultureIgnoreCase); | ||||
| 
 | ||||
|             var specVersion = CloudEventsSpecVersion.Default; | ||||
|             if (specVersionHeaders.ContainsKey(SpecVersionKafkaHeader1)) | ||||
|             { | ||||
|                 specVersion = CloudEventsSpecVersion.V0_1; | ||||
|             } | ||||
|             else if (specVersionHeaders.ContainsKey(SpecVersionKafkaHeader2)) | ||||
|             { | ||||
|                 var specVersionValue = Encoding.UTF8.GetString(specVersionHeaders[SpecVersionKafkaHeader2].GetValueBytes() ?? Array.Empty<byte>()); | ||||
|                 if (specVersionValue == "0.2") | ||||
|                 { | ||||
|                     specVersion = CloudEventsSpecVersion.V0_2; | ||||
|                 } | ||||
|                 else if (specVersionValue == "0.3") | ||||
|                 { | ||||
|                     specVersion = CloudEventsSpecVersion.V0_3; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return specVersion; | ||||
|         } | ||||
| 
 | ||||
|         private static (bool isBinaryMode, string contentType) IsBinaryMode(Message<string, object> message) | ||||
|         { | ||||
|             var contentTypeHeader = message.Headers.FirstOrDefault(x => string.Equals(x.Key, KafkaCloudEventMessage.KafkaContentTypeAttributeName)); | ||||
|             if (contentTypeHeader != null) | ||||
|             { | ||||
|                 var value = Encoding.UTF8.GetString(contentTypeHeader.GetValueBytes()); | ||||
|                 if (!string.IsNullOrEmpty( value) && value.StartsWith(StructuredContentTypePrefix, StringComparison.InvariantCultureIgnoreCase)) | ||||
|                 { | ||||
|                     return (true, value); | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             return (false, null); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,88 @@ | |||
| // Copyright (c) Cloud Native Foundation.  | ||||
| // Licensed under the Apache 2.0 license. | ||||
| // See LICENSE file in the project root for full license information. | ||||
| 
 | ||||
| namespace CloudNative.CloudEvents.Kafka | ||||
| { | ||||
|     using CloudNative.CloudEvents.Extensions; | ||||
|     using Confluent.Kafka; | ||||
|     using System; | ||||
|     using System.IO; | ||||
|     using System.Text; | ||||
| 
 | ||||
|     public class KafkaCloudEventMessage : Message<string, byte[]> | ||||
|     { | ||||
|         public const string KafkaHeaderPerfix = "ce_"; | ||||
| 
 | ||||
|         public const string KafkaContentTypeAttributeName = "content-type"; | ||||
| 
 | ||||
|         public KafkaCloudEventMessage(CloudEvent cloudEvent, ContentMode contentMode, ICloudEventFormatter formatter) | ||||
|         { | ||||
|             if (cloudEvent.Data == null) | ||||
|             { | ||||
|                 throw new ArgumentNullException(nameof(cloudEvent.Data)); | ||||
|             } | ||||
| 
 | ||||
|             Headers = new Headers(); | ||||
| 
 | ||||
|             Key = ExtractPartitionKey(cloudEvent);             | ||||
| 
 | ||||
|             if (contentMode == ContentMode.Structured) | ||||
|             { | ||||
|                 Value = formatter.EncodeStructuredEvent(cloudEvent, out var contentType); | ||||
|                 Headers.Add(KafkaContentTypeAttributeName, Encoding.UTF8.GetBytes(contentType.MediaType)); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 if (cloudEvent.Data is byte[] byteData) | ||||
|                 { | ||||
|                     Value = byteData; | ||||
|                 } | ||||
|                 else if (cloudEvent.Data is Stream dataStream) | ||||
|                 { | ||||
|                     if (dataStream is MemoryStream dataMemoryStream) | ||||
|                     { | ||||
|                         Value = dataMemoryStream.ToArray(); | ||||
|                     } | ||||
|                     else | ||||
|                     { | ||||
|                         var buffer = new MemoryStream(); | ||||
|                         dataStream.CopyTo(buffer); | ||||
|                         Value = buffer.ToArray(); | ||||
|                     } | ||||
|                 } | ||||
|                 else | ||||
|                 { | ||||
|                     throw new InvalidOperationException($"{cloudEvent.Data.GetType()} type is not supported for Cloud Event's Value."); | ||||
|                 } | ||||
| 
 | ||||
|                 Headers.Add(KafkaContentTypeAttributeName, Encoding.UTF8.GetBytes(cloudEvent.DataContentType?.MediaType));                 | ||||
|             } | ||||
| 
 | ||||
|             MapHeaders(cloudEvent, formatter); | ||||
|         } | ||||
| 
 | ||||
|         private void MapHeaders(CloudEvent cloudEvent, ICloudEventFormatter formatter) | ||||
|         { | ||||
|             foreach (var attr in cloudEvent.GetAttributes()) | ||||
|             { | ||||
|                 if (string.Equals(attr.Key, CloudEventAttributes.DataAttributeName(cloudEvent.SpecVersion))                     | ||||
|                     || string.Equals(attr.Key, CloudEventAttributes.DataContentTypeAttributeName(cloudEvent.SpecVersion)) | ||||
|                     || string.Equals(attr.Key, PartitioningExtension.PartitioningKeyAttributeName)) | ||||
|                 { | ||||
|                     continue; | ||||
|                 } | ||||
| 
 | ||||
|                 Headers.Add(KafkaHeaderPerfix + attr.Key,  | ||||
|                     formatter.EncodeAttribute(cloudEvent.SpecVersion, attr.Key, attr.Value, cloudEvent.Extensions.Values)); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         protected string ExtractPartitionKey(CloudEvent cloudEvent) | ||||
|         { | ||||
|             var extension = cloudEvent.Extension<PartitioningExtension>(); | ||||
| 
 | ||||
|             return extension?.PartitioningKeyValue; | ||||
|         } | ||||
|     } | ||||
| }   | ||||
|  | @ -5,4 +5,5 @@ | |||
| using System.Runtime.CompilerServices; | ||||
| 
 | ||||
| [assembly: InternalsVisibleTo("CloudNative.CloudEvents.Amqp")] | ||||
| [assembly: InternalsVisibleTo("CloudNative.CloudEvents.Mqtt")] | ||||
| [assembly: InternalsVisibleTo("CloudNative.CloudEvents.Mqtt")] | ||||
| [assembly: InternalsVisibleTo("CloudNative.CloudEvents.Kafka")] | ||||
|  | @ -214,7 +214,13 @@ namespace CloudNative.CloudEvents | |||
|         /// <returns>Extension instance if registered</returns> | ||||
|         public T Extension<T>() | ||||
|         { | ||||
|             return (T)Extensions[typeof(T)]; | ||||
|             var key = typeof(T); | ||||
|             if (Extensions.TryGetValue(key, out var extension)) | ||||
|             { | ||||
|                 return (T)extension; | ||||
|             } | ||||
| 
 | ||||
|             return default(T); | ||||
|         } | ||||
| 
 | ||||
|         /// <summary> | ||||
|  |  | |||
|  | @ -0,0 +1,66 @@ | |||
| // Copyright (c) Cloud Native Foundation.  | ||||
| // Licensed under the Apache 2.0 license. | ||||
| // See LICENSE file in the project root for full license information. | ||||
| 
 | ||||
| namespace CloudNative.CloudEvents.Extensions | ||||
| { | ||||
|     using System; | ||||
|     using System.Collections.Generic; | ||||
| 
 | ||||
|     public class PartitioningExtension : ICloudEventExtension | ||||
|     { | ||||
|         public const string PartitioningKeyAttributeName = "partitionkey"; | ||||
| 
 | ||||
|         IDictionary<string, object> _attributes = new Dictionary<string, object>(); | ||||
| 
 | ||||
|         public string PartitioningKeyValue | ||||
|         { | ||||
|             get => _attributes[PartitioningKeyAttributeName] as string; | ||||
|             set => _attributes[PartitioningKeyAttributeName] = value; | ||||
|         } | ||||
| 
 | ||||
|         public PartitioningExtension(string partitioningKeyValue = null) | ||||
|         { | ||||
|             PartitioningKeyValue = partitioningKeyValue; | ||||
|         } | ||||
| 
 | ||||
|         void ICloudEventExtension.Attach(CloudEvent cloudEvent) | ||||
|         { | ||||
|             var eventAttributes = cloudEvent.GetAttributes(); | ||||
|             if (_attributes == eventAttributes) | ||||
|             { | ||||
|                 // already done | ||||
|                 return; | ||||
|             } | ||||
| 
 | ||||
|             foreach (var attr in _attributes) | ||||
|             { | ||||
|                 if (attr.Value != null) | ||||
|                 { | ||||
|                     eventAttributes[attr.Key] = attr.Value; | ||||
|                 } | ||||
|             } | ||||
|             _attributes = eventAttributes; | ||||
|         } | ||||
| 
 | ||||
|         bool ICloudEventExtension.ValidateAndNormalize(string key, ref dynamic value) | ||||
|         { | ||||
|             if (string.Equals(key, PartitioningKeyAttributeName)) | ||||
|             { | ||||
|                 if (value is string) | ||||
|                 { | ||||
|                     return true; | ||||
|                 } | ||||
| 
 | ||||
|                 throw new InvalidOperationException(Strings.ErrorPartitioningKeyValueIsaNotAString); | ||||
|             } | ||||
| 
 | ||||
|             return false; | ||||
|         } | ||||
| 
 | ||||
|         public Type GetAttributeType(string name) | ||||
|         { | ||||
|             return string.Equals(name, PartitioningKeyAttributeName) ? typeof(string) : null; | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -19,7 +19,7 @@ namespace CloudNative.CloudEvents { | |||
|     // class via a tool like ResGen or Visual Studio. | ||||
|     // To add or remove a member, edit your .ResX file then rerun ResGen | ||||
|     // with the /str option, or rebuild your VS project. | ||||
|     [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] | ||||
|     [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "15.0.0.0")] | ||||
|     [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] | ||||
|     [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] | ||||
|     internal class Strings { | ||||
|  | @ -87,6 +87,15 @@ namespace CloudNative.CloudEvents { | |||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   Looks up a localized string similar to The 'key' attribute value must be a string. | ||||
|         /// </summary> | ||||
|         internal static string ErrorPartitioningKeyValueIsaNotAString { | ||||
|             get { | ||||
|                 return ResourceManager.GetString("ErrorPartitioningKeyValueIsaNotAString", resourceCulture); | ||||
|             } | ||||
|         } | ||||
|          | ||||
|         /// <summary> | ||||
|         ///   Looks up a localized string similar to The 'sampledrate' attribute value must be an integer. | ||||
|         /// </summary> | ||||
|  |  | |||
|  | @ -126,6 +126,9 @@ | |||
|   <data name="ErrorIdValueIsNotAString" xml:space="preserve"> | ||||
|     <value>The 'id' attribute value must be a string</value> | ||||
|   </data> | ||||
|   <data name="ErrorPartitioningKeyValueIsaNotAString" xml:space="preserve"> | ||||
|     <value>The 'key' attribute value must be a string</value> | ||||
|   </data> | ||||
|   <data name="ErrorSampledRateValueIsaNotAnInteger" xml:space="preserve"> | ||||
|     <value>The 'sampledrate' attribute value must be an integer</value> | ||||
|   </data> | ||||
|  |  | |||
|  | @ -20,6 +20,7 @@ | |||
| 
 | ||||
|   <ItemGroup> | ||||
|     <ProjectReference Include="..\..\src\CloudNative.CloudEvents.Amqp\CloudNative.CloudEvents.Amqp.csproj" /> | ||||
|     <ProjectReference Include="..\..\src\CloudNative.CloudEvents.Kafka\CloudNative.CloudEvents.Kafka.csproj" /> | ||||
|     <ProjectReference Include="..\..\src\CloudNative.CloudEvents.Mqtt\CloudNative.CloudEvents.Mqtt.csproj" /> | ||||
|     <ProjectReference Include="..\..\src\CloudNative.CloudEvents\CloudNative.CloudEvents.csproj" /> | ||||
|   </ItemGroup> | ||||
|  |  | |||
|  | @ -50,6 +50,19 @@ namespace CloudNative.CloudEvents.UnitTests | |||
|             "    \"data\" : \"test\"\n" + | ||||
|             "}"; | ||||
| 
 | ||||
| 
 | ||||
|         const string jsonPartitioningKey = | ||||
|             "{\n" + | ||||
|             "    \"specversion\" : \"0.3\",\n" + | ||||
|             "    \"type\" : \"com.github.pull.create\",\n" + | ||||
|             "    \"source\" : \"https://github.com/cloudevents/spec/pull/123\",\n" + | ||||
|             "    \"id\" : \"A234-1234-1234\",\n" + | ||||
|             "    \"time\" : \"2018-04-05T17:31:00Z\",\n" + | ||||
|             "    \"partitionkey\" : \"1\",\n" + | ||||
|             "    \"datacontenttype\" : \"text/plain\",\n" + | ||||
|             "    \"data\" : \"test\"\n" + | ||||
|             "}"; | ||||
| 
 | ||||
|         [Fact] | ||||
|         public void DistTraceParse() | ||||
|         { | ||||
|  | @ -185,5 +198,25 @@ namespace CloudNative.CloudEvents.UnitTests | |||
| 
 | ||||
|             Assert.Equal(1, cloudEvent.Extension<SamplingExtension>().SampledRate.Value); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public void PartitioningParse() | ||||
|         { | ||||
|             var jsonFormatter = new JsonEventFormatter(); | ||||
|             var cloudEvent = jsonFormatter.DecodeStructuredEvent(Encoding.UTF8.GetBytes(jsonPartitioningKey), new PartitioningExtension()); | ||||
| 
 | ||||
|             Assert.Equal("1", cloudEvent.Extension<PartitioningExtension>().PartitioningKeyValue); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public void PartitioningJsonTranscode() | ||||
|         { | ||||
|             var jsonFormatter = new JsonEventFormatter(); | ||||
|             var cloudEvent1 = jsonFormatter.DecodeStructuredEvent(Encoding.UTF8.GetBytes(jsonPartitioningKey)); | ||||
|             var jsonData = jsonFormatter.EncodeStructuredEvent(cloudEvent1, out var contentType); | ||||
|             var cloudEvent = jsonFormatter.DecodeStructuredEvent(jsonData, new PartitioningExtension()); | ||||
| 
 | ||||
|             Assert.Equal("1", cloudEvent.Extension<PartitioningExtension>().PartitioningKeyValue); | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | @ -0,0 +1,180 @@ | |||
| // Copyright (c) Cloud Native Foundation.  | ||||
| // Licensed under the Apache 2.0 license. | ||||
| // See LICENSE file in the project root for full license information. | ||||
| 
 | ||||
| namespace CloudNative.CloudEvents.UnitTests | ||||
| { | ||||
|     using System; | ||||
|     using System.Net.Mime; | ||||
|     using CloudNative.CloudEvents.Amqp; | ||||
|     using CloudNative.CloudEvents.Kafka; | ||||
|     using Newtonsoft.Json; | ||||
|     using Xunit; | ||||
|     using Confluent.Kafka; | ||||
|     using Newtonsoft.Json.Linq; | ||||
|     using System.Collections.Generic; | ||||
|     using System.Text; | ||||
|     using CloudNative.CloudEvents.Extensions; | ||||
| 
 | ||||
|     public class KafkaTest | ||||
|     { | ||||
|         [Fact] | ||||
|         public void KafkaStructuredMessageTest() | ||||
|         { | ||||
|             // Kafka doesn't provide any way to get to the message transport level to do the test properly | ||||
|             // and it doesn't have an embedded version of a server for .Net so the lowest we can get is  | ||||
|             // the `Message<T, K>` | ||||
| 
 | ||||
|             var jsonEventFormatter = new JsonEventFormatter(); | ||||
| 
 | ||||
|             var cloudEvent = new CloudEvent(CloudEventsSpecVersion.V0_3, | ||||
|                 "com.github.pull.create", | ||||
|                 source: new Uri("https://github.com/cloudevents/spec/pull"), | ||||
|                 subject: "123") | ||||
|             { | ||||
|                 Id = "A234-1234-1234", | ||||
|                 Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc), | ||||
|                 DataContentType = new ContentType(MediaTypeNames.Text.Xml), | ||||
|                 Data = "<much wow=\"xml\"/>" | ||||
|             }; | ||||
| 
 | ||||
|             var attrs = cloudEvent.GetAttributes(); | ||||
|             attrs["comexampleextension1"] = "value"; | ||||
|             attrs["comexampleextension2"] = new { othervalue = 5 }; | ||||
| 
 | ||||
|             var message = new KafkaCloudEventMessage(cloudEvent, ContentMode.Structured, new JsonEventFormatter()); | ||||
| 
 | ||||
|             Assert.True(message.IsCloudEvent()); | ||||
| 
 | ||||
|             // using serialization to create fully independent copy thus simulating message transport | ||||
|             // real transport will work in a similar way | ||||
|             var serialized = JsonConvert.SerializeObject(message, new HeaderConverter()); | ||||
|             var messageCopy = JsonConvert.DeserializeObject<Message<string, byte[]>>(serialized, new HeadersConverter(), new HeaderConverter()); | ||||
| 
 | ||||
|             Assert.True(messageCopy.IsCloudEvent()); | ||||
|             var receivedCloudEvent = messageCopy.ToCloudEvent(jsonEventFormatter); | ||||
| 
 | ||||
|             Assert.Equal(CloudEventsSpecVersion.Default, receivedCloudEvent.SpecVersion); | ||||
|             Assert.Equal("com.github.pull.create", receivedCloudEvent.Type); | ||||
|             Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull"), receivedCloudEvent.Source); | ||||
|             Assert.Equal("123", receivedCloudEvent.Subject); | ||||
|             Assert.Equal("A234-1234-1234", receivedCloudEvent.Id); | ||||
|             Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(), | ||||
|                 receivedCloudEvent.Time.Value.ToUniversalTime()); | ||||
|             Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.DataContentType); | ||||
|             Assert.Equal("<much wow=\"xml\"/>", receivedCloudEvent.Data); | ||||
| 
 | ||||
|             var attr = receivedCloudEvent.GetAttributes(); | ||||
|             Assert.Equal("value", (string)attr["comexampleextension1"]); | ||||
|             Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue); | ||||
|         } | ||||
| 
 | ||||
|         [Fact] | ||||
|         public void KafkaBinaryMessageTest() | ||||
|         { | ||||
|             // Kafka doesn't provide any way to get to the message transport level to do the test properly | ||||
|             // and it doesn't have an embedded version of a server for .Net so the lowest we can get is  | ||||
|             // the `Message<T, K>` | ||||
| 
 | ||||
|             var jsonEventFormatter = new JsonEventFormatter(); | ||||
|             var cloudEvent = new CloudEvent("com.github.pull.create", | ||||
|                 new Uri("https://github.com/cloudevents/spec/pull/123"), | ||||
|                 extensions: new PartitioningExtension()) | ||||
|             { | ||||
|                 Id = "A234-1234-1234", | ||||
|                 Time = new DateTime(2018, 4, 5, 17, 31, 0, DateTimeKind.Utc), | ||||
|                 DataContentType = new ContentType(MediaTypeNames.Text.Xml), | ||||
|                 Data = Encoding.UTF8.GetBytes("<much wow=\"xml\"/>") | ||||
|             }; | ||||
| 
 | ||||
|             var attrs = cloudEvent.GetAttributes(); | ||||
|             attrs["comexampleextension1"] = "value"; | ||||
|             attrs["comexampleextension2"] = new { othervalue = 5 }; | ||||
|             cloudEvent.Extension<PartitioningExtension>().PartitioningKeyValue = "hello much wow"; | ||||
| 
 | ||||
|             var message = new KafkaCloudEventMessage(cloudEvent, ContentMode.Binary, new JsonEventFormatter()); | ||||
|             Assert.True(message.IsCloudEvent()); | ||||
| 
 | ||||
|             // using serialization to create fully independent copy thus simulating message transport | ||||
|             // real transport will work in a similar way | ||||
|             var serialized = JsonConvert.SerializeObject(message, new HeaderConverter()); | ||||
|             var messageCopy = JsonConvert.DeserializeObject<Message<string, byte[]>>(serialized, new HeadersConverter(), new HeaderConverter()); | ||||
| 
 | ||||
|             Assert.True(messageCopy.IsCloudEvent()); | ||||
|             var receivedCloudEvent = messageCopy.ToCloudEvent(jsonEventFormatter, new PartitioningExtension()); | ||||
| 
 | ||||
|             Assert.Equal(CloudEventsSpecVersion.Default, receivedCloudEvent.SpecVersion); | ||||
|             Assert.Equal("com.github.pull.create", receivedCloudEvent.Type); | ||||
|             Assert.Equal(new Uri("https://github.com/cloudevents/spec/pull/123"), receivedCloudEvent.Source); | ||||
|             Assert.Equal("A234-1234-1234", receivedCloudEvent.Id); | ||||
|             Assert.Equal(DateTime.Parse("2018-04-05T17:31:00Z").ToUniversalTime(), | ||||
|                 receivedCloudEvent.Time.Value.ToUniversalTime()); | ||||
|             Assert.Equal(new ContentType(MediaTypeNames.Text.Xml), receivedCloudEvent.DataContentType); | ||||
|             Assert.Equal(Encoding.UTF8.GetBytes("<much wow=\"xml\"/>"), receivedCloudEvent.Data); | ||||
|             Assert.Equal("hello much wow", receivedCloudEvent.Extension<PartitioningExtension>().PartitioningKeyValue); | ||||
| 
 | ||||
|             var attr = receivedCloudEvent.GetAttributes(); | ||||
|             Assert.Equal("value", (string)attr["comexampleextension1"]); | ||||
|             Assert.Equal(5, (int)((dynamic)attr["comexampleextension2"]).othervalue); | ||||
|         } | ||||
| 
 | ||||
|         private class HeadersConverter : JsonConverter | ||||
|         { | ||||
|             public override bool CanConvert(Type objectType) | ||||
|             { | ||||
|                 return objectType == typeof(Headers); | ||||
|             } | ||||
| 
 | ||||
|             public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||||
|             { | ||||
|                 if (reader.TokenType == JsonToken.Null) | ||||
|                 { | ||||
|                     return null; | ||||
|                 } | ||||
|                 else  | ||||
|                 { | ||||
|                     var surrogate = serializer.Deserialize<List<Header>>(reader); | ||||
|                     var headers = new Headers(); | ||||
| 
 | ||||
|                     foreach(var header in surrogate) | ||||
|                     { | ||||
|                         headers.Add(header.Key, header.GetValueBytes()); | ||||
|                     } | ||||
|                     return headers; | ||||
|                 } | ||||
|             } | ||||
| 
 | ||||
|             public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | ||||
|             { | ||||
|                 throw new NotImplementedException(); | ||||
|             } | ||||
|         } | ||||
| 
 | ||||
|         private class HeaderConverter : JsonConverter | ||||
|         { | ||||
|             private class HeaderContainer | ||||
|             { | ||||
|                 public string Key { get; set; } | ||||
|                 public byte[] Value { get; set; } | ||||
|             } | ||||
| 
 | ||||
|             public override bool CanConvert(Type objectType) | ||||
|             { | ||||
|                 return objectType == typeof(Header) || objectType == typeof(IHeader); | ||||
|             } | ||||
| 
 | ||||
|             public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer) | ||||
|             { | ||||
|                 var headerContainer = serializer.Deserialize<HeaderContainer>(reader); | ||||
|                 return new Header(headerContainer.Key, headerContainer.Value); | ||||
|             } | ||||
| 
 | ||||
|             public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer) | ||||
|             { | ||||
|                 var header = (IHeader)value; | ||||
|                 var container = new HeaderContainer { Key = header.Key, Value = header.GetValueBytes() }; | ||||
|                 serializer.Serialize(writer, container); | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
		Loading…
	
		Reference in New Issue