Skip to content

Commit 2ac731e

Browse files
authored
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: #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 <[email protected]> * fixup: make the `cloneWith()` method is dependent on interfaces Signed-off-by: Lance Ball <[email protected]> * fixup: remove unnecessary cast Signed-off-by: Lance Ball <[email protected]>
1 parent 320354f commit 2ac731e

File tree

5 files changed

+73
-36
lines changed

5 files changed

+73
-36
lines changed

src/event/cloudevent.ts

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -147,7 +147,7 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
147147
toJSON(): Record<string, unknown> {
148148
const event = { ...this };
149149
event.time = new Date(this.time as string).toISOString();
150-
event.data = !isBinary(this.data) ? this.data : undefined;
150+
event.data = this.#_data;
151151
return event;
152152
}
153153

@@ -184,30 +184,30 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
184184
}
185185

186186
/**
187-
* Clone a CloudEvent with new/update attributes
188-
* @param {object} options attributes to augment the CloudEvent with an `data` property
187+
* Clone a CloudEvent with new/updated attributes
188+
* @param {object} options attributes to augment the CloudEvent without a `data` property
189189
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
190190
* @throws if the CloudEvent does not conform to the schema
191191
* @return {CloudEvent} returns a new CloudEvent<T>
192192
*/
193193
public cloneWith(options: Partial<Exclude<CloudEventV1<never>, "data">>, strict?: boolean): CloudEvent<T>;
194194
/**
195-
* Clone a CloudEvent with new/update attributes
196-
* @param {object} options attributes to augment the CloudEvent with a `data` property
195+
* Clone a CloudEvent with new/updated attributes and new data
196+
* @param {object} options attributes to augment the CloudEvent with a `data` property and type
197197
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
198198
* @throws if the CloudEvent does not conform to the schema
199199
* @return {CloudEvent} returns a new CloudEvent<D>
200200
*/
201-
public cloneWith<D>(options: Partial<CloudEvent<D>>, strict?: boolean): CloudEvent<D>;
201+
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict?: boolean): CloudEvent<D>;
202202
/**
203-
* Clone a CloudEvent with new/update attributes
203+
* Clone a CloudEvent with new/updated attributes and possibly different data types
204204
* @param {object} options attributes to augment the CloudEvent
205205
* @param {boolean} strict whether or not to use strict validation when cloning (default: true)
206206
* @throws if the CloudEvent does not conform to the schema
207207
* @return {CloudEvent} returns a new CloudEvent
208208
*/
209209
public cloneWith<D>(options: Partial<CloudEventV1<D>>, strict = true): CloudEvent<D | T> {
210-
return new CloudEvent(Object.assign({}, this.toJSON(), options), strict);
210+
return CloudEvent.cloneWith(this, options, strict);
211211
}
212212

213213
/**
@@ -217,4 +217,22 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
217217
[Symbol.for("nodejs.util.inspect.custom")](): string {
218218
return this.toString();
219219
}
220+
221+
/**
222+
* Clone a CloudEvent with new or updated attributes.
223+
* @param {CloudEventV1<any>} event an object that implements the {@linkcode CloudEventV1} interface
224+
* @param {Partial<CloudEventV1<any>>} options an object with new or updated attributes
225+
* @param {boolean} strict `true` if the resulting event should be valid per the CloudEvent specification
226+
* @throws {ValidationError} if `strict` is `true` and the resulting event is invalid
227+
* @returns {CloudEvent<any>} a CloudEvent cloned from `event` with `options` applied.
228+
*/
229+
public static cloneWith(
230+
event: CloudEventV1<any>,
231+
options: Partial<CloudEventV1<any>>,
232+
strict = true): CloudEvent<any> {
233+
if (event instanceof CloudEvent) {
234+
event = event.toJSON() as CloudEventV1<any>;
235+
}
236+
return new CloudEvent(Object.assign({}, event, options), strict);
237+
}
220238
}

