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/enum generator+openapi3 #14

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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";

Expand All @@ -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'){
Expand All @@ -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/)
Expand Down
8 changes: 6 additions & 2 deletions example/v2/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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();
Expand Down
7 changes: 7 additions & 0 deletions example/v2/nested/nested/nested/nesteParams3.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
nestedLvl3:
name: nestedLvl3
description: Parameters nested in folder 3 times
in: formData
required: true
type: string
7 changes: 7 additions & 0 deletions example/v2/nested/nested/nestedParams2.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
nestedLvl2:
name: nestedLvl2
description: Parameters nested in folder 2 times
in: formData
required: true
type: string
7 changes: 7 additions & 0 deletions example/v2/nested/nestedParams.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
parameters:
nestedLvl1:
name: nestedLvl1
description: Parameters nested in folder 1 time
in: formData
required: true
type: string
45 changes: 45 additions & 0 deletions example/v3/app.ts
Original file line number Diff line number Diff line change
@@ -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 });
35 changes: 35 additions & 0 deletions example/v3/enum.ts
Original file line number Diff line number Diff line change
@@ -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
}
44 changes: 44 additions & 0 deletions example/v3/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
import { Router } from "https://deno.land/x/oak/mod.ts";

const books = new Map<string, any>();
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
}
2 changes: 0 additions & 2 deletions lib/helpers/createSpecification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] || {};
});
Expand Down
8 changes: 5 additions & 3 deletions lib/helpers/getSpecificationObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand All @@ -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);
Expand Down
13 changes: 8 additions & 5 deletions lib/helpers/parseApiFile.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
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.
* @function
* @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)
)
);
}
4 changes: 4 additions & 0 deletions lib/helpers/parseApiFileContent.ts
Original file line number Diff line number Diff line change
@@ -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.
Expand All @@ -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);
}
Expand Down
48 changes: 48 additions & 0 deletions lib/helpers/swaggerEnumParser.ts
Original file line number Diff line number Diff line change
@@ -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 };
}
7 changes: 4 additions & 3 deletions lib/helpers/updateSpecificationObject.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)
);
}
4 changes: 2 additions & 2 deletions lib/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down
2 changes: 1 addition & 1 deletion test.sh
Original file line number Diff line number Diff line change
@@ -1 +1 @@
deno test --unstable --allow-read test/test.ts
deno test --unstable --allow-read --no-check test/test.ts
Loading