/* Copyright 2021 The CloudEvents Authors SPDX-License-Identifier: Apache-2.0 */ import { Binding, Deserializer, CloudEvent, CloudEventV1, CONSTANTS, Message, ValidationError, Headers } from "../.."; export { MQTT, MQTTMessageFactory }; export type { MQTTMessage }; /** * Extends the base {@linkcode Message} interface to include MQTT attributes, some of which * are aliases of the {Message} attributes. */ interface MQTTMessage extends Message { /** * Identifies this message as a PUBLISH packet. MQTTMessages created with * the `binary` and `structured` Serializers will contain a "Content Type" * property in the PUBLISH record. * @see https://github.com/cloudevents/spec/blob/v1.0.1/mqtt-protocol-binding.md#3-mqtt-publish-message-mapping */ PUBLISH: Record | undefined /** * Alias of {Message#body} */ payload: T | undefined, /** * Alias of {Message#headers} */ "User Properties": Headers | undefined } /** * Binding for MQTT transport support * @implements @linkcode Binding */ const MQTT: Binding = { binary, structured, toEvent: toEvent as Deserializer, isEvent }; /** * Converts a CloudEvent into an MQTTMessage with the event's data as the message payload * @param {CloudEventV1} event a CloudEvent * @returns {MQTTMessage} the event serialized as an MQTTMessage with binary encoding * @implements {Serializer} */ function binary(event: CloudEventV1): MQTTMessage { let properties; if (event instanceof CloudEvent) { properties = event.toJSON(); } else { properties = event; } const body = properties.data as T; delete properties.data; return MQTTMessageFactory(event.datacontenttype as string, properties, body); } /** * Converts a CloudEvent into an MQTTMessage with the event as the message payload * @param {CloudEventV1} event a CloudEvent * @returns {MQTTMessage} the event serialized as an MQTTMessage with structured encoding * @implements {Serializer} */ function structured(event: CloudEventV1): MQTTMessage { let body; if (event instanceof CloudEvent) { body = event.toJSON(); } else { body = event; } return MQTTMessageFactory(CONSTANTS.DEFAULT_CE_CONTENT_TYPE, {}, body) as MQTTMessage; } /** * A helper function to create an MQTTMessage object, with "User Properties" as an alias * for "headers" and "payload" an alias for body, and a "PUBLISH" record with a "Content Type" * property. * @param {string} contentType the "Content Type" attribute on PUBLISH * @param {Record} headers the headers and "User Properties" * @param {T} body the message body/payload * @returns {MQTTMessage} a message initialized with the provided attributes */ function MQTTMessageFactory(contentType: string, headers: Record, body: T): MQTTMessage { return { PUBLISH: { "Content Type": contentType }, body, get payload() { return this.body as T; }, headers: headers as Headers, get "User Properties"() { return this.headers as any; } }; } /** * Converts an MQTTMessage into a CloudEvent * @param {Message} message the message to deserialize * @param {boolean} strict determines if a ValidationError will be thrown on bad input - defaults to false * @returns {CloudEventV1} an event * @implements {Deserializer} */ function toEvent(message: Message, strict = false): CloudEventV1 | CloudEventV1[] { if (strict && !isEvent(message)) { throw new ValidationError("No CloudEvent detected"); } if (isStructuredMessage(message as MQTTMessage)) { const evt = (typeof message.body === "string") ? JSON.parse(message.body): message.body; return new CloudEvent({ ...evt as CloudEventV1 }, false); } else { return new CloudEvent({ ...message.headers, data: message.body as T, }, false); } } /** * Determine if the message is a CloudEvent * @param {Message} message an MQTTMessage * @returns {boolean} true if the message contains an event */ function isEvent(message: Message): boolean { return isBinaryMessage(message) || isStructuredMessage(message as MQTTMessage); } function isBinaryMessage(message: Message): boolean { return (!!message.headers.id && !!message.headers.source && !! message.headers.type && !!message.headers.specversion); } function isStructuredMessage(message: MQTTMessage): boolean { if (!message) { return false; } return (message.PUBLISH && message?.PUBLISH["Content Type"]?.startsWith(CONSTANTS.MIME_CE_JSON)) || false; }