Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for OpenAPI 3 #181

Draft
wants to merge 15 commits into
base: master
Choose a base branch
from
4 changes: 2 additions & 2 deletions .eslintrc
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"extends": ["eslint:recommended", "eslint-config-hapi"],
"extends": ["eslint:recommended", "@hapi/eslint-config-hapi"],
"parserOptions": {
"ecmaVersion": 2017,
"ecmaVersion": 2018,
"sourceType": "module"
},
"env": {
Expand Down
2 changes: 1 addition & 1 deletion .npmrc
Original file line number Diff line number Diff line change
@@ -1 +1 @@
package-lock=false
package-lock=true
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think we can just remove this file, this is the default. Never seen a project with a .npmrc file

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have create a new branch next to land changes in.

In the case of this particular .npmrc file, I think it can be removed. Checking one in can be useful at times.

2 changes: 1 addition & 1 deletion .vscode/launch.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,4 +15,4 @@
"console": "internalConsole"
}
]
}
}
48 changes: 48 additions & 0 deletions lib/api-dto-mapper.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
'use strict';

const Oas2Strategy = require('./mapping-strategies/oas2');
const Oas3Strategy = require('./mapping-strategies/oas3');

class ApiDto {
constructor(spec, baseDir, mappingStrategy) {

this.baseDir = baseDir;
this.spec = spec;
this._mappingStrategy = mappingStrategy;
}

get basePath() {

return this._mappingStrategy.getBasePath.call(this);
}

get customAuthSchemes() {

return this._mappingStrategy.getCustomAuthSchemes.call(this);
}

get customAuthStrategies() {

return this._mappingStrategy.getCustomAuthStrategies.call(this);
}

get operations() {

return this._mappingStrategy.getOperations.call(this);
}

getOperation(method, path) {

return this.operations.find((op) => op.method === method && op.path === path);
}
}

const toDto = (spec, baseDir) => {

const mappingStrategy = spec.swagger ? Oas2Strategy : Oas3Strategy;
return new ApiDto(spec, baseDir, mappingStrategy);
};

