diff --git a/.eslintrc b/.eslintrc index 6d472328..d3697827 100644 --- a/.eslintrc +++ b/.eslintrc @@ -14,6 +14,9 @@ "node": true, "mocha": true }, + "plugins": [ + "header" + ], "rules": { "no-var": "error", "standard/no-callback-literal": "off", @@ -31,6 +34,7 @@ "valid-jsdoc": "warn", "semi": ["error", "always"], "quotes": ["error", "double", { "allowTemplateLiterals": true }], - "@typescript-eslint/no-explicit-any": "off" + "@typescript-eslint/no-explicit-any": "off", + "header/header": [2, "block", ["", " Copyright 2021 The CloudEvents Authors"," SPDX-License-Identifier: Apache-2.0", ""], 2] } } diff --git a/CHANGELOG.md b/CHANGELOG.md index 6d45d2dc..ac015487 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,22 @@ All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. +### [4.0.3](https://www.github.com/cloudevents/sdk-javascript/compare/v4.0.2...v4.0.3) (2021-07-06) + + +### Bug Fixes + +* do not modify incoming event's specversion ([#419](https://www.github.com/cloudevents/sdk-javascript/issues/419)) ([22e42dd](https://www.github.com/cloudevents/sdk-javascript/commit/22e42ddb80d21058a74219a1c24b409c245f030f)) +* do not modify incoming event's specversion ([#419](https://www.github.com/cloudevents/sdk-javascript/issues/419)) ([7c05ade](https://www.github.com/cloudevents/sdk-javascript/commit/7c05adee7b3d5d56ff5602f044a9581534ab8957)) +* throw on validation if extensions are improperly named ([#420](https://www.github.com/cloudevents/sdk-javascript/issues/420)) ([7f6b658](https://www.github.com/cloudevents/sdk-javascript/commit/7f6b658858533bfbc33edbec30d79099aeb0d021)) + + +### Miscellaneous + +* add copyrights header and lint rules ([#418](https://www.github.com/cloudevents/sdk-javascript/issues/418)) ([80d987c](https://www.github.com/cloudevents/sdk-javascript/commit/80d987c1f6046efb5e0c89b0472d653ccd35ee2c)) +* add Lance Ball to maintainers in package.json ([#411](https://www.github.com/cloudevents/sdk-javascript/issues/411)) ([d68b85a](https://www.github.com/cloudevents/sdk-javascript/commit/d68b85a2278e46e0f5dac44b561cfcb1dd8b5404)) +* be more forgiving parsing JSON as a string ([#417](https://www.github.com/cloudevents/sdk-javascript/issues/417)) ([db4be6b](https://www.github.com/cloudevents/sdk-javascript/commit/db4be6b1da479f27903efc6694d06f7cc8b054e2)) + ### [4.0.2](https://www.github.com/cloudevents/sdk-javascript/compare/v4.0.1...v4.0.2) (2021-04-21) diff --git a/cucumber.js b/cucumber.js index abde5e23..5f59d71f 100644 --- a/cucumber.js +++ b/cucumber.js @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + // cucumber.js let common = [ "--require-module ts-node/register", // Load TypeScript module diff --git a/package-lock.json b/package-lock.json index 3707f285..e875933f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,6 +1,6 @@ { "name": "cloudevents", - "version": "4.0.2", + "version": "4.0.3", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -637,15 +637,6 @@ "ajv": "*" } }, - "@types/axios": { - "version": "0.14.0", - "resolved": "https://registry.npmjs.org/@types/axios/-/axios-0.14.0.tgz", - "integrity": "sha1-7CMA++fX3d1+udOr+HmZlkyvzkY=", - "dev": true, - "requires": { - "axios": "*" - } - }, "@types/cacheable-request": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/@types/cacheable-request/-/cacheable-request-6.0.1.tgz", @@ -1650,16 +1641,42 @@ "dev": true }, "browserslist": { - "version": "4.14.7", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.14.7.tgz", - "integrity": "sha512-BSVRLCeG3Xt/j/1cCGj1019Wbty0H+Yvu2AOuZSuoaUWn3RatbL33Cxk+Q4jRMRAbOm0p7SLravLjpnT6s0vzQ==", + "version": "4.16.6", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.16.6.tgz", + "integrity": "sha512-Wspk/PqO+4W9qp5iUTJsa1B/QrYn1keNCcEP5OvP7WBwT4KaDly0uONYmC6Xa3Z5IqnUgS0KcgLYu1l74x0ZXQ==", "dev": true, "requires": { - "caniuse-lite": "^1.0.30001157", - "colorette": "^1.2.1", - "electron-to-chromium": "^1.3.591", + "caniuse-lite": "^1.0.30001219", + "colorette": "^1.2.2", + "electron-to-chromium": "^1.3.723", "escalade": "^3.1.1", - "node-releases": "^1.1.66" + "node-releases": "^1.1.71" + }, + "dependencies": { + "caniuse-lite": { + "version": "1.0.30001230", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001230.tgz", + "integrity": "sha512-5yBd5nWCBS+jWKTcHOzXwo5xzcj4ePE/yjtkZyUV1BTUmrBaA9MRGC+e7mxnqXSA90CmCA8L3eKLaSUkt099IQ==", + "dev": true + }, + "colorette": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-1.2.2.tgz", + "integrity": "sha512-MKGMzyfeuutC/ZJ1cba9NqcNpfeqMUcYmyF1ZFY6/Cn7CNSAKx6a+s48sqLqyAiZuaP2TcqMhoo+dlwFnVxT9w==", + "dev": true + }, + "electron-to-chromium": { + "version": "1.3.739", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.739.tgz", + "integrity": "sha512-+LPJVRsN7hGZ9EIUUiWCpO7l4E3qBYHNadazlucBfsXBbccDFNKUBAgzE68FnkWGJPwD/AfKhSzL+G+Iqb8A4A==", + "dev": true + }, + "node-releases": { + "version": "1.1.72", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.72.tgz", + "integrity": "sha512-LLUo+PpH3dU6XizX3iVoubUNheF/owjXCZZ5yACDxNnPtgFuludV1ZL3ayK1kVep42Rmm0+R9/Y60NQbZ2bifw==", + "dev": true + } } }, "buffer": { @@ -1815,12 +1832,6 @@ "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", "dev": true }, - "caniuse-lite": { - "version": "1.0.30001158", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001158.tgz", - "integrity": "sha512-s5loVYY+yKpuVA3HyW8BarzrtJvwHReuzugQXlv1iR3LKSReoFXRm86mT6hT7PEF5RxW+XQZg+6nYjlywYzQ+g==", - "dev": true - }, "capture-stack-trace": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/capture-stack-trace/-/capture-stack-trace-1.0.1.tgz", @@ -2888,12 +2899,6 @@ "integrity": "sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==", "dev": true }, - "electron-to-chromium": { - "version": "1.3.598", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.3.598.tgz", - "integrity": "sha512-G5Ztk23/ubLYVPxPXnB1uu105uzIPd4xB/D8ld8x1GaSC9+vU9NZL16nYZya8H77/7CCKKN7dArzJL3pBs8N7A==", - "dev": true - }, "emoji-regex": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-7.0.3.tgz", @@ -3194,6 +3199,12 @@ "regexpp": "^3.0.0" } }, + "eslint-plugin-header": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-header/-/eslint-plugin-header-3.1.1.tgz", + "integrity": "sha512-9vlKxuJ4qf793CmeeSrZUvVClw6amtpghq3CuWcB5cUNnWHQhgcqy5eF8oVKFk1G3Y/CbchGfEaw3wiIJaNmVg==", + "dev": true + }, "eslint-plugin-import": { "version": "2.22.1", "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.22.1.tgz", @@ -4531,9 +4542,9 @@ "dev": true }, "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { "minimist": "^1.2.5", @@ -4689,9 +4700,9 @@ } }, "hosted-git-info": { - "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==", "dev": true }, "html-escaper": { @@ -5913,9 +5924,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==", + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "dev": true }, "lodash.flattendeep": { @@ -6539,12 +6550,6 @@ "process-on-spawn": "^1.0.0" } }, - "node-releases": { - "version": "1.1.67", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-1.1.67.tgz", - "integrity": "sha512-V5QF9noGFl3EymEwUYzO+3NTDpGfQB4ve6Qfnzf3UNydMhjQRVPR1DZTuvWiLzaFJYw2fmDwAfnRNEVb64hSIg==", - "dev": true - }, "normalize-package-data": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz", @@ -8152,9 +8157,9 @@ "dev": true }, "set-getter": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.0.tgz", - "integrity": "sha1-12nBgsnVpR9AkUXy+6guXoboA3Y=", + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/set-getter/-/set-getter-0.1.1.tgz", + "integrity": "sha512-9sVWOy+gthr+0G9DzqqLaYNA7+5OKkSmcqjL9cBpDEaZrr3ShQlyX2cZ/O/ozE41oxn/Tt0LGEM/w4Rub3A3gw==", "dev": true, "requires": { "to-object-path": "^0.3.0" @@ -9376,9 +9381,9 @@ } }, "underscore": { - "version": "1.11.0", - "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.11.0.tgz", - "integrity": "sha512-xY96SsN3NA461qIRKZ/+qox37YXPtSBswMGfiNptr+wrt6ds4HaMw23TP612fEyGekRE6LNRiLYr/aqbHXNedw==", + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/underscore/-/underscore-1.13.1.tgz", + "integrity": "sha512-hzSoAVtJF+3ZtiFX0VgfFPHEDRm7Y/QPjGyNo4TVdnDTdft3tr8hEkD25a1jC+TjTuE7tkHGKkhwCgs9dgBB2g==", "dev": true }, "unified": { diff --git a/package.json b/package.json index f4924aa3..1ed53f4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "cloudevents", - "version": "4.0.2", + "version": "4.0.3", "description": "CloudEvents SDK for JavaScript", "main": "dist/index.js", "scripts": { @@ -39,6 +39,65 @@ "name": "Fábio José de Moraes", "email": "fabiojose@gmail.com", "url": "https://github.com/fabiojose" + }, + { + "name": "Lance Ball", + "email": "lball@redhat.com", + "url": "https://github.com/lance" + }, + { + "name": "Lucas Holmquist", + "email": "lholmqui@redhat.com", + "url": "https://github.com/lholmquist" + }, + { + "name": "Grant Timmerman", + "url": "https://github.com/grant" + }, + { + "name": "Daniel Bevenius", + "email": "daniel.bevenius@gmail.com", + "url": "https://github.com/danbev" + }, + { + "name": "Helio Frota", + "url": "https://github.com/helio-frota" + }, + { + "name": "Doug Davis", + "email": "dug@us.ibm.com", + "url": "https://github.com/duglin" + }, + { + "name": "Remi Cattiau", + "email": "rcattiau@gmail.com", + "url": "https://github.com/loopingz" + }, + { + "name": "Michele Angioni", + "url": "https://github.com/micheleangioni" + }, + { + "name": "Ali Ok", + "email": "aliok@redhat.com", + "url": "https://github.com/aliok" + }, + { + "name": "Philip Hayes", + "url": "https://github.com/deewhyweb" + }, + { + "name": "Jingwen Peng", + "url": "https://github.com/pengsrc" + }, + { + "name": "Sidharth Vinod", + "email": "sidharthv96@gmail.com", + "url": "https://github.com/sidharthv96" + }, + { + "name": "Matej Vasek", + "url": "https://github.com/matejvasek" } ], "license": "Apache-2.0", @@ -70,6 +129,7 @@ "eslint": "^7.3.0", "eslint-config-prettier": "^6.11.0", "eslint-config-standard": "^14.1.1", + "eslint-plugin-header": "^3.1.1", "eslint-plugin-import": "^2.20.2", "eslint-plugin-node": "^11.1.0", "eslint-plugin-prettier": "^3.1.4", diff --git a/src/constants.ts b/src/constants.ts index 30d1dbe5..93e9f5f8 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + const CONSTANTS = Object.freeze({ CHARSET_DEFAULT: "utf-8", EXTENSIONS_PREFIX: "ce-", diff --git a/src/event/cloudevent.ts b/src/event/cloudevent.ts index 5df0b9c4..4fa91fa5 100644 --- a/src/event/cloudevent.ts +++ b/src/event/cloudevent.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { v4 as uuidv4 } from "uuid"; import { Emitter } from ".."; diff --git a/src/event/interfaces.ts b/src/event/interfaces.ts index bfc3a0e9..8fa02010 100644 --- a/src/event/interfaces.ts +++ b/src/event/interfaces.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + /** * The object interface for CloudEvents 1.0. * @see https://github.com/cloudevents/spec/blob/v1.0/spec.md diff --git a/src/event/schemas.ts b/src/event/schemas.ts index c27e44bf..4b7bd1d4 100644 --- a/src/event/schemas.ts +++ b/src/event/schemas.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + export const schemaV1 = { $ref: "#/definitions/event", definitions: { diff --git a/src/event/spec.ts b/src/event/spec.ts index 3c3abc31..1d308bb3 100644 --- a/src/event/spec.ts +++ b/src/event/spec.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import Ajv from "ajv"; import { ValidationError, isBase64 } from "./validation"; @@ -23,14 +28,21 @@ export function validateCloudEvent(event: CloudEventV03 | CloudEventV1): boolean if (!isValidAgainstSchemaV1(event)) { throw new ValidationError("invalid payload", isValidAgainstSchemaV1.errors); } - return true; } else if (event.specversion === Version.V03) { if (!isValidAgainstSchemaV03(event)) { throw new ValidationError("invalid payload", isValidAgainstSchemaV03.errors); } - return checkDataContentEncoding(event); + checkDataContentEncoding(event); + } else { + return false; + } + // attribute names must all be lowercase + for (const key in event) { + if (key !== key.toLowerCase()) { + throw new ValidationError(`invalid attribute name: ${key}`); + } } - return false; + return true; } function checkDataContentEncoding(event: CloudEventV03): boolean { diff --git a/src/event/validation.ts b/src/event/validation.ts index 645df308..6279cce1 100644 --- a/src/event/validation.ts +++ b/src/event/validation.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { ErrorObject } from "ajv"; /** @@ -14,7 +19,7 @@ export class ValidationError extends TypeError { // @ts-ignore errors?.reduce( (accum: string, err: Record) => - (accum as string).concat(` + accum.concat(` ${err instanceof Object ? JSON.stringify(err) : err}`), message, ) diff --git a/src/index.ts b/src/index.ts index b3ae0a48..5257f6dc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { CloudEvent, Version } from "./event/cloudevent"; import { ValidationError } from "./event/validation"; import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces"; diff --git a/src/message/http/headers.ts b/src/message/http/headers.ts index 8e2f7fee..e4becc3a 100644 --- a/src/message/http/headers.ts +++ b/src/message/http/headers.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { PassThroughParser, DateParser, MappedParser } from "../../parsers"; import { CloudEvent } from "../.."; import { Headers } from "../"; diff --git a/src/message/http/index.ts b/src/message/http/index.ts index f1626607..3712fa44 100644 --- a/src/message/http/index.ts +++ b/src/message/http/index.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { CloudEvent, CloudEventV03, CloudEventV1, CONSTANTS, Mode, Version } from "../.."; import { Message, Headers } from ".."; @@ -12,7 +17,14 @@ import { import { isStringOrObjectOrThrow, ValidationError } from "../../event/validation"; import { JSONParser, MappedParser, Parser, parserByContentType } from "../../parsers"; -// implements Serializer +/** + * Serialize a CloudEvent for HTTP transport in binary mode + * @implements {Serializer} + * @see https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#31-binary-content-mode + * + * @param {CloudEvent} event The event to serialize + * @returns {Message} a Message object with headers and body + */ export function binary(event: CloudEvent): Message { const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE }; const headers: Headers = { ...contentType, ...headersFor(event) }; @@ -27,7 +39,14 @@ export function binary(event: CloudEvent): Message { }; } -// implements Serializer +/** + * Serialize a CloudEvent for HTTP transport in structured mode + * @implements {Serializer} + * @see https://github.com/cloudevents/spec/blob/v1.0.1/http-protocol-binding.md#32-structured-content-mode + * + * @param {CloudEvent} event the CloudEvent to be serialized + * @returns {Message} a Message object with headers and body + */ export function structured(event: CloudEvent): Message { if (event.data_base64) { // The event's data is binary - delete it @@ -41,9 +60,15 @@ export function structured(event: CloudEvent): Message { }; } -// implements Detector -// TODO: this could probably be optimized +/** + * Determine if a Message is a CloudEvent + * @implements {Detector} + * + * @param {Message} message an incoming Message object + * @returns {boolean} true if this Message is a CloudEvent + */ export function isEvent(message: Message): boolean { + // TODO: this could probably be optimized try { deserialize(message); return true; @@ -54,6 +79,7 @@ export function isEvent(message: Message): boolean { /** * Converts a Message to a CloudEvent + * @implements {Deserializer} * * @param {Message} message the incoming message * @return {CloudEvent} A new {CloudEvent} instance @@ -61,11 +87,8 @@ export function isEvent(message: Message): boolean { export function deserialize(message: Message): CloudEvent { const cleanHeaders: Headers = sanitize(message.headers); const mode: Mode = getMode(cleanHeaders); - let version = getVersion(mode, cleanHeaders, message.body); - if (version !== Version.V03 && version !== Version.V1) { - console.error(`Unknown spec version ${version}. Default to ${Version.V1}`); - version = Version.V1; - } + const version = getVersion(mode, cleanHeaders, message.body); + switch (mode) { case Mode.BINARY: return parseBinary(message, version); @@ -125,7 +148,7 @@ function getVersion(mode: Mode, headers: Headers, body: string | Record } = {}; - const parserMap: Record = version === Version.V1 ? v1binaryParsers : v03binaryParsers; + const parserMap: Record = version === Version.V03 ? v03binaryParsers : v1binaryParsers; for (const header in parserMap) { if (sanitizedHeaders[header]) { const mappedParser: MappedParser = parserMap[header]; eventObj[mappedParser.name] = mappedParser.parser.parse(sanitizedHeaders[header]); delete sanitizedHeaders[header]; + delete headers[header]; } } // Every unprocessed header can be an extension - for (const header in sanitizedHeaders) { + for (const header in headers) { if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) { eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = headers[header]; } @@ -191,13 +215,13 @@ 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.V1 ? v1structuredParsers : v03structuredParsers; + const parserMap: Record = version === Version.V03 ? v03structuredParsers : v1structuredParsers; for (const key in parserMap) { const property = incoming[key]; if (property) { - const parser: MappedParser = parserMap[key]; - eventObj[parser.name] = parser.parser.parse(property as string); + const mappedParser: MappedParser = parserMap[key]; + eventObj[mappedParser.name] = mappedParser.parser.parse(property as string); } delete incoming[key]; } diff --git a/src/message/index.ts b/src/message/index.ts index 2d05f486..6365d299 100644 --- a/src/message/index.ts +++ b/src/message/index.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { IncomingHttpHeaders } from "http"; import { CloudEvent } from ".."; import { binary, deserialize, structured, isEvent } from "./http"; @@ -6,6 +11,12 @@ import { binary, deserialize, structured, isEvent } from "./http"; * Binding is an interface for transport protocols to implement, * which provides functions for sending CloudEvent Messages over * the wire. + * @interface + * + * @property {@link Serializer} `binary` - converts a CloudEvent into a Message in binary mode + * @property {@link Serializer} `structured` - converts a CloudEvent into a Message in structured mode + * @property {@link Deserializer} `toEvent` - converts a Message into a CloudEvent + * @property {@link Detector} `isEvent` - determines if a Message can be converted to a CloudEvent */ export interface Binding { binary: Serializer; @@ -17,6 +28,7 @@ export interface Binding { /** * Headers is an interface representing transport-agnostic headers as * key/value string pairs + * @interface */ export interface Headers extends IncomingHttpHeaders { [key: string]: string | string[] | undefined; @@ -25,6 +37,9 @@ export interface Headers extends IncomingHttpHeaders { /** * Message is an interface representing a CloudEvent as a * transport-agnostic message + * @interface + * @property {@linkcode Headers} `headers` - the headers for the event Message + * @property string `body` - the body of the event Message */ export interface Message { headers: Headers; @@ -33,6 +48,7 @@ export interface Message { /** * An enum representing the two transport modes, binary and structured + * @interface */ export enum Mode { BINARY = "binary", @@ -42,6 +58,7 @@ export enum Mode { /** * Serializer is an interface for functions that can convert a * CloudEvent into a Message. + * @interface */ export interface Serializer { (event: CloudEvent): Message; @@ -50,6 +67,7 @@ export interface Serializer { /** * Deserializer is a function interface that converts a * Message to a CloudEvent + * @interface */ export interface Deserializer { (message: Message): CloudEvent; @@ -58,12 +76,16 @@ export interface Deserializer { /** * Detector is a function interface that detects whether * a message contains a valid CloudEvent + * @interface */ export interface Detector { (message: Message): boolean; } -// HTTP Message capabilities +/** + * Bindings for HTTP transport support + * @implements {@linkcode Binding} + */ export const HTTP: Binding = { binary: binary as Serializer, structured: structured as Serializer, diff --git a/src/parsers.ts b/src/parsers.ts index 37890e00..9af91039 100644 --- a/src/parsers.ts +++ b/src/parsers.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import CONSTANTS from "./constants"; import { isString, isDefinedOrThrow, isStringOrObjectOrThrow, ValidationError } from "./event/validation"; @@ -17,6 +22,14 @@ export class JSONParser implements Parser { * @return {object} the parsed JSON payload. */ parse(payload: Record | string): string { + if (typeof payload === "string") { + // This is kind of a hack, but the payload data could be JSON in the form of a single + // string, such as "some data". But without the quotes in the string, JSON.parse blows + // up. We can check for this scenario and add quotes. Not sure if this is ideal. + if (!/^[[|{|"]/.test(payload)) { + payload = `"${payload}"`; + } + } if (this.decorator) { payload = this.decorator.parse(payload); } diff --git a/src/transport/emitter.ts b/src/transport/emitter.ts index 2dc132e6..c5002cf2 100644 --- a/src/transport/emitter.ts +++ b/src/transport/emitter.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { CloudEvent } from "../event/cloudevent"; import { HTTP, Message, Mode } from "../message"; import { EventEmitter } from "events"; @@ -5,15 +10,17 @@ import { EventEmitter } from "events"; /** * Options is an additional, optional dictionary of options that may * be passed to an EmitterFunction and TransportFunction + * @interface */ export interface Options { [key: string]: string | Record | unknown; } /** - * EmitterFunction is an invokable interface returned by the emitterFactory - * function. Invoke an EmitterFunction with a CloudEvent and optional transport + * EmitterFunction is an invokable interface returned by {@linkcode emitterFor}. + * Invoke an EmitterFunction with a CloudEvent and optional transport * options to send the event as a Message across supported transports. + * @interface */ export interface EmitterFunction { (event: CloudEvent, options?: Options): Promise; @@ -23,6 +30,7 @@ export interface EmitterFunction { * TransportFunction is an invokable interface provided to the emitterFactory. * A TransportFunction's responsiblity is to send a JSON encoded event Message * across the wire. + * @interface */ export interface TransportFunction { (message: Message, options?: Options): Promise; @@ -30,11 +38,12 @@ export interface TransportFunction { const emitterDefaults = { binding: HTTP, mode: Mode.BINARY }; /** - * emitterFactory creates and returns an EmitterFunction using the supplied - * TransportFunction. The returned EmitterFunction will invoke the Binding's - * `binary` or `structured` function to convert a CloudEvent into a JSON - * Message based on the Mode provided, and invoke the TransportFunction with - * the Message and any supplied options. + * Creates and returns an {@linkcode EmitterFunction} using the supplied + * {@linkcode TransportFunction}. The returned {@linkcode EmitterFunction} + * will invoke the {@linkcode Binding}'s `binary` or `structured` function + * to convert a {@linkcode CloudEvent} into a JSON + * {@linkcode Message} based on the {@linkcode Mode} provided, and invoke the + * TransportFunction with the Message and any supplied options. * * @param {TransportFunction} fn a TransportFunction that can accept an event Message * @param { {Binding, Mode} } options network binding and message serialization options @@ -62,7 +71,7 @@ export function emitterFor(fn: TransportFunction, options = emitterDefaults): Em } /** - * A static class to emit CloudEvents within an application + * A helper class to emit CloudEvents within an application */ export class Emitter { /** @@ -97,7 +106,7 @@ export class Emitter { * Emit an event inside this application * * @param {CloudEvent} event to emit - * @param {boolean} ensureDelivery fail the promise if one listener fail + * @param {boolean} ensureDelivery fail the promise if one listener fails * @return {void} */ static async emitEvent(event: CloudEvent, ensureDelivery = true): Promise { diff --git a/src/transport/http/index.ts b/src/transport/http/index.ts index 845475d0..60eaad2c 100644 --- a/src/transport/http/index.ts +++ b/src/transport/http/index.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { Message, Options } from "../.."; import axios from "axios"; diff --git a/src/transport/protocols.ts b/src/transport/protocols.ts index 4b46cad1..26e81e5c 100644 --- a/src/transport/protocols.ts +++ b/src/transport/protocols.ts @@ -1,6 +1,12 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + /** * An enum representing the transport protocols for an event */ + export const enum Protocol { HTTPBinary, HTTPStructured, diff --git a/test/conformance/steps.ts b/test/conformance/steps.ts index 8b3377de..92a9cf2d 100644 --- a/test/conformance/steps.ts +++ b/test/conformance/steps.ts @@ -1,4 +1,10 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + /* eslint-disable @typescript-eslint/ban-ts-comment */ + import { assert } from "chai"; import { Given, When, Then, World } from "cucumber"; import { Message, Headers, HTTP } from "../../src"; diff --git a/test/integration/cloud_event_test.ts b/test/integration/cloud_event_test.ts index cdb34a29..efede825 100644 --- a/test/integration/cloud_event_test.ts +++ b/test/integration/cloud_event_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import path from "path"; import fs from "fs"; diff --git a/test/integration/constants_test.ts b/test/integration/constants_test.ts index f76c98f2..e98ec871 100644 --- a/test/integration/constants_test.ts +++ b/test/integration/constants_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import { expect } from "chai"; import { CONSTANTS } from "../../src"; diff --git a/test/integration/emitter_factory_test.ts b/test/integration/emitter_factory_test.ts index 3de763bd..9df2a063 100644 --- a/test/integration/emitter_factory_test.ts +++ b/test/integration/emitter_factory_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { expect } from "chai"; import nock from "nock"; diff --git a/test/integration/emitter_singleton_test.ts b/test/integration/emitter_singleton_test.ts index bfa53db2..5749ed2a 100644 --- a/test/integration/emitter_singleton_test.ts +++ b/test/integration/emitter_singleton_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { emitterFor, HTTP, Mode, Message, Emitter } from "../../src"; diff --git a/test/integration/message_test.ts b/test/integration/message_test.ts index 127f6f6d..5246acaf 100644 --- a/test/integration/message_test.ts +++ b/test/integration/message_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import path from "path"; import fs from "fs"; @@ -59,6 +64,26 @@ describe("HTTP transport", () => { expect(HTTP.isEvent(message)).to.be.true; }); + it("Respects extension attribute casing (even if against spec)", () => { + // Now create a message that is an event + const message = { + body: `{ "greeting": "hello" }`, + headers: { + [CONSTANTS.CE_HEADERS.ID]: "1234", + [CONSTANTS.CE_HEADERS.SOURCE]: "test", + [CONSTANTS.CE_HEADERS.TYPE]: "test.event", + [CONSTANTS.CE_HEADERS.SPEC_VERSION]: Version.V1, + "ce-LUNCH": "tacos", + }, + }; + expect(HTTP.isEvent(message)).to.be.true; + const event: CloudEvent = HTTP.toEvent(message); + expect(event.LUNCH).to.equal("tacos"); + expect(function () { + event.validate(); + }).to.throw("invalid attribute name: LUNCH"); + }); + it("Can detect CloudEvent binary Messages with weird versions", () => { // Now create a message that is an event const message = { @@ -71,7 +96,9 @@ describe("HTTP transport", () => { }, }; expect(HTTP.isEvent(message)).to.be.true; - expect(HTTP.toEvent(message)).not.to.throw; + const event: CloudEvent = HTTP.toEvent(message); + expect(event.specversion).to.equal("11.8"); + expect(event.validate()).to.be.false; }); it("Can detect CloudEvent structured Messages with weird versions", () => { diff --git a/test/integration/parser_test.ts b/test/integration/parser_test.ts index b5d678a4..c08ba68e 100644 --- a/test/integration/parser_test.ts +++ b/test/integration/parser_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { expect } from "chai"; @@ -47,11 +52,19 @@ describe("JSON Event Format Parser", () => { it("Throw error when payload is an invalid JSON", () => { // setup - const payload = "gg"; + const payload = "{gg"; const parser = new Parser(); // TODO: Should the parser catch the SyntaxError and re-throw a ValidationError? - expect(parser.parse.bind(parser, payload)).to.throw(SyntaxError, "Unexpected token g in JSON at position 0"); + expect(parser.parse.bind(parser, payload)).to.throw(SyntaxError, "Unexpected token g in JSON at position 1"); + }); + + it("Accepts a string as valid JSON", () => { + // setup + const payload = "I am a string!"; + const parser = new Parser(); + + expect(parser.parse(payload)).to.equal("I am a string!"); }); it("Must accept when the payload is a string well formed as JSON", () => { diff --git a/test/integration/sdk_test.ts b/test/integration/sdk_test.ts index 42a8cf9f..f31653b6 100644 --- a/test/integration/sdk_test.ts +++ b/test/integration/sdk_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { expect } from "chai"; import { CloudEvent, Version } from "../../src"; diff --git a/test/integration/spec_03_tests.ts b/test/integration/spec_03_tests.ts index 27cb3d84..ad841223 100644 --- a/test/integration/spec_03_tests.ts +++ b/test/integration/spec_03_tests.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { expect } from "chai"; import { CloudEvent, Version, ValidationError, Mode } from "../../src"; diff --git a/test/integration/spec_1_tests.ts b/test/integration/spec_1_tests.ts index 6da9774a..87fd0220 100644 --- a/test/integration/spec_1_tests.ts +++ b/test/integration/spec_1_tests.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { expect } from "chai"; import { CloudEvent, Version, ValidationError } from "../../src"; diff --git a/test/integration/utilities_test.ts b/test/integration/utilities_test.ts index f387e600..a0b8a2ec 100644 --- a/test/integration/utilities_test.ts +++ b/test/integration/utilities_test.ts @@ -1,3 +1,8 @@ +/* + Copyright 2021 The CloudEvents Authors + SPDX-License-Identifier: Apache-2.0 +*/ + import "mocha"; import { expect } from "chai"; import { isStringOrThrow, equalsOrThrow, isBase64, asData } from "../../src/event/validation";