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:
Lance Ball 2022-01-07 14:24:56 -05:00 committed by GitHub
parent 320354f750
commit 2ac731eb88
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 73 additions and 36 deletions

View File

@ -147,7 +147,7 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
toJSON(): Record<string, unknown> {
const event = { ...this };
event.time = new Date(this.time as string).toISOString();
event.data = !isBinary(this.data) ? this.data : undefined;
event.data = this.#_data;
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
* @param {object} options attributes to augment the CloudEvent with an `data` property
* Clone a CloudEvent with new/updated attributes
* @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)
* @throws if the CloudEvent does not conform to the schema
* @return {CloudEvent} returns a new CloudEvent<T>
*/
public cloneWith(options: Partial<Exclude<CloudEventV1<never>, "data">>, strict?: boolean): CloudEvent<T>;
/**
* Clone a CloudEvent with new/update attributes
* @param {object} options attributes to augment the CloudEvent with a `data` property
* Clone a CloudEvent with new/updated attributes and new data
* @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)
* @throws if the CloudEvent does not conform to the schema
* @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 {boolean} strict whether or not to use strict validation when cloning (default: true)
* @throws if the CloudEvent does not conform to the schema
* @return {CloudEvent} returns a new CloudEvent
*/
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 {
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);
}
}

View File

@ -4,7 +4,7 @@
*/
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
import { CloudEvent } from "../..";
import { CloudEventV1 } from "../..";
import { Headers } from "../";
import { Version } from "../../event/cloudevent";
import CONSTANTS from "../../constants";
@ -24,7 +24,7 @@ export const requiredHeaders = [
* @param {CloudEvent} event a CloudEvent
* @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 = {};
let headerMap: Readonly<{ [key: string]: MappedParser }>;
if (event.specversion === Version.V1) {

View File

@ -4,7 +4,7 @@
*/
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
import { Message, Headers } from "..";
import { Message, Headers, Binding } from "..";
import {
headersFor,
@ -25,7 +25,7 @@ import { JSONParser, MappedParser, Parser, parserByContentType } from "../../par
* @param {CloudEvent} event The event to serialize
* @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 headers: Headers = { ...contentType, ...headersFor(event) };
let body = event.data;
@ -47,10 +47,10 @@ export function binary<T>(event: CloudEvent<T>): Message {
* @param {CloudEvent} event the CloudEvent to be serialized
* @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) {
// The event's data is binary - delete it
event = event.cloneWith({ data: undefined });
event = (event as CloudEvent).cloneWith({ data: undefined });
}
return {
headers: {
@ -67,7 +67,7 @@ export function structured<T>(event: CloudEvent<T>): Message {
* @param {Message} message an incoming Message object
* @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
try {
deserialize(message);
@ -84,7 +84,7 @@ export function isEvent(message: Message): boolean {
* @param {Message} message the incoming message
* @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 mode: Mode = getMode(cleanHeaders);
const version = getVersion(mode, cleanHeaders, message.body);
@ -261,3 +261,14 @@ function parseBatched<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
});
return ret;
}
/**
* Bindings for HTTP transport support
* @implements {@linkcode Binding}
*/
export const HTTP: Binding = {
binary,
structured,
toEvent: deserialize,
isEvent: isEvent,
};

View File

@ -4,8 +4,10 @@
*/
import { IncomingHttpHeaders } from "http";
import { CloudEvent } from "..";
import { binary, deserialize, structured, isEvent } from "./http";
import { CloudEventV1 } from "..";
// reexport the HTTP protocol binding
export * from "./http";
/**
* Binding is an interface for transport protocols to implement,
@ -39,11 +41,11 @@ export interface Headers extends IncomingHttpHeaders {
* transport-agnostic message
* @interface
* @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;
body: string | unknown;
body: T | string | Buffer | unknown;
}
/**
@ -62,7 +64,7 @@ export enum Mode {
* @interface
*/
export interface Serializer {
<T>(event: CloudEvent<T>): Message;
<T>(event: CloudEventV1<T>): Message;
}
/**
@ -71,7 +73,7 @@ export interface Serializer {
* @interface
*/
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 {
(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,
};

View File

@ -5,11 +5,13 @@
import "mocha";
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",
source: "http://cloudevents.io",
specversion: Version.V1,
};
describe("The SDK Requirements", () => {
@ -34,4 +36,19 @@ describe("The SDK Requirements", () => {
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");
});
});
});