Commit 1a07df2d authored by krlwlfrt's avatar krlwlfrt
Browse files

feat: add tool to generate documentation for routes

parent 62ca2c33
Loading
Loading
Loading
Loading
+28 −3
Original line number Diff line number Diff line
@@ -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",
+7 −1
Original line number Diff line number Diff line
@@ -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",

src/cli.ts

0 → 100644
+83 −0
Original line number Diff line number Diff line
/*
 * 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 <https://www.gnu.org/licenses/>.
 */
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 <mdPath>')
  .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);
}

src/common.ts

0 → 100644
+168 −0
Original line number Diff line number Diff line
/*
 * 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 <https://www.gnu.org/licenses/>.
 */
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<RouteWithMetaInformation[]> {
  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', '<br>')}\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('<br>')} |
`;
  if (typeof route.obligatoryParameters === 'object' && Object.keys(route.obligatoryParameters).length > 0) {
    let parameterTable = '<table><tr><th>parameter</th><th>type</th></tr>';

    Object.keys(route.obligatoryParameters).forEach((parameter) => {
      let type = route.obligatoryParameters![parameter];

      if (typeof nodes[type] !== 'undefined') {
        type = getLinkedNameForNode(type, nodes[type]);
      }

      parameterTable += `<tr><td>${parameter}</td><td>${type}</td></tr>`;
    });

    parameterTable += '</table>';

    output += `| obligatory parameters | ${parameterTable} |`;
  }
  output += '\n\n';

  return output;
}
+13 −1
Original line number Diff line number Diff line
@@ -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() {
Loading