src/message/http/headers.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { PassThroughParser, DateParser, MappedParser } from "../../parsers";
7-
import { CloudEvent } from "../..";
7+
import { CloudEventV1 } from "../..";
88
import { Headers } from "../";
99
import { Version } from "../../event/cloudevent";
1010
import CONSTANTS from "../../constants";
@@ -24,7 +24,7 @@ export const requiredHeaders = [
2424
* @param {CloudEvent} event a CloudEvent
2525
* @returns {Object} the headers that will be sent for the event
2626
*/
27-
export function headersFor<T>(event: CloudEvent<T>): Headers {
27+
export function headersFor<T>(event: CloudEventV1<T>): Headers {
2828
const headers: Headers = {};
2929
let headerMap: Readonly<{ [key: string]: MappedParser }>;
3030
if (event.specversion === Version.V1) {

src/message/http/index.ts

Lines changed: 17 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
*/
55

66
import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
7-
import { Message, Headers } from "..";
7+
import { Message, Headers, Binding } from "..";
88

99
import {
1010
headersFor,
@@ -25,7 +25,7 @@ import { JSONParser, MappedParser, Parser, parserByContentType } from "../../par
2525
* @param {CloudEvent} event The event to serialize
2626
* @returns {Message} a Message object with headers and body
2727
*/
28-
export function binary<T>(event: CloudEvent<T>): Message {
28+
function binary<T>(event: CloudEventV1<T>): Message {
2929
const contentType: Headers = { [CONSTANTS.HEADER_CONTENT_TYPE]: CONSTANTS.DEFAULT_CONTENT_TYPE };
3030
const headers: Headers = { ...contentType, ...headersFor(event) };
3131
let body = event.data;
@@ -47,10 +47,10 @@ export function binary<T>(event: CloudEvent<T>): Message {
4747
* @param {CloudEvent} event the CloudEvent to be serialized
4848
* @returns {Message} a Message object with headers and body
4949
*/
50-
export function structured<T>(event: CloudEvent<T>): Message {
50+
function structured<T>(event: CloudEventV1<T>): Message {
5151
if (event.data_base64) {
5252
// The event's data is binary - delete it
53-
event = event.cloneWith({ data: undefined });
53+
event = (event as CloudEvent).cloneWith({ data: undefined });
5454
}
5555
return {
5656
headers: {
@@ -67,7 +67,7 @@ export function structured<T>(event: CloudEvent<T>): Message {
6767
* @param {Message} message an incoming Message object
6868
* @returns {boolean} true if this Message is a CloudEvent
6969
*/
70-
export function isEvent(message: Message): boolean {
70+
function isEvent(message: Message): boolean {
7171
// TODO: this could probably be optimized
7272
try {
7373
deserialize(message);
@@ -84,7 +84,7 @@ export function isEvent(message: Message): boolean {
8484
* @param {Message} message the incoming message
8585
* @return {CloudEvent} A new {CloudEvent} instance
8686
*/
87-
export function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
87+
function deserialize<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
8888
const cleanHeaders: Headers = sanitize(message.headers);
8989
const mode: Mode = getMode(cleanHeaders);
9090
const version = getVersion(mode, cleanHeaders, message.body);
@@ -261,3 +261,14 @@ function parseBatched<T>(message: Message): CloudEvent<T> | CloudEvent<T>[] {
261261
});
262262
return ret;
263263
}
264+
265+
/**
266+
* Bindings for HTTP transport support
267+
* @implements {@linkcode Binding}
268+
*/
269+
export const HTTP: Binding = {
270+
binary,
271+
structured,
272+
toEvent: deserialize,
273+
isEvent: isEvent,
274+
};

src/message/index.ts

Lines changed: 9 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,10 @@
44
*/
55

66
import { IncomingHttpHeaders } from "http";
7-
import { CloudEvent } from "..";
8-
import { binary, deserialize, structured, isEvent } from "./http";
7+
import { CloudEventV1 } from "..";
8+
9+
// reexport the HTTP protocol binding
10+
export * from "./http";
911

1012
/**
1113
* Binding is an interface for transport protocols to implement,
@@ -39,11 +41,11 @@ export interface Headers extends IncomingHttpHeaders {
3941
* transport-agnostic message
4042
* @interface
4143
* @property {@linkcode Headers} `headers` - the headers for the event Message
42-
* @property string `body` - the body of the event Message
44+
* @property {T | string | Buffer | unknown} `body` - the body of the event Message
4345
*/
44-
export interface Message {
46+
export interface Message<T = string> {
4547
headers: Headers;
46-
body: string | unknown;
48+
body: T | string | Buffer | unknown;
4749
}
4850

4951
/**
@@ -62,7 +64,7 @@ export enum Mode {
6264
* @interface
6365
*/
6466
export interface Serializer {
65-
<T>(event: CloudEvent<T>): Message;
67+
<T>(event: CloudEventV1<T>): Message;
6668
}
6769

6870
/**
@@ -71,7 +73,7 @@ export interface Serializer {
7173
* @interface
7274
*/
7375
export interface Deserializer {
74-
<T>(message: Message): CloudEvent<T> | CloudEvent<T>[];
76+
<T>(message: Message): CloudEventV1<T> | CloudEventV1<T>[];
7577
}
7678

7779
/**
@@ -82,14 +84,3 @@ export interface Deserializer {
8284
export interface Detector {
8385
(message: Message): boolean;
8486
}
85-
86-
/**
87-
* Bindings for HTTP transport support
88-
* @implements {@linkcode Binding}
89-
*/
90-
export const HTTP: Binding = {
91-
binary: binary as Serializer,
92-
structured: structured as Serializer,
93-
toEvent: deserialize as Deserializer,
94-
isEvent: isEvent as Detector,
95-
};

test/integration/sdk_test.ts

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
import "mocha";
77
import { expect } from "chai";
8-
import { CloudEvent, Version } from "../../src";
8+
import { CloudEvent, CloudEventV1, Version } from "../../src";
99

10-
const fixture = {
10+
const fixture: CloudEventV1<undefined> = {
11+
id: "123",
1112
type: "org.cloudevents.test",
1213
source: "http://cloudevents.io",
14+
specversion: Version.V1,
1315
};
1416

1517
describe("The SDK Requirements", () => {
@@ -34,4 +36,19 @@ describe("The SDK Requirements", () => {
3436
expect(new CloudEvent(fixture).specversion).to.equal(Version.V1);
3537
});
3638
});
39+
40+
describe("Cloning events", () => {
41+
it("should clone simple objects that adhere to the CloudEventV1 interface", () => {
42+
const copy = CloudEvent.cloneWith(fixture, { id: "456" }, false);
43+
expect(copy.id).to.equal("456");
44+
expect(copy.type).to.equal(fixture.type);
45+
expect(copy.source).to.equal(fixture.source);
46+
expect(copy.specversion).to.equal(fixture.specversion);
47+
});
48+
49+
it("should clone simple objects with data that adhere to the CloudEventV1 interface", () => {
50+
const copy = CloudEvent.cloneWith(fixture, { data: { lunch: "tacos" } }, false);
51+
expect(copy.data.lunch).to.equal("tacos");
52+
});
53+
});
3754
});

0 commit comments

Comments
 (0)