Signed-off-by: Philip Sanetra <code@psanetra.de>
This commit is contained in:
parent
c09a9cc20a
commit
2d5fab1b71
|
@ -91,3 +91,6 @@ typings/
|
|||
|
||||
# Package lock
|
||||
package-lock.json
|
||||
|
||||
# Jetbrains IDE directories
|
||||
.idea
|
||||
|
|
|
@ -28,11 +28,11 @@ app.post("/", (req, res) => {
|
|||
const responseEventMessage = new CloudEvent({
|
||||
source: '/',
|
||||
type: 'event:response',
|
||||
...event
|
||||
...event,
|
||||
data: {
|
||||
hello: 'world'
|
||||
}
|
||||
});
|
||||
responseEventMessage.data = {
|
||||
hello: 'world'
|
||||
};
|
||||
|
||||
// const message = HTTP.binary(responseEventMessage)
|
||||
const message = HTTP.structured(responseEventMessage)
|
||||
|
|
|
@ -9,7 +9,7 @@ import { Emitter } from "..";
|
|||
|
||||
import { CloudEventV1 } from "./interfaces";
|
||||
import { validateCloudEvent } from "./spec";
|
||||
import { ValidationError, isBinary, asBase64, isValidType } from "./validation";
|
||||
import { ValidationError, isBinary, asBase64, isValidType, base64AsBinary } from "./validation";
|
||||
|
||||
/**
|
||||
* An enum representing the CloudEvent specification version
|
||||
|
@ -33,7 +33,7 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
|
|||
dataschema?: string;
|
||||
subject?: string;
|
||||
time?: string;
|
||||
#_data?: T;
|
||||
data?: T;
|
||||
data_base64?: string;
|
||||
|
||||
// Extensions should not exist as it's own object, but instead
|
||||
|
@ -85,12 +85,21 @@ export class CloudEvent<T = undefined> implements CloudEventV1<T> {
|
|||
delete properties.dataschema;
|
||||
|
||||
this.data_base64 = properties.data_base64 as string;
|
||||
|
||||
if (this.data_base64) {
|
||||
this.data = base64AsBinary(this.data_base64) as unknown as T;
|
||||
}
|
||||
|
||||
delete properties.data_base64;
|
||||
|
||||
this.schemaurl = properties.schemaurl as string;
|
||||
delete properties.schemaurl;
|
||||
|
||||
this.data = properties.data;
|
||||
if (isBinary(properties.data)) {
|
||||
this.data_base64 = asBase64(properties.data as unknown as Buffer);
|
||||
}
|
||||
|
||||
this.data = typeof properties.data !== "undefined" ? properties.data : this.data;
|
||||
delete properties.data;
|
||||
|
||||
// sanity checking
|
||||
|
@ -127,17 +136,6 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
Object.freeze(this);
|
||||
}
|
||||
|
||||
get data(): T | undefined {
|
||||
return this.#_data;
|
||||
}
|
||||
|
||||
set data(value: T | undefined) {
|
||||
if (isBinary(value)) {
|
||||
this.data_base64 = asBase64(value as unknown as Buffer);
|
||||
}
|
||||
this.#_data = value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Used by JSON.stringify(). The name is confusing, but this method is called by
|
||||
* JSON.stringify() when converting this object to JSON.
|
||||
|
@ -147,7 +145,11 @@ 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 = this.#_data;
|
||||
|
||||
if (event.data_base64 && event.data) {
|
||||
delete event.data;
|
||||
}
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
|
@ -230,9 +232,6 @@ See: https://github.com/cloudevents/spec/blob/v1.0/spec.md#type-system`);
|
|||
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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,6 +8,19 @@ import { ErrorObject } from "ajv";
|
|||
export type TypeArray = Int8Array | Uint8Array | Int16Array | Uint16Array |
|
||||
Int32Array | Uint32Array | Uint8ClampedArray | Float32Array | Float64Array;
|
||||
|
||||
const globalThisPolyfill = (function() {
|
||||
try {
|
||||
return globalThis;
|
||||
}
|
||||
catch (e) {
|
||||
try {
|
||||
return self;
|
||||
}
|
||||
catch (e) {
|
||||
return global;
|
||||
}
|
||||
}
|
||||
}());
|
||||
|
||||
/**
|
||||
* An Error class that will be thrown when a CloudEvent
|
||||
|
@ -86,6 +99,14 @@ export const asBuffer = (value: string | Buffer | TypeArray): Buffer =>
|
|||
throw new TypeError("is not buffer or a valid binary");
|
||||
})();
|
||||
|
||||
export const base64AsBinary = (base64String: string): Uint8Array => {
|
||||
const toBinaryString = (base64Str: string): string => globalThisPolyfill.atob
|
||||
? globalThisPolyfill.atob(base64Str)
|
||||
: Buffer.from(base64Str, "base64").toString("binary");
|
||||
|
||||
return Uint8Array.from(toBinaryString(base64String), (c) => c.charCodeAt(0));
|
||||
};
|
||||
|
||||
export const asBase64 =
|
||||
(value: string | Buffer | TypeArray): string => asBuffer(value).toString("base64");
|
||||
|
||||
|
|
|
@ -4,6 +4,7 @@
|
|||
*/
|
||||
|
||||
import { Binding, Deserializer, CloudEvent, CloudEventV1, CONSTANTS, Message, ValidationError, Headers } from "../..";
|
||||
import { base64AsBinary } from "../../event/validation";
|
||||
|
||||
export {
|
||||
MQTT, MQTTMessageFactory
|
||||
|
@ -50,14 +51,16 @@ const MQTT: Binding = {
|
|||
* @implements {Serializer}
|
||||
*/
|
||||
function binary<T>(event: CloudEventV1<T>): MQTTMessage<T> {
|
||||
let properties;
|
||||
if (event instanceof CloudEvent) {
|
||||
properties = event.toJSON();
|
||||
} else {
|
||||
properties = event;
|
||||
const properties = { ...event };
|
||||
|
||||
let body = properties.data as T;
|
||||
|
||||
if (!body && properties.data_base64) {
|
||||
body = base64AsBinary(properties.data_base64) as unknown as T;
|
||||
}
|
||||
const body = properties.data as T;
|
||||
|
||||
delete properties.data;
|
||||
delete properties.data_base64;
|
||||
|
||||
return MQTTMessageFactory(event.datacontenttype as string, properties, body);
|
||||
}
|
||||
|
|
|
@ -7,24 +7,39 @@ import path from "path";
|
|||
import fs from "fs";
|
||||
|
||||
import { expect } from "chai";
|
||||
import { CloudEvent, ValidationError, Version } from "../../src";
|
||||
import { CloudEvent, CloudEventV1, ValidationError, Version } from "../../src";
|
||||
import { asBase64 } from "../../src/event/validation";
|
||||
|
||||
const type = "org.cncf.cloudevents.example";
|
||||
const source = "http://unit.test";
|
||||
const id = "b46cf653-d48a-4b90-8dfa-355c01061361";
|
||||
|
||||
const fixture = {
|
||||
const fixture = Object.freeze({
|
||||
id,
|
||||
specversion: Version.V1,
|
||||
source,
|
||||
type,
|
||||
data: `"some data"`,
|
||||
};
|
||||
data: `"some data"`
|
||||
});
|
||||
|
||||
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
|
||||
const image_base64 = asBase64(imageData);
|
||||
|
||||
// Do not replace this with the assignment of a class instance
|
||||
// as we just want to test if we can enumerate all explicitly defined fields!
|
||||
const cloudEventV1InterfaceFields: (keyof CloudEventV1<unknown>)[] = Object.keys({
|
||||
id: "",
|
||||
type: "",
|
||||
data: undefined,
|
||||
data_base64: "",
|
||||
source: "",
|
||||
time: "",
|
||||
datacontenttype: "",
|
||||
dataschema: "",
|
||||
specversion: "",
|
||||
subject: ""
|
||||
} as Required<CloudEventV1<unknown>>);
|
||||
|
||||
describe("A CloudEvent", () => {
|
||||
it("Can be constructed with a typed Message", () => {
|
||||
const ce = new CloudEvent(fixture);
|
||||
|
@ -78,6 +93,58 @@ describe("A CloudEvent", () => {
|
|||
new CloudEvent({ ExtensionWithCaps: "extension value", ...fixture });
|
||||
}).throw("invalid extension name");
|
||||
});
|
||||
|
||||
it("CloudEventV1 interface fields should be enumerable", () => {
|
||||
const classInstanceKeys = Object.keys(new CloudEvent({ ...fixture }));
|
||||
|
||||
for (const key of cloudEventV1InterfaceFields) {
|
||||
expect(classInstanceKeys).to.contain(key);
|
||||
}
|
||||
});
|
||||
|
||||
it("throws TypeError on trying to set any field value", () => {
|
||||
const ce = new CloudEvent({
|
||||
...fixture,
|
||||
mycustomfield: "initialValue"
|
||||
});
|
||||
|
||||
const keySet = new Set([...cloudEventV1InterfaceFields, ...Object.keys(ce)]);
|
||||
|
||||
expect(keySet).not.to.be.empty;
|
||||
|
||||
for (const cloudEventKey of keySet) {
|
||||
let threw = false;
|
||||
|
||||
try {
|
||||
ce[cloudEventKey] = "newValue";
|
||||
} catch (err) {
|
||||
threw = true;
|
||||
expect(err).to.be.instanceOf(TypeError);
|
||||
expect((err as TypeError).message).to.include("Cannot assign to read only property");
|
||||
}
|
||||
|
||||
if (!threw) {
|
||||
expect.fail(`Assigning a value to ${cloudEventKey} did not throw`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
describe("toJSON()", () => {
|
||||
it("does not return data field if data_base64 field is set to comply with JSON format spec 3.1.1", () => {
|
||||
const binaryData = new Uint8Array([1,2,3]);
|
||||
|
||||
const ce = new CloudEvent({
|
||||
...fixture,
|
||||
data: binaryData
|
||||
});
|
||||
|
||||
expect(ce.data).to.be.equal(binaryData);
|
||||
|
||||
const json = ce.toJSON();
|
||||
expect(json.data).to.not.exist;
|
||||
expect(json.data_base64).to.be.equal("AQID");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("A 1.0 CloudEvent", () => {
|
||||
|
|
|
@ -32,12 +32,12 @@ const ext2Name = "extension2";
|
|||
const ext2Value = "acme";
|
||||
|
||||
// Binary data as base64
|
||||
const dataBinary = Uint32Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
|
||||
const dataBinary = Uint8Array.from(JSON.stringify(data), (c) => c.codePointAt(0) as number);
|
||||
const data_base64 = asBase64(dataBinary);
|
||||
|
||||
// Since the above is a special case (string as binary), let's test
|
||||
// with a real binary file one is likely to encounter in the wild
|
||||
const imageData = new Uint32Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
|
||||
const imageData = new Uint8Array(fs.readFileSync(path.join(process.cwd(), "test", "integration", "ce.png")));
|
||||
const image_base64 = asBase64(imageData);
|
||||
|
||||
const PUBLISH = {"Content Type": "application/json; charset=utf-8"};
|
||||
|
@ -281,14 +281,14 @@ describe("MQTT transport", () => {
|
|||
|
||||
it("Converts base64 encoded data to binary when deserializing structured messages", () => {
|
||||
const message = MQTT.structured(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
||||
it("Converts base64 encoded data to binary when deserializing binary messages", () => {
|
||||
const message = MQTT.binary(fixture.cloneWith({ data: imageData, datacontenttype: "image/png" }));
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(imageData);
|
||||
expect(eventDeserialized.data_base64).to.equal(image_base64);
|
||||
});
|
||||
|
@ -302,7 +302,7 @@ describe("MQTT transport", () => {
|
|||
|
||||
it("Does not parse binary data from binary messages with content type application/json", () => {
|
||||
const message = MQTT.binary(fixture.cloneWith({ data: dataBinary }));
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint32Array>;
|
||||
const eventDeserialized = MQTT.toEvent(message) as CloudEvent<Uint8Array>;
|
||||
expect(eventDeserialized.data).to.deep.equal(dataBinary);
|
||||
expect(eventDeserialized.data_base64).to.equal(data_base64);
|
||||
});
|
||||
|
|
|
@ -19,7 +19,7 @@ const data = {
|
|||
};
|
||||
const subject = "subject-x0";
|
||||
|
||||
let cloudevent = new CloudEvent({
|
||||
const cloudevent = new CloudEvent({
|
||||
specversion: Version.V1,
|
||||
id,
|
||||
source,
|
||||
|
@ -120,8 +120,8 @@ describe("CloudEvents Spec v1.0", () => {
|
|||
});
|
||||
|
||||
it("defaut ID create when an empty string", () => {
|
||||
cloudevent = cloudevent.cloneWith({ id: "" });
|
||||
expect(cloudevent.id.length).to.be.greaterThan(0);
|
||||
const testEvent = cloudevent.cloneWith({ id: "" });
|
||||
expect(testEvent.id.length).to.be.greaterThan(0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -160,11 +160,11 @@ describe("CloudEvents Spec v1.0", () => {
|
|||
describe("'time'", () => {
|
||||
it("must adhere to the format specified in RFC 3339", () => {
|
||||
const d = new Date();
|
||||
cloudevent = cloudevent.cloneWith({ time: d.toString() }, false);
|
||||
const testEvent = cloudevent.cloneWith({ time: d.toString() }, false);
|
||||
// ensure that we always get back the same thing we passed in
|
||||
expect(cloudevent.time).to.equal(d.toString());
|
||||
expect(testEvent.time).to.equal(d.toString());
|
||||
// ensure that when stringified, the timestamp is in RFC3339 format
|
||||
expect(JSON.parse(JSON.stringify(cloudevent)).time).to.equal(new Date(d.toString()).toISOString());
|
||||
expect(JSON.parse(JSON.stringify(testEvent)).time).to.equal(new Date(d.toString()).toISOString());
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
Loading…
Reference in New Issue