diff --git a/README.md b/README.md index 4253ba4..5592410 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,8 @@ To make sure your end specification is valid, do read the most up-to date offici **swagger-jsdoc** enables you to integrate [Swagger](http://swagger.io) using [`JSDoc`](https://jsdoc.app/) comments in your code. Just add `@swagger` on top of your DocBlock and declare the meaning of your code in YAML complying to the OpenAPI specification. +**Enums can be generated automatically (openapi 3.x ONLY)** - just add `@swagger-enum` instead of `@swagger` mentioned above and everything will be autogenerated from the following definition ([see example](./example/v3/enum.ts)). Then you can refer to this enum when you use `$ref: '#/components/schemas/EnumName'` instead of type definition in some schema ([see example](./example/v3/routes.ts)) + ```ts import { swaggerDoc } from "https://deno.land/x/deno_swagger_doc/mod.ts"; @@ -28,17 +30,19 @@ const swaggerDefinition = { }, host: `localhost:8000`, // Host (optional) basePath: '/', // Base path (optional) + swagger: '2.0', // Swagger version (optional) + openapi: '3.0.0', // Openapi version (optional) }; const options = { swaggerDefinition, // Path to the API docs // Note that this path is relative to the current directory from which the Node.js is ran, not the application itself. - apis: ['./example/v2/routes.ts', './example/v2/parameters.yaml'], + apis: ['./example/v2/routes.ts', './example/v2/**/*.yaml'], }; // Initialize swagger-jsdoc -> returns validated swagger spec in json format -const swaggerSpec = swaggerDoc(options); +const swaggerSpec = await swaggerDoc(options); app.use(async (context, next) => { if(context.request.url.pathname === '/swagger.json'){ @@ -60,6 +64,15 @@ If you facing any issue due to TypeScript type checking. Please use the `--no-ch `denon run --no-check --allow-net --allow-read --unstable ./example/v2/app.ts` +**openapi 3.x version** +`deno run --no-check --allow-net --allow-read --unstable ./example/v3/app.ts` + +## Breaking change: + +If **your swagger.json is empty**, please check, whether you awai swaggerDoc call in app.ts + +`const swaggerSpec = `**await**` swaggerDoc(options);` + ## Stay in touch * Author - [Raja SIngh](https://www.linkedin.com/in/raja-singh-a097458a/) diff --git a/example/v2/app.ts b/example/v2/app.ts index 6f1e926..6b26072 100644 --- a/example/v2/app.ts +++ b/example/v2/app.ts @@ -11,6 +11,7 @@ const swaggerDefinition = { }, host: `localhost:8000`, // Host (optional) basePath: '/', // Base path (optional) + swagger: '2.0', // Swagger version (optional) }; // Options for the swagger docs @@ -19,11 +20,14 @@ const options = { swaggerDefinition, // Path to the API docs // Note that this path is relative to the current directory from which the Node.js is ran, not the application itself. - apis: ['./example/v2/routes.ts', './example/v2/parameters.yaml'], + apis: [ + './example/v2/routes.ts', + './example/v2/**/*.yaml' + ], }; // Initialize swagger-jsdoc -> returns validated swagger spec in json format -const swaggerSpec = swaggerDoc(options); +const swaggerSpec = await swaggerDoc(options); const app = new Application(); diff --git a/example/v2/nested/nested/nested/nesteParams3.yaml b/example/v2/nested/nested/nested/nesteParams3.yaml new file mode 100644 index 0000000..c4619f2 --- /dev/null +++ b/example/v2/nested/nested/nested/nesteParams3.yaml @@ -0,0 +1,7 @@ +parameters: + nestedLvl3: + name: nestedLvl3 + description: Parameters nested in folder 3 times + in: formData + required: true + type: string diff --git a/example/v2/nested/nested/nestedParams2.yaml b/example/v2/nested/nested/nestedParams2.yaml new file mode 100644 index 0000000..d4fbed8 --- /dev/null +++ b/example/v2/nested/nested/nestedParams2.yaml @@ -0,0 +1,7 @@ +parameters: + nestedLvl2: + name: nestedLvl2 + description: Parameters nested in folder 2 times + in: formData + required: true + type: string diff --git a/example/v2/nested/nestedParams.yaml b/example/v2/nested/nestedParams.yaml new file mode 100644 index 0000000..8942a77 --- /dev/null +++ b/example/v2/nested/nestedParams.yaml @@ -0,0 +1,7 @@ +parameters: + nestedLvl1: + name: nestedLvl1 + description: Parameters nested in folder 1 time + in: formData + required: true + type: string diff --git a/example/v3/app.ts b/example/v3/app.ts new file mode 100644 index 0000000..f557b8c --- /dev/null +++ b/example/v3/app.ts @@ -0,0 +1,45 @@ +import { Application } from "https://deno.land/x/oak/mod.ts"; +import { router } from "./routes.ts"; +import { swaggerDoc } from "../../mod.ts"; + +const swaggerDefinition = { + info: { + // API informations (required) + title: 'Hello World', // Title (required) + version: '1.0.0', // Version (required) + description: 'A sample API', // Description (optional) + }, + openapi: '3.0.0', // openapi version (required) +}; + +// Options for the swagger docs +const options = { + // Import swaggerDefinitions + swaggerDefinition, + // Path to the API docs + // Note that this path is relative to the current directory from which the Node.js is ran, not the application itself. + apis: [ + './example/v3/routes.ts', + './example/v3/**/*.yaml', + './example/v3/enum.ts' + ], +}; + +// Initialize swagger-jsdoc -> returns validated swagger spec in json format +const swaggerSpec = await swaggerDoc(options); + + +const app = new Application(); +app.use(async (context, next) => { + if(context.request.url.pathname === '/swagger.json'){ + context.response.headers.set('Content-Type', 'application/json'); + context.response.status = 200; + context.response.body = swaggerSpec + }else{ + await next(); + } +}); +app.use(router.routes()); +app.use(router.allowedMethods()); + +await app.listen({ port: 8000 }); \ No newline at end of file diff --git a/example/v3/enum.ts b/example/v3/enum.ts new file mode 100644 index 0000000..381d55d --- /dev/null +++ b/example/v3/enum.ts @@ -0,0 +1,35 @@ +/** + * @swagger-enum + */ +export enum EnumOfNumbers { + NOT_FOUND = 404, + SERVER_ERROR = 500, + REDIRECT = 301 +} + +/** + * @swagger-enum + */ +export enum EnumOfStrings { + NOT_FOUND = 'A', + SERVER_ERROR = 'B', + REDIRECT = 'C' +} + +/** + * @swagger-enum + */ +export enum MixedEnum { + NOT_FOUND = 'A', + SERVER_ERROR = 500, + REDIRECT = 'B' +} + +/** + * @swagger-enum + */ +export enum NoCustomValues { + VALUE1, + VALUE2, + VALUE3 +} \ No newline at end of file diff --git a/example/v3/routes.ts b/example/v3/routes.ts new file mode 100644 index 0000000..2cb782a --- /dev/null +++ b/example/v3/routes.ts @@ -0,0 +1,44 @@ +import { Router } from "https://deno.land/x/oak/mod.ts"; + +const books = new Map(); +books.set("1", { + id: "1", + title: "The Hound of the Baskervilles", + author: "Conan Doyle, Arthur", +}); + +const router = new Router(); + + /** + * @swagger + * /: + * get: + * description: Returns the homepage + * responses: + * 200: + * description: hello world + * content: + * application/json: + * schema: + * type: object + * properties: + * value: + * description: Enum usage example + * $ref: '#/components/schemas/NoCustomValues' + */ +router + .get("/", (context) => { + context.response.body = "Hello world!"; + }) + .get("/book", (context) => { + context.response.body = Array.from(books.values()); + }) + .get("/book/:id", (context) => { + if (context.params && context.params.id && books.has(context.params.id)) { + context.response.body = books.get(context.params.id); + } + }); + + export { + router + } \ No newline at end of file diff --git a/lib/helpers/createSpecification.ts b/lib/helpers/createSpecification.ts index cd2c14e..27e100d 100644 --- a/lib/helpers/createSpecification.ts +++ b/lib/helpers/createSpecification.ts @@ -21,12 +21,10 @@ export function createSpecification(definition: any) { const v3 = [...v2, 'components']; if (specification.openapi) { - specification.openapi = specification.openapi; v3.forEach((property) => { specification[property] = specification[property] || {}; }); } else if (specification.swagger) { - specification.swagger = specification.swagger; v2.forEach((property) => { specification[property] = specification[property] || {}; }); diff --git a/lib/helpers/getSpecificationObject.ts b/lib/helpers/getSpecificationObject.ts index 3181453..1a7a85f 100644 --- a/lib/helpers/getSpecificationObject.ts +++ b/lib/helpers/getSpecificationObject.ts @@ -4,7 +4,7 @@ import { finalizeSpecificationObject } from './finalizeSpecificationObject.ts'; import { updateSpecificationObject } from './updateSpecificationObject.ts'; -export function getSpecificationObject(options : any) { +export async function getSpecificationObject(options: any) { // Get input definition and prepare the specification's skeleton const definition = options.swaggerDefinition || options.definition; const specification = createSpecification(definition); @@ -13,8 +13,10 @@ export function getSpecificationObject(options : any) { const apiPaths = options.apis; for (let i = 0; i < apiPaths.length; i += 1) { - const parsedFile = parseApiFile(apiPaths[i]); - updateSpecificationObject(parsedFile, specification); + const parsedFiles = await parseApiFile(apiPaths[i]); + await parsedFiles.forEach(async parsedFile => { + await updateSpecificationObject(parsedFile, specification); + }); } return finalizeSpecificationObject(specification); diff --git a/lib/helpers/parseApiFile.ts b/lib/helpers/parseApiFile.ts index b53768a..a25bf39 100644 --- a/lib/helpers/parseApiFile.ts +++ b/lib/helpers/parseApiFile.ts @@ -1,4 +1,5 @@ import * as path from "https://deno.land/std/path/mod.ts"; +import { expandGlobSync } from "https://deno.land/std/fs/mod.ts"; import { parseApiFileContent } from "./parseApiFileContent.ts"; /** * Parses the provided API file for JSDoc comments. @@ -6,9 +7,11 @@ import { parseApiFileContent } from "./parseApiFileContent.ts"; * @param {string} file - File to be parsed * @returns {{jsdoc: array, yaml: array}} JSDoc comments and Yaml files */ -export function parseApiFile(file: any) { - const fileContent = Deno.readTextFileSync(file); - const ext = path.extname(file); - - return parseApiFileContent(fileContent, ext); +export async function parseApiFile(file: any) { + return await Array.from(expandGlobSync(file)).map( + async f => await parseApiFileContent( + Deno.readTextFileSync(f.path), + path.extname(f.path) + ) + ); } diff --git a/lib/helpers/parseApiFileContent.ts b/lib/helpers/parseApiFileContent.ts index a118e18..937025e 100644 --- a/lib/helpers/parseApiFileContent.ts +++ b/lib/helpers/parseApiFileContent.ts @@ -1,5 +1,6 @@ import jsYaml from 'https://dev.jspm.io/js-yaml'; import doctrine from 'https://dev.jspm.io/doctrine'; +import { swaggerEnumParser } from './swaggerEnumParser.ts'; /** * Parse the provided API file content. @@ -21,6 +22,9 @@ export function parseApiFileContent(fileContent: any, ext: any) { const regexResults = fileContent.match(jsDocRegex); if (regexResults) { for (let i = 0; i < regexResults.length; i += 1) { + const { content, jsDoc } = swaggerEnumParser(fileContent, regexResults[i]); + fileContent = content; + regexResults[i] = jsDoc; const jsDocComment = doctrine.parse(regexResults[i], { unwrap: true }); jsDocComments.push(jsDocComment); } diff --git a/lib/helpers/swaggerEnumParser.ts b/lib/helpers/swaggerEnumParser.ts new file mode 100644 index 0000000..20664ca --- /dev/null +++ b/lib/helpers/swaggerEnumParser.ts @@ -0,0 +1,48 @@ +/** + * Parses jsDocs containing @swagger-enum to openapi definition of followed enum + * + * @param content file content + * @param jsDoc matched jsDoc + * @returns { + * jsDoc: string - new jsDoc containing enum definition + * content: string - new content without processed enums + * } + */ +export function swaggerEnumParser(content: string, jsDoc: string): {content: string, jsDoc: string} { + if (jsDoc.indexOf('@swagger-enum') !== -1) { + const endOfJsDocComment = content.indexOf(jsDoc) + jsDoc.length; + content = content.substring(endOfJsDocComment); + const [_all, enumName, enumDef] = content.match(/enum\s([^\s]*)\s{([^}]*)}/m) as RegExpMatchArray; + if (enumName && enumDef) { + const enumItems = enumDef.split(/[\r\n]+/).filter((i: string) => i.trim() !== ''); + if (enumItems.length) { + let itemsString = ''; + let type = 'number'; + enumItems.forEach((item: string) => { + if (item.indexOf('=') === -1) { + itemsString += '* - ' + item.trim().replace(/,$/, '') + '\n'; + } else { + const itemParts = item.split('='); + if (Number.isNaN(Number(itemParts[1].trim().replace(/,$/, '')))) { + type = 'string'; + } + itemsString += ' * - ' + itemParts[1].trim().replace(/,$/, '') + '\n'; + } + }); + return { + content, + jsDoc: `/** + * @swagger + * components: + * schemas: + * ${enumName}: + * type: ${type} + * enum: +${itemsString} */` + }; + } + } + } + // no swagger-enum present + return { content, jsDoc }; +} \ No newline at end of file diff --git a/lib/helpers/updateSpecificationObject.ts b/lib/helpers/updateSpecificationObject.ts index 2d705c2..70e38a6 100644 --- a/lib/helpers/updateSpecificationObject.ts +++ b/lib/helpers/updateSpecificationObject.ts @@ -8,11 +8,12 @@ import { filterJsDocComments } from './filterJsDocComments.ts'; * @param {object} parsedFile - Parsed API file. * @param {object} specification - Specification accumulator. */ -export function updateSpecificationObject(parsedFile :any, specification: any) { - addDataToSwaggerObject(specification, parsedFile.yaml); +export async function updateSpecificationObject(parsedFile :any, specification: any) { + const {yaml, jsdoc} = await parsedFile; + addDataToSwaggerObject(specification, yaml); addDataToSwaggerObject( specification, - filterJsDocComments(parsedFile.jsdoc) + filterJsDocComments(jsdoc) ); } \ No newline at end of file diff --git a/lib/index.ts b/lib/index.ts index 92c8d31..4e1e277 100644 --- a/lib/index.ts +++ b/lib/index.ts @@ -14,13 +14,13 @@ import { getSpecificationObject } from './helpers/getSpecificationObject.ts'; * @returns {object} Output specification * @requires swagger-parser */ -export function swaggerDoc (options : any) { +export async function swaggerDoc (options : any) { if ((!options.swaggerDefinition || !options.definition) && !options.apis) { throw new Error('Provided options are incorrect.'); } try { - const specificationObject = getSpecificationObject(options); + const specificationObject = await getSpecificationObject(options); return specificationObject; } catch (err) { diff --git a/test.sh b/test.sh index 8093883..d27bdc3 100644 --- a/test.sh +++ b/test.sh @@ -1 +1 @@ -deno test --unstable --allow-read test/test.ts \ No newline at end of file +deno test --unstable --allow-read --no-check test/test.ts \ No newline at end of file diff --git a/test/test.ts b/test/test.ts index 4e929b5..69a5658 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,23 +1,43 @@ import { assertEquals } from 'https://deno.land/std/testing/asserts.ts'; import { swaggerDoc } from "../mod.ts"; import { SwaggerJson } from './testData.ts'; +import { SwaggerJsonV3 } from './testDataV3.ts'; -const swaggerDefinition = { + Deno.test('swaggerDoc() v2', async () => { + const swaggerDefinition = { + info: { + title: 'Hello World', + version: '1.0.0', + description: 'A sample API', + }, + host: `localhost:8000`, + basePath: '/', + }; + + const options = { + swaggerDefinition, + apis: ['./example/v2/routes.ts', './example/v2/**/*.yaml'], + }; + + const swaggerSpec = await swaggerDoc(options); + assertEquals(swaggerSpec, JSON.parse(SwaggerJson)); + }); + +Deno.test('swaggerDoc() v3', async () => { + const swaggerDefinition = { info: { title: 'Hello World', version: '1.0.0', description: 'A sample API', }, - host: `localhost:8000`, - basePath: '/', + openapi: '3.0.0', }; - + const options = { swaggerDefinition, - apis: ['./example/v2/routes.ts', './example/v2/parameters.yaml'], + apis: ['./example/v3/routes.ts', './example/v3/enum.ts', './example/v3/**/*.yaml'], }; - Deno.test('swaggerDoc()', async () => { - const swaggerSpec = swaggerDoc(options); - assertEquals(swaggerSpec, JSON.parse(SwaggerJson)); - }); + const swaggerSpec = await swaggerDoc(options); + assertEquals(swaggerSpec, JSON.parse(SwaggerJsonV3)); +}); diff --git a/test/testData.ts b/test/testData.ts index 67bf5b2..c7b3140 100644 --- a/test/testData.ts +++ b/test/testData.ts @@ -1 +1 @@ -export const SwaggerJson: string = `{"info":{"title":"Hello World","version":"1.0.0","description":"A sample API"},"host":"localhost:8000","basePath":"/","swagger":"2.0","paths":{"/":{"get":{"description":"Returns the homepage","responses":{"200":{"description":"hello world"}}}}},"definitions":{},"responses":{},"parameters":{"username":{"name":"username","description":"Username to use for login.","in":"formData","required":true,"type":"string"}},"securityDefinitions":{},"tags":[]}`; \ No newline at end of file +export const SwaggerJson = `{"info":{"title":"Hello World","version":"1.0.0","description":"A sample API"},"host":"localhost:8000","basePath":"/","swagger":"2.0","paths":{"/":{"get":{"description":"Returns the homepage","responses":{"200":{"description":"hello world"}}}}},"definitions":{},"responses":{},"parameters":{"nestedLvl3":{"name":"nestedLvl3","description":"Parameters nested in folder 3 times","in":"formData","required":true,"type":"string"},"nestedLvl2":{"name":"nestedLvl2","description":"Parameters nested in folder 2 times","in":"formData","required":true,"type":"string"},"nestedLvl1":{"name":"nestedLvl1","description":"Parameters nested in folder 1 time","in":"formData","required":true,"type":"string"},"username":{"name":"username","description":"Username to use for login.","in":"formData","required":true,"type":"string"}},"securityDefinitions":{},"tags":[]}`; \ No newline at end of file diff --git a/test/testDataV3.ts b/test/testDataV3.ts new file mode 100644 index 0000000..79fae0b --- /dev/null +++ b/test/testDataV3.ts @@ -0,0 +1 @@ +export const SwaggerJsonV3 = `{"info":{"title":"Hello World","version":"1.0.0","description":"A sample API"},"openapi":"3.0.0","paths":{"/":{"get":{"description":"Returns the homepage","responses":{"200":{"description":"hello world","content":{"application/json":{"schema":{"type":"object","properties":{"value":{"description":"Enum usage example","$ref":"#/components/schemas/NoCustomValues"}}}}}}}}}},"components":{"schemas":{"EnumOfNumbers":{"type":"number","enum":[404,500,301]},"EnumOfStrings":{"type":"string","enum":["A","B","C"]},"MixedEnum":{"type":"string","enum":["A",500,"B"]},"NoCustomValues":{"type":"number","enum":["VALUE1","VALUE2","VALUE3"]}}},"tags":[]}`; \ No newline at end of file