UNPKG

@asyncapi/generator

Version:

The AsyncAPI generator. It can generate documentation, code, anything!

175 lines (159 loc) 6.91 kB
const path = require('path'); const utils = require('./utils'); const logMessages = require('./logMessages'); const { convertToOldAPI } = require('@asyncapi/parser'); const { ConvertDocumentParserAPIVersion, NewParser } = require('@asyncapi/multi-parser'); const parser = module.exports; /** * Convert the template defined value `apiVersion: 'v1'` to only contain the numeric value `1`. */ parser.sanitizeTemplateApiVersion = (apiVersion) => { if (!apiVersion) return; if (apiVersion && apiVersion.length > 1) { return Number(apiVersion.substring(1)); } return Number(apiVersion); }; parser.parse = async (asyncapi, oldOptions, generator) => { let apiVersion = this.sanitizeTemplateApiVersion(generator.templateConfig.apiVersion); // Defaulting to apiVersion v1 to convert it to the Parser-API v1 afterwards. if (!this.usesNewAPI(generator.templateConfig)) { apiVersion = 1; } const options = convertOldOptionsToNew(oldOptions, generator); const parser = NewParser(apiVersion, {parserOptions: options, includeSchemaParsers: true}); const { document, diagnostics } = await parser.parse(asyncapi, options); if (!document) { return {document, diagnostics}; } const correctDocument = this.getProperApiDocument(document, generator.templateConfig); return {document: correctDocument, diagnostics}; }; /** * If the template expect one of the Parser-API versions, it must be above 0 */ parser.usesNewAPI = (templateConfig = {}) => { return this.sanitizeTemplateApiVersion(templateConfig.apiVersion) > 0; }; /** * Based on the current parsed AsyncAPI document, convert it to expected API version from the template. */ parser.getProperApiDocument = (asyncapiDocument, templateConfig = {}) => { const apiVersion = this.sanitizeTemplateApiVersion(templateConfig.apiVersion); if (apiVersion === undefined) { // Convert to old API from JS Parser v1 return convertToOldAPI(asyncapiDocument); } return ConvertDocumentParserAPIVersion(asyncapiDocument, apiVersion); }; /** * Converts old parser options to the new format. * * @private - This function should not be used outside this module. * @param {object} oldOptions - The old options to convert. * @param {object} generator - The generator instance. * @returns {object} The converted options. */ // The new options for the v2 Parser are different from those for the v1 version, but in order not to release Generator v2, we are converting the old options of Parser to the new ones. function convertOldOptionsToNew(oldOptions, generator) { if (!oldOptions) return; const newOptions = {}; if (typeof oldOptions.path === 'string') { newOptions.source = oldOptions.path; } if (typeof oldOptions.applyTraits === 'boolean') { newOptions.applyTraits = oldOptions.applyTraits; } const resolvers = []; if (generator?.mapBaseUrlToFolder?.url) { resolvers.push(...getMapBaseUrlToFolderResolvers(generator.mapBaseUrlToFolder)); } if (oldOptions.resolve) { resolvers.push(...convertOldResolvers(oldOptions.resolve)); } if (resolvers.length) { newOptions.__unstable = {}; newOptions.__unstable.resolver = { resolvers, }; } return newOptions; } /** * Creates a custom resolver that maps urlToFolder.url to urlToFolder.folder * Building your custom resolver is explained here: https://apitools.dev/json-schema-ref-parser/docs/plugins/resolvers.html * * @private * @param {object} urlToFolder to resolve url e.g. https://schema.example.com/crm/ to a folder e.g. ./test/docs/. * @return {{read(*, *, *): Promise<unknown>, canRead(*): boolean, order: number}} */ function getMapBaseUrlToFolderResolvers({ url: baseUrl, folder: baseDir }) { const resolver = { order: 1, canRead: true, async read(uri) { const uriStr = uri.toString(); const localpath = uriStr.replace(baseUrl, baseDir); // Why: a $ref under the mapped baseUrl can contain `../` segments that the parser passes // through unnormalized, letting the resolved file path escape baseDir (path traversal). // Only guard refs that actually fall under baseUrl so unrelated http(s) refs keep their behavior. if (uriStr.startsWith(baseUrl)) { const resolvedBase = path.resolve(baseDir); const resolvedTarget = path.resolve(localpath); // Lexical containment runs first: it rejects `../` traversal even when the target file // does not exist (realpath below would throw ENOENT before we could report it). if (resolvedTarget !== resolvedBase && !resolvedTarget.startsWith(resolvedBase + path.sep)) { throw new Error(logMessages.mappedRefOutsideBaseFolder(uriStr, baseUrl)); } // Why: path.resolve is purely lexical and ignores symlinks, so a symlink inside baseDir // pointing elsewhere would pass the check above yet read a file outside baseDir. // Canonicalize with realpath to close that gap. realpath needs the path to exist; a // missing target is already proven contained lexically, so ENOENT is safe to ignore. const realBase = await utils.realpath(resolvedBase); const realTarget = await utils.realpath(resolvedTarget).catch((err) => { if (err.code === 'ENOENT') return null; throw err; }); if (realTarget !== null && realTarget !== realBase && !realTarget.startsWith(realBase + path.sep)) { // Why: reaching here means the lexical check above passed but canonicalization still // escaped baseDir — that can only happen via a symlink, so report it as such. throw new Error(logMessages.mappedRefSymlinkOutsideBaseFolder(uriStr, baseUrl)); } } try { return (await utils.readFile(localpath)).toString(); } catch (err) { throw new Error(logMessages.errorOpeningFile(localpath)); } } }; return [ { schema: 'http', ...resolver, }, { schema: 'https', ...resolver, }, ]; }; function convertOldResolvers(resolvers = {}) { // NOSONAR if (Object.keys(resolvers).length === 0) return []; return Object.entries(resolvers).map(([protocol, resolver]) => { return { schema: protocol, order: resolver.order || 1, canRead: (uri) => canReadFn(uri, resolver.canRead), read: (uri) => { return resolver.read({ url: uri.valueOf(), extension: uri.suffix() }); } }; }); } function canReadFn(uri, canRead) { const value = uri.valueOf(); if (typeof canRead === 'boolean') return canRead; if (typeof canRead === 'string') return canRead === value; if (Array.isArray(canRead)) return canRead.includes(value); if (canRead instanceof RegExp) return canRead.test(value); if (typeof canRead === 'function') { return canRead({ url: value, extension: uri.suffix() }); } return false; } module.exports.convertOldOptionsToNew = convertOldOptionsToNew;