refactor: adjust code to new configuration

This commit is contained in:
Karl-Philipp Wulfert
2019-06-05 17:13:28 +02:00
parent e70d5dccab
commit 4d4f7bf7ac
9 changed files with 221 additions and 118 deletions

View File

@@ -23,14 +23,17 @@ import {Converter, getValidatableTypesFromReflection} from './schema';
import {validateFiles, writeReport} from './validate'; import {validateFiles, writeReport} from './validate';
// handle unhandled promise rejections // handle unhandled promise rejections
process.on('unhandledRejection', (error: Error) => { process.on('unhandledRejection', async (error: Error) => {
Logger.error(error.message); await Logger.error(error.message);
Logger.info(error.stack); Logger.info(error.stack);
process.exit(1); process.exit(1);
}); });
commander commander
.version(JSON.parse(readFileSync(resolve(__dirname, '..', 'package.json')).toString()).version); .version(JSON.parse(
readFileSync(resolve(__dirname, '..', 'package.json'))
.toString(),
).version);
commander commander
.command('routes <srcPath> <mdPath>') .command('routes <srcPath> <mdPath>')
@@ -46,7 +49,7 @@ commander
const routes = await gatherRouteInformation(projectReflection); const routes = await gatherRouteInformation(projectReflection);
// initialize markdown output // initialize markdown output
let output: string = '# Routes\n\n'; let output = '# Routes\n\n';
// generate documentation for all routes // generate documentation for all routes
routes.forEach((routeWithMetaInformation) => { routes.forEach((routeWithMetaInformation) => {
@@ -84,7 +87,8 @@ commander
Logger.info(`Trying to find a package.json for ${srcPath}.`); Logger.info(`Trying to find a package.json for ${srcPath}.`);
let path = srcPath; let path = srcPath;
// TODO: this check should be less ugly! // TODO: this check should be less ugly! --- What is this doing anyway?
// tslint:disable-next-line:no-magic-numbers
while (!existsSync(join(path, 'package.json')) && path.length > 5) { while (!existsSync(join(path, 'package.json')) && path.length > 5) {
path = resolve(path, '..'); path = resolve(path, '..');
} }
@@ -103,9 +107,10 @@ commander
validatableTypes.forEach((type) => { validatableTypes.forEach((type) => {
const schema = coreConverter.getSchema(type, coreVersion); const schema = coreConverter.getSchema(type, coreVersion);
// tslint:disable-next-line:no-magic-numbers
const stringifiedSchema = JSON.stringify(schema, null, 2); const stringifiedSchema = JSON.stringify(schema, null, 2);
const file = join(schemaPath, type + '.json'); const file = join(schemaPath, `${type}.json`);
// write schema to file // write schema to file
writeFileSync(file, stringifiedSchema); writeFileSync(file, stringifiedSchema);
@@ -126,9 +131,13 @@ commander
const errorsPerFile = await validateFiles(schemaPath, testPath); const errorsPerFile = await validateFiles(schemaPath, testPath);
let unexpected = false; let unexpected = false;
Object.keys(errorsPerFile).forEach((file) => { for (const file in errorsPerFile) {
if (!errorsPerFile.hasOwnProperty(file)) {
continue;
}
unexpected = unexpected || errorsPerFile[file].some((error) => !error.expected); unexpected = unexpected || errorsPerFile[file].some((error) => !error.expected);
}); }
if (typeof relativeReportPath !== 'undefined') { if (typeof relativeReportPath !== 'undefined') {
const reportPath = resolve(relativeReportPath); const reportPath = resolve(relativeReportPath);
@@ -138,7 +147,7 @@ commander
if (!unexpected) { if (!unexpected) {
Logger.ok('Successfully finished validation.'); Logger.ok('Successfully finished validation.');
} else { } else {
Logger.error('Unexpected errors occurred during validation'); await Logger.error('Unexpected errors occurred during validation');
process.exit(1); process.exit(1);
} }
}); });

View File

@@ -53,14 +53,35 @@ export interface RouteWithMetaInformation {
* Instance of the route * Instance of the route
*/ */
route: { route: {
/**
* Error names of a route
*/
errorNames: Error[]; errorNames: Error[];
/**
* Method of the route
*/
method: string; method: string;
/**
* Obligatory parameters of the route
*/
obligatoryParameters: { obligatoryParameters: {
[k: string]: string; [k: string]: string;
} };
/**
* Name of the request body
*/
requestBodyName: string; requestBodyName: string;
/**
* Name of the response body
*/
responseBodyName: string; responseBodyName: string;
/**
* Status code on success
*/
statusCodeSuccess: number; statusCodeSuccess: number;
/**
* URL fragment
*/
urlFragment: string; urlFragment: string;
}; };
} }
@@ -93,13 +114,21 @@ export interface NodesWithMetaInformation {
* A schema with definitions * A schema with definitions
*/ */
interface SchemaWithDefinitions extends JSONSchema { interface SchemaWithDefinitions extends JSONSchema {
definitions: { [name: string]: Definition }; /**
* Definitions of the schema
*/
definitions: { [name: string]: Definition; };
} }
/** /**
* An expectable error * An expectable error
*/ */
export type ExpectableValidationError = ValidationError & { expected: boolean }; export interface ExpectableValidationError extends ValidationError {
/**
* Whether or not the error is expected
*/
expected: boolean;
}
/** /**
* A map of files and their expectable validation errors * A map of files and their expectable validation errors
@@ -162,7 +191,9 @@ export function getTsconfigPath(startPath: string): string {
let tsconfigPath = startPath; let tsconfigPath = startPath;
// see https://stackoverflow.com/questions/9652043/identifying-the-file-system-root-with-node-js // see https://stackoverflow.com/questions/9652043/identifying-the-file-system-root-with-node-js
const root = (platform() === 'win32') ? process.cwd().split(sep)[0] : '/'; const root = (platform() === 'win32') ? process
.cwd()
.split(sep)[0] : '/';
// repeat until a tsconfig.json is found // repeat until a tsconfig.json is found
while (!existsSync(join(tsconfigPath, 'tsconfig.json'))) { while (!existsSync(join(tsconfigPath, 'tsconfig.json'))) {

View File

@@ -72,18 +72,15 @@ async function packCliJs(): Promise<void> {
let internalRequire: string | null = null; let internalRequire: string | null = null;
const adjustedContent = '#!/usr/bin/env node\n\n' + content const adjustedContent = content
.split('\n') .split('\n')
.map((line, lineNumber) => { .map((line, lineNumber) => {
// check for exports (cli.js is not allowed to export anything) // check for exports (cli.js is not allowed to export anything)
if (Array.isArray(line.match(/^\s*((exports)|(module\.exports))/))) { if (Array.isArray(line.match(/^\s*((exports)|(module\.exports))/))) {
throw new Error( throw new Error(
'Line ' + `Line '${lineNumber}' in 'cli.js' exports something. cli.js is not for exporting. Line was:
lineNumber + ${line}`,
' in cli.js has a reference to the exports object. cli.js is not for exporting. Line was: "'
+ line
+ '"',
); );
} }
@@ -92,7 +89,9 @@ async function packCliJs(): Promise<void> {
const match = line.match(/^(\s*)(const|var) ([a-z0-9_]*) = require\(("[^"]+"|'[^']+')\);$/i); const match = line.match(/^(\s*)(const|var) ([a-z0-9_]*) = require\(("[^"]+"|'[^']+')\);$/i);
if (match !== null) { if (match !== null) {
// tslint:disable-next-line:no-magic-numbers
const importedName = match[3]; const importedName = match[3];
// tslint:disable-next-line:no-magic-numbers
const moduleName = match[4].substring(1, match[4].length - 1); const moduleName = match[4].substring(1, match[4].length - 1);
// if it begins with '.' and not ends with json // if it begins with '.' and not ends with json
@@ -100,19 +99,23 @@ async function packCliJs(): Promise<void> {
// is the first internal require // is the first internal require
if (internalRequire !== null) { if (internalRequire !== null) {
return 'const ' + importedName + ' = ' + internalRequire + ';'; return `const ${importedName} = ${internalRequire};`;
} }
// only the first import needs a require // only the first import needs a require
internalRequire = importedName; internalRequire = importedName;
return 'const ' + importedName + ' = require("./index");';
return `const ${importedName} = require("./index");`;
} }
} }
return line; return line;
}) })
.join('\n'); .join('\n');
return await writeFilePromisified(path, adjustedContent); return writeFilePromisified(path, `#!/usr/bin/env node
${adjustedContent}`);
} }
/** /**
@@ -127,11 +130,11 @@ async function getAllTypeDefinitions(): Promise<string[]> {
], ],
}); });
const promises = fileNames.map((fileName) => { const promises = fileNames.map(async (fileName) => {
return readFilePromisified(fileName, 'utf8'); return readFilePromisified(fileName, 'utf8');
}); });
return await Promise.all(promises); return Promise.all(promises);
} }
/** /**
@@ -147,7 +150,7 @@ async function packTypeDefinitions(): Promise<void> {
const typeDefinitions = await getAllTypeDefinitions(); const typeDefinitions = await getAllTypeDefinitions();
// pack TypeScript definition files // pack TypeScript definition files
const imports: { [k: string]: string[] } = {}; const imports: { [k: string]: string[]; } = {};
const referenceLines: string[] = []; const referenceLines: string[] = [];
@@ -163,6 +166,7 @@ async function packTypeDefinitions(): Promise<void> {
if (line.indexOf('/// <reference') === 0) { if (line.indexOf('/// <reference') === 0) {
referenceLines.push(line); referenceLines.push(line);
return '// moved referenced line'; return '// moved referenced line';
} }
@@ -170,13 +174,15 @@ async function packTypeDefinitions(): Promise<void> {
const match = line.match(/^import {([^}].*)} from '([^'].*)';$/); const match = line.match(/^import {([^}].*)} from '([^'].*)';$/);
if (match !== null) { if (match !== null) {
// extract module // tslint:disable-next-line:no-magic-numbers - extract module
const module = match[2]; const module = match[2];
// extract imported objects // extract imported objects
const importedObjects = match[1].split(',').map((object) => { const importedObjects = match[1]
return object.trim(); .split(',')
}); .map((object) => {
return object.trim();
});
// add list of already imported objects for module // add list of already imported objects for module
if (typeof imports[module] === 'undefined') { if (typeof imports[module] === 'undefined') {
@@ -195,27 +201,31 @@ async function packTypeDefinitions(): Promise<void> {
// replace import line // replace import line
if (objectsToImport.length === 0) { if (objectsToImport.length === 0) {
return '// extraneous removed import'; return '// extraneous removed import';
} else {
return 'import { ' + objectsToImport.join(', ') + ' } from \'' + module + '\';';
} }
return `import {${objectsToImport.join(', ')}} from '${module}';`;
} }
return line; return line;
}) })
// filter lines which contain "local" imports // filter lines which contain "local" imports
.filter((line) => { .filter((line) => {
return !line.match(/^import .* from '\./); return line.match(/^import .* from '\./) === null;
}) })
// concat all lines separated by new lines // concat all lines separated by new lines
.join('\n'); .join('\n');
if (allDefinitions.length > 0) { if (allDefinitions.length > 0) {
if (referenceLines.length > 0) { if (referenceLines.length > 0) {
allDefinitions = referenceLines.join('\n') + '\n\n' + allDefinitions; allDefinitions = `${referenceLines.join('\n')}
${allDefinitions}`;
} }
// write packed TypeScript definition files // write packed TypeScript definition files
return await writeFilePromisified(path, PACK_IDENTIFIER + '\n\n' + allDefinitions); return writeFilePromisified(path, `${PACK_IDENTIFIER}
${allDefinitions}`);
} }
} }
@@ -233,17 +243,21 @@ async function getAllJavaScriptModules(): Promise<JavaScriptModule[]> {
const promises = fileNames.map(async (fileName) => { const promises = fileNames.map(async (fileName) => {
const fileContent = await readFilePromisified(fileName, 'utf8'); const fileContent = await readFilePromisified(fileName, 'utf8');
const directory = dirname(fileName).replace(new RegExp('^' + join(cwd(), 'lib')), ''); const directory = dirname(fileName)
.replace(new RegExp(`^${join(cwd(), 'lib')}`), '');
return { return {
content: '(function() {\n' + fileContent + '\n})();\n', content: `(function() {
${fileContent}
})();
`,
dependencies: getAllInternalDependencies(fileContent), dependencies: getAllInternalDependencies(fileContent),
directory: directory, directory: directory,
name: basename(fileName, '.js'), name: basename(fileName, '.js'),
}; };
}); });
return await Promise.all(promises); return Promise.all(promises);
} }
/** /**
@@ -271,13 +285,16 @@ async function packJavaScriptFiles(): Promise<void> {
// replace lines with internal requires // replace lines with internal requires
if (match !== null) { if (match !== null) {
// match[6] or match[8] contain the modulePath // tslint:disable-next-line:no-magic-numbers - match[6] or match[8] contain the modulePath
if (typeof match[6] === 'undefined') { if (typeof match[6] === 'undefined') {
// tslint:disable-next-line:no-magic-numbers
match[6] = match[8]; match[6] = match[8];
} }
const whiteSpace = match[1] ? match[1] : ''; const whiteSpace = (typeof match[1] === 'string' && match[1].length > 0) ? match[1] : '';
// tslint:disable-next-line:no-magic-numbers
const importedName = match[3]; const importedName = match[3];
// tslint:disable-next-line:no-magic-numbers
const modulePath = match[6]; const modulePath = match[6];
// leave line unchanged if it is a "global" import // leave line unchanged if it is a "global" import
@@ -286,22 +303,23 @@ async function packJavaScriptFiles(): Promise<void> {
} }
// replace internal requires with `module.exports` // replace internal requires with `module.exports`
if (existsSync(join(cwd(), 'lib', module.directory, modulePath + '.js'))) { if (existsSync(join(cwd(), 'lib', module.directory, `${modulePath}.js`))) {
return whiteSpace + 'const ' + importedName + ' = module.exports;'; return `${whiteSpace}const ${importedName} = module.exports;`;
} }
if (existsSync(join(cwd(), 'src', module.directory, modulePath))) { if (existsSync(join(cwd(), 'src', module.directory, modulePath))) {
return whiteSpace + 'const ' + importedName + ' = require(\'../src/' + modulePath + '\');'; return `${whiteSpace} const ${importedName} = require(../src/${modulePath});`;
} }
Logger.warn('Import ' + importedName + ' could not be found in module.directory ' + modulePath); Logger.warn(`Import ${importedName} could not be found in module.directory ${modulePath}.`);
} }
return line; return line;
}) })
.join('\n'); .join('\n');
return '// Module: ' + module.name + '\n' + module.content; return `// Module: ${module.name}
${module.content}`;
}) })
// concat them separated by new lines // concat them separated by new lines
.join('\n\n\n\n\n') .join('\n\n\n\n\n')
@@ -332,10 +350,15 @@ async function packJavaScriptFiles(): Promise<void> {
if (wholeCode.length > 0) { if (wholeCode.length > 0) {
// add meta lines to the file // add meta lines to the file
wholeCode = '"use strict";\nObject.defineProperty(exports, "__esModule", { value: true });\n\n' + wholeCode; wholeCode = `"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
${wholeCode}`;
// write packed JavaScript files // write packed JavaScript files
return await writeFilePromisified(path, PACK_IDENTIFIER + '\n\n' + wholeCode); return writeFilePromisified(path, `${PACK_IDENTIFIER}
${wholeCode}`);
} }
} }
@@ -351,8 +374,9 @@ async function deleteFileIfExistingAndPacked(path: string): Promise<void> {
// check if packed by this script // check if packed by this script
if (content.indexOf(PACK_IDENTIFIER) === 0) { if (content.indexOf(PACK_IDENTIFIER) === 0) {
Logger.log('Found `' + path + '` which is packed by this script. Deleting it...'); Logger.log(`Found '${path}' which is packed by this script. Deleting it...`);
return await unlinkPromisified(path);
return unlinkPromisified(path);
} }
} catch (err) { } catch (err) {
if (err.code === 'ENOENT') { if (err.code === 'ENOENT') {
@@ -372,23 +396,26 @@ function getAllInternalDependencies(moduleContent: string): string[] {
moduleContent.match(/^\s*(const|var) [a-z0-9_]* = require\("([^"]+)"\)|require\('([^']+)'\);$/gmi); moduleContent.match(/^\s*(const|var) [a-z0-9_]* = require\("([^"]+)"\)|require\('([^']+)'\);$/gmi);
if (Array.isArray(requireLines)) { if (Array.isArray(requireLines)) {
return requireLines.map((requireLine) => { return requireLines
const matches = requireLine.match(/require\("([^"]+)"\)|require\('([^']+)'\);$/i); .map((requireLine) => {
const matches = requireLine.match(/require\("([^"]+)"\)|require\('([^']+)'\);$/i);
// previously matched require line does not contain a require?! // previously matched require line does not contain a require?!
if (matches === null) { if (matches === null) {
throw new Error(); throw new Error();
} }
// return only the moduleName // return only the moduleName
return matches[1]; return matches[1];
}).filter((moduleName) => { })
// filter out internal modules beginning with './' and not ending with '.json' .filter((moduleName) => {
return /^[.]{1,2}\/(?!.*\.json$).*$/i.test(moduleName); // filter out internal modules beginning with './' and not ending with '.json'
}).map((internalModuleName) => { return /^[.]{1,2}\/(?!.*\.json$).*$/i.test(moduleName);
// cut './' from the name })
return internalModuleName.substring(2); .map((internalModuleName) => {
}); // cut './' from the name
return internalModuleName.substring('./'.length);
});
} }
return []; return [];
@@ -417,11 +444,13 @@ function topologicalSort(modules: JavaScriptModule[]): JavaScriptModule[] {
}); });
// sort graph and return as an array of sorted modules // sort graph and return as an array of sorted modules
return topoSort.array(nodes, edges).map((moduleName: string) => { return topoSort
return modules.find((module) => { .array(nodes, edges)
return module.name === moduleName; .map((moduleName: string) => {
return modules.find((module) => {
return module.name === moduleName;
});
}); });
});
} }
/** /**

View File

@@ -17,5 +17,8 @@
* @validatable * @validatable
*/ */
export interface Foo { export interface Foo {
/**
* Dummy parameter
*/
lorem: 'ipsum'; lorem: 'ipsum';
} }

View File

@@ -14,7 +14,9 @@
*/ */
import {asyncPool} from '@krlwlfrt/async-pool'; import {asyncPool} from '@krlwlfrt/async-pool';
import {Logger} from '@openstapps/logger'; import {Logger} from '@openstapps/logger';
import {basename, dirname, join} from 'path';
import {ProjectReflection} from 'typedoc'; import {ProjectReflection} from 'typedoc';
import {Type} from 'typedoc/dist/lib/models';
import {NodesWithMetaInformation, NodeWithMetaInformation, RouteWithMetaInformation} from './common'; import {NodesWithMetaInformation, NodeWithMetaInformation, RouteWithMetaInformation} from './common';
/** /**
@@ -32,17 +34,20 @@ export async function gatherRouteInformation(reflection: ProjectReflection): Pro
throw new Error('Project reflection doesn\'t contain any modules.'); throw new Error('Project reflection doesn\'t contain any modules.');
} }
// tslint:disable-next-line:no-magic-numbers
await asyncPool(2, reflection.children, async (module) => { await asyncPool(2, reflection.children, async (module) => {
if (Array.isArray(module.children) && module.children.length > 0) { if (Array.isArray(module.children) && module.children.length > 0) {
// tslint:disable-next-line:no-magic-numbers
await asyncPool(2, module.children, (async (node) => { await asyncPool(2, module.children, (async (node) => {
if (Array.isArray(node.extendedTypes) && node.extendedTypes.length > 0) { if (Array.isArray(node.extendedTypes) && node.extendedTypes.length > 0) {
if (node.extendedTypes.some((extendedType) => { if (node.extendedTypes.some((extendedType) => {
return (extendedType as any).name === 'SCAbstractRoute'; // tslint:disable-next-line:completed-docs
return (extendedType as (Type & { name: string; })).name === 'SCAbstractRoute';
})) { })) {
Logger.info(`Found ${node.name} in ${module.originalName}.`); Logger.info(`Found ${node.name} in ${module.originalName}.`);
if (module.originalName.match(/\.d\.ts$/)) { if (Array.isArray(module.originalName.match(/\.d\.ts$/))) {
module.originalName = module.originalName.substr(0, module.originalName.length - 5); module.originalName = join(dirname(module.originalName), basename(module.originalName, '.d.ts'));
Logger.info(`Using compiled version of module in ${module.originalName}.`); Logger.info(`Using compiled version of module in ${module.originalName}.`);
} }
@@ -71,17 +76,18 @@ export async function gatherRouteInformation(reflection: ProjectReflection): Pro
* @param node Node itself * @param node Node itself
* @param humanize Whether to humanize the name or not * @param humanize Whether to humanize the name or not
*/ */
export function getLinkedNameForNode(name: string, node: NodeWithMetaInformation, humanize: boolean = false): string { export function getLinkedNameForNode(name: string, node: NodeWithMetaInformation, humanize = false): string {
const humanizeString = require('humanize-string'); const humanizeString = require('humanize-string');
let printableName = name; let printableName = name;
if (humanize) { if (humanize) {
printableName = humanizeString(name.substr(2)); printableName = humanizeString(name.substr('SC'.length));
} }
let link = `[${printableName}]`; let link = `[${printableName}]`;
link += `(${getLinkForNode(name, node)})`; link += `(${getLinkForNode(name, node)})`;
return link; return link;
} }
@@ -93,12 +99,16 @@ export function getLinkedNameForNode(name: string, node: NodeWithMetaInformation
*/ */
export function getLinkForNode(name: string, node: NodeWithMetaInformation): string { export function getLinkForNode(name: string, node: NodeWithMetaInformation): string {
let link = 'https://openstapps.gitlab.io/core/'; let link = 'https://openstapps.gitlab.io/core/';
const module = node.module.toLowerCase().split('/').join('_'); const module = node.module
.toLowerCase()
.split('/')
.join('_');
if (node.type === 'Type alias') { if (node.type === 'Type alias') {
link += 'modules/'; link += 'modules/';
link += `_${module}_`; link += `_${module}_`;
link += `.html#${name.toLowerCase()}`; link += `.html#${name.toLowerCase()}`;
return link; return link;
} }
@@ -110,6 +120,7 @@ export function getLinkForNode(name: string, node: NodeWithMetaInformation): str
link += `${type}/`; link += `${type}/`;
link += `_${module}_`; link += `_${module}_`;
link += `.${name.toLowerCase()}.html`; link += `.${name.toLowerCase()}.html`;
return link; return link;
} }
@@ -117,7 +128,7 @@ export function getLinkForNode(name: string, node: NodeWithMetaInformation): str
* Generate documentation snippet for one route * Generate documentation snippet for one route
* *
* @param routeWithInfo A route instance with its meta information * @param routeWithInfo A route instance with its meta information
* @param nodes * @param nodes Nodes with meta information
*/ */
export function generateDocumentationForRoute(routeWithInfo: RouteWithMetaInformation, export function generateDocumentationForRoute(routeWithInfo: RouteWithMetaInformation,
nodes: NodesWithMetaInformation): string { nodes: NodesWithMetaInformation): string {
@@ -143,14 +154,20 @@ export function generateDocumentationForRoute(routeWithInfo: RouteWithMetaInform
| request | ${getLinkedNameForNode(route.requestBodyName, nodes[route.requestBodyName])} | | request | ${getLinkedNameForNode(route.requestBodyName, nodes[route.requestBodyName])} |
| response | ${getLinkedNameForNode(route.responseBodyName, nodes[route.responseBodyName])} | | response | ${getLinkedNameForNode(route.responseBodyName, nodes[route.responseBodyName])} |
| success code | ${route.statusCodeSuccess} | | success code | ${route.statusCodeSuccess} |
| errors | ${route.errorNames.map((error) => { | errors | ${route.errorNames
return getLinkedNameForNode(error.name, nodes[error.name]); .map((error) => {
}).join('<br>')} | return getLinkedNameForNode(error.name, nodes[error.name]);
})
.join('<br>')} |
`; `;
if (typeof route.obligatoryParameters === 'object' && Object.keys(route.obligatoryParameters).length > 0) { if (typeof route.obligatoryParameters === 'object' && Object.keys(route.obligatoryParameters).length > 0) {
let parameterTable = '<table><tr><th>parameter</th><th>type</th></tr>'; let parameterTable = '<table><tr><th>parameter</th><th>type</th></tr>';
Object.keys(route.obligatoryParameters).forEach((parameter) => { for (const parameter in route.obligatoryParameters) {
if (!route.obligatoryParameters.hasOwnProperty(parameter)) {
continue;
}
let type = route.obligatoryParameters![parameter]; let type = route.obligatoryParameters![parameter];
if (typeof nodes[type] !== 'undefined') { if (typeof nodes[type] !== 'undefined') {
@@ -158,7 +175,7 @@ export function generateDocumentationForRoute(routeWithInfo: RouteWithMetaInform
} }
parameterTable += `<tr><td>${parameter}</td><td>${type}</td></tr>`; parameterTable += `<tr><td>${parameter}</td><td>${type}</td></tr>`;
}); }
parameterTable += '</table>'; parameterTable += '</table>';
@@ -182,14 +199,14 @@ export function getNodeMetaInformationMap(projectReflection: ProjectReflection):
} }
// iterate over modules // iterate over modules
projectReflection.children.forEach((module: any) => { projectReflection.children.forEach((module) => {
if (Array.isArray(module.children) && module.children.length > 0) { if (Array.isArray(module.children) && module.children.length > 0) {
// iterate over types // iterate over types
module.children.forEach((node: any) => { module.children.forEach((node) => {
// add node with module and type // add node with module and type
nodes[node.name] = { nodes[node.name] = {
module: module.name.substring(1, module.name.length - 1), module: module.name.substring(1, module.name.length - 1),
type: node.kindString, type: node.kindString!,
}; };
}); });
} }

View File

@@ -12,10 +12,10 @@
* You should have received a copy of the GNU General Public License along with * You should have received a copy of the GNU General Public License along with
* this program. If not, see <https://www.gnu.org/licenses/>. * this program. If not, see <https://www.gnu.org/licenses/>.
*/ */
import * as ajv from 'ajv'; import * as Ajv from 'ajv';
import {Schema as JSONSchema} from 'jsonschema'; import {Schema as JSONSchema} from 'jsonschema';
import {join} from 'path'; import {join} from 'path';
import {DEFAULT_CONFIG, SchemaGenerator} from 'ts-json-schema-generator'; import {DEFAULT_CONFIG, Definition, SchemaGenerator} from 'ts-json-schema-generator';
import {createFormatter} from 'ts-json-schema-generator/dist/factory/formatter'; import {createFormatter} from 'ts-json-schema-generator/dist/factory/formatter';
import {createParser} from 'ts-json-schema-generator/dist/factory/parser'; import {createParser} from 'ts-json-schema-generator/dist/factory/parser';
import {createProgram} from 'ts-json-schema-generator/dist/factory/program'; import {createProgram} from 'ts-json-schema-generator/dist/factory/program';
@@ -28,8 +28,15 @@ import {getTsconfigPath, isSchemaWithDefinitions} from './common';
* Converts TypeScript source files to JSON schema files * Converts TypeScript source files to JSON schema files
*/ */
export class Converter { export class Converter {
private generator: SchemaGenerator; /**
private schemaValidator: ajv.Ajv; * Generator instance
*/
private readonly generator: SchemaGenerator;
/**
* Schema validator instance
*/
private readonly schemaValidator: Ajv.Ajv;
/** /**
* Create a new converter * Create a new converter
@@ -58,24 +65,24 @@ export class Converter {
createFormatter(config), createFormatter(config),
); );
// create ajv instance // create Ajv instance
this.schemaValidator = new ajv(); this.schemaValidator = new Ajv();
this.schemaValidator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json')); this.schemaValidator.addMetaSchema(require('ajv/lib/refs/json-schema-draft-06.json'));
} }
/** /**
* Get schema for specific StAppsCore type * Get schema for specific StAppsCore type
* *
* @param {string} type Type to get the schema for * @param type Type to get the schema for
* @param {string} version Version to set for the schema * @param version Version to set for the schema
* @returns {Schema} Generated schema * @returns Generated schema
*/ */
getSchema(type: string, version: string): JSONSchema { getSchema(type: string, version: string): JSONSchema {
// generate schema for this file/type // generate schema for this file/type
const schema: JSONSchema = this.generator.createSchema(type); const schema: JSONSchema = this.generator.createSchema(type);
// set id of schema // set id of schema
schema.id = 'https://core.stapps.tu-berlin.de/v' + version + '/lib/schema/' + type + '.json'; schema.id = `https://core.stapps.tu-berlin.de/v${version}/lib/schema/${type}.json`;
if (isSchemaWithDefinitions(schema)) { if (isSchemaWithDefinitions(schema)) {
const selfReference = { const selfReference = {
@@ -88,7 +95,10 @@ export class Converter {
delete selfReference.id; delete selfReference.id;
// add self reference to definitions // add self reference to definitions
schema.definitions['SC' + type] = Object.assign({}, selfReference as any); schema.definitions[`SC${type}`] = {
...{},
...selfReference as unknown as Definition,
};
} }
if (!this.schemaValidator.validateSchema(schema)) { if (!this.schemaValidator.validateSchema(schema)) {
@@ -119,7 +129,7 @@ export function getValidatableTypesFromReflection(projectReflection: ProjectRefl
// check if type has annotation @validatable // check if type has annotation @validatable
if (typeof type.comment === 'object' if (typeof type.comment === 'object'
&& Array.isArray(type.comment.tags) && Array.isArray(type.comment.tags)
&& type.comment.tags.find((tag) => tag.tagName === 'validatable')) { && type.comment.tags.findIndex((tag) => tag.tagName === 'validatable') >= 0) {
// add type to list // add type to list
validatableTypes.push(type.name); validatableTypes.push(type.name);
} }

View File

@@ -18,12 +18,7 @@ import {PathLike} from 'fs';
import {Schema, Validator as JSONSchemaValidator, ValidatorResult} from 'jsonschema'; import {Schema, Validator as JSONSchemaValidator, ValidatorResult} from 'jsonschema';
import * as mustache from 'mustache'; import * as mustache from 'mustache';
import {basename, join, resolve} from 'path'; import {basename, join, resolve} from 'path';
import { import {ExpectableValidationErrors, globPromisified, readFilePromisified, writeFilePromisified} from './common';
ExpectableValidationErrors,
globPromisified,
readFilePromisified,
writeFilePromisified,
} from './common';
/** /**
* StAppsCore validator * StAppsCore validator
@@ -32,7 +27,7 @@ export class Validator {
/** /**
* Map of schema names to schemas * Map of schema names to schemas
*/ */
private readonly schemas: { [type: string]: Schema } = {}; private readonly schemas: { [type: string]: Schema; } = {};
/** /**
* JSONSchema validator instance * JSONSchema validator instance
@@ -60,7 +55,7 @@ export class Validator {
Logger.log(`Adding schemas from ${schemaDir} to validator.`); Logger.log(`Adding schemas from ${schemaDir} to validator.`);
// Iterate over schema files // tslint:disable-next-line:no-magic-numbers - iterate over schema files
await asyncPool(2, schemaFiles, async (file) => { await asyncPool(2, schemaFiles, async (file) => {
// read schema file // read schema file
const buffer = await readFilePromisified(file); const buffer = await readFilePromisified(file);
@@ -84,7 +79,7 @@ export class Validator {
* @param instance Instance to validate * @param instance Instance to validate
* @param schema Name of schema to validate instance against or the schema itself * @param schema Name of schema to validate instance against or the schema itself
*/ */
public validate(instance: any, schema: string | Schema): ValidatorResult { public validate(instance: unknown, schema: string | Schema): ValidatorResult {
if (typeof schema === 'string') { if (typeof schema === 'string') {
// if you want to access a schema that is contained in the validator object // if you want to access a schema that is contained in the validator object
if (typeof this.schemas[schema] !== 'object') { if (typeof this.schemas[schema] !== 'object') {
@@ -92,10 +87,10 @@ export class Validator {
} }
return this.validator.validate(instance, this.schemas[schema]); return this.validator.validate(instance, this.schemas[schema]);
} else {
// if you have a schema and want to validate it directly
return this.validator.validate(instance, schema);
} }
// if you have a schema and want to validate it directly
return this.validator.validate(instance, schema);
} }
} }
@@ -122,7 +117,7 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
// map of errors per file // map of errors per file
const errors: ExpectableValidationErrors = {}; const errors: ExpectableValidationErrors = {};
// iterate over files to test // tslint:disable-next-line:no-magic-numbers - iterate over files to test
await asyncPool(2, testFiles, async (testFile) => { await asyncPool(2, testFiles, async (testFile) => {
const testFileName = basename(testFile); const testFileName = basename(testFile);
@@ -145,7 +140,7 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
errors[testFileName] = []; errors[testFileName] = [];
// iterate over errors // iterate over errors
result.errors.forEach((error) => { for (const error of result.errors) {
// get idx of expected error // get idx of expected error
const errorIdx = expectedErrors.indexOf(error.name); const errorIdx = expectedErrors.indexOf(error.name);
let expected = false; let expected = false;
@@ -156,7 +151,7 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
expected = true; expected = true;
} else { } else {
unexpectedErrors++; unexpectedErrors++;
Logger.error(`Unexpected error ${error.name} in ${testFile}`); await Logger.error(`Unexpected error ${error.name} in ${testFile}`);
} }
// add error to list of errors // add error to list of errors
@@ -164,12 +159,13 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
...error, ...error,
expected, expected,
}); });
}); }
} }
if (expectedErrors.length > 0) { if (expectedErrors.length > 0) {
expectedErrors.forEach((error) => { for (const error of expectedErrors) {
Logger.error(`Extraneous expected error '${error}' in ${testFile}.`); await Logger.error(`Extraneous expected error '${error}' in ${testFile}.`);
errors[testFileName].push({ errors[testFileName].push({
argument: false, argument: false,
expected: false, expected: false,
@@ -177,9 +173,9 @@ export async function validateFiles(schemaDir: string, resourcesDir: string): Pr
message: `expected error ${error} did not occur`, message: `expected error ${error} did not occur`,
name: `expected ${error}`, name: `expected ${error}`,
property: 'unknown', property: 'unknown',
schema: undefined as any, schema: 'undefined',
}); });
}); }
} else if (unexpectedErrors === 0) { } else if (unexpectedErrors === 0) {
Logger.info(`Successfully validated ${testFile}.`); Logger.info(`Successfully validated ${testFile}.`);
} }
@@ -203,15 +199,21 @@ export async function writeReport(reportPath: PathLike, errors: ExpectableValida
let output = ''; let output = '';
Object.keys(errors).forEach((fileName) => { for (const fileName in errors) {
if (!errors.hasOwnProperty(fileName)) {
continue;
}
let fileOutput = ''; let fileOutput = '';
errors[fileName].forEach((error, idx) => { errors[fileName].forEach((error, idx) => {
fileOutput += mustache.render(errorTemplate, { fileOutput += mustache.render(errorTemplate, {
idx: idx + 1, idx: idx + 1,
// tslint:disable-next-line:no-magic-numbers
instance: JSON.stringify(error.instance, null, 2), instance: JSON.stringify(error.instance, null, 2),
message: error.message, message: error.message,
property: error.property, property: error.property,
// tslint:disable-next-line:no-magic-numbers
schema: JSON.stringify(error.schema, null, 2), schema: JSON.stringify(error.schema, null, 2),
status: (error.expected) ? 'alert-success' : 'alert-danger', status: (error.expected) ? 'alert-success' : 'alert-danger',
}); });
@@ -221,7 +223,7 @@ export async function writeReport(reportPath: PathLike, errors: ExpectableValida
errors: fileOutput, errors: fileOutput,
testFile: fileName, testFile: fileName,
}); });
}); }
buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'report.html.mustache')); buffer = await readFilePromisified(resolve(__dirname, '..', 'resources', 'report.html.mustache'));
const reportTemplate = buffer.toString(); const reportTemplate = buffer.toString();

View File

@@ -23,7 +23,7 @@ process.on('unhandledRejection', (err) => {
process.exit(1); process.exit(1);
}); });
@suite(timeout(10000), slow(5000)) @suite(timeout(20000), slow(10000))
export class CommonSpec { export class CommonSpec {
@test @test
async getTsconfigPath() { async getTsconfigPath() {

View File

@@ -23,7 +23,7 @@ process.on('unhandledRejection', (err) => {
process.exit(1); process.exit(1);
}); });
@suite(timeout(15000), slow(5000)) @suite(timeout(20000), slow(10000))
export class SchemaSpec { export class SchemaSpec {
@test @test
async getSchema() { async getSchema() {
@@ -39,6 +39,7 @@ export class SchemaSpec {
additionalProperties: false, additionalProperties: false,
properties: { properties: {
lorem: { lorem: {
description: 'Dummy parameter',
enum: [ enum: [
'ipsum', 'ipsum',
], ],
@@ -54,6 +55,7 @@ export class SchemaSpec {
id: 'https://core.stapps.tu-berlin.de/v0.0.1/lib/schema/Foo.json', id: 'https://core.stapps.tu-berlin.de/v0.0.1/lib/schema/Foo.json',
properties: { properties: {
lorem: { lorem: {
description: 'Dummy parameter',
enum: [ enum: [
'ipsum', 'ipsum',
], ],