Merge pull request #2 from fabiojose/start

The start of JavaScript SDK
This commit is contained in:
Fabio José 2018-12-03 17:30:05 -02:00 committed by GitHub
commit ff3ae0a343
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 720 additions and 0 deletions

77
.gitignore vendored Normal file
View File

@ -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/

232
README.md
View File

@ -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)

4
index.js Normal file
View File

@ -0,0 +1,4 @@
var Cloudevent = require('./lib/cloudevent.js');
module.exports = Cloudevent;

View File

@ -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;

73
lib/cloudevent.js Normal file
View File

@ -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;

14
lib/formats/json_0_1.js Normal file
View File

@ -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;

63
lib/specs/spec_0_1.js Normal file
View File

@ -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;

40
lib/specs/spec_0_2.js Normal file
View File

@ -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;

39
package.json Normal file
View File

@ -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"
}
}

View File

@ -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());
});
});
});
});
});

View File

@ -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');
});
});
});
});

50
test/http_binding_0_1.js Normal file
View File

@ -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());
});
});
});
});
});