Skip to content

Commit

Permalink
fixup! lib: implement API versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
lundibundi committed Jun 27, 2017
1 parent 16c9448 commit d99092e
Show file tree
Hide file tree
Showing 5 changed files with 97 additions and 22 deletions.
18 changes: 11 additions & 7 deletions lib/applications.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const semver = require('semver');

const errors = require('./errors');

const apps = {};
Expand All @@ -8,14 +10,16 @@ module.exports = apps;
// Generic application class. You are free to substitute it with whatever suits
// your needs.
// name - application name
// api - application API
// version - api version of 'name' application
// api - object that contains interfaces as its fields each of which
// contains functions by method names. Each method has the following
// signature (connection, <0 or more method arguments>, callback)
// version - application version of 'name' application
//
class Application {
constructor(name, api, version) {
this.name = name;
[this.name, this.version] = name.split('@');
if (!semver.valid(this.version)) this.version = version;
this.api = api;
this.version = version;
}

// Call application method
Expand Down Expand Up @@ -69,19 +73,19 @@ apps.createAppsIndex = (applications) => {
}
if (!application.version) {
// no version means latest version
if (appVersions.get('latest')) {
if (appVersions.has('latest')) {
throw new Error(
`Multiple entries of '${application.name} without version`
);
} else {
appVersions.set('latest', application);
}
} else {
appVersions.set(application.version.toString(), application);
appVersions.set(application.version, application);

// save latest version to fill missing latest versions later
const latestApp = latestApps.get(application.name);
if (!latestApp || application.version > latestApp.version) {
if (!latestApp || semver.gt(application.version, latestApp.version)) {
latestApps.set(application.name, application);
}
}
Expand Down
28 changes: 27 additions & 1 deletion lib/server.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
'use strict';

const semver = require('semver');

const apps = require('./applications');
const Connection = require('./connection');
const SimpleAuthPolicy = require('./simple-auth-policy');
Expand All @@ -24,6 +26,15 @@ const initServer = function(
applications = apps.createAppsIndex(applications);
}

// versions cached for efficient search when provided version is a range
this._cachedVersions = {};
applications.forEach((appVersions, appName) => {
this._cachedVersions[appName] =
Array.from(appVersions)
.filter(app => app[0] !== 'latest')
.sort((a, b) => semver.rcompare(a[0], b[0]));
});

if (typeof(authPolicy) === 'number') {
heartbeatInterval = authPolicy;
authPolicy = null;
Expand Down Expand Up @@ -87,7 +98,22 @@ class Server {
_getApplication(name, version) {
const appVersions = this.applications.get(name);
if (!appVersions) return null;
if (version) return appVersions.get(version);
if (version) {
// when version is not a range simply return matched
if (semver.valid(version)) return appVersions.get(version);

// search matching version, first matched will be the latest
try {
const range = new semver.Range(version);
for (const [ver, app] of this._cachedVersions[name]) {
if (range.test(ver)) return app;
}
} catch (error) {
// TODO (lundibundi): after loggers are implemented write using them
// console.info('Client provided invalid version: ', error);
}
return null;
}
return appVersions.get('latest');
}

Expand Down
21 changes: 10 additions & 11 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@
"./lib/simple-auth-policy.js": false
},
"dependencies": {
"semver": "^5.3.0",
"websocket": "^1.0.24"
},
"devDependencies": {
Expand Down
51 changes: 48 additions & 3 deletions test/node/api-versions.js
Original file line number Diff line number Diff line change
Expand Up @@ -30,8 +30,8 @@ const interfacesVLatest = {
}
};

const appV1 = new jstp.Application(app.name, interfacesV1, 1);
const appV2 = new jstp.Application(app.name, interfacesV2, 2);
const appV1 = new jstp.Application(app.name, interfacesV1, '1.0.0');
const appV2 = new jstp.Application(app.name, interfacesV2, '2.0.0');
const appVlatest = new jstp.Application(app.name, interfacesVLatest);

let server;
Expand All @@ -42,10 +42,16 @@ test.afterEach((done) => {
connection.close();
connection = null;
}
server.close();
if (server) server.close();
done();
});

test.test('must allow to specify version in application name', (test) => {
const appV1 = new jstp.Application('app@1.0.0', interfacesV1);
test.strictSame(appV1.version, '1.0.0');
test.end();
});

test.test('must handle specific versions', (test) => {
const serverConfig = {
applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback
Expand Down Expand Up @@ -95,6 +101,45 @@ test.test('must handle specific versions', (test) => {
});
});

test.test('must handle version ranges', (test) => {
const serverConfig = {
applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback
};
server = jstp.net.createServer(serverConfig);
server.listen(0, () => {
test.plan(6);

const port = server.address().port;

let client = null;
// ^1.0.0
client = {
connectPolicy: new jstp.SimpleConnectPolicy(null, null, '^1.0.0')
};
jstp.net.connect(app.name, client, port, (error, conn) => {
test.assertNot(error, 'connect must not return an error');
conn.callMethod('calculator', 'answer', [], (error, result) => {
test.assertNot(error, 'callMethod must not return an error');
test.strictSame(result, 42);
conn.close();
});
});

// > v1.0.0
client = {
connectPolicy: new jstp.SimpleConnectPolicy(null, null, '>1.0.0')
};
jstp.net.connect(app.name, client, port, 'localhost', (error, conn) => {
test.assertNot(error, 'connect must not return an error');
conn.callMethod('calculator', 'answer', [], (error, result) => {
test.assertNot(error, 'callMethod must not return an error');
test.strictSame(result, 24);
conn.close();
});
});
});
});

test.test('must return an error on connect to nonexistent version', (test) => {
const serverConfig =
{ applications: [appV1, appV2, appVlatest], authPolicy: app.authCallback };
Expand Down

0 comments on commit d99092e

Please sign in to comment.