diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..8c8b239 --- /dev/null +++ b/.gitignore @@ -0,0 +1,77 @@ +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# TypeScript v1 declaration files +typings/ + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variables file +.env + +# parcel-bundler cache (https://parceljs.org/) +.cache + +# next.js build output +.next + +# nuxt.js build output +.nuxt + +# vuepress build output +.vuepress/dist + +# Serverless directories +.serverless + +# FuseBox cache +.fusebox/ + diff --git a/README.md b/README.md index 5ba39f8..47eb0a9 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,234 @@ # sdk-javascript Javascript SDK for CloudEvents + +> This is a WIP + +# Repository Structure + +```text +├── index.js +├── lib +│   ├── bindings +│   │   └── http +│   │   └── structured_0_1.js +│   ├── cloudevent.js +│   ├── format +│   │   └── json_0_1.js +│   └── specs +│   ├── spec_0_1.js +│   └── spec_0_2.js +├── LICENSE +├── package.json +├── README.md +└── test + ├── cloudevent_spec_0_1.js + ├── cloudevent_spec_0_2.js + └── http_binding_0_1.js +``` + +* `index.js`: library exports + +* `lib/bindings`: every binding implementation goes here + +* `lib/bindings/http`: every http binding implementation goes here + +* `lib/bindings/http/structured_0_1.js`: implementation of structured HTTP Binding + +* `lib/cloudevent.js`: implementation of Cloudevent, an interface + +* `lib/format/`: every format implementation goes here + +* `lib/format/json_0_1.js`: implementation for JSON formatting [version 0.1](https://github.com/cloudevents/spec/blob/v0.1/json-format.md) + +* `lib/specs/`: every spec implementation goes here + +* `lib/specs/spec_0_1.js`: implementation for spec [version 0.1](https://github.com/cloudevents/spec/blob/v0.1/spec.md) + +* `lib/specs/spec_0_2.js`: implementation for spec [version 0.2](https://github.com/cloudevents/spec/blob/master/spec.md) + +* `test/cloudevent_spec_0_1.js`: unit testing for spec 0.1 + +* `test/cloudevent_spec_0_2.js`: unit testing for spec 0.2 + +# Unit Testing + +The unit test checks the result of formatted payload and the constraints. + +```bash + +npm test + +``` + +# The API + +## `Cloudevent` class + +```js + +/* + * Format the payload and return an Object. + */ +Object Cloudevent.format() + +/* + * Format the payload as String. + */ +String Cloudevent.toString() + +``` + +## `Formatter` classes + +Every formatter class must implement these methods to work properly. + +```js + +/* + * Format the Cloudevent payload argument and return an Object. + */ +Object Formatter.format(payload) + +/* + * Format the Cloudevent payload as String. + */ +String Formatter.toString(payload) + +``` + +## `Spec` classes + +Every Spec class must implement these methods to work properly. + +```js + +/* + * The constructor must receives the Cloudevent type. + */ +Spec(Cloudevent) + +/* + * Checks the spec constraints, throwing an error if do not pass. + */ +Spec.check() + +``` +## `Binding` classes + +Every Binding class must implement these methods to work properly. + +```js + +/* + * The constructor must receives the map of configurations. + */ +Binding(config) + +/* + * Emits the event using an instance of Cloudevent. + */ +Binding.emit(cloudevent) + +``` + +# How to use + +The `Cloudevent` constructor arguments. + +```js + +/* + * spec : if is null, set the spec 0.1 impl + * format: if is null, set the JSON Format 0.1 impl + */ +Cloudevent(spec, format); + +``` + +## How to construct instances? + +```js +/* + * Constructs a default instance with: + * - Spec 0.1 + * - JSON Format 0.1 + */ +var cloudevent01 = new Cloudevent(); + +/* + * Implemented using Builder Design Pattern + */ +cloudevent01 + .type("com.github.pull.create") + .source("urn:event:from:myapi/resourse/123"); + +/* + * Backward compatibility by injecting methods from spec implementation to Cloudevent + */ +cloudevent01 + .eventTypeVersion("1.0"); + +/* + * Constructs an instance with: + * - Spec 0.2 + * - JSON Format 0.1 + */ +var cloudevent02 = new Cloudevent(Cloudevent.specs['0.2']); + +/* + * Different specs, but the same API. + */ +cloudevent02 + .type("com.github.pull.create") + .source("urn:event:from:myapi/resourse/123"); + +``` + +## How to get the formatted payload? + +```js +var cloudevent = new Cloudevent() + .type("com.github.pull.create") + .source("urn:event:from:myapi/resourse/123"); + +/* + * Format the payload and return it. + */ +var formatted = cloudevent.format(); + +``` + +## How to emit an event? + +```js +// The event +var cloudevent = new Cloudevent() + .type("com.github.pull.create") + .source("urn:event:from:myapi/resourse/123"); + +// The binding configuration using POST +var config = { + method: 'POST', + url : 'https://mywebhook.com' +}; + +// The binding instance +var binding = Cloudevent.bindings['http-structured0.1'](config); + +// Emit the event using Promise +binding.emit(cloudevent) + .then(response => { + // Treat the response + console.log(response.data); + + }).catch(err => { + // Treat the error + console.error(err); + }); +``` + +> See how to implement the method injection [here](lib/specs/spec_0_1.js#L17) +> +> Learn about [Builder Design Pattern](https://en.wikipedia.org/wiki/Builder_pattern) +> +> Check out the produced event payload using this [tool](https://webhook.site) diff --git a/index.js b/index.js new file mode 100644 index 0000000..081f835 --- /dev/null +++ b/index.js @@ -0,0 +1,4 @@ +var Cloudevent = require('./lib/cloudevent.js'); + +module.exports = Cloudevent; + diff --git a/lib/bindings/http/structured_0_1.js b/lib/bindings/http/structured_0_1.js new file mode 100644 index 0000000..0502173 --- /dev/null +++ b/lib/bindings/http/structured_0_1.js @@ -0,0 +1,24 @@ +var axios = require("axios"); + +function HTTPStructured(configuration){ + this.config = configuration; + + this.config['headers'] = { + 'Content-Type':'application/cloudevents+json; charset=utf-8' + }; +} + +HTTPStructured.prototype.emit = function(cloudevent){ + + // Create new request object + var _config = JSON.parse(JSON.stringify(this.config)); + + // Set the cloudevent payload + _config['data'] = cloudevent.format(); + + // Return the Promise + return axios.request(_config); +} + +module.exports = HTTPStructured; + diff --git a/lib/cloudevent.js b/lib/cloudevent.js new file mode 100644 index 0000000..e8acf5f --- /dev/null +++ b/lib/cloudevent.js @@ -0,0 +1,73 @@ +var Spec_0_1 = require('./specs/spec_0_1.js'); +var Spec_0_2 = require('./specs/spec_0_2.js'); +var JSONFormatter_0_1 = require('./formats/json_0_1.js'); +var HTTPStructured_0_1 = require('./bindings/http/structured_0_1.js'); + +/* + * Class created using the Builder Design Pattern. + * + * https://en.wikipedia.org/wiki/Builder_pattern + */ +function Cloudevent(_spec, _formatter){ + this.spec = (_spec) ? new _spec(Cloudevent) : new Spec_0_1(Cloudevent); + this.formatter = (_formatter) ? _formatter : new JSONFormatter_0_1(); +} + +/* + * To format the payload using the formatter + */ +Cloudevent.prototype.format = function(){ + // Check the constraints + this.spec.check(); + + // Then, format + return this.formatter.format(this.spec.payload); +} + +Cloudevent.prototype.toString = function(){ + return this.formatter.toString(this.spec.payload); +} + +Cloudevent.prototype.type = function(type){ + this.spec.type(type); + return this; +} + +Cloudevent.prototype.source = function(_source){ + this.spec.source(_source); + return this; +} + +Cloudevent.prototype.id = function(_id){ + this.spec.id(_id); + return this; +} + +Cloudevent.prototype.time = function(_time){ + this.spec.time(_time); + return this; +} + +/* + * Export the specs + */ +Cloudevent.specs = { + '0.1': Spec_0_1, + '0.2': Spec_0_2 +}; + +/* + * Export the formats + */ +Cloudevent.formats = { + 'json' : JSONFormatter_0_1, + 'json0.1': JSONFormatter_0_1 +}; + +Cloudevent.bindings = { + 'http-structured' : HTTPStructured_0_1, + 'http-structured0.1' : HTTPStructured_0_1 +}; + +module.exports = Cloudevent; + diff --git a/lib/formats/json_0_1.js b/lib/formats/json_0_1.js new file mode 100644 index 0000000..7f0ca46 --- /dev/null +++ b/lib/formats/json_0_1.js @@ -0,0 +1,14 @@ + +function JSONFormatter(){ + +} + +JSONFormatter.prototype.format = function(payload){ + return payload; +} + +JSONFormatter.prototype.toString = function(payload){ + return JSON.stringify(payload); +} + +module.exports = JSONFormatter; diff --git a/lib/specs/spec_0_1.js b/lib/specs/spec_0_1.js new file mode 100644 index 0000000..3933e40 --- /dev/null +++ b/lib/specs/spec_0_1.js @@ -0,0 +1,63 @@ +var uuid = require('uuid/v4'); + +function Spec_0_1(_caller){ + this.payload = { + cloudEventsVersion: '0.1', + eventID: uuid() + }; + + /* + * Used to inject backward compatibility functions or attributes. + */ + this.caller = _caller; + + /* + * Inject the method to set the version related to data attribute. + */ + this.caller.prototype.eventTypeVersion = function(_version){ + this.spec.eventTypeVersion(_version); + } +} + +/* + * Check the constraints. + * + * throw an error if do not pass. + */ +Spec_0_1.prototype.check = function() { + + if(!this.payload['eventType']){ + throw {message: "'eventType' is invalid"}; + } + +} + +Spec_0_1.prototype.type = function(_type){ + this.payload['eventType'] = _type; + return this; +} + +Spec_0_1.prototype.eventTypeVersion = function(version){ + this.payload['eventTypeVersion'] = version; + return this; +} + +Spec_0_1.prototype.source = function(_source){ + this.payload['source'] = _source; + return this; +} + +Spec_0_1.prototype.id = function(_id){ + this.payload['eventID'] = _id; + return this; +} + +Spec_0_1.prototype.time = function(_time){ + this.payload['eventTime'] = _time.toISOString(); + return this; +} + +//TODO another attributes . . . + +module.exports = Spec_0_1; + diff --git a/lib/specs/spec_0_2.js b/lib/specs/spec_0_2.js new file mode 100644 index 0000000..94ca6bb --- /dev/null +++ b/lib/specs/spec_0_2.js @@ -0,0 +1,40 @@ +var uuid = require('uuid/v4'); + +function Spec_0_2(){ + this.payload = { + specversion: '0.2', + id: uuid() + }; +} + +/* + * Check the spec constraints. + */ +Spec_0_2.prototype.check = function(){ + +} + +Spec_0_2.prototype.type = function(_type){ + this.payload['type'] = _type; + return this; +} + +Spec_0_2.prototype.source = function(_source){ + this.payload['source'] = _source; + return this; +} + +Spec_0_2.prototype.id = function(_id){ + this.payload['id'] = _id; + return this; +} + +Spec_0_2.prototype.time = function(_time){ + this.payload['time'] = _time.toISOString(); + return this; +} + +//TODO another attributes . . . + +module.exports = Spec_0_2; + diff --git a/package.json b/package.json new file mode 100644 index 0000000..0a36443 --- /dev/null +++ b/package.json @@ -0,0 +1,39 @@ +{ + "name": "cloudevents", + "version": "0.0.1", + "description": "CloudEvents SDK for JavaScript", + "main": "index.js", + "scripts": { + "test": "./node_modules/.bin/mocha -C test/*.js" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/cloudevents/sdk-javascript.git" + }, + "keywords": [ + "events", + "cloudevents", + "sdk" + ], + "author": "cloudevents.io", + "contributors": { + "name": "Fábio José de Moraes", + "email": "fabiojose@gmail.com", + "url": "https://github.com/fabiojose" + }, + "license": "Apache-2.0", + "bugs": { + "url": "https://github.com/cloudevents/sdk-javascript/issues" + }, + "homepage": "https://github.com/cloudevents/sdk-javascript#readme", + "dependencies": { + "axios": "0.18.0", + "uuid": "3.3.2" + }, + "devDependencies": { + "chai": "4.2.0", + "mocha": "5.2.0", + "chai-http": "4.2.0", + "nock": "10.0.2" + } +} diff --git a/test/cloudevent_spec_0_1.js b/test/cloudevent_spec_0_1.js new file mode 100644 index 0000000..66bf5b1 --- /dev/null +++ b/test/cloudevent_spec_0_1.js @@ -0,0 +1,72 @@ +var expect = require("chai").expect; +var Cloudevent = require("../index.js"); + +const type = "com.github.pull.create"; +const source = "urn:event:from:myapi/resourse/123"; +const time = new Date(); + +var cloudevent = new Cloudevent() + .type(type) + .source(source); + +describe("CloudEvents Spec 0.1 - JavaScript SDK", () => { + + describe("JSON Format", () => { + + describe("Required context attributes", () => { + it("requires 'eventType'", () => { + expect(cloudevent.format()).to.have.property('eventType'); + }); + + it("requires 'cloudEventsVersion'", () => { + expect(cloudevent.format()).to.have.property('cloudEventsVersion'); + }); + + it("requires 'source'", () => { + expect(cloudevent.format()).to.have.property('source'); + }); + + it("requires 'eventID'", () => { + expect(cloudevent.format()).to.have.property('eventID'); + }); + }); + + describe("Backward compatibility", () => { + it("should have 'eventTypeVersion'", () => { + cloudevent.eventTypeVersion("1.0"); + expect(cloudevent.format()).to.have.property('eventTypeVersion'); + }); + }); + + describe("The Constraint check", () => { + describe("'eventType'", () => { + it("should throw an error when is an empty string", () => { + cloudevent.type(""); + expect(cloudevent.format.bind(cloudevent)) + .to + .throw("'eventType' is invalid"); + }); + + it("must be a non-empty string", () => { + cloudevent.type(type); + cloudevent.format(); + }); + + it("should be prefixed with a reverse-DNS name", () => { + //TODO how to assert it? + }); + }); + + //TODO another attributes . . . + + describe("'eventTime'", () => { + it("must adhere to the format specified in RFC 3339", () => { + cloudevent.time(time); + expect(cloudevent.format()['eventTime']).to.equal(time.toISOString()); + }); + }); + }); + + }); + +}); diff --git a/test/cloudevent_spec_0_2.js b/test/cloudevent_spec_0_2.js new file mode 100644 index 0000000..a94faa1 --- /dev/null +++ b/test/cloudevent_spec_0_2.js @@ -0,0 +1,32 @@ +var expect = require("chai").expect; +var Cloudevent = require("../index.js"); + +var cloudevent = new Cloudevent(Cloudevent.specs['0.2']) + .type("com.github.pull.create") + .source("urn:event:from:myapi/resourse/123"); + +describe("CloudEvents Spec 0.2 - JavaScript SDK", () => { + + describe("JSON Format", () => { + + describe("Required context attributes", () => { + it("requires 'type'", () => { + expect(cloudevent.format()).to.have.property('type'); + }); + + it("requires 'specversion'", () => { + expect(cloudevent.format()).to.have.property('specversion'); + }); + + it("requires 'source'", () => { + expect(cloudevent.format()).to.have.property('source'); + }); + + it("requires 'id'", () => { + expect(cloudevent.format()).to.have.property('id'); + }); + }); + + }); + +}); diff --git a/test/http_binding_0_1.js b/test/http_binding_0_1.js new file mode 100644 index 0000000..4b34965 --- /dev/null +++ b/test/http_binding_0_1.js @@ -0,0 +1,50 @@ +var expect = require("chai").expect; +var Cloudevent = require("../index.js"); +var nock = require("nock"); + +const type = "com.github.pull.create"; +const source = "urn:event:from:myapi/resourse/123"; +const webhook = "https://cloudevents.io/webhook"; +const contentType = "application/cloudevents+json; charset=utf-8"; + +var cloudevent = new Cloudevent() + .type(type) + .source(source); + +var httpcfg = { + method : 'POST', + url : webhook + '/json' +}; + +var httpstructured_0_1 = + new Cloudevent.bindings['http-structured0.1'](httpcfg); + +describe("HTTP Transport Binding - Version 0.1", () => { + beforeEach(() => { + // Mocking the webhook + nock(webhook) + .post("/json") + .reply(201, {status: 'accepted'}); + }); + + describe("Structured", () => { + describe("JSON Format", () => { + it("requires '" + contentType + "' Content-Type in header", () => { + return httpstructured_0_1.emit(cloudevent) + .then(response => { + expect(response.config.headers['Content-Type']) + .to.equal(contentType); + }); + }); + + it("the request should be correct", () => { + return httpstructured_0_1.emit(cloudevent) + .then(response => { + expect(JSON.parse(response.config.data)) + .to.deep.equal(cloudevent.format()); + }); + }); + }); + }); +}); +