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

Feature/#22 open api 3 spec #63

Merged
Merged
Show file tree
Hide file tree
Changes from 13 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,4 +5,6 @@ build
.idea
*.iml
test_spec.json
test_spec_v3.json
api-spec.json
api-spec_v3.json
Copy link
Contributor

@kiprasmel kiprasmel Aug 12, 2020

Choose a reason for hiding this comment

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

I'd just add test_spec*.json once instead. Same with api-spec*.json.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

11 changes: 8 additions & 3 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,12 @@
*/

import { Express } from 'express';
import { OpenAPIV2 } from 'openapi-types';
import { OpenAPIV2,OpenAPIV3 } from 'openapi-types';

/** re-export for ease of use for the end user */
export {
OpenAPIV2, //
OpenAPIV2,
OpenAPIV3
};

/** Options for `handleResponses` */
Expand All @@ -28,7 +29,9 @@ export interface HandleResponsesOptions {
specOutputPath?: string;

/** either the Swagger specification or a function with one argument, which returns the spec */
predefinedSpec?: object | OpenAPIV2.Document | ((spec: OpenAPIV2.Document) => OpenAPIV2.Document);
predefinedSpec?: object | OpenAPIV2.Document | OpenAPIV3.Document |
((spec: OpenAPIV2.Document) => OpenAPIV2.Document) |
((spec: OpenAPIV3.Document) => OpenAPIV3.Document);

/** how often to write the openAPI specification to file */
writeIntervalMs?: number;
Expand Down Expand Up @@ -91,4 +94,6 @@ export function init(

export const getSpec: () => object | OpenAPIV2.Document;

export const getSpecV3: (callback: (err: object | string, specV3: object | OpenAPIV3.Document) => void) => void

export const setPackageInfoPath: (pkgInfoPath: string) => void;
109 changes: 87 additions & 22 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ const fs = require('fs');
const path = require('path');
const swaggerUi = require('swagger-ui-express');
const utils = require('./lib/utils');

const { generateMongooseModelsSpec } = require('./lib/mongoose');
const { generateTagsSpec, matchingTags } = require('./lib/tags');
const { convertOpenApiVersionToV3, getSpecByVersion, versions } = require('./lib/openapi');
const processors = require('./lib/processors');
const listEndpoints = require('express-list-endpoints');

Expand Down Expand Up @@ -97,11 +99,55 @@ function updateSpecFromPackage() {
}

/**
* @description serve the openAPI docs with swagger at a specified path / url
* @description Builds api spec middleware
*
* @returns Middleware
*/
function apiSpecMiddleware(specV2, version) {
return (req, res) => {
getSpecByVersion(specV2, version, (err, openApiSpec) => {
if (!err) {
res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(openApiSpec, null, 2));
}
});
};
}

/**
* @description Builds swagger serve middleware
*
* @returns Middleware
*/
function swaggerServeMiddleware(specV2, version) {
return (req, res) => {
getSpecByVersion(specV2, version, (err, openApiSpec) => {
if (!err) {
res.setHeader('Content-Type', 'text/html');
swaggerUi.setup(openApiSpec)(req, res);
}
});
};
}

/**
* @description Applies spec middlewares
*/
function applySpecMiddlewares(spec, version = '') {

const apiSpecBasePath = packageInfo.baseUrlPath.concat('/api-spec');
const baseSwaggerServePath = packageInfo.baseUrlPath.concat('/' + swaggerUiServePath);

Copy link
Contributor

Choose a reason for hiding this comment

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

Not sure if the packageInfo will always be defined - above [1] it's being checked before getting accessed - make sure everything's fine:D

[1] https://github.com/mpashkovskiy/express-oas-generator/pull/63/files#diff-168726dbe96b3ce427e7fedce31bb0bcR70-R99

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Yep, it is! Before initializing the middlewares prepareSpec is called, which is using updateSpecFromPackage.

app.use(apiSpecBasePath.concat('/' + version), apiSpecMiddleware(spec, version));
app.use(baseSwaggerServePath.concat('/' + version), swaggerUi.serve, swaggerServeMiddleware(spec, version));
}

/**
* @description Prepares spec to be served
*
* @returns void
*/
function serveApiDocs() {
function prepareSpec() {
spec = { swagger: '2.0', paths: {} };

const endpoints = listEndpoints(app);
Expand Down Expand Up @@ -141,15 +187,22 @@ function serveApiDocs() {
spec.definitions = mongooseModelsSpecs || {};
updateSpecFromPackage();
spec = patchSpec(predefinedSpec);
app.use(packageInfo.baseUrlPath + '/api-spec', (req, res, next) => {
Copy link
Owner

@mpashkovskiy mpashkovskiy Aug 6, 2020

Choose a reason for hiding this comment

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

Is it possible to have next line like specV3 = convertOpenApiVersionToV3(spec); and then serve it separately as described in the comment in the issue?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done!

res.setHeader('Content-Type', 'application/json');
res.send(JSON.stringify(patchSpec(predefinedSpec), null, 2));
next();
});
app.use(packageInfo.baseUrlPath + '/' + swaggerUiServePath, swaggerUi.serve, (req, res) => {
res.setHeader('Content-Type', 'text/html');
swaggerUi.setup(patchSpec(predefinedSpec))(req, res);
});
}

/**
* @description serve the openAPI docs with swagger at a specified path / url
*
* @returns void
*/
function serveApiDocs() {
prepareSpec();

applySpecMiddlewares(spec, versions.OPEN_API_V2);

applySpecMiddlewares(spec, versions.OPEN_API_V3);

// Base path middleware should be applied after specific versions
applySpecMiddlewares(spec);
}

/**
Expand Down Expand Up @@ -275,7 +328,7 @@ function handleResponses(expressApp,

updateDefinitionsSpec(options.mongooseModels);
updateTagsSpec(options.tags || options.mongooseModels);

/** middleware to handle RESPONSES */
app.use((req, res, next) => {
try {
Expand All @@ -289,17 +342,21 @@ function handleResponses(expressApp,
firstResponseProcessing = false;
lastRecordTime = ts;

fs.writeFile(specOutputPath, JSON.stringify(spec, null, 2), 'utf8', err => {
const fullPath = path.resolve(specOutputPath);

if (err) {
/**
* TODO - this is broken - the error will be caught and ignored in the catch below.
* See https://github.com/mpashkovskiy/express-oas-generator/pull/39#discussion_r340026645
*/
throw new Error(`Cannot store the specification into ${fullPath} because of ${err.message}`);
fs.writeFileSync(specOutputPath, JSON.stringify(spec, null, 2), 'utf8');

convertOpenApiVersionToV3(spec, (err, specV3) => {
if (!err) {
const parsedSpecOutputPath = path.parse(specOutputPath);
const {name, ext} = parsedSpecOutputPath;
parsedSpecOutputPath.base = name.concat('_').concat(versions.OPEN_API_V3).concat(ext);

const v3Path = path.format(parsedSpecOutputPath);

fs.writeFileSync(v3Path, JSON.stringify(specV3), 'utf8');
}
});
/** TODO - Log that open api v3 could not be generated */
});

}
} catch (e) {
/** TODO - shouldn't we do something here? */
Expand Down Expand Up @@ -381,6 +438,13 @@ const getSpec = () => {
return patchSpec(predefinedSpec);
};

/**
* @type { typeof import('./index').getSpecV3 }
*/
const getSpecV3 = callback => {
convertOpenApiVersionToV3(getSpec(), callback);
};

/**
* @type { typeof import('./index').setPackageInfoPath }
*
Expand All @@ -395,5 +459,6 @@ module.exports = {
handleRequests,
init,
getSpec,
getSpecV3,
setPackageInfoPath
};
25 changes: 25 additions & 0 deletions lib/openapi.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
const openApiVersionConverter = require('swagger2openapi');

module.exports.versions = {
OPEN_API_V2:'v2',
OPEN_API_V3:'v3'
};

module.exports.getSpecByVersion = (specV2, version, callback) => {
const defaultSpec = (specV2, callback) => callback(null, specV2);
const availableSpecs = {
[this.versions.OPEN_API_V2]: defaultSpec,
[this.versions.OPEN_API_V3]: this.convertOpenApiVersionToV3,
};
const specByVersion = availableSpecs[version] || defaultSpec;

return specByVersion(specV2, callback);
};

module.exports.convertOpenApiVersionToV3 = (specV2, callback) => {
const options = {patch: true, warnOnly: true};

openApiVersionConverter.convertObj(specV2, options, function(err, results) {
callback(err, results && results.openapi);
});
};
Loading