Skip to content

Commit 1f8efc2

Browse files
committed
lib(cloudevent): make extensions a part of the event object
Signed-off-by: Lance Ball <[email protected]>
1 parent d3de2c5 commit 1f8efc2

File tree

15 files changed

+157
-197
lines changed

15 files changed

+157
-197
lines changed

src/event/cloudevent.ts

Lines changed: 52 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ const { v4: uuidv4 } = require("uuid");
22

33
import { CloudEventV1, validateV1, CloudEventV1Attributes } from "./v1";
44
import { CloudEventV03, validateV03, CloudEventV03Attributes } from "./v03";
5-
import Extensions from "./extensions";
5+
import { ValidationError } from "./validation";
66

77
export const enum Version { V1 = "1.0", V03 = "0.3" };
88

9+
910
/**
1011
* A CloudEvent describes event data in common formats to provide
1112
* interoperability across services, platforms and systems.
@@ -22,48 +23,77 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
2223
time?: string|Date;
2324
data?: any;
2425
data_base64?: any;
25-
extensions?: Extensions;
26+
[key: string]: any; // Extensions should not exist as it's own object, but instead be keyed directly from the event
2627

2728
// V03 deprecated attributes
2829
schemaurl?: string;
2930
datacontentencoding?: string;
3031

3132
constructor(event: CloudEventV1 | CloudEventV1Attributes | CloudEventV03 | CloudEventV03Attributes) {
33+
// copy the incoming event so that we can delete properties as we go
34+
// everything left after we have deleted know properties becomes an extension
35+
let properties = { ...event };
36+
3237
// @ts-ignore Attribute types don't have an ID
33-
this.id = event.id || uuidv4();
34-
this.type = event.type;
35-
this.source = event.source;
38+
this.id = properties.id || uuidv4();
39+
delete properties.id;
40+
41+
this.type = properties.type;
42+
delete properties.type;
43+
44+
this.source = properties.source;
45+
delete properties.source;
46+
3647
// @ts-ignore Attribute types don't have a specversion
37-
this.specversion = event.specversion as Version || Version.V1;
38-
this.datacontenttype = event.datacontenttype;
39-
this.subject = event.subject;
40-
this.time = event.time;
41-
this.data = event.data;
48+
this.specversion = properties.specversion as Version || Version.V1;
49+
delete properties.specversion;
50+
51+
this.datacontenttype = properties.datacontenttype;
52+
delete properties.datacontenttype;
53+
54+
this.subject = properties.subject;
55+
delete properties.subject;
56+
57+
this.time = properties.time;
58+
delete properties.time;
59+
60+
this.data = properties.data;
61+
delete properties.data;
62+
4263
// @ts-ignore - dataschema is not on a CloudEventV03
43-
this.dataschema = event.dataschema;
64+
this.dataschema = properties.dataschema;
65+
delete properties.dataschema;
66+
4467
// @ts-ignore - dataBase64 is not on CloudEventV03
45-
this.data_base64 = event.data_base64;
68+
this.data_base64 = properties.data_base64;
69+
delete properties.data_base64;
4670

71+
// @ts-ignore - dataContentEncoding is not on a CloudEventV1
72+
this.datacontentencoding = properties.datacontentencoding;
73+
delete properties.datacontentencoding;
74+
75+
// @ts-ignore - schemaurl is not on a CloudEventV1
76+
this.schemaurl = properties.schemaurl;
77+
delete properties.schemaurl;
78+
79+
// Make sure time has a default value and whatever is provided is formatted
4780
if (!this.time) {
4881
this.time = new Date().toISOString();
4982
} else if (this.time instanceof Date) {
5083
this.time = this.time.toISOString();
5184
}
5285

53-
// @ts-ignore - dataContentEncoding is not on a CloudEventV1
54-
this.datacontentencoding = event.datacontentencoding;
55-
// @ts-ignore - schemaurl is not on a CloudEventV1
56-
this.schemaurl = event.schemaurl;
57-
58-
this.extensions = { ...event.extensions };
59-
60-
// TODO: Deprecated in 1.0
6186
// sanity checking
6287
if (this.specversion === Version.V1 && this.schemaurl) {
6388
throw new TypeError("cannot set schemaurl on version 1.0 event");
6489
} else if (this.specversion === Version.V03 && this.dataschema) {
6590
throw new TypeError("cannot set dataschema on version 0.3 event");
6691
}
92+
93+
// finally process any remaining properties - these are extensions
94+
for (let [key, value] of Object.entries(properties)) {
95+
this[key] = value;
96+
}
6797
}
6898

6999
/**
@@ -76,6 +106,8 @@ export class CloudEvent implements CloudEventV1, CloudEventV03 {
76106
return validateV1(this);
77107
} else if (this.specversion === Version.V03) {
78108
return validateV03(this);
109+
} else {
110+
throw new ValidationError("invalid payload");
79111
}
80112
return false;
81113
}

src/event/extensions.ts

Lines changed: 0 additions & 11 deletions
This file was deleted.

src/event/index.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
export * from "./cloudevent";
2-
export * from "./extensions";
32
export * from "./validation";
43
export * from "./v1";
54
export * from "./v03";

src/event/v03/cloudevent.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
import Extensions from "../extensions";
2-
31
/**
42
* The object interface for CloudEvents 0.3.
53
* @see https://github.com/cloudevents/spec/blob/v0.3/spec.md
@@ -132,5 +130,5 @@ export interface CloudEventV03Attributes {
132130
/**
133131
* [OPTIONAL] CloudEvents extension attributes.
134132
*/
135-
extensions?: Extensions;
133+
[key: string]: any
136134
}

