From 15f6505a580b2bbf8d6b2e89feea10cbd40ab827 Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 19 Jul 2023 07:53:03 -0700 Subject: [PATCH] BREAKING CHANGE: use string instead of enum for `Version` (#561) TypeScript does not consider enum values equivalent, even if the string representation is the same. So, when a module imports `cloudevents` and also has a dependency on `cloudevents` this can cause conflicts where the `CloudEvent.version` attribute is not considered equal when, in fact, it is. Changing the `enum` to a string is pretty straightforward, but should be considered a breaking change since TypeScript dependents will potentially fail the build with a change like this. Signed-off-by: Lance Ball --- src/event/cloudevent.ts | 16 +++++++--------- src/event/spec.ts | 4 ++-- src/index.ts | 7 ++++--- src/message/http/headers.ts | 4 ++-- src/message/http/index.ts | 16 ++++++++-------- test/integration/cloud_event_test.ts | 8 ++++---- test/integration/kafka_tests.ts | 8 ++++---- test/integration/message_test.ts | 18 +++++++++--------- test/integration/mqtt_tests.ts | 10 +++++----- test/integration/sdk_test.ts | 10 +++++----- test/integration/spec_1_tests.ts | 4 ++-- 11 files changed, 52 insertions(+), 53 deletions(-) diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index d3cf281d..4da7075a 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -12,12 +12,10 @@ import { validateCloudEvent } from "./spec"; import { ValidationError, isBinary, asBase64, isValidType, base64AsBinary } from "./validation"; /** - * An enum representing the CloudEvent specification version + * Constants representing the CloudEvent specification version */ -export const enum Version { - V1 = "1.0", - V03 = "0.3", -} +export const V1 = "1.0"; +export const V03 = "0.3"; /** * A CloudEvent describes event data in common formats to provide @@ -28,7 +26,7 @@ export class CloudEvent implements CloudEventV1 { id: string; type: string; source: string; - specversion: Version; + specversion: string; datacontenttype?: string; dataschema?: string; subject?: string; @@ -69,7 +67,7 @@ export class CloudEvent implements CloudEventV1 { this.source = properties.source as string; delete (properties as any).source; - this.specversion = (properties.specversion as Version) || Version.V1; + this.specversion = (properties.specversion) || V1; delete properties.specversion; this.datacontenttype = properties.datacontenttype; @@ -103,9 +101,9 @@ export class CloudEvent implements CloudEventV1 { delete properties.data; // sanity checking - if (this.specversion === Version.V1 && this.schemaurl) { + if (this.specversion === V1 && this.schemaurl) { throw new TypeError("cannot set schemaurl on version 1.0 event"); - } else if (this.specversion === Version.V03 && this.dataschema) { + } else if (this.specversion === V03 && this.dataschema) { throw new TypeError("cannot set dataschema on version 0.3 event"); } diff --git a/src/event/spec.ts b/src/event/spec.ts index bfab4f3d..042da8ed 100644 --- a/src/event/spec.ts +++ b/src/event/spec.ts @@ -6,12 +6,12 @@ import { ValidationError } from "./validation"; import { CloudEventV1 } from "./interfaces"; -import { Version } from "./cloudevent"; +import { V1 } from "./cloudevent"; import validate from "../schema/v1"; export function validateCloudEvent(event: CloudEventV1): boolean { - if (event.specversion === Version.V1) { + if (event.specversion === V1) { if (!validate(event)) { throw new ValidationError("invalid payload", (validate as any).errors); } diff --git a/src/index.ts b/src/index.ts index e09e9d1a..ab0148db 100644 --- a/src/index.ts +++ b/src/index.ts @@ -3,13 +3,13 @@ SPDX-License-Identifier: Apache-2.0 */ -import { CloudEvent, Version } from "./event/cloudevent"; +import { CloudEvent, V1, V03 } from "./event/cloudevent"; import { ValidationError } from "./event/validation"; import { CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; import { Options, TransportFunction, EmitterFunction, emitterFor, Emitter } from "./transport/emitter"; import { httpTransport } from "./transport/http"; -import { +import { Headers, Mode, Binding, HTTP, Kafka, KafkaEvent, KafkaMessage, Message, MQTT, MQTTMessage, MQTTMessageFactory, Serializer, Deserializer } from "./message"; @@ -18,7 +18,8 @@ import CONSTANTS from "./constants"; export { // From event CloudEvent, - Version, + V1, + V03, ValidationError, Mode, HTTP, diff --git a/src/message/http/headers.ts b/src/message/http/headers.ts index 770832f3..b0cc519b 100644 --- a/src/message/http/headers.ts +++ b/src/message/http/headers.ts @@ -6,7 +6,7 @@ import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; import { CloudEventV1 } from "../.."; import { Headers } from "../"; -import { Version } from "../../event/cloudevent"; +import { V1 } from "../../event/cloudevent"; import CONSTANTS from "../../constants"; export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM]; @@ -27,7 +27,7 @@ export const requiredHeaders = [ export function headersFor(event: CloudEventV1): Headers { const headers: Headers = {}; let headerMap: Readonly<{ [key: string]: MappedParser }>; - if (event.specversion === Version.V1) { + if (event.specversion === V1) { headerMap = v1headerMap; } else { headerMap = v03headerMap; diff --git a/src/message/http/index.ts b/src/message/http/index.ts index e4cd7387..cdba2629 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -5,7 +5,7 @@ import { types } from "util"; -import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; +import { CloudEvent, CloudEventV1, CONSTANTS, Mode, V1, V03 } from "../.."; import { Message, Headers, Binding } from ".."; import { @@ -147,7 +147,7 @@ function getVersion(mode: Mode, headers: Headers, body: string | Record).specversion; } } - return Version.V1; + return V1; } /** @@ -155,11 +155,11 @@ function getVersion(mode: Mode, headers: Headers, body: string | Record(message: Message, version: Version): CloudEvent { +function parseBinary(message: Message, version: string): CloudEvent { const headers = { ...message.headers }; let body = message.body; @@ -169,7 +169,7 @@ function parseBinary(message: Message, version: Version): CloudEvent { const sanitizedHeaders = sanitize(headers); const eventObj: { [key: string]: unknown | string | Record } = {}; - const parserMap: Record = version === Version.V03 ? v03binaryParsers : v1binaryParsers; + const parserMap: Record = version === V03 ? v03binaryParsers : v1binaryParsers; for (const header in parserMap) { if (sanitizedHeaders[header]) { @@ -206,11 +206,11 @@ function parseBinary(message: Message, version: Version): CloudEvent { * Creates a new CloudEvent instance based on the provided payload and headers. * * @param {Message} message the incoming Message - * @param {Version} version the spec version of this message (v1 or v03) + * @param {string} version the spec version of this message (v1 or v03) * @returns {CloudEvent} a new CloudEvent instance for the provided headers and payload * @throws {ValidationError} if the payload and header combination do not conform to the spec */ -function parseStructured(message: Message, version: Version): CloudEvent { +function parseStructured(message: Message, version: string): CloudEvent { const payload = message.body; const headers = message.headers; @@ -227,7 +227,7 @@ function parseStructured(message: Message, version: Version): CloudEvent { const incoming = { ...(parser.parse(payload as string) as Record) }; const eventObj: { [key: string]: unknown } = {}; - const parserMap: Record = version === Version.V03 ? v03structuredParsers : v1structuredParsers; + const parserMap: Record = version === V03 ? v03structuredParsers : v1structuredParsers; for (const key in parserMap) { const property = incoming[key]; diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index 829afff8..f45a6f68 100644 --- a/test/integration/cloud_event_test.ts +++ b/test/integration/cloud_event_test.ts @@ -7,7 +7,7 @@ import path from "path"; import fs from "fs"; import { expect } from "chai"; -import { CloudEvent, CloudEventV1, ValidationError, Version } from "../../src"; +import { CloudEvent, CloudEventV1, ValidationError, V1 } from "../../src"; import { asBase64 } from "../../src/event/validation"; const type = "org.cncf.cloudevents.example"; @@ -16,7 +16,7 @@ const id = "b46cf653-d48a-4b90-8dfa-355c01061361"; const fixture = Object.freeze({ id, - specversion: Version.V1, + specversion: V1, source, type, data: `"some data"` @@ -165,7 +165,7 @@ describe("A 1.0 CloudEvent", () => { }); it("can be constructed with an ID", () => { - const ce = new CloudEvent({ id: "1234", specversion: Version.V1, source, type }); + const ce = new CloudEvent({ id: "1234", specversion: V1, source, type }); expect(ce.id).to.equal("1234"); }); @@ -280,7 +280,7 @@ describe("A 1.0 CloudEvent", () => { const obj = JSON.parse(json as string); expect(obj.type).to.equal(type); expect(obj.source).to.equal(source); - expect(obj.specversion).to.equal(Version.V1); + expect(obj.specversion).to.equal(V1); }); it("throws if the provded source is empty string", () => { diff --git a/test/integration/kafka_tests.ts b/test/integration/kafka_tests.ts index ac0e0c47..cb858f49 100644 --- a/test/integration/kafka_tests.ts +++ b/test/integration/kafka_tests.ts @@ -7,7 +7,7 @@ import path from "path"; import fs from "fs"; import { expect } from "chai"; -import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { CloudEvent, CONSTANTS, V1 } from "../../src"; import { asBase64 } from "../../src/event/validation"; import { Message, Kafka, KafkaMessage, KafkaEvent } from "../../src/message"; import { KAFKA_CE_HEADERS } from "../../src/message/kafka/headers"; @@ -43,7 +43,7 @@ const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test const image_base64 = asBase64(imageData); const fixture = new CloudEvent({ - specversion: Version.V1, + specversion: V1, id, type, source, @@ -233,7 +233,7 @@ describe("Kafka transport", () => { expect(message.body).to.equal(data); // validate all headers expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); - expect(message.headers[KAFKA_CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); + expect(message.headers[KAFKA_CE_HEADERS.SPEC_VERSION]).to.equal(V1); expect(message.headers[KAFKA_CE_HEADERS.ID]).to.equal(id); expect(message.headers[KAFKA_CE_HEADERS.TYPE]).to.equal(type); expect(message.headers[KAFKA_CE_HEADERS.SOURCE]).to.equal(source); @@ -249,7 +249,7 @@ describe("Kafka transport", () => { expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); // Parse the message body as JSON, then validate the attributes const body = JSON.parse(message.body as string); - expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1); expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 83502d4f..93699c61 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -8,7 +8,7 @@ import fs from "fs"; import { expect } from "chai"; import { IncomingHttpHeaders } from "http"; -import { CloudEvent, CONSTANTS, Version } from "../../src"; +import { CloudEvent, CONSTANTS, V1, V03 } from "../../src"; import { asBase64 } from "../../src/event/validation"; import { Message, HTTP } from "../../src/message"; @@ -154,7 +154,7 @@ describe("HTTP transport", () => { [CONSTANTS.CE_HEADERS.ID]: "1234", [CONSTANTS.CE_HEADERS.SOURCE]: "test", [CONSTANTS.CE_HEADERS.TYPE]: "test.event", - [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, + [CONSTANTS.CE_HEADERS.SPEC_VERSION]: V1, "ce-LUNCH": "tacos", }, }; @@ -237,7 +237,7 @@ describe("HTTP transport", () => { id, type, source, - specversion: Version.V1, + specversion: V1, data: { lunch: "tacos" }, }); const message: Message = { @@ -250,7 +250,7 @@ describe("HTTP transport", () => { describe("Specification version V1", () => { const fixture = new CloudEvent({ - specversion: Version.V1, + specversion: V1, id, type, source, @@ -268,7 +268,7 @@ describe("HTTP transport", () => { expect(message.body).to.equal(JSON.stringify(data)); // validate all headers expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); - expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V1); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(V1); expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); @@ -284,7 +284,7 @@ describe("HTTP transport", () => { expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); // Parse the message body as JSON, then validate the attributes const body = JSON.parse(message.body as string); - expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1); expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); @@ -353,7 +353,7 @@ describe("HTTP transport", () => { describe("Specification version V03", () => { const fixture = new CloudEvent({ - specversion: Version.V03, + specversion: V03, id, type, source, @@ -371,7 +371,7 @@ describe("HTTP transport", () => { expect(message.body).to.equal(JSON.stringify(data)); // validate all headers expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(datacontenttype); - expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(Version.V03); + expect(message.headers[CONSTANTS.CE_HEADERS.SPEC_VERSION]).to.equal(V03); expect(message.headers[CONSTANTS.CE_HEADERS.ID]).to.equal(id); expect(message.headers[CONSTANTS.CE_HEADERS.TYPE]).to.equal(type); expect(message.headers[CONSTANTS.CE_HEADERS.SOURCE]).to.equal(source); @@ -387,7 +387,7 @@ describe("HTTP transport", () => { expect(message.headers[CONSTANTS.HEADER_CONTENT_TYPE]).to.equal(CONSTANTS.DEFAULT_CE_CONTENT_TYPE); // Parse the message body as JSON, then validate the attributes const body = JSON.parse(message.body as string); - expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V03); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V03); expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); diff --git a/test/integration/mqtt_tests.ts b/test/integration/mqtt_tests.ts index 62d9a8bb..bd4b73fe 100644 --- a/test/integration/mqtt_tests.ts +++ b/test/integration/mqtt_tests.ts @@ -7,7 +7,7 @@ import path from "path"; import fs from "fs"; import { expect } from "chai"; -import { CloudEvent, CONSTANTS, Version, Headers } from "../../src"; +import { CloudEvent, CONSTANTS, V1, Headers } from "../../src"; import { asBase64 } from "../../src/event/validation"; import { Message, MQTT, MQTTMessage } from "../../src/message"; @@ -43,7 +43,7 @@ const image_base64 = asBase64(imageData); const PUBLISH = {"Content Type": "application/json; charset=utf-8"}; const fixture = new CloudEvent({ - specversion: Version.V1, + specversion: V1, id, type, source, @@ -216,7 +216,7 @@ describe("MQTT transport", () => { expect(message.body).to.equal(data); // validate all headers expect(message.headers.datacontenttype).to.equal(datacontenttype); - expect(message.headers.specversion).to.equal(Version.V1); + expect(message.headers.specversion).to.equal(V1); expect(message.headers.id).to.equal(id); expect(message.headers.type).to.equal(type); expect(message.headers.source).to.equal(source); @@ -232,7 +232,7 @@ describe("MQTT transport", () => { expect(message.body).to.equal(data); // validate all headers expect(message["User Properties"]?.datacontenttype).to.equal(datacontenttype); - expect(message["User Properties"]?.specversion).to.equal(Version.V1); + expect(message["User Properties"]?.specversion).to.equal(V1); expect(message["User Properties"]?.id).to.equal(id); expect(message["User Properties"]?.type).to.equal(type); expect(message["User Properties"]?.source).to.equal(source); @@ -249,7 +249,7 @@ describe("MQTT transport", () => { expect(message.body).to.deep.equal(message.payload); expect(message.payload).to.deep.equal(fixture.toJSON()); const body = message.body as Record; - expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(Version.V1); + expect(body[CONSTANTS.CE_ATTRIBUTES.SPEC_VERSION]).to.equal(V1); expect(body[CONSTANTS.CE_ATTRIBUTES.ID]).to.equal(id); expect(body[CONSTANTS.CE_ATTRIBUTES.TYPE]).to.equal(type); expect(body[CONSTANTS.CE_ATTRIBUTES.SOURCE]).to.equal(source); diff --git a/test/integration/sdk_test.ts b/test/integration/sdk_test.ts index 04483c07..de853648 100644 --- a/test/integration/sdk_test.ts +++ b/test/integration/sdk_test.ts @@ -5,13 +5,13 @@ import "mocha"; import { expect } from "chai"; -import { CloudEvent, CloudEventV1, Version } from "../../src"; +import { CloudEvent, CloudEventV1, V1, V03 } from "../../src"; const fixture: CloudEventV1 = { id: "123", type: "org.cloudevents.test", source: "http://cloudevents.io", - specversion: Version.V1, + specversion: V1, }; describe("The SDK Requirements", () => { @@ -25,15 +25,15 @@ describe("The SDK Requirements", () => { expect( new CloudEvent({ ...fixture, - specversion: Version.V03, + specversion: V03, }, false).specversion, - ).to.equal(Version.V03); + ).to.equal(V03); }); }); describe("v1.0", () => { it("should create an event using the right spec version", () => { - expect(new CloudEvent(fixture).specversion).to.equal(Version.V1); + expect(new CloudEvent(fixture).specversion).to.equal(V1); }); }); diff --git a/test/integration/spec_1_tests.ts b/test/integration/spec_1_tests.ts index af899223..7bd3b919 100644 --- a/test/integration/spec_1_tests.ts +++ b/test/integration/spec_1_tests.ts @@ -5,7 +5,7 @@ import "mocha"; import { expect } from "chai"; -import { CloudEvent, Version, ValidationError } from "../../src"; +import { CloudEvent, V1, ValidationError } from "../../src"; import { asBase64 } from "../../src/event/validation"; import Constants from "../../src/constants"; @@ -20,7 +20,7 @@ const data = { const subject = "subject-x0"; const cloudevent = new CloudEvent({ - specversion: Version.V1, + specversion: V1, id, source, type,