Skip to content

Commit f3953a9

Browse files
authored
feat: introduce Message, Serializer, Deserializer and Binding interfaces (#324)
* lib(messages): Implement a 4.0 Messages and other supporting interfaces This commit introduces the Message, Serializer and Deserializer, and Binding interfaces used to convert a CloudEvent into a Message that can be sent across a transport protocol. The first protocol implemented for this is HTTP, and some of the functionality formerly in src/transport/http has been simplified, reduced and/or moved to /src/messages/http. Test for V1 and V3 events are in place. Conformance tests have been modified to use these new interfaces vs. the HTTP Receiver class. Signed-off-by: Lance Ball <lball@redhat.com>
1 parent 17d4bc8 commit f3953a9

21 files changed

+668
-1703
lines changed

examples/express-ex/index.js

+12-5
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,7 @@
1-
/* eslint-disable no-console */
1+
/* eslint-disable */
22

33
const express = require("express");
4-
const {Receiver} = require("cloudevents");
5-
4+
const { Receiver } = require("cloudevents");
65
const app = express();
76

87
app.use((req, res, next) => {
@@ -25,8 +24,16 @@ app.post("/", (req, res) => {
2524

2625
try {
2726
const event = Receiver.accept(req.headers, req.body);
28-
console.log(`Accepted event: ${event}`);
29-
res.status(201).json(event);
27+
// respond as an event
28+
const responseEventMessage = new CloudEvent({
29+
source: '/',
30+
type: 'event:response',
31+
...event
32+
});
33+
responseEventMessage.data = {
34+
hello: 'world'
35+
};
36+
res.status(201).json(responseEventMessage);
3037
} catch (err) {
3138
console.error(err);
3239
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));

examples/express-ex/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
"author": "fabiojose@gmail.com",
1515
"license": "Apache-2.0",
1616
"dependencies": {
17-
"cloudevents": "~3.0.0",
17+
"cloudevents": "^3.1.0",
1818
"express": "^4.17.1"
1919
}
2020
}

src/event/cloudevent.ts

+6
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
156156
this.#_data = value;
157157
}
158158

159+
/**
160+
* Used by JSON.stringify(). The name is confusing, but this method is called by
161+
* JSON.stringify() when converting this object to JSON.
162+
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
163+
* @return {object} this event as a plain object
164+
*/
159165
toJSON(): Record<string, unknown> {
160166
const event = { ...this };
161167
event.time = this.time;

src/index.ts

+15-9
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,9 @@ import { ValidationError } from "./event/validation";
33
import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";
44

55
import { Emitter, TransportOptions } from "./transport/emitter";
6-
import { Receiver, Mode } from "./transport/receiver";
6+
import { Receiver } from "./transport/receiver";
77
import { Protocol } from "./transport/protocols";
8-
import { Headers, headersFor } from "./transport/http/headers";
8+
import { Headers, Mode, Binding, HTTP, Message, Serializer, Deserializer, headersFor } from "./message";
99

1010
import CONSTANTS from "./constants";
1111

@@ -18,14 +18,20 @@ export {
1818
CloudEventV1Attributes,
1919
Version,
2020
ValidationError,
21-
// From transport
22-
Emitter,
23-
Receiver,
24-
Mode,
25-
Protocol,
26-
TransportOptions,
21+
// From message
2722
Headers,
28-
headersFor,
23+
Mode,
24+
Binding,
25+
Message,
26+
Deserializer,
27+
Serializer,
28+
headersFor, // TODO: Deprecated. Remove for 4.0
29+
HTTP,
30+
// From transport
31+
Emitter, // TODO: Deprecated. Remove for 4.0
32+
Receiver, // TODO: Deprecated. Remove for 4.0
33+
Protocol, // TODO: Deprecated. Remove for 4.0
34+
TransportOptions, // TODO: Deprecated. Remove for 4.0
2935
// From Constants
3036
CONSTANTS,
3137
};

src/transport/http/versions.ts src/message/http/headers.ts

+91
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,97 @@
11
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
2+
import { ValidationError, CloudEvent } from "../..";
3+
import { Headers } from "../";
4+
import { Version } from "../../event/cloudevent";
25
import CONSTANTS from "../../constants";
36

7+
export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM];
8+
export const requiredHeaders = [
9+
CONSTANTS.CE_HEADERS.ID,
10+
CONSTANTS.CE_HEADERS.SOURCE,
11+
CONSTANTS.CE_HEADERS.TYPE,
12+
CONSTANTS.CE_HEADERS.SPEC_VERSION,
13+
];
14+
15+
/**
16+
* Validates cloud event headers and their values
17+
* @param {Headers} headers event transport headers for validation
18+
* @throws {ValidationError} if the headers are invalid
19+
* @return {boolean} true if headers are valid
20+
*/
21+
export function validate(headers: Headers): Headers {
22+
const sanitizedHeaders = sanitize(headers);
23+
24+
// if content-type exists, be sure it's an allowed type
25+
const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE];
26+
const noContentType = !allowedContentTypes.includes(contentTypeHeader);
27+
if (contentTypeHeader && noContentType) {
28+
throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]);
29+
}
30+
31+
requiredHeaders
32+
.filter((required: string) => !sanitizedHeaders[required])
33+
.forEach((required: string) => {
34+
throw new ValidationError(`header '${required}' not found`);
35+
});
36+
37+
if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) {
38+
sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
39+
}
40+
41+
return sanitizedHeaders;
42+
}
43+
44+
/**
45+
* Returns the HTTP headers that will be sent for this event when the HTTP transmission
46+
* mode is "binary". Events sent over HTTP in structured mode only have a single CE header
47+
* and that is "ce-id", corresponding to the event ID.
48+
* @param {CloudEvent} event a CloudEvent
49+
* @returns {Object} the headers that will be sent for the event
50+
*/
51+
export function headersFor(event: CloudEvent): Headers {
52+
const headers: Headers = {};
53+
let headerMap: Readonly<{ [key: string]: MappedParser }>;
54+
if (event.specversion === Version.V1) {
55+
headerMap = v1headerMap;
56+
} else {
57+
headerMap = v03headerMap;
58+
}
59+
60+
// iterate over the event properties - generate a header for each
61+
Object.getOwnPropertyNames(event).forEach((property) => {
62+
const value = event[property];
63+
if (value) {
64+
const map: MappedParser | undefined = headerMap[property] as MappedParser;
65+
if (map) {
66+
headers[map.name] = map.parser.parse(value as string) as string;
67+
} else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) {
68+
headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string;
69+
}
70+
}
71+
});
72+
// Treat time specially, since it's handled with getters and setters in CloudEvent
73+
if (event.time) {
74+
headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string;
75+
}
76+
return headers;
77+
}
78+
79+
/**
80+
* Sanitizes incoming headers by lowercasing them and potentially removing
81+
* encoding from the content-type header.
82+
* @param {Headers} headers HTTP headers as key/value pairs
83+
* @returns {Headers} the sanitized headers
84+
*/
85+
export function sanitize(headers: Headers): Headers {
86+
const sanitized: Headers = {};
87+
88+
Array.from(Object.keys(headers))
89+
.filter((header) => Object.hasOwnProperty.call(headers, header))
90+
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));
91+
92+
return sanitized;
93+
}
94+
495
function parser(name: string, parser = new PassThroughParser()): MappedParser {
596
return { name: name, parser: parser };
697
}

0 commit comments

Comments
 (0)