Sindbad~EG File Manager
const doctrine = require('doctrine');
const parser = require('swagger-parser');
const YAML = require('yaml');
const {
hasEmptyProperty,
convertGlobPaths,
extractAnnotations,
mergeDeep,
extractYamlFromJsDoc,
isTagPresentInTags,
} = require('./utils');
/**
* Prepare the swagger/openapi specification object.
* @see https://github.com/OAI/OpenAPI-Specification/tree/master/versions
* @param {object} definition - The `definition` or `swaggerDefinition` from options.
* @returns {object} swaggerObject
*/
function prepare(definition) {
const swaggerObject = JSON.parse(JSON.stringify(definition));
const specificationTemplate = {
v2: [
'paths',
'definitions',
'responses',
'parameters',
'securityDefinitions',
],
v3: [
'paths',
'definitions',
'responses',
'parameters',
'securityDefinitions',
'components',
],
v4: ['components', 'channels'],
};
const getVersion = () => {
if (swaggerObject.asyncapi) {
return 'v4';
}
if (swaggerObject.openapi) {
return 'v3';
}
if (swaggerObject.swagger) {
return 'v2';
}
swaggerObject.swagger = '2.0';
return 'v2';
};
const version = getVersion();
specificationTemplate[version].forEach((property) => {
swaggerObject[property] = swaggerObject[property] || {};
});
swaggerObject.tags = swaggerObject.tags || [];
return swaggerObject;
}
/**
* @param {object} obj
* @param {string} ext
*/
function format(swaggerObject, ext) {
if (ext === '.yml' || ext === '.yaml') {
return YAML.stringify(swaggerObject);
}
return swaggerObject;
}
/**
* OpenAPI specification validator does not accept empty values for a few properties.
* Solves validator error: "Schema error should NOT have additional properties"
* @param {object} swaggerObject
* @returns {object} swaggerObject
*/
function clean(swaggerObject) {
for (const prop of [
'definitions',
'responses',
'parameters',
'securityDefinitions',
]) {
if (hasEmptyProperty(swaggerObject[prop])) {
delete swaggerObject[prop];
}
}
return swaggerObject;
}
/**
* Parse the swagger object and remove useless properties if necessary.
*
* @param {object} swaggerObject - Swagger object from parsing the api files.
* @returns {object} The specification.
*/
function finalize(swaggerObject, options) {
let specification = swaggerObject;
parser.parse(swaggerObject, (err, api) => {
if (!err) {
specification = api;
}
});
if (specification.openapi) {
specification = clean(specification);
}
return format(specification, options.format);
}
/**
* @param {object} swaggerObject
* @param {object} annotation
* @param {string} property
*/
function organize(swaggerObject, annotation, property) {
// Root property on purpose.
// @see https://github.com/OAI/OpenAPI-Specification/blob/master/proposals/002_Webhooks.md#proposed-solution
if (property === 'x-webhooks') {
swaggerObject[property] = mergeDeep(
swaggerObject[property],
annotation[property]
);
}
// Other extensions can be in varying places depending on different vendors and opinions.
// The following return makes it so that they are not put in `paths` in the last case.
// New specific extensions will need to be handled on case-by-case if to be included in `paths`.
if (property.startsWith('x-')) return;
const commonProperties = [
'components',
'consumes',
'produces',
'paths',
'schemas',
'securityDefinitions',
'responses',
'parameters',
'definitions',
'channels',
];
if (commonProperties.includes(property)) {
for (const definition of Object.keys(annotation[property])) {
swaggerObject[property][definition] = mergeDeep(
swaggerObject[property][definition],
annotation[property][definition]
);
}
} else if (property === 'tags') {
const { tags } = annotation;
if (Array.isArray(tags)) {
for (const tag of tags) {
if (!isTagPresentInTags(tag, swaggerObject.tags)) {
swaggerObject.tags.push(tag);
}
}
} else if (!isTagPresentInTags(tags, swaggerObject.tags)) {
swaggerObject.tags.push(tags);
}
} else {
// Paths which are not defined as "paths" property, starting with a slash "/"
swaggerObject.paths[property] = mergeDeep(
swaggerObject.paths[property],
annotation[property]
);
}
}
/**
* @param {object} options
* @returns {object} swaggerObject
*/
function build(options) {
YAML.defaultOptions.keepCstNodes = true;
// Get input definition and prepare the specification's skeleton
const definition = options.swaggerDefinition || options.definition;
const specification = prepare(definition);
const yamlDocsAnchors = new Map();
const yamlDocsErrors = [];
const yamlDocsReady = [];
for (const filePath of convertGlobPaths(options.apis)) {
const {
yaml: yamlAnnotations,
jsdoc: jsdocAnnotations,
} = extractAnnotations(filePath, options.encoding);
if (yamlAnnotations.length) {
for (const annotation of yamlAnnotations) {
const parsed = Object.assign(YAML.parseDocument(annotation), {
filePath,
});
const anchors = parsed.anchors.getNames();
if (anchors.length) {
for (const anchor of anchors) {
yamlDocsAnchors.set(anchor, parsed);
}
} else if (parsed.errors && parsed.errors.length) {
// Attach the relevent yaml section to the error for verbose logging
parsed.errors.forEach((err) => {
err.annotation = annotation;
});
yamlDocsErrors.push(parsed);
} else {
yamlDocsReady.push(parsed);
}
}
}
if (jsdocAnnotations.length) {
for (const annotation of jsdocAnnotations) {
const jsDocComment = doctrine.parse(annotation, { unwrap: true });
for (const doc of extractYamlFromJsDoc(jsDocComment)) {
const parsed = Object.assign(YAML.parseDocument(doc), { filePath });
const anchors = parsed.anchors.getNames();
if (anchors.length) {
for (const anchor of anchors) {
yamlDocsAnchors.set(anchor, parsed);
}
} else if (parsed.errors && parsed.errors.length) {
// Attach the relevent yaml section to the error for verbose logging
parsed.errors.forEach((err) => {
err.annotation = doc;
});
yamlDocsErrors.push(parsed);
} else {
yamlDocsReady.push(parsed);
}
}
}
}
}
if (yamlDocsErrors.length) {
for (const docWithErr of yamlDocsErrors) {
const errsToDelete = [];
docWithErr.errors.forEach((error, index) => {
if (error.name === 'YAMLReferenceError') {
// This should either be a smart regex or ideally a YAML library method using the error.range.
// The following works with both pretty and not pretty errors.
const refErr = error.message
.split('Aliased anchor not found: ')
.filter((a) => a)
.join('')
.split(' at line')[0];
const anchor = yamlDocsAnchors.get(refErr);
const anchorString = anchor.cstNode.toString();
const originalString = docWithErr.cstNode.toString();
const readyDocument = YAML.parseDocument(
`${anchorString}\n${originalString}`
);
yamlDocsReady.push(readyDocument);
errsToDelete.push(index);
}
});
// reverse sort the deletion array so we always delete from the end
errsToDelete.sort((a, b) => b - a);
// Cleanup solved errors in order to allow for parser to pass through.
for (const errIndex of errsToDelete) {
docWithErr.errors.splice(errIndex, 1);
}
}
// Format errors into a printable/throwable string
const errReport = yamlDocsErrors
.filter((doc) => doc.errors.length)
.map(({ errors, filePath }) => {
let str = `Error in ${filePath} :\n`;
if (options.verbose) {
str += errors
.map(
(e) =>
`${e.toString()}\nImbedded within:\n\`\`\`\n ${e.annotation.replace(
/\n/g,
'\n '
)}\n\`\`\``
)
.join('\n');
} else {
str += errors.map((e) => e.toString()).join('\n');
}
return str;
})
.filter((error) => !!error);
if (errReport.length) {
if (options.failOnErrors) {
throw new Error(errReport);
}
// Place to provide feedback for errors. Previously throwing, now reporting only.
console.info(
'Not all input has been taken into account at your final specification.'
);
console.error(`Here's the report: \n\n\n ${errReport}`);
}
}
for (const document of yamlDocsReady) {
const parsedDoc = document.toJSON();
for (const property in parsedDoc) {
organize(specification, parsedDoc, property);
}
}
return finalize(specification, options);
}
module.exports = { prepare, build, organize, finalize, format };
Sindbad File Manager Version 1.0, Coded By Sindbad EG ~ The Terrorists