src/event/v03/schema.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -39,9 +39,6 @@ const schemaV03 = {
3939
type: {
4040
$ref: "#/definitions/type"
4141
},
42-
extensions: {
43-
$ref: "#/definitions/extensions"
44-
},
4542
source: {
4643
$ref: "#/definitions/source"
4744
}
@@ -74,9 +71,6 @@ const schemaV03 = {
7471
type: "string",
7572
minLength: 1
7673
},
77-
extensions: {
78-
type: "object"
79-
},
8074
source: {
8175
format: "uri-reference",
8276
type: "string"

src/event/v03/spec.ts

Lines changed: 16 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,11 @@
11
import { v4 as uuidv4 } from "uuid";
22
import Ajv from "ajv";
3-
import { ValidationError, isValidType } from "../validation";
3+
import { ValidationError, isValidType, isBase64 } from "../validation";
44
import { CloudEventV03, CloudEventV03Attributes } from "./cloudevent";
55
import { CloudEvent } from "../..";
6-
7-
const RESERVED_ATTRIBUTES = {
8-
type: "type",
9-
specversion: "specversion",
10-
source: "source",
11-
id: "id",
12-
time: "time",
13-
schemaurl: "schemaurl",
14-
datacontentencoding: "datacontentencoding",
15-
datacontenttype: "datacontenttype",
16-
subject: "subject",
17-
data: "data"
18-
};
19-
206
import { schema } from "./schema";
21-
import Extensions from "../extensions";
7+
import CONSTANTS from "../../constants";
8+
229
const ajv = new Ajv({ extendRefs: true });
2310
const isValidAgainstSchema = ajv.compile(schema);
2411

@@ -36,14 +23,21 @@ export function validateV03(event: CloudEventV03): boolean {
3623
if (!isValidAgainstSchema(event)) {
3724
throw new ValidationError("invalid payload", isValidAgainstSchema.errors);
3825
}
39-
if (event.extensions) checkExtensions(event.extensions);
40-
return true;
26+
return checkDataContentEncoding(event);
4127
}
4228

43-
function checkExtensions(extensions: Extensions) {
44-
for (const key in extensions) {
45-
if (Object.prototype.hasOwnProperty.call(RESERVED_ATTRIBUTES, key)) {
46-
throw new ValidationError(`Reserved attribute name: '${key}'`);
29+
function checkDataContentEncoding(event: CloudEventV03): boolean {
30+
if (event.datacontentencoding) {
31+
// we only support base64
32+
const encoding = event.datacontentencoding.toLocaleLowerCase("en-US");
33+
if (encoding !== CONSTANTS.ENCODING_BASE64) {
34+
throw new ValidationError("invalid payload", [`Unsupported content encoding: ${encoding}`]);
35+
} else {
36+
if (!isBase64(event.data)) {
37+
throw new ValidationError("invalid payload", [`Invalid content encoding of data: ${event.data}`]);
38+
}
4739
}
4840
}
41+
return true;
4942
}
43+

src/event/v1/cloudevent.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import Extensions from "../extensions";
21
/**
32
* The object interface for CloudEvents 1.0.
43
* @see https://github.com/cloudevents/spec/blob/v1.0/spec.md
@@ -128,9 +127,10 @@ export interface CloudEventV1Attributes {
128127
* data is in binary form.
129128
* @see https://github.com/cloudevents/spec/blob/v1.0/json-format.md#31-handling-of-data
130129
*/
131-
data_base64?: string
130+
data_base64?: string;
131+
132132
/**
133133
* [OPTIONAL] CloudEvents extension attributes.
134134
*/
135-
extensions?: Extensions;
135+
[key: string]: any;
136136
}

src/event/v1/spec.ts

Lines changed: 0 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -5,25 +5,11 @@ import { CloudEvent } from "../cloudevent";
55
import { CloudEventV1, CloudEventV1Attributes } from "./cloudevent";
66
import { ValidationError, isValidType } from "../validation";
77

8-
import Extensions from "../extensions";
98
import { schemaV1 } from "./schema";
109

1110
const ajv = new Ajv({ extendRefs: true });
1211
const isValidAgainstSchema = ajv.compile(schemaV1);
1312

14-
const RESERVED_ATTRIBUTES = {
15-
type: "type",
16-
specversion: "specversion",
17-
source: "source",
18-
id: "id",
19-
time: "time",
20-
dataschema: "dataschema",
21-
datacontenttype: "datacontenttype",
22-
subject: "subject",
23-
data: "data",
24-
data_base64: "data_base64"
25-
};
26-
2713
export function createV1(attributes: CloudEventV1Attributes): CloudEventV1 {
2814
const event: CloudEventV1 = {
2915
specversion: schemaV1.definitions.specversion.const,
@@ -38,14 +24,5 @@ export function validateV1(event: CloudEventV1): boolean {
3824
if (!isValidAgainstSchema(event)) {
3925
throw new ValidationError("invalid payload", isValidAgainstSchema.errors);
4026
}
41-
if (event.extensions) checkExtensions(event.extensions);
4227
return true;
4328
}
44-
45-
function checkExtensions(extensions: Extensions) {
46-
for (const key in extensions) {
47-
if (Object.prototype.hasOwnProperty.call(RESERVED_ATTRIBUTES, key) && extensions[key]) {
48-
throw new ValidationError(`Reserved attribute name: '${key}'`);
49-
}
50-
}
51-
}

src/transport/http/binary_receiver.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ export class BinaryHTTPReceiver {
4040
// Clone and low case all headers names
4141
const sanitizedHeaders = validate(headers);
4242

43-
const eventObj: { [key: string]: any } = { extensions: {} };
43+
const eventObj: { [key: string]: any } = {};
4444
const parserMap = this.version === Version.V1 ? v1Parsers : v03Parsers;
4545

4646
parserMap.forEach((mappedParser: MappedParser, header: string) => {
@@ -56,7 +56,7 @@ export class BinaryHTTPReceiver {
5656
// Every unprocessed header can be an extension
5757
for (const header in sanitizedHeaders) {
5858
if (header.startsWith(CONSTANTS.EXTENSIONS_PREFIX)) {
59-
eventObj.extensions[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = sanitizedHeaders[header];
59+
eventObj[header.substring(CONSTANTS.EXTENSIONS_PREFIX.length)] = sanitizedHeaders[header];
6060
}
6161
}
6262

src/transport/http/headers.ts

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import { ValidationError, CloudEvent } from "../..";
2-
import { headerMap as v1Map } from "./v1";
2+
import { headerMap as v1Map, headerMap } from "./v1";
33
import { headerMap as v03Map } from "./v03";
44
import { Version } from "../../event";
55
import { MappedParser } from "../../parsers";
@@ -44,28 +44,24 @@ export function validate(headers: Headers): Headers {
4444
*/
4545
export function headersFor(event: CloudEvent): Headers {
4646
const headers: Headers = {};
47-
let headerMap;
47+
let headerMap: Map<string, MappedParser>;
4848
if (event.specversion === Version.V1) {
4949
headerMap = v1Map;
5050
} else {
5151
headerMap = v03Map;
5252
}
5353

54-
headerMap.forEach((mapped: MappedParser, getterName: string) => {
55-
// @ts-ignore No index signature with a parameter of type 'string' was found on type 'CloudEvent'.
56-
const value = event[getterName];
54+
// iterate over the event properties - generate a header for each
55+
Object.getOwnPropertyNames(event).forEach((property) => {
56+
const value = event[property];
5757
if (value) {
58-
headers[mapped.name] = mapped.parser.parse(value);
59-
}
60-
if (event.extensions) {
61-
Object.keys(event.extensions)
62-
.filter((ext) => Object.hasOwnProperty.call(event.extensions, ext))
63-
.forEach((ext) => {
64-
headers[`ce-${ext}`] = event.extensions![ext];
65-
});
58+
const map: MappedParser|undefined = headerMap.get(property);
59+
if (map) { headers[map.name] = map.parser.parse(value); }
60+
else if (property !== CONSTANTS.DATA_ATTRIBUTE && property !== `${CONSTANTS.DATA_ATTRIBUTE}_base64`) {
61+
headers[`${CONSTANTS.EXTENSIONS_PREFIX}${property}`] = value;
62+
}
6663
}
6764
});
68-
6965
return headers;
7066
}
7167

0 commit comments

Comments
 (0)