From 1a07df2d676895264f43b0138645d7c13f60c9db Mon Sep 17 00:00:00 2001 From: Karl-Philipp Wulfert Date: Thu, 13 Dec 2018 12:43:55 +0100 Subject: [PATCH] feat: add tool to generate documentation for routes --- package-lock.json | 31 +++- package.json | 8 +- src/cli.ts | 83 +++++++++ src/common.ts | 168 ++++++++++++++++++ .../BookAvailabilityRequest.ts | 14 +- src/types.ts | 66 +++++++ 6 files changed, 365 insertions(+), 5 deletions(-) create mode 100644 src/cli.ts create mode 100644 src/common.ts create mode 100644 src/types.ts diff --git a/package-lock.json b/package-lock.json index dfbcc4fb..d7c9ef43 100644 --- a/package-lock.json +++ b/package-lock.json @@ -265,6 +265,12 @@ "integrity": "sha512-pGF/zvYOACZ/gLGWdQH8zSwteQS1epp68yRcVLJMgUck/MjEn/FBYmPub9pXT8C1e4a8YZfHo1CKyV8q1vKUnQ==", "dev": true }, + "@types/humanize-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@types/humanize-string/-/humanize-string-1.0.0.tgz", + "integrity": "sha512-lfaNfcTSt2DLiF1V8kXMhT4rX7ggkc10wI9SqTrxFMNTIfaafXHCL5DS1q2J/i+Be3EBQyG+Ls8GSbKngvSIkw==", + "dev": true + }, "@types/inquirer": { "version": "0.0.43", "resolved": "https://registry.npmjs.org/@types/inquirer/-/inquirer-0.0.43.tgz", @@ -341,6 +347,16 @@ "@types/request": "*" } }, + "@types/rimraf": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@types/rimraf/-/rimraf-2.0.2.tgz", + "integrity": "sha512-Hm/bnWq0TCy7jmjeN5bKYij9vw5GrDFWME4IuxV08278NtU/VdGbzsBohcCUJ7+QMqmUq5hpRKB39HeQWJjztQ==", + "dev": true, + "requires": { + "@types/glob": "*", + "@types/node": "*" + } + }, "@types/rx": { "version": "4.1.1", "resolved": "http://registry.npmjs.org/@types/rx/-/rx-4.1.1.tgz", @@ -887,9 +903,9 @@ } }, "commander": { - "version": "2.18.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.18.0.tgz", - "integrity": "sha512-6CYPa+JP2ftfRU2qkDK+UTVeQYosOg/2GbcjIcKPHfinyOLPVGXu/ovN86RP49Re5ndJK1N0kuiidFFuepc4ZQ==", + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.19.0.tgz", + "integrity": "sha512-6tvAOO+D6OENvRAh524Dh9jcfKTYDQAqvqezbCW82xj5X0pSrcpxtvRKHLG0yBY6SD7PSDrJaj+0AiOcKVd1Xg==", "dev": true }, "compare-func": { @@ -1799,6 +1815,15 @@ "sshpk": "^1.7.0" } }, + "humanize-string": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/humanize-string/-/humanize-string-1.0.2.tgz", + "integrity": "sha512-PH5GBkXqFxw5+4eKaKRIkD23y6vRd/IXSl7IldyJxEXpDH9SEIXRORkBtkGni/ae2P7RVOw6Wxypd2tGXhha1w==", + "dev": true, + "requires": { + "decamelize": "^1.0.0" + } + }, "humps": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/humps/-/humps-2.0.1.tgz", diff --git a/package.json b/package.json index 6fff7b67..aa8bfb95 100644 --- a/package.json +++ b/package.json @@ -16,7 +16,7 @@ "scripts": { "build": "npm run tslint && npm run compile && npm run pack && npm run schema && npm run documentation", "changelog": "conventional-changelog -p angular -i CHANGELOG.md -s -r 0", - "compile": "tsc", + "compile": "tsc && rimraf lib/cli.js lib/common.js lib/types.js", "documentation": "typedoc --includeDeclarations --excludeExternals --mode modules --out docs src", "pack": "openstapps-pack", "prepareOnly": "npm run build", @@ -46,11 +46,17 @@ "@openstapps/logger": "0.0.3", "@openstapps/projectmanagement": "0.0.1", "@types/chai": "4.1.7", + "@types/humanize-string": "1.0.0", "@types/node": "10.12.10", + "@types/rimraf": "2.0.2", + "async-pool-native": "0.1.0", "chai": "4.2.0", + "commander": "2.19.0", "conventional-changelog-cli": "2.0.11", + "humanize-string": "1.0.2", "mocha": "5.2.0", "mocha-typescript": "1.1.17", + "rimraf": "2.6.2", "ts-node": "7.0.1", "tslint": "5.11.0", "typedoc": "0.13.0", diff --git a/src/cli.ts b/src/cli.ts new file mode 100644 index 00000000..a28716c2 --- /dev/null +++ b/src/cli.ts @@ -0,0 +1,83 @@ +/* + * Copyright (C) 2018 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {execSync} from 'child_process'; +import * as commander from 'commander'; +import {existsSync, mkdirSync, readFileSync, writeFileSync} from 'fs'; +import {resolve} from 'path'; +import {ProjectReflection} from 'typedoc'; +import {gatherRouteInformation, generateDocumentationForRoute, logger, rimrafPromisifed} from './common'; +import {NodesWithMetaInformation} from './types'; + +commander.version(JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json')).toString()).version); + +commander + .command('routes ') + .action(async (relativeMdPath) => { + if (!existsSync(resolve('tmp'))) { + // create tmp directory + mkdirSync(resolve('tmp')); + } + + const command = resolve('node_modules', '.bin', 'typedoc'); + const jsonPath = resolve('tmp', 'out.json'); + const mdPath = resolve(relativeMdPath); + + logger.info(`Using Typedoc from ${command}.`); + + const result = execSync(`${command} --includeDeclarations --excludeExternals --mode modules --json ${jsonPath}`); + + result.toString().split('\n').forEach((line) => { + if (line.length > 0) { + logger.info(line); + } + }); + + const jsonContent: ProjectReflection = JSON.parse(readFileSync(jsonPath).toString()); + + const nodes: NodesWithMetaInformation = {}; + + jsonContent.children.forEach((module: any) => { + if (Array.isArray(module.children) && module.children.length > 0) { + module.children.forEach((node: any) => { + nodes[node.name] = { + module: module.name.substring(1, module.name.length - 1), + type: node.kindString, + }; + }); + } + }); + + const routes = await gatherRouteInformation(jsonContent); + + let output: string = '# Routes\n\n'; + + routes.forEach((routeWithMetaInformation) => { + output += generateDocumentationForRoute(routeWithMetaInformation, nodes); + }); + + writeFileSync(mdPath, output); + + logger.ok(`Route documentation written to ${mdPath}.`); + + // remove temporary files + await rimrafPromisifed(resolve('tmp')); + }); + +commander.parse(process.argv); + +if (commander.args.length < 2) { + commander.outputHelp(); + process.exit(1); +} diff --git a/src/common.ts b/src/common.ts new file mode 100644 index 00000000..363a2817 --- /dev/null +++ b/src/common.ts @@ -0,0 +1,168 @@ +/* + * Copyright (C) 2018 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {Logger} from '@openstapps/logger'; +import {asyncPool} from 'async-pool-native/dist/async-pool'; +import humanizeString = require('humanize-string'); +import * as rimraf from 'rimraf'; +import {ProjectReflection} from 'typedoc'; +import {promisify} from 'util'; +import {NodesWithMetaInformation, NodeWithMetaInformation, RouteWithMetaInformation} from './types'; + +/** + * Initialized logger + */ +export const logger = new Logger(); + +/** + * Promisified rimraf + */ +export const rimrafPromisifed = promisify(rimraf); + +/** + * Gather relevant information of routes + * + * This gathers the information for all routes that implement the abstract class SCAbstractRoute. + * Furthermore it instantiates every route and adds it to the information. + * + * @param reflection Contents of the JSON representation which Typedoc generates + */ +export async function gatherRouteInformation(reflection: ProjectReflection): Promise { + const routes: RouteWithMetaInformation[] = []; + + await asyncPool(2, reflection.children, async (module: any) => { + if (Array.isArray(module.children) && module.children.length > 0) { + await asyncPool(2, module.children, (async (node: any) => { + if (Array.isArray(node.extendedTypes) && node.extendedTypes.length > 0) { + if (node.extendedTypes.some((extendedType: any) => { + return extendedType.name === 'SCAbstractRoute'; + })) { + logger.info(`Found ${node.name} in ${module.originalName}.`); + + const importedModule = await import(module.originalName); + + const route = new importedModule[node.name](); + + routes.push({description: node.comment, name: node.name, route}); + } + } + })); + } + }); + + return routes; +} + +/** + * Get a linked name for a node + * + * @param name Name of the node + * @param node Node itself + * @param humanize Whether to humanize the name or not + */ +export function getLinkedNameForNode(name: string, node: NodeWithMetaInformation, humanize: boolean = false): string { + let printableName = name; + + if (humanize) { + printableName = humanizeString(name.substr(2)); + } + + let link = `[${printableName}]`; + link += `(${getLinkForNode(name, node)})`; + return link; +} + +/** + * Get link for a node + * + * @param name Name of the node + * @param node Node itself + */ +export function getLinkForNode(name: string, node: NodeWithMetaInformation): string { + let link = 'https://openstapps.gitlab.io/core/'; + const module = node.module.toLowerCase().split('/').join('_'); + + if (node.type === 'Type alias') { + link += 'modules/'; + link += `_${module}_`; + link += `.html#${name.toLowerCase()}`; + return link; + } + + let type = 'classes'; + if (node.type !== 'Class') { + type = `${node.type.toLowerCase()}s`; + } + + link += `${type}/`; + link += `_${module}_`; + link += `.${name.toLowerCase()}.html`; + return link; +} + +/** + * Generate documentation snippet for one route + * + * @param routeWithInfo A route instance with its meta information + * @param nodes + */ +export function generateDocumentationForRoute(routeWithInfo: RouteWithMetaInformation, + nodes: NodesWithMetaInformation): string { + let output = ''; + + const route = routeWithInfo.route; + + output += `## \`${route.method} ${route.urlFragment}\``; + output += ` ${getLinkedNameForNode(routeWithInfo.name, nodes[routeWithInfo.name], true)}\n\n`; + + if (typeof routeWithInfo.description.shortText === 'string') { + output += `**${routeWithInfo.description.shortText}**\n\n`; + } + + if (typeof routeWithInfo.description.text === 'string') { + output += `${routeWithInfo.description.text.replace('\n', '
')}\n\n`; + } + + output += `### Definition + +| parameter | value | +| --- | --- | +| request | ${getLinkedNameForNode(route.requestBodyName, nodes[route.requestBodyName])} | +| response | ${getLinkedNameForNode(route.responseBodyName, nodes[route.responseBodyName])} | +| success code | ${route.statusCodeSuccess} | +| errors | ${route.errorNames.map((errorName) => { + return getLinkedNameForNode(errorName, nodes[errorName]); + }).join('
')} | +`; + if (typeof route.obligatoryParameters === 'object' && Object.keys(route.obligatoryParameters).length > 0) { + let parameterTable = ''; + + Object.keys(route.obligatoryParameters).forEach((parameter) => { + let type = route.obligatoryParameters![parameter]; + + if (typeof nodes[type] !== 'undefined') { + type = getLinkedNameForNode(type, nodes[type]); + } + + parameterTable += ``; + }); + + parameterTable += '
parametertype
${parameter}${type}
'; + + output += `| obligatory parameters | ${parameterTable} |`; + } + output += '\n\n'; + + return output; +} diff --git a/src/core/protocol/routes/bookAvailability/BookAvailabilityRequest.ts b/src/core/protocol/routes/bookAvailability/BookAvailabilityRequest.ts index f28f5595..3f28e362 100644 --- a/src/core/protocol/routes/bookAvailability/BookAvailabilityRequest.ts +++ b/src/core/protocol/routes/bookAvailability/BookAvailabilityRequest.ts @@ -41,7 +41,19 @@ export interface SCBookAvailabilityRequestByUuid { } /** - * Route for book availiability + * Route for book availability + * + * This checks if a book is available in a library. + * + * **Example**: + * + * `POST https://example.com/bookAvailability` + * + * ```json + * { + * "isbn": "978-3-16-148410-0" + * } + * ``` */ export class SCBookAvailabilityRoute extends SCAbstractRoute { constructor() { diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 00000000..43ca248a --- /dev/null +++ b/src/types.ts @@ -0,0 +1,66 @@ +/* + * Copyright (C) 2018 StApps + * This program is free software: you can redistribute it and/or modify it + * under the terms of the GNU General Public License as published by the Free + * Software Foundation, version 3. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along with + * this program. If not, see . + */ +import {SCAbstractRoute} from './core/Route'; + +/** + * A route instance with its relevant meta information + */ +export interface RouteWithMetaInformation { + /** + * Description of the route + */ + description: { + /** + * Short text of the description - title + */ + shortText?: string; + /** + * Text of the description + */ + text?: string; + }; + /** + * Name of the route + */ + name: string; + /** + * Instance of the route + */ + route: SCAbstractRoute; +} + +/** + * A node with its relevant meta information + */ +export interface NodeWithMetaInformation { + /** + * Module the node belongs to + */ + module: string; + /** + * Type of the node + */ + type: string; +} + +/** + * A map of nodes indexed by their name + */ +export interface NodesWithMetaInformation { + /** + * Index signature + */ + [k: string]: NodeWithMetaInformation; +}