Skip to content

Commit

Permalink
Merge pull request #14 from Pchelolo/docs
Browse files Browse the repository at this point in the history
Docs: Handle docs and listings in a more generic way
  • Loading branch information
Marko Obrovac committed Mar 2, 2016
2 parents 052741b + 5ad097e commit 2e496e2
Show file tree
Hide file tree
Showing 8 changed files with 191 additions and 33 deletions.
30 changes: 11 additions & 19 deletions lib/hyperswitch.js
Expand Up @@ -101,10 +101,11 @@ HyperSwitch.prototype.makeChild = function(req, options) {

// A default listing handler for URIs that end in / and don't have any
// handlers associated with it otherwise.
HyperSwitch.prototype.defaultListingHandler = function(value, hyper, req) {
HyperSwitch.prototype.defaultListingHandler = function(match, hyper, req) {
var rq = req.query;
if (rq.spec !== undefined && value.specRoot) {
var spec = Object.assign({}, value.specRoot, {
if (rq.spec !== undefined
&& match.value.specRoot && !match.value.specRoot['x-listing']) {
var spec = Object.assign({}, match.value.specRoot, {
// Set the base path dynamically
basePath: req.uri.toString().replace(/\/$/, '')
});
Expand All @@ -118,28 +119,20 @@ HyperSwitch.prototype.defaultListingHandler = function(value, hyper, req) {
status: 200,
body: spec
});
} else if (rq.doc !== undefined) {
} else if (rq.doc !== undefined
&& (match.value.specRoot && !match.value.specRoot['x-listing'] || rq.path)) {
// Return swagger UI & load spec from /?spec
if (!req.query.path) {
req.query.path = '/index.html';
}
return swaggerUI(hyper, req);
} else if (/\btext\/html\b/.test(req.headers.accept)
&& req.uri.path.length <= 2) {
&& (!match.value.specRoot || match.value.specRoot['x-listing'])) {
// Browser request and above api level
req.query.path = '/index.html';
var html = '<div id="swagger-ui-container" class="swagger-ui-wrap">'
+ '<div class="info_title">Wikimedia REST API</div>';
if (req.uri.path.length === 1) {
html += '<h2>Domains:</h2>'
+ '<div class="info_description markdown"><ul>'
+ req.params._ls.map(function(domain) {
return '<li><a href="' + encodeURIComponent(domain)
+ '/v1/?doc">' + domain + '</a></li>';
}).join('\n')
+ '</ul></div>';
} else {
html += '<h2>APIs:</h2>'
+ '<div class="info_title">Wikimedia REST API</div>'
+ '<h2>APIs:</h2>'
+ '<div class="info_description markdown"><ul>'
+ req.params._ls.filter(function(item) {
return item !== 'sys';
Expand All @@ -149,15 +142,14 @@ HyperSwitch.prototype.defaultListingHandler = function(value, hyper, req) {
+ '/?doc">' + api + '</a></li>';
}).join('\n')
+ '</ul>';
}
html += "<h3>JSON listing</h3><p>To retrieve a regular JSON listing, you can either "
+ "omit the <code>Accept</code> header, or send one that does not contain "
+ "<code>text/html</code>.</p></div>";

return swaggerUI(hyper, req)
.then(function(res) {
res.body = res.body
.replace(/window\.swaggerUi\.load/, '')
.replace(/window\.swaggerUi\.load\(\);/, '')
.replace(/<div id="swagger-ui-container" class="swagger-ui-wrap">/, html);
return res;
});
Expand Down Expand Up @@ -348,7 +340,7 @@ HyperSwitch.prototype._request = function(req, options) {
if (!match.value) { match.value = {}; }
if (!match.value.path) { match.value.path = '_defaultListingHandler'; }
handler = function(hyper, req) {
return self.defaultListingHandler(match.value, hyper, req);
return self.defaultListingHandler(match, hyper, req);
};
}

Expand Down
9 changes: 8 additions & 1 deletion lib/router.js
Expand Up @@ -29,6 +29,7 @@ function ApiScope(init) {
this.globals = init.globals || {};
this.operations = init.operations || {};
this.prefixPath = init.prefixPath || '';
this.rootScope = init.rootScope;
}

ApiScope.prototype.makeChild = function(overrides) {
Expand Down Expand Up @@ -466,18 +467,24 @@ Router.prototype._handlePaths = function(rootNode, spec, scope) {
* @return {Promise<void>}
*/
Router.prototype._handleSwaggerSpec = function(node, spec, scope, parentSegment) {
if (parentSegment && parentSegment.name === 'api') {
// Client is using multi-api feature, mark root spec as listing spec
scope.rootScope.specRoot['x-listing'] = true;
}

if (!parentSegment || parentSegment.name === 'api') {
var listingNode = node.getChild('');
if (listingNode) {
scope = scope.makeChild({
specRoot: listingNode.value.specRoot,
operations: {},
prefixPath: ''
prefixPath: '',
});
} else {
// This is first time we've seen this api, so create new specRoot for it.
scope = this._createNewApiRoot(node, spec, scope);
}
scope.rootScope = scope;
}
Object.assign(scope.specRoot.definitions, spec.definitions || {});
Object.assign(scope.specRoot.securityDefinitions, spec.securityDefinitions || {});
Expand Down
20 changes: 8 additions & 12 deletions lib/swaggerUI.js
Expand Up @@ -62,18 +62,14 @@ function staticServe(hyper, req) {
body: body
});
})
.catch(function(e) {
if (e && e.code === 'ENOENT') {
return new HTTPError({
status: 404,
body: {
type: 'not_found',
title: 'Not found.',
}
});
} else {
throw e;
}
.catch({ code: 'ENOENT' }, function() {
return new HTTPError({
status: 404,
body: {
type: 'not_found',
title: 'Not found.'
}
});
});
}

Expand Down
2 changes: 1 addition & 1 deletion package.json
@@ -1,6 +1,6 @@
{
"name": "hyperswitch",
"version": "0.2.1",
"version": "0.2.2",
"description": "REST API creation framework",
"main": "index.js",
"scripts": {
Expand Down
117 changes: 117 additions & 0 deletions test/hyperswitch/docs.js
@@ -0,0 +1,117 @@
"use strict";

var assert = require('../utils/assert.js');
var Server = require('../utils/server.js');
var preq = require('preq');
var P = require('bluebird');

describe('Documentation handling', function() {
var server = new Server('test/hyperswitch/docs_config.yaml');

before(function() { return server.start(); });

it('should list APIs using the generic listing handler', function() {
return preq.get({
uri: server.hostPort + '/'
})
.then(function(res) {
assert.deepEqual(res.status, 200);
assert.contentType(res, 'application/json');
assert.deepEqual(res.body, {
items: [ 'v1' ]
});
});
});

it('should retrieve the spec', function() {
return preq.get({
uri: server.hostPort + '/v1/?spec'
})
.then(function(res) {
assert.deepEqual(res.status, 200);
assert.contentType(res, 'application/json');
assert.deepEqual(res.body.swagger, '2.0');
});
});

it('should retrieve the swagger-ui main page', function() {
return preq.get({
uri: server.hostPort + '/v1/?doc'
})
.then(function(res) {
assert.deepEqual(res.status, 200);
assert.contentType(res, 'text/html');
assert.deepEqual(/<html/.exec(res.body)[0], '<html');
})
.catch(function (e) {
console.log(e);
});
});

it('should retrieve all dependencies of the swagger-ui main page', function() {
return preq.get({ uri: server.hostPort + '/v1/?doc' })
.then(function(res) {
var assertions = [];
var linkRegex = /<link\s[^>]*href=["']([^"']+)["']/g;
var scriptRegex = /<script\s[^>]*src=["']([^"']+)["']/g;
var match;
while (match = linkRegex.exec(res.body)) {
assertions.push(match[1]);
}
while (match = scriptRegex.exec(res.body)) {
assertions.push(match[1]);
}
return P.all(assertions.map(function(path) {
return preq.get({ uri: server.hostPort + '/' + path })
.then(function(res) {
assert.deepEqual(res.status, 200);
});
}));
});
});

it('should retrieve API listing in html', function() {
return preq.get({
uri: server.hostPort + '/',
headers: {
accept: 'text/html'
}
})
.then(function(res) {
assert.deepEqual(res.status, 200);
assert.contentType(res, 'text/html');
assert.deepEqual(/<html/.exec(res.body)[0], '<html');
});
});

it('should throw error for static serve', function() {
return preq.get({
uri: server.hostPort + '/v1/?doc=&path=/this_is_no_a_path',
headers: {
accept: 'text/html'
}
})
.then(function() {
throw new Error('Error should be thrown');
}, function(e) {
assert.deepEqual(e.status, 404);
});
});

it('should disallow unsecure relative paths for static serve', function() {
return preq.get({
uri: server.hostPort + '/v1/?doc=&path=../../../Test',
headers: {
accept: 'text/html'
}
})
.then(function() {
throw new Error('Error should be thrown');
}, function(e) {
assert.deepEqual(e.status, 500);
assert.deepEqual(e.body.detail, 'Error: Invalid path.');
});
});

after(function() { return server.stop(); });
});
28 changes: 28 additions & 0 deletions test/hyperswitch/docs_config.yaml
@@ -0,0 +1,28 @@
spec_root: &spec_root
paths:
/{api:v1}:
x-modules:
- type: inline
spec:
paths:
/test:
get:
x-request-handler:
- return_result:
return:
status: 200
body: 'test'

# Finally, a standard service-runner config.
info:
name: hyperswitch

services:
- name: test_service
module: ./lib/server
conf:
port: 12345
spec: *spec_root
salt: secret
default_page_size: 1
user_agent: HyperSwitch-testsuite
11 changes: 11 additions & 0 deletions test/hyperswitch/hyperswitch.js
Expand Up @@ -196,5 +196,16 @@ describe('HyperSwitch context', function() {
});
});

it('Should retrieve the if multi-api is not used', function() {
return preq.get({
uri: server.hostPort + '/?spec'
})
.then(function(res) {
assert.deepEqual(res.status, 200);
assert.contentType(res, 'application/json');
assert.deepEqual(res.body.swagger, '2.0');
});
});

after(function() { return server.stop(); });
});
7 changes: 7 additions & 0 deletions test/utils/assert.js
Expand Up @@ -26,5 +26,12 @@ function notDeepEqual(result, expected, message) {
}
}

function contentType(res, expected) {
var actual = res.headers['content-type'];
deepEqual(actual, expected,
'Expected content-type to be ' + expected + ', but was ' + actual);
}

module.exports.deepEqual = deepEqual;
module.exports.notDeepEqual = notDeepEqual;
module.exports.contentType = contentType;

0 comments on commit 2e496e2

Please sign in to comment.