chore(refactor): prefer interfaces over concrete classes (#457)
* chore(refactor): protocol bindings use interfaces This change modifies the protocol binding interfaces such as `Binding`, `Serializer` and the like to use the `CloudEventV1` interface instead of the implementation class `CloudEvent`. This should make extending the interfaces simpler as this work has grown out of efforts around the implementation of a second transport interface, Kafka. See: https://github.com/cloudevents/sdk-javascript/pull/455/ This commit also includes the addition of a generic type to the `Message` interface, defaulting to `string`. There is also some minor clean up involving what is exported from the `message/http` modules. Now, instead of exporting the entire implementation, only the `HTTP` binding implementation is exported, and it is then reexported by `message`. Also, a static `CloudEvent.cloneWith()` method has been added which the instance methods now use. Signed-off-by: Lance Ball <lball@redhat.com> * fixup: make the `cloneWith()` method is dependent on interfaces Signed-off-by: Lance Ball <lball@redhat.com> * fixup: remove unnecessary cast Signed-off-by: Lance Ball <lball@redhat.com>
This commit is contained in:
parent
320354f750
commit
2ac731eb88
|
@ -147,7 +147,7 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
||||||
toJSON(): Record<string, unknown> {
|
toJSON(): Record<string, unknown> {
|
||||||
const event = { ...this };
|
const event = { ...this };
|
||||||
event.time = new Date(this.time as string).toISOString();
|
event.time = new Date(this.time as string).toISOString();
|
||||||
event.data = !isBinary(this.data) ? this.data : undefined;
|
event.data = this.#_data;
|
||||||
return event;
|
return event;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -184,30 +184,30 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clone a CloudEvent with new/update attributes
|
* Clone a CloudEvent with new/updated attributes
|
||||||
* @param {object} options attributes to augment the CloudEvent with an `data` property
|
* @param {object} options attributes to augment the CloudEvent without a `data` property
|
||||||
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
||||||
* @throws if the CloudEvent does not conform to the schema
|
* @throws if the CloudEvent does not conform to the schema
|
||||||
* @return {CloudEvent} returns a new CloudEvent<T>
|
* @return {CloudEvent} returns a new CloudEvent<T>
|
||||||
*/
|
*/
|
||||||
public cloneWith(options: Partial<Exclude<CloudEventV1<never>, "data">>, strict?: boolean): CloudEvent<T>;
|
public cloneWith(options: Partial<Exclude<CloudEventV1<never>, "data">>, strict?: boolean): CloudEvent<T>;
|
||||||
/**
|
/**
|
||||||
* Clone a CloudEvent with new/update attributes
|
* Clone a CloudEvent with new/updated attributes and new data
|
||||||
* @param {object} options attributes to augment the CloudEvent with a `data` property
|
* @param {object} options attributes to augment the CloudEvent with a `data` property and type
|
||||||
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
||||||
* @throws if the CloudEvent does not conform to the schema
|
* @throws if the CloudEvent does not conform to the schema
|
||||||
* @return {CloudEvent} returns a new CloudEvent<D>
|
* @return {CloudEvent} returns a new CloudEvent<D>
|
||||||
*/
|
*/
|
||||||
public cloneWith<D>(options: Partial<CloudEvent<D>>, strict?: boolean): CloudEvent<D>;
|
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict?: boolean): CloudEvent<D>;
|
||||||
/**
|
/**
|
||||||
* Clone a CloudEvent with new/update attributes
|
* Clone a CloudEvent with new/updated attributes and possibly different data types
|
||||||
* @param {object} options attributes to augment the CloudEvent
|
* @param {object} options attributes to augment the CloudEvent
|
||||||
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
|
||||||
* @throws if the CloudEvent does not conform to the schema
|
* @throws if the CloudEvent does not conform to the schema
|
||||||
* @return {CloudEvent} returns a new CloudEvent
|
* @return {CloudEvent} returns a new CloudEvent
|
||||||
*/
|
*/
|
||||||
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict = true): CloudEvent<D | T> {
|
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict = true): CloudEvent<D | T> {
|
||||||
return new CloudEvent(Object.assign({}, this.toJSON(), options), strict);
|
return CloudEvent.cloneWith(this, options, strict);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -217,4 +217,22 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
||||||
[Symbol.for("nodejs.util.inspect.custom")](): string {
|
[Symbol.for("nodejs.util.inspect.custom")](): string {
|
||||||
return this.toString();
|
return this.toString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clone a CloudEvent with new or updated attributes.
|
||||||
|
* @param {CloudEventV1<any>} event an object that implements the {@linkcode CloudEventV1} interface
|
||||||
|
* @param {Partial<CloudEventV1<any>>} options an object with new or updated attributes
|
||||||
|
* @param {boolean} strict `true` if the resulting event should be valid per the CloudEvent specification
|
||||||
|
* @throws {ValidationError} if `strict` is `true` and the resulting event is invalid
|
||||||
|
* @returns {CloudEvent<any>} a CloudEvent cloned from `event` with `options` applied.
|
||||||
|
*/
|
||||||
|
public static cloneWith(
|
||||||
|
event: CloudEventV1<any>,
|
||||||
|
options: Partial<CloudEventV1<any>>,
|
||||||
|
strict = true): CloudEvent<any> {
|
||||||
|
if (event instanceof CloudEvent) {
|
||||||
|
event = event.toJSON() as CloudEventV1<any>;
|
||||||
|
}
|
||||||
|
return new CloudEvent(Object.assign({}, event, options), strict);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
|
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
|
||||||
import { CloudEvent } from "../..";
|
import { CloudEventV1 } from "../..";
|
||||||
import { Headers } from "../";
|
import { Headers } from "../";
|
||||||
import { Version } from "../../event/cloudevent";
|
import { Version } from "../../event/cloudevent";
|
||||||
import CONSTANTS from "../../constants";
|
import CONSTANTS from "../../constants";
|
||||||
|
@ -24,7 +24,7 @@ export const requiredHeaders = [
|
||||||
* @param {CloudEvent} event a CloudEvent
|
* @param {CloudEvent} event a CloudEvent
|
||||||
* @returns {Object} the headers that will be sent for the event
|
* @returns {Object} the headers that will be sent for the event
|
||||||
*/
|
*/
|
||||||
export function headersFor<T>(event: CloudEvent<T>): Headers {
|
export function headersFor<T>(event: CloudEventV1<T>): Headers {
|
||||||
const headers: Headers = {};
|
const headers: Headers = {};
|
||||||
let headerMap: Readonly<{ [key: string]: MappedParser }>;
|
let headerMap: Readonly<{ [key: string]: MappedParser }>;
|
||||||
if (event.specversion === Version.V1) {
|
if (event.specversion === Version.V1) {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
|
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
|
||||||
import { Message, Headers } from "..";
|
import { Message, Headers, Binding } from "..";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
headersFor,
|
headersFor,
|
||||||
|
@ -25,7 +25,7 @@ import { JSONParser, MappedParser, Parser, parserByContentType } from "../../par
|
||||||
* @param {CloudEvent} event The event to serialize
|
* @param {CloudEvent} event The event to serialize
|
||||||
* @returns {Message} a Message object with headers and body
|
* @returns {Message} a Message object with headers and body
|
||||||
*/
|
*/
|
||||||
export function binary<T>(event: CloudEvent<T>): Message {
|
function binary<T>(event: CloudEventV1<T>): Message {
|
||||||
const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE };
|
const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE };
|
||||||
const headers: Headers = { ...contentType, ...headersFor(event) };
|
const headers: Headers = { ...contentType, ...headersFor(event) };
|
||||||
let body = event.data;
|
let body = event.data;
|
||||||
|
@ -47,10 +47,10 @@ export function binary<T>(event: CloudEvent<T>): Message {
|
||||||
* @param {CloudEvent} event the CloudEvent to be serialized
|
* @param {CloudEvent} event the CloudEvent to be serialized
|
||||||
* @returns {Message} a Message object with headers and body
|
* @returns {Message} a Message object with headers and body
|
||||||
*/
|
*/
|
||||||
export function structured<T>(event: CloudEvent<T>): Message {
|
function structured<T>(event: CloudEventV1<T>): Message {
|
||||||
if (event.data_base64) {
|
if (event.data_base64) {
|
||||||
// The event's data is binary - delete it
|
// The event's data is binary - delete it
|
||||||
event = event.cloneWith({ data: undefined });
|
event = (event as CloudEvent).cloneWith({ data: undefined });
|
||||||
}
|
}
|
||||||
return {
|
return {
|
||||||
headers: {
|
headers: {
|
||||||
|
@ -67,7 +67,7 @@ export function structured<T>(event: CloudEvent<T>): Message {
|
||||||
* @param {Message} message an incoming Message object
|
* @param {Message} message an incoming Message object
|
||||||
* @returns {boolean} true if this Message is a CloudEvent
|
* @returns {boolean} true if this Message is a CloudEvent
|
||||||
*/
|
*/
|
||||||
export function isEvent(message: Message): boolean {
|
function isEvent(message: Message): boolean {
|
||||||
// TODO: this could probably be optimized
|
// TODO: this could probably be optimized
|
||||||
try {
|
try {
|
||||||
deserialize(message);
|
deserialize(message);
|
||||||
|
@ -84,7 +84,7 @@ export function isEvent(message: Message): boolean {
|
||||||
* @param {Message} message the incoming message
|
* @param {Message} message the incoming message
|
||||||
* @return {CloudEvent} A new {CloudEvent} instance
|
* @return {CloudEvent} A new {CloudEvent} instance
|
||||||
*/
|
*/
|
||||||
export function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
||||||
const cleanHeaders: Headers = sanitize(message.headers);
|
const cleanHeaders: Headers = sanitize(message.headers);
|
||||||
const mode: Mode = getMode(cleanHeaders);
|
const mode: Mode = getMode(cleanHeaders);
|
||||||
const version = getVersion(mode, cleanHeaders, message.body);
|
const version = getVersion(mode, cleanHeaders, message.body);
|
||||||
|
@ -261,3 +261,14 @@ function parseBatched<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
|
||||||
});
|
});
|
||||||
return ret;
|
return ret;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Bindings for HTTP transport support
|
||||||
|
* @implements {@linkcode Binding}
|
||||||
|
*/
|
||||||
|
export const HTTP: Binding = {
|
||||||
|
binary,
|
||||||
|
structured,
|
||||||
|
toEvent: deserialize,
|
||||||
|
isEvent: isEvent,
|
||||||
|
};
|
||||||
|
|
|
@ -4,8 +4,10 @@
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { IncomingHttpHeaders } from "http";
|
import { IncomingHttpHeaders } from "http";
|
||||||
import { CloudEvent } from "..";
|
import { CloudEventV1 } from "..";
|
||||||
import { binary, deserialize, structured, isEvent } from "./http";
|
|
||||||
|
// reexport the HTTP protocol binding
|
||||||
|
export * from "./http";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Binding is an interface for transport protocols to implement,
|
* Binding is an interface for transport protocols to implement,
|
||||||
|
@ -39,11 +41,11 @@ export interface Headers extends IncomingHttpHeaders {
|
||||||
* transport-agnostic message
|
* transport-agnostic message
|
||||||
* @interface
|
* @interface
|
||||||
* @property {@linkcode Headers} `headers` - the headers for the event Message
|
* @property {@linkcode Headers} `headers` - the headers for the event Message
|
||||||
* @property string `body` - the body of the event Message
|
* @property {T | string | Buffer | unknown} `body` - the body of the event Message
|
||||||
*/
|
*/
|
||||||
export interface Message {
|
export interface Message<T = string> {
|
||||||
headers: Headers;
|
headers: Headers;
|
||||||
body: string | unknown;
|
body: T | string | Buffer | unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -62,7 +64,7 @@ export enum Mode {
|
||||||
* @interface
|
* @interface
|
||||||
*/
|
*/
|
||||||
export interface Serializer {
|
export interface Serializer {
|
||||||
<T>(event: CloudEvent<T>): Message;
|
<T>(event: CloudEventV1<T>): Message;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -71,7 +73,7 @@ export interface Serializer {
|
||||||
* @interface
|
* @interface
|
||||||
*/
|
*/
|
||||||
export interface Deserializer {
|
export interface Deserializer {
|
||||||
<T>(message: Message): CloudEvent<T> | CloudEvent<T>[];
|
<T>(message: Message): CloudEventV1<T> | CloudEventV1<T>[];
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -82,14 +84,3 @@ export interface Deserializer {
|
||||||
export interface Detector {
|
export interface Detector {
|
||||||
(message: Message): boolean;
|
(message: Message): boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Bindings for HTTP transport support
|
|
||||||
* @implements {@linkcode Binding}
|
|
||||||
*/
|
|
||||||
export const HTTP: Binding = {
|
|
||||||
binary: binary as Serializer,
|
|
||||||
structured: structured as Serializer,
|
|
||||||
toEvent: deserialize as Deserializer,
|
|
||||||
isEvent: isEvent as Detector,
|
|
||||||
};
|
|
||||||
|
|
|
@ -5,11 +5,13 @@
|
||||||
|
|
||||||
import "mocha";
|
import "mocha";
|
||||||
import { expect } from "chai";
|
import { expect } from "chai";
|
||||||
import { CloudEvent, Version } from "../../src";
|
import { CloudEvent, CloudEventV1, Version } from "../../src";
|
||||||
|
|
||||||
const fixture = {
|
const fixture: CloudEventV1<undefined> = {
|
||||||
|
id: "123",
|
||||||
type: "org.cloudevents.test",
|
type: "org.cloudevents.test",
|
||||||
source: "http://cloudevents.io",
|
source: "http://cloudevents.io",
|
||||||
|
specversion: Version.V1,
|
||||||
};
|
};
|
||||||
|
|
||||||
describe("The SDK Requirements", () => {
|
describe("The SDK Requirements", () => {
|
||||||
|
@ -34,4 +36,19 @@ describe("The SDK Requirements", () => {
|
||||||
expect(new CloudEvent(fixture).specversion).to.equal(Version.V1);
|
expect(new CloudEvent(fixture).specversion).to.equal(Version.V1);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Cloning events", () => {
|
||||||
|
it("should clone simple objects that adhere to the CloudEventV1 interface", () => {
|
||||||
|
const copy = CloudEvent.cloneWith(fixture, { id: "456" }, false);
|
||||||
|
expect(copy.id).to.equal("456");
|
||||||
|
expect(copy.type).to.equal(fixture.type);
|
||||||
|
expect(copy.source).to.equal(fixture.source);
|
||||||
|
expect(copy.specversion).to.equal(fixture.specversion);
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should clone simple objects with data that adhere to the CloudEventV1 interface", () => {
|
||||||
|
const copy = CloudEvent.cloneWith(fixture, { data: { lunch: "tacos" } }, false);
|
||||||
|
expect(copy.data.lunch).to.equal("tacos");
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
Loading…
Reference in New Issue