diff --git a/src/CloudNative.CloudEvents/CloudEvent.cs b/src/CloudNative.CloudEvents/CloudEvent.cs index bd149f1..fa669c1 100644 --- a/src/CloudNative.CloudEvents/CloudEvent.cs +++ b/src/CloudNative.CloudEvents/CloudEvent.cs @@ -71,8 +71,13 @@ namespace CloudNative.CloudEvents /// CloudEvents specification version /// Extensions to be added to this CloudEvents public CloudEvent(CloudEventsSpecVersion specVersion, IEnumerable extensions) + : this(new CloudEventAttributes(specVersion, extensions), extensions) { - attributes = new CloudEventAttributes(specVersion, extensions); + } + + private CloudEvent(CloudEventAttributes attributes, IEnumerable extensions) + { + this.attributes = attributes; var extensionMap = new Dictionary(); if (extensions != null) { @@ -155,11 +160,11 @@ namespace CloudNative.CloudEvents /// specification which the event uses. This enables the interpretation of the context. /// /// - public CloudEventsSpecVersion SpecVersion - { - get => attributes.SpecVersion; - set => attributes.SpecVersion = value; - } + public CloudEventsSpecVersion SpecVersion => attributes.SpecVersion; + + // TODO: Consider exposing publicly. + internal CloudEvent WithSpecVersion(CloudEventsSpecVersion newSpecVersion) => + new CloudEvent(attributes.WithSpecVersion(newSpecVersion), Extensions.Values); /// /// CloudEvents 'subject' attribute. This describes the subject of the event in the context diff --git a/src/CloudNative.CloudEvents/CloudEventAttributes.cs b/src/CloudNative.CloudEvents/CloudEventAttributes.cs index 57266a3..01462ea 100644 --- a/src/CloudNative.CloudEvents/CloudEventAttributes.cs +++ b/src/CloudNative.CloudEvents/CloudEventAttributes.cs @@ -14,6 +14,19 @@ namespace CloudNative.CloudEvents /// public class CloudEventAttributes : IDictionary { + private static readonly List> attributeNameMethods = new List> + { + DataAttributeName, + DataContentTypeAttributeName, + DataSchemaAttributeName, + IdAttributeName, + SourceAttributeName, + SpecVersionAttributeName, + SubjectAttributeName, + TimeAttributeName, + TypeAttributeName, + }; + readonly CloudEventsSpecVersion specVersion; IDictionary dict = new Dictionary(StringComparer.InvariantCultureIgnoreCase); @@ -24,8 +37,7 @@ namespace CloudNative.CloudEvents { this.extensions = extensions; this.specVersion = specVersion; - dict[SpecVersionAttributeName(specVersion)] = (specVersion == CloudEventsSpecVersion.V0_1 ? "0.1" : - (specVersion == CloudEventsSpecVersion.V0_2 ? "0.2" : (specVersion == CloudEventsSpecVersion.V0_3 ? "0.3" : "1.0"))); + dict[SpecVersionAttributeName(specVersion)] = SpecVersionString(specVersion); } int ICollection>.Count => dict.Count; @@ -36,100 +48,44 @@ namespace CloudNative.CloudEvents ICollection IDictionary.Values => dict.Values; - public CloudEventsSpecVersion SpecVersion + public CloudEventsSpecVersion SpecVersion => specVersion; + + // TODO: Consider exposing publicly. + internal CloudEventAttributes WithSpecVersion(CloudEventsSpecVersion newVersion) { - get + var newAttributes = new CloudEventAttributes(newVersion, extensions); + foreach (var kv in dict) { - object val; - if (dict.TryGetValue(SpecVersionAttributeName(CloudEventsSpecVersion.V0_1), out val) || - dict.TryGetValue(SpecVersionAttributeName(CloudEventsSpecVersion.V0_2), out val) || - dict.TryGetValue(SpecVersionAttributeName(CloudEventsSpecVersion.V0_3), out val) || - dict.TryGetValue(SpecVersionAttributeName(CloudEventsSpecVersion.V1_0), out val)) + // The constructor will have populated the spec version, so we can skip it. + if (!kv.Key.Equals(SpecVersionAttributeName(this.SpecVersion), StringComparison.InvariantCultureIgnoreCase)) { - return (val as string) == "0.1" ? CloudEventsSpecVersion.V0_1 : - (val as string) == "0.2" ? CloudEventsSpecVersion.V0_2 : - (val as string) == "0.3" ? CloudEventsSpecVersion.V0_3 : CloudEventsSpecVersion.V1_0; - - } - - return CloudEventsSpecVersion.Default; - } - set - { - // this setter sets the version and initiates a transform to the new target version if - // required. The transformation may fail under some circumstances where CloudEvents - // versions are in mutual conflict - - var currentSpecVersion = SpecVersion; - object val; - if (dict.TryGetValue(SpecVersionAttributeName(CloudEventsSpecVersion.V0_1), out val)) - { - if (value == CloudEventsSpecVersion.V0_1 && (val as string) == "0.1") - { - return; - } - } - else if ( dict.TryGetValue(SpecVersionAttributeName(), out val)) // 0.2, 0.3 and 1.0 are the same - { - if ((value == CloudEventsSpecVersion.V0_2 && (val as string) == "0.2") || - (value == CloudEventsSpecVersion.V0_3 && (val as string) == "0.3") || - (value == CloudEventsSpecVersion.V1_0 && (val as string) == "1.0")) - { - return; - } - } - - // transform to new version - var copy = new Dictionary(dict); - dict.Clear(); - this[SpecVersionAttributeName(value)] = value == CloudEventsSpecVersion.V0_1 ? "0.1" : (value == CloudEventsSpecVersion.V0_2 ? "0.2" : (value == CloudEventsSpecVersion.V0_3 ? "0.3" : "1.0")); - foreach (var kv in copy) - { - if (SpecVersionAttributeName(CloudEventsSpecVersion.V0_2).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase) || - SpecVersionAttributeName(CloudEventsSpecVersion.V0_3).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase) || - SpecVersionAttributeName(CloudEventsSpecVersion.V0_1).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase) || - SpecVersionAttributeName(CloudEventsSpecVersion.V1_0).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - continue; - } - if (DataContentTypeAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[DataContentTypeAttributeName(value)] = kv.Value; - } - else if (DataAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[DataAttributeName(value)] = kv.Value; - } - else if (IdAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[IdAttributeName(value)] = kv.Value; - } - else if (DataSchemaAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[DataSchemaAttributeName(value)] = kv.Value; - } - else if (SourceAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[SourceAttributeName(value)] = kv.Value; - } - else if (SubjectAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[SubjectAttributeName(value)] = kv.Value; - } - else if (TimeAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[TimeAttributeName(value)] = kv.Value; - } - else if (TypeAttributeName(currentSpecVersion).Equals(kv.Key, StringComparison.InvariantCultureIgnoreCase)) - { - this[TypeAttributeName(value)] = kv.Value; - } - else - { - this[kv.Key] = kv.Value; - } + string newAttributeName = ConvertAttributeName(kv.Key, SpecVersion, newVersion); + newAttributes[newAttributeName] = kv.Value; } } + return newAttributes; + } + + private static string SpecVersionString(CloudEventsSpecVersion version) => + version switch + { + CloudEventsSpecVersion.V0_1 => "0.1", + CloudEventsSpecVersion.V0_2 => "0.2", + CloudEventsSpecVersion.V0_3 => "0.3", + CloudEventsSpecVersion.V1_0 => "1.0", + _ => throw new ArgumentOutOfRangeException($"Unknown spec version: {version}") + }; + + private static string ConvertAttributeName(string name, CloudEventsSpecVersion fromVersion, CloudEventsSpecVersion toVersion) + { + foreach (var method in attributeNameMethods) + { + if (name.Equals(method(fromVersion), StringComparison.InvariantCultureIgnoreCase)) + { + return method(toVersion); + } + } + return name; } public object this[string key] @@ -144,12 +100,14 @@ namespace CloudNative.CloudEvents } set { + // Allow the "setting" of the spec version so long as it doesn't actually modify anything. + if (key.Equals(SpecVersionAttributeName(SpecVersion), StringComparison.InvariantCultureIgnoreCase) && !Equals(dict[key], value)) + { + throw new InvalidOperationException(Strings.ErrorSpecVersionCannotBeModified); + } + if (value is null) { - if (key.Equals(SpecVersionAttributeName(this.SpecVersion), StringComparison.InvariantCultureIgnoreCase)) - { - throw new InvalidOperationException(Strings.ErrorSpecVersionCannotBeCleared); - } dict.Remove(key); return; } @@ -267,7 +225,7 @@ namespace CloudNative.CloudEvents { if (item.Key.Equals(SpecVersionAttributeName(this.SpecVersion), StringComparison.InvariantCultureIgnoreCase)) { - throw new InvalidOperationException(Strings.ErrorSpecVersionCannotBeCleared); + throw new InvalidOperationException(Strings.ErrorSpecVersionCannotBeModified); } return dict.Remove(item); } @@ -276,7 +234,7 @@ namespace CloudNative.CloudEvents { if (key.Equals(SpecVersionAttributeName(this.SpecVersion), StringComparison.InvariantCultureIgnoreCase)) { - throw new InvalidOperationException(Strings.ErrorSpecVersionCannotBeCleared); + throw new InvalidOperationException(Strings.ErrorSpecVersionCannotBeModified); } return dict.Remove(key); } @@ -297,15 +255,6 @@ namespace CloudNative.CloudEvents throw new InvalidOperationException(Strings.ErrorTypeValueIsNotAString); } - else if (key.Equals(SpecVersionAttributeName(this.SpecVersion), StringComparison.InvariantCultureIgnoreCase)) - { - if (value is string) - { - return true; - } - - throw new InvalidOperationException(Strings.ErrorSpecVersionValueIsNotAString); - } else if (key.Equals(IdAttributeName(this.SpecVersion), StringComparison.InvariantCultureIgnoreCase)) { if (value is string) diff --git a/src/CloudNative.CloudEvents/Strings.Designer.cs b/src/CloudNative.CloudEvents/Strings.Designer.cs index 6755593..e2924d3 100644 --- a/src/CloudNative.CloudEvents/Strings.Designer.cs +++ b/src/CloudNative.CloudEvents/Strings.Designer.cs @@ -160,11 +160,11 @@ namespace CloudNative.CloudEvents { } /// - /// Looks up a localized string similar to The 'specversion' attribute cannot be cleared. + /// Looks up a localized string similar to The 'specversion' attribute cannot be modified. /// - internal static string ErrorSpecVersionCannotBeCleared { + internal static string ErrorSpecVersionCannotBeModified { get { - return ResourceManager.GetString("ErrorSpecVersionCannotBeCleared", resourceCulture); + return ResourceManager.GetString("ErrorSpecVersionCannotBeModified", resourceCulture); } } diff --git a/src/CloudNative.CloudEvents/Strings.resx b/src/CloudNative.CloudEvents/Strings.resx index a5ea510..c5fa1e0 100644 --- a/src/CloudNative.CloudEvents/Strings.resx +++ b/src/CloudNative.CloudEvents/Strings.resx @@ -150,8 +150,8 @@ The 'source' attribute value must be a valid absolute or relative URI - - The 'specversion' attribute cannot be cleared + + The 'specversion' attribute cannot be modified The 'specversion' attribute value must be a string diff --git a/test/CloudNative.CloudEvents.UnitTests/ConstructorTest.cs b/test/CloudNative.CloudEvents.UnitTests/ConstructorTest.cs index 93831f4..8a6669d 100644 --- a/test/CloudNative.CloudEvents.UnitTests/ConstructorTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/ConstructorTest.cs @@ -118,7 +118,7 @@ namespace CloudNative.CloudEvents.UnitTests attrs["comexampleextension1"] = "value"; attrs["comexampleextension2"] = new { othervalue = 5 }; - cloudEvent.SpecVersion = CloudEventsSpecVersion.V0_2; + cloudEvent = cloudEvent.WithSpecVersion(CloudEventsSpecVersion.V0_2); Assert.Equal(CloudEventsSpecVersion.V0_2, cloudEvent.SpecVersion); Assert.Equal("com.github.pull.create", cloudEvent.Type); @@ -149,7 +149,7 @@ namespace CloudNative.CloudEvents.UnitTests var attrs = cloudEvent.GetAttributes(); attrs["comexampleextension1"] = "value"; - cloudEvent.SpecVersion = CloudEventsSpecVersion.V1_0; + cloudEvent = cloudEvent.WithSpecVersion(CloudEventsSpecVersion.V1_0); Assert.Equal(CloudEventsSpecVersion.V1_0, cloudEvent.SpecVersion); Assert.Equal("com.github.pull.create", cloudEvent.Type); diff --git a/test/CloudNative.CloudEvents.UnitTests/JsonTest.cs b/test/CloudNative.CloudEvents.UnitTests/JsonTest.cs index 8131c41..9a59f2a 100644 --- a/test/CloudNative.CloudEvents.UnitTests/JsonTest.cs +++ b/test/CloudNative.CloudEvents.UnitTests/JsonTest.cs @@ -78,7 +78,7 @@ namespace CloudNative.CloudEvents.UnitTests { var jsonFormatter = new JsonEventFormatter(); var cloudEvent = jsonFormatter.DecodeStructuredEvent(Encoding.UTF8.GetBytes(jsonv02)); - cloudEvent.SpecVersion = CloudEventsSpecVersion.V0_1; + cloudEvent = cloudEvent.WithSpecVersion(CloudEventsSpecVersion.V0_1); var jsonData = jsonFormatter.EncodeStructuredEvent(cloudEvent, out var contentType); var cloudEvent2 = jsonFormatter.DecodeStructuredEvent(jsonData); @@ -96,7 +96,7 @@ namespace CloudNative.CloudEvents.UnitTests { var jsonFormatter = new JsonEventFormatter(); var cloudEvent = jsonFormatter.DecodeStructuredEvent(Encoding.UTF8.GetBytes(jsonv10)); - cloudEvent.SpecVersion = CloudEventsSpecVersion.V0_2; + cloudEvent = cloudEvent.WithSpecVersion(CloudEventsSpecVersion.V0_2); var jsonData = jsonFormatter.EncodeStructuredEvent(cloudEvent, out var contentType); var cloudEvent2 = jsonFormatter.DecodeStructuredEvent(jsonData);