module.exports = {
toDto
};
3 changes: 3 additions & 0 deletions lib/caller.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,13 @@
* @see https://code.google.com/p/v8/wiki/JavaScriptStackTraceApi
*/
const caller = function (depth) {

const pst = Error.prepareStackTrace;

Error.prepareStackTrace = function (_, frames) {

const stack = frames.map((frame) => {

return frame.getFileName();
});

Expand Down
107 changes: 58 additions & 49 deletions lib/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ const Path = require('path');
const Parser = require('swagger-parser');
const Utils = require('./utils');
const Routes = require('./routes');
const ApiDtoMapper = require('./api-dto-mapper');
const Yaml = require('js-yaml');
const Fs = require('fs');
const Util = require('util');
Expand All @@ -32,26 +33,32 @@ const optionsSchema = Joi.object({
}).required();

const stripVendorExtensions = function (obj) {

if (Util.isArray(obj)) {
const clean = [];
for (const value of obj) {
clean.push(stripVendorExtensions(value));
}

return clean;
}

if (Util.isObject(obj)) {
const clean = {};
for (const [key, value] of Object.entries(obj)) {
if (!key.match(/\x-(.*)/)) {
clean[key] = stripVendorExtensions(value);
}
}

return clean;
}

return obj;
};

const requireApi = function (path) {

let document;

if (path.match(/\.ya?ml?/)) {
Expand All @@ -65,69 +72,51 @@ const requireApi = function (path) {
return document;
};

const register = async function (server, options, next) {

const validation = optionsSchema.validate(options);
const exposePluginApi = (server, spec) => {

Hoek.assert(!validation.error, validation.error);

const { api, cors, vhost, handlers, extensions, outputvalidation } = validation.value;
let { docs, docspath } = validation.value;
const spec = await Parser.validate(api);

spec.basePath = Utils.unsuffix(Utils.prefix(spec.basePath || '/', '/'), '/');

//Expose plugin api
server.expose({
getApi() {

return spec;
},
setHost: function setHost(host) {

spec.host = host;
}
});
};

let basedir;
let apiDocument;
const registerCustomAuth = async (server, apiDto) => {

if (Util.isString(api)) {
apiDocument = requireApi(api);
basedir = Path.dirname(Path.resolve(api));
}
else {
apiDocument = api;
basedir = CALLER_DIR;
}
await Promise.all(
apiDto.customAuthSchemes.map(async ({ scheme, path }) => {

await server.register({
plugin: require(path),
options: {
name: scheme
}
});
})
);

if (spec['x-hapi-auth-schemes']) {
for (const [name, path] of Object.entries(spec['x-hapi-auth-schemes'])) {
const scheme = require(Path.resolve(Path.join(basedir, path)));
await Promise.all(
apiDto.customAuthStrategies.map(async ({ strategy, config }) => {

await server.register({
plugin: scheme,
plugin: require(config.path),
options: {
name
name: strategy,
scheme: config.scheme || config.type,
lookup: config.name,
where: config.in
}
});
}
}
if (spec.securityDefinitions) {
for (const [name, security] of Object.entries(spec.securityDefinitions)) {
if (security['x-hapi-auth-strategy']) {
const strategy = require(Path.resolve(Path.join(basedir, security['x-hapi-auth-strategy'])));

await server.register({
plugin: strategy,
options: {
name,
scheme: security.type,
lookup: security.name,
where: security.in
}
});
}
}
}
})
);
};

const registerDocsPath = (server, docs, docspath, api, basePath, cors, vhost) => {

if (docspath !== '/api-docs' && docs.path === '/api-docs') {
server.log(['warn'], 'docspath is deprecated. Use docs instead.');
Expand All @@ -138,12 +127,13 @@ const register = async function (server, options, next) {
}

let apiPath = docs.path;
if (docs.prefixBasePath){
if (docs.prefixBasePath) {
docs.path = Utils.prefix(docs.path, '/');
docs.path = Utils.unsuffix(docs.path, '/');
apiPath = spec.basePath + docs.path;
apiPath = basePath + docs.path;
}

let apiDocument = Util.isString(api) ? requireApi(api) : api;
if (docs.stripExtensions) {
apiDocument = stripVendorExtensions(apiDocument);
}
Expand All @@ -154,6 +144,7 @@ const register = async function (server, options, next) {
path: apiPath,
config: {
handler(request, h) {

return apiDocument;
},
cors,
Expand All @@ -164,9 +155,27 @@ const register = async function (server, options, next) {
},
vhost
});
};

const register = async function (server, options, next) {

const validation = optionsSchema.validate(options);

Hoek.assert(!validation.error, validation.error);

const { api, cors, vhost, handlers, extensions, outputvalidation } = validation.value;
const { docs, docspath } = validation.value;

const basedir = Util.isString(api) ? Path.dirname(Path.resolve(api)) : CALLER_DIR;
const spec = await Parser.validate(api);
const apiDto = ApiDtoMapper.toDto(spec, basedir);

exposePluginApi(server, spec);

const routes = await Routes.create(server, { api: spec, basedir, cors, vhost, handlers, extensions, outputvalidation });
await registerCustomAuth(server, apiDto);

registerDocsPath(server, docs, docspath, api, apiDto.basePath, cors, vhost);
const routes = await Routes.create({ api: apiDto, cors, vhost, handlers, extensions, outputvalidation });
for (const route of routes) {
server.route(route);
}
Expand Down
77 changes: 77 additions & 0 deletions lib/mapping-strategies/oas2.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
'use strict';

const Path = require('path');
const Utils = require('../../lib/utils');

const getBasePath = function () {

return Utils.unsuffix(Utils.prefix(this.spec.basePath || '/', '/'), '/');
};

const getCustomAuthSchemes = function () {

const schemes = this.spec['x-hapi-auth-schemes'] || {};

return Object.entries(schemes)
.map(([scheme, pathToScheme]) => ({ scheme, path: Path.join(this.baseDir, pathToScheme) })
);
};

const getCustomAuthStrategies = function () {

const strategies = this.spec.securityDefinitions || {};

return Object.entries(strategies)
.filter(([, config]) => config['x-hapi-auth-strategy'])
.map(([strategy, { 'x-hapi-auth-strategy': pathToStrategy, ...rest }]) =>
({
strategy,
config: {
...rest,
path: Path.join(this.baseDir, pathToStrategy)
}
})
);
};

const mapSecurity = function (security = []) {

return security.flatMap((auth) =>

Object.entries(auth).map(([strategy, scopes]) => ({ strategy, scopes }))
);
};

const getOperations = function () {

const globalSecurity = this.spec.security;
const paths = this.spec.paths || {};

return Object.entries(paths).flatMap(([path, operations]) =>

Object.entries(operations)
.filter(([method]) => Utils.isHttpMethod(method))
.map(([method, operation]) => ({
path,
method,
description: operation.description,
operationId: operation.operationId,
tags: operation.tags,
security: mapSecurity(operation.security || globalSecurity),
mediaTypes: {
request: operation.consumes || this.spec.consumes
},
parameters: [...(operations.parameters || []), ...(operation.parameters || [])],
handler: operations['x-hapi-handler'] && Path.join(this.baseDir, operations['x-hapi-handler']),
responses: operation.responses,
customOptions: operation['x-hapi-options']
}))
);
};

module.exports = {
getBasePath,
getCustomAuthSchemes,
getCustomAuthStrategies,
getOperations
};
Loading