From 87e15241dec9240c8d4cc3deed07ffbf2477df16 Mon Sep 17 00:00:00 2001 From: Jon Skeet Date: Thu, 28 Jan 2021 10:15:14 +0000 Subject: [PATCH] Make the spec version of an event immutable This commit includes two WithSpecVersion methods, one in CloudEvent and the other in CloudEventAttributes, which convert from one version to another. These are currently internal, but we can expose them later if we wish. Fixes #65 Fixes #66 Creating this commit has raised more issues to discuss: - CloudEventSpecVersion.Default is a public enum value; changing that later would be a breaking change in difficult-to-document ways. - In general, CloudEventSpecVersion feels like it deserves to be a class with well-known specific instances, rather than an enum. That would make various things much simpler. - Given that attributes other than data have a limited set of types, I suspect it's worth having a CloudEventAttribute type encapsulating that. Signed-off-by: Jon Skeet --- src/CloudNative.CloudEvents/CloudEvent.cs | 17 +- .../CloudEventAttributes.cs | 161 ++++++------------ .../Strings.Designer.cs | 6 +- src/CloudNative.CloudEvents/Strings.resx | 4 +- .../ConstructorTest.cs | 4 +- .../JsonTest.cs | 4 +- 6 files changed, 75 insertions(+), 121 deletions(-) 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);