From 54f242b79e03dbba382f5016a1279ddf392c354f Mon Sep 17 00:00:00 2001 From: Lance Ball Date: Wed, 6 May 2020 13:25:16 -0400 Subject: [PATCH] feat: expose a mode and version agnostic event receiver (#120) Event receivers in the wild may not always know what version or mode an incoming event is. Instead of requiring developers to inspect the headers themselves, the SDK should provide an HTTP receiver that is capable of figuring out what the version and mode (structured/binary) of an incoming event is and handle it appropriately. In determining the best way to expose this, I chose to modify the API a little bit. Now, instead of `const CloudEvent = require('cloudevents-sdk');` users need to destructure it. ```js const { HTTPReceiver, CloudEvent } = require('cloudevents-sdk'); ``` This change should not be backported to 1.x. Fixes: https://github.com/cloudevents/sdk-javascript/issues/93 Signed-off-by: Lance Ball --- index.js | 6 +- lib/bindings/http/http_receiver.js | 61 +++++++++++++ .../http/promiscuous_receiver_test.js | 90 +++++++++++++++++++ .../http/receiver_structured_1_test.js | 2 +- test/bindings/http/unmarshaller_0_3_tests.js | 2 +- test/spec_0_3_tests.js | 2 +- test/spec_1_tests.js | 2 +- 7 files changed, 160 insertions(+), 5 deletions(-) create mode 100644 lib/bindings/http/http_receiver.js create mode 100644 test/bindings/http/promiscuous_receiver_test.js diff --git a/index.js b/index.js index 67d9ac3..22e87aa 100644 --- a/index.js +++ b/index.js @@ -1,3 +1,7 @@ const CloudEvent = require("./lib/cloudevent.js"); +const HTTPReceiver = require("./lib/bindings/http/http_receiver.js"); -module.exports = CloudEvent; +module.exports = { + CloudEvent, + HTTPReceiver +}; diff --git a/lib/bindings/http/http_receiver.js b/lib/bindings/http/http_receiver.js new file mode 100644 index 0000000..0912864 --- /dev/null +++ b/lib/bindings/http/http_receiver.js @@ -0,0 +1,61 @@ +const V03Binary = require("./receiver_binary_0_3"); +const V03Structured = require("./receiver_structured_0_3.js"); +const V1Binary = require("./receiver_binary_1.js"); +const V1Structured = require("./receiver_structured_1.js"); +const constants = require("./constants"); + +class HTTPReceiver { + constructor() { + this.receivers = { + v1: { + structured: new V1Structured(), + binary: new V1Binary() + }, + v03: { + structured: new V03Structured(), + binary: new V03Binary() + } + }; + } + + accept(headers, body) { + const mode = getMode(headers); + const version = getVersion(mode, headers, body); + switch (version) { + case constants.SPEC_V1: + return this.receivers.v1[mode].parse(body, headers); + case constants.SPEC_V03: + return this.receivers.v03[mode].parse(body, headers); + default: + console.error( + `Unknown spec version ${version}. Default to ${constants.SPEC_V1}`); + return this.receivers.v1[mode].parse(body, headers); + } + } +} + +function getMode(headers) { + let mode = "binary"; + const contentType = headers[constants.HEADER_CONTENT_TYPE]; + if (contentType && contentType.startsWith(constants.MIME_CE)) { + mode = "structured"; + } + return mode; +} + +function getVersion(mode, headers, body) { + let version = constants.SPEC_V1; // default to 1.0 + + if (mode === "binary") { + // Check the headers for the version + const versionHeader = headers[constants.DEFAULT_SPEC_VERSION_HEADER]; + if (versionHeader) { version = versionHeader; } + } else { + // structured mode - the version is in the body + version = body instanceof String + ? JSON.parse(body).specversion : body.specversion; + } + return version; +} + +module.exports = HTTPReceiver; diff --git a/test/bindings/http/promiscuous_receiver_test.js b/test/bindings/http/promiscuous_receiver_test.js new file mode 100644 index 0000000..e550916 --- /dev/null +++ b/test/bindings/http/promiscuous_receiver_test.js @@ -0,0 +1,90 @@ +const { expect } = require("chai"); +const { CloudEvent, HTTPReceiver } = require("../../../index.js"); +const constants = require("../../../lib/bindings/http/constants.js"); + +const receiver = new HTTPReceiver(); +const id = "1234"; +const type = "org.cncf.cloudevents.test"; +const source = "urn:event:from:myapi/resourse/123"; +const data = { + lunch: "sushi" +}; + +describe("HTTP Transport Binding Receiver for CloudEvents", () => { + describe("V1", () => { + const specversion = "1.0"; + + it("Structured data returns a CloudEvent", () => { + const payload = { + id, + type, + source, + data, + specversion + }; + + const headers = { + [constants.HEADER_CONTENT_TYPE]: constants.MIME_CE_JSON + }; + + const event = receiver.accept(headers, payload); + validateEvent(event, specversion); + }); + + it("Binary data returns a CloudEvent", () => { + const headers = { + [constants.HEADER_CONTENT_TYPE]: constants.DEFAULT_CONTENT_TYPE, + [constants.DEFAULT_SPEC_VERSION_HEADER]: specversion, + [constants.BINARY_HEADERS_1.ID]: id, + [constants.BINARY_HEADERS_1.TYPE]: type, + [constants.BINARY_HEADERS_1.SOURCE]: source + }; + + const event = receiver.accept(headers, data); + validateEvent(event, specversion); + }); + }); + + describe("V03", () => { + const specversion = "0.3"; + + it("Structured data returns a CloudEvent", () => { + const payload = { + id, + type, + source, + data, + specversion + }; + + const headers = { + [constants.HEADER_CONTENT_TYPE]: constants.MIME_CE_JSON + }; + + const event = receiver.accept(headers, payload); + validateEvent(event, specversion); + }); + + it("Binary data returns a CloudEvent", () => { + const headers = { + [constants.HEADER_CONTENT_TYPE]: constants.DEFAULT_CONTENT_TYPE, + [constants.DEFAULT_SPEC_VERSION_HEADER]: specversion, + [constants.BINARY_HEADERS_03.ID]: id, + [constants.BINARY_HEADERS_03.TYPE]: type, + [constants.BINARY_HEADERS_03.SOURCE]: source + }; + + const event = receiver.accept(headers, data); + validateEvent(event, specversion); + }); + }); +}); + +function validateEvent(event, specversion) { + expect(event instanceof CloudEvent).to.equal(true); + expect(event.getId()).to.equal(id); + expect(event.getType()).to.equal(type); + expect(event.getSource()).to.equal(source); + expect(event.getData()).to.deep.equal(data); + expect(event.getSpecversion()).to.equal(specversion); +} diff --git a/test/bindings/http/receiver_structured_1_test.js b/test/bindings/http/receiver_structured_1_test.js index caee580..2a5e9ee 100644 --- a/test/bindings/http/receiver_structured_1_test.js +++ b/test/bindings/http/receiver_structured_1_test.js @@ -1,6 +1,6 @@ const expect = require("chai").expect; const v1 = require("../../../v1/index.js"); -const CloudEvent = require("../../../index.js"); +const { CloudEvent } = require("../../../index.js"); const { asBase64 } = require("../../../lib/utils/fun.js"); diff --git a/test/bindings/http/unmarshaller_0_3_tests.js b/test/bindings/http/unmarshaller_0_3_tests.js index 6bc3deb..b9c807b 100644 --- a/test/bindings/http/unmarshaller_0_3_tests.js +++ b/test/bindings/http/unmarshaller_0_3_tests.js @@ -1,6 +1,6 @@ const expect = require("chai").expect; const Unmarshaller = require("../../../lib/bindings/http/unmarshaller_0_3.js"); -const CloudEvent = require("../../../index.js"); +const { CloudEvent } = require("../../../index.js"); const v03 = require("../../../v03/index.js"); const type = "com.github.pull.create"; diff --git a/test/spec_0_3_tests.js b/test/spec_0_3_tests.js index ad6cfbf..2f96acc 100644 --- a/test/spec_0_3_tests.js +++ b/test/spec_0_3_tests.js @@ -1,6 +1,6 @@ const expect = require("chai").expect; const Spec03 = require("../lib/specs/spec_0_3.js"); -const CloudEvent = require("../index.js"); +const { CloudEvent } = require("../index.js"); const { v4: uuidv4 } = require("uuid"); const id = uuidv4(); diff --git a/test/spec_1_tests.js b/test/spec_1_tests.js index 71ace86..af211f2 100644 --- a/test/spec_1_tests.js +++ b/test/spec_1_tests.js @@ -1,6 +1,6 @@ const expect = require("chai").expect; const Spec1 = require("../lib/specs/spec_1.js"); -const CloudEvent = require("../index.js"); +const { CloudEvent } = require("../index.js"); const { v4: uuidv4 } = require("uuid"); const { asBase64 } = require("../lib/utils/fun.js");