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> {
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue