Skip to content

feat: introduce Message, Serializer, Deserializer and Binding interfaces #324

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 12 commits into from
Aug 26, 2020
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 19 additions & 6 deletions examples/express-ex/index.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
/* eslint-disable no-console */
/* eslint-disable */

const express = require("express");
const {Receiver} = require("cloudevents");

const { Message, CloudEvent, HTTP } = require("cloudevents");
const { response } = require("express");
const app = express();

app.use((req, res, next) => {
Expand All @@ -23,10 +23,23 @@ app.post("/", (req, res) => {
console.log("HEADERS", req.headers);
console.log("BODY", req.body);

const message = {
headers: req.headers,
body: req.body
};

try {
const event = Receiver.accept(req.headers, req.body);
console.log(`Accepted event: ${event}`);
res.status(201).json(event);
const event = HTTP.toEvent(message);
// respond as an event
const responseEventMessage = new CloudEvent({
source: '/',
type: 'event:response',
...event
});
responseEventMessage.data = {
hello: 'world'
};
res.status(201).json(responseEventMessage);
} catch (err) {
console.error(err);
res.status(415).header("Content-Type", "application/json").send(JSON.stringify(err));
Expand Down
6 changes: 6 additions & 0 deletions src/event/cloudevent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,12 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
this.#_data = value;
}

/**
* Used by JSON.stringify(). The name is confusing, but this method is called by
* JSON.stringify() when converting this object to JSON.
* @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/JSON/stringify
* @return {object} this event as a plain object
*/
toJSON(): Record<string, unknown> {
const event = { ...this };
event.time = this.time;
Expand Down
24 changes: 15 additions & 9 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,9 @@ import { ValidationError } from "./event/validation";
import { CloudEventV03, CloudEventV03Attributes, CloudEventV1, CloudEventV1Attributes } from "./event/interfaces";

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

import CONSTANTS from "./constants";

Expand All @@ -18,14 +18,20 @@ export {
CloudEventV1Attributes,
Version,
ValidationError,
// From transport
Emitter,
Receiver,
Mode,
Protocol,
TransportOptions,
// From message
Headers,
headersFor,
Mode,
Binding,
Message,
Deserializer,
Serializer,
headersFor, // TODO: Deprecated. Remove for 4.0
HTTP,
// From transport
Emitter, // TODO: Deprecated. Remove for 4.0
Receiver, // TODO: Deprecated. Remove for 4.0
Protocol, // TODO: Deprecated. Remove for 4.0
TransportOptions, // TODO: Deprecated. Remove for 4.0
// From Constants
CONSTANTS,
};
91 changes: 91 additions & 0 deletions src/transport/http/versions.ts → src/message/http/headers.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,97 @@
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
import { ValidationError, CloudEvent } from "../..";
import { Headers } from "../";
import { Version } from "../../event/cloudevent";
import CONSTANTS from "../../constants";

export const allowedContentTypes = [CONSTANTS.DEFAULT_CONTENT_TYPE, CONSTANTS.MIME_JSON, CONSTANTS.MIME_OCTET_STREAM];
export const requiredHeaders = [
CONSTANTS.CE_HEADERS.ID,
CONSTANTS.CE_HEADERS.SOURCE,
CONSTANTS.CE_HEADERS.TYPE,
CONSTANTS.CE_HEADERS.SPEC_VERSION,
];

/**
* Validates cloud event headers and their values
* @param {Headers} headers event transport headers for validation
* @throws {ValidationError} if the headers are invalid
* @return {boolean} true if headers are valid
*/
export function validate(headers: Headers): Headers {
const sanitizedHeaders = sanitize(headers);

// if content-type exists, be sure it's an allowed type
const contentTypeHeader = sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE];
const noContentType = !allowedContentTypes.includes(contentTypeHeader);
if (contentTypeHeader && noContentType) {
throw new ValidationError("invalid content type", [sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]]);
}

requiredHeaders
.filter((required: string) => !sanitizedHeaders[required])
.forEach((required: string) => {
throw new ValidationError(`header '${required}' not found`);
});

if (!sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE]) {
sanitizedHeaders[CONSTANTS.HEADER_CONTENT_TYPE] = CONSTANTS.MIME_JSON;
}

return sanitizedHeaders;
}

/**
* Returns the HTTP headers that will be sent for this event when the HTTP transmission
* mode is "binary". Events sent over HTTP in structured mode only have a single CE header
* and that is "ce-id", corresponding to the event ID.
* @param {CloudEvent} event a CloudEvent
* @returns {Object} the headers that will be sent for the event
*/
export function headersFor(event: CloudEvent): Headers {
const headers: Headers = {};
let headerMap: Readonly<{ [key: string]: MappedParser }>;
if (event.specversion === Version.V1) {
headerMap = v1headerMap;
} else {
headerMap = v03headerMap;
}

// iterate over the event properties - generate a header for each
Object.getOwnPropertyNames(event).forEach((property) => {
const value = event[property];
if (value) {
const map: MappedParser | undefined = headerMap[property] as MappedParser;
if (map) {
headers[map.name] = map.parser.parse(value as string) as string;
} else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) {
headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value as string;
}
}
});
// Treat time specially, since it's handled with getters and setters in CloudEvent
if (event.time) {
headers[CONSTANTS.CE_HEADERS.TIME] = event.time as string;
}
return headers;
}

/**
* Sanitizes incoming headers by lowercasing them and potentially removing
* encoding from the content-type header.
* @param {Headers} headers HTTP headers as key/value pairs
* @returns {Headers} the sanitized headers
*/
export function sanitize(headers: Headers): Headers {
const sanitized: Headers = {};

Array.from(Object.keys(headers))
.filter((header) => Object.hasOwnProperty.call(headers, header))
.forEach((header) => (sanitized[header.toLowerCase()] = headers[header]));

return sanitized;
}

function parser(name: string, parser = new PassThroughParser()): MappedParser {
return { name: name, parser: parser };
}
Expand Down
Loading