Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chore(refactor): prefer interfaces over concrete classes #457

Merged
merged 3 commits into from
Jan 7, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 26 additions & 8 deletions src/event/cloudevent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note: It's not clear why we originally were unsetting the data property in this function if it was in binary form. But I don't think we should be doing that. Making this change had no effect on the tests. Happy to discuss what the reason is for this code as it was if anyone recalls.

return event;
}

Expand Down Expand Up @@ -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);
}

/**
Expand All @@ -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 changes: 2 additions & 2 deletions src/message/http/headers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand All @@ -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) {
Expand Down
23 changes: 17 additions & 6 deletions src/message/http/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

import { CloudEvent, CloudEventV1, CONSTANTS, Mode, Version } from "../..";
import { Message, Headers } from "..";
import { Message, Headers, Binding } from "..";

import {
headersFor,
Expand All @@ -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;
Expand All @@ -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: {
Expand All @@ -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);
Expand All @@ -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);
Expand Down Expand Up @@ -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,
};
27 changes: 9 additions & 18 deletions src/message/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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;
}

/**
Expand All @@ -62,7 +64,7 @@ export enum Mode {
* @interface
*/
export interface Serializer {
<T>(event: CloudEvent<T>): Message;
<T>(event: CloudEventV1<T>): Message;
}

/**
Expand All @@ -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>[];
}

/**
Expand All @@ -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,
};
21 changes: 19 additions & 2 deletions test/integration/sdk_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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");
});
});
});