UNPKG

boats

Version:

Beautiful Open / Async Template System - Write less yaml with BOATS and Nunjucks.

237 lines (236 loc) 10.2 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); const tslib_1 = require("tslib"); require("ts-replace-all"); const upath_1 = tslib_1.__importDefault(require("upath")); const deepmerge_1 = tslib_1.__importDefault(require("deepmerge")); const js_yaml_1 = tslib_1.__importDefault(require("js-yaml")); const picomatch_1 = tslib_1.__importDefault(require("picomatch")); const nunjucks_1 = require("nunjucks"); const pathInjector_1 = require("./pathInjector"); class Injector { constructor() { this.fileToRouteMap = {}; } /** * Render the base template and inject content if provided */ injectAndRender(inputPath, inputIndexYaml, boatsRc, isAsyncApi) { const fullPath = upath_1.default.join(upath_1.default.toUnix(process.cwd()), inputPath); const pathFromIndexToBoatsRc = upath_1.default.relative('.', upath_1.default.dirname(inputIndexYaml)); const relativePathToRoot = upath_1.default.relative(upath_1.default.dirname(inputPath), upath_1.default.dirname(inputIndexYaml)); const picomatchOptions = boatsRc.picomatchOptions || { bash: true }; const injector = new pathInjector_1.PathInjector(boatsRc.paths, pathFromIndexToBoatsRc); const yaml = this.convertRootRefToRelative((0, nunjucks_1.render)(fullPath), relativePathToRoot, injector); // @ts-ignore if (!global.boatsInject) { return yaml; } if (!/\/(paths|channels)\//.test(inputPath)) { return yaml; } if (/index\./.test(upath_1.default.basename(inputPath))) { if (isAsyncApi) { this.mapChannelIndex(yaml, inputPath); } else { this.mapPathIndex(yaml, inputPath); } return yaml; } let jsonTemplate = js_yaml_1.default.load(yaml); // @ts-ignore for (const { toAllOperations } of global.boatsInject) { if (this.shouldInject(toAllOperations, inputPath, picomatchOptions)) { jsonTemplate = this.mergeInjection(jsonTemplate, relativePathToRoot, injector, toAllOperations.content); } } return js_yaml_1.default.dump(jsonTemplate, { lineWidth: 1000 }); } /** * Merge the JSON from the YAML with the JSON injection content * * @param {object} jsonTemplate JSON representation of the YAML file * @param {string} relativePathToRoot Path from current file to root index (../ repeated) * @param {PathInjector} injector Converts shorthand absolute paths to absolutes * @param {object} content Content to be injected * * @return {object} Merged JSON of the template */ mergeInjection(jsonTemplate, relativePathToRoot, injector, content) { if (!jsonTemplate || !content) { return jsonTemplate; } if (typeof content === 'object') { content = JSON.stringify(content); } content = this.convertRootRefToRelative(content, relativePathToRoot, injector); const renderedString = (0, nunjucks_1.renderString)(content, {}); const injectionContent = js_yaml_1.default.load(renderedString); return (0, deepmerge_1.default)(jsonTemplate, injectionContent); } buildInjectRuleObject(injection) { return { excludeChannels: [], includeOnlyChannels: [], excludePaths: [], includeOnlyPaths: [], includeMethods: [], ...injection }; } shouldSkipMethod(injectRule, method) { if (injectRule.includeMethods.length) { const methodsRegex = new RegExp(`\\b(${injectRule.includeMethods.join('|')})\\b`, 'i'); return !methodsRegex.test(method); } return false; } /** * Checks if the content should be injected * * @param {object} injection Injection rule * @param {string} inputPath Path to target file * * @param {object} picomatchOptions node_modules/@types/picomatch/index.d.ts PicomatchOptions not exported from the types * @return {boolean} True if the path satisfies the rule */ shouldInject(injection, inputPath, picomatchOptions) { if (!injection) { return false; } const injectRule = this.buildInjectRuleObject(injection); const operationName = this.fileToRouteMap[inputPath]; const methodName = upath_1.default.basename(inputPath).replace(/\..*/, ''); if (/channels/.test(inputPath) && false === this.shouldInjectToChannels(operationName, injectRule, methodName, picomatchOptions)) { return false; } if (/paths/.test(inputPath) && false === this.shouldInjectToPaths(operationName, injectRule, methodName, picomatchOptions)) { return false; } return true; } /** * Returns false when the channel should not be injected into * else returns true */ shouldInjectToChannels(operationName, injectRule, methodName, picomatchOptions) { // Exclude channels if (this.globCheck(operationName, injectRule.excludeChannels, picomatchOptions, methodName)) { return false; } // Specifically include a channel if (injectRule.includeOnlyChannels.length > 0 && !this.globCheck(operationName, injectRule.includeOnlyChannels, picomatchOptions, methodName)) { return false; } // include method if (this.shouldSkipMethod(injectRule, methodName)) { return false; } return true; } /** * Returns false when the path should not be injected into * else returns true * * @param operationName The URL path (Open API) or the Channel path (Async API) * @param injectRule The injection rule, defined in th injection tpl helper * @param methodName The name of the method, Open API in BOATS, the file name is the method name, eg something/get.yml.. GET == method * @param picomatchOptions The https://www.npmjs.com/package/picomatch options (injected via the boatsrc) */ shouldInjectToPaths(operationName, injectRule, methodName, picomatchOptions) { // Exclude a path completely if (this.globCheck(operationName, injectRule.excludePaths, picomatchOptions, methodName)) { return false; } // Specifically include a path if (injectRule.includeOnlyPaths.length > 0 && !this.globCheck(operationName, injectRule.includeOnlyPaths, picomatchOptions, methodName)) { return false; } // include method if (this.shouldSkipMethod(injectRule, methodName)) { return false; } return true; } /** * Pico matching the path against the rules in the inject object * @param needle * @param haystack * @param picoOptions node_modules/@types/picomatch/index.d.ts PicomatchOptions not exported from the types * @param currentMethod */ globCheck(needle, haystack, picoOptions, currentMethod) { let resp = false; if (typeof needle !== 'string') { // catch for tpl not included in a manual index file return resp; } haystack.forEach((hay) => { let stringToCheck; let methodsToCheck; if (typeof hay === 'string') { stringToCheck = hay; } else if (typeof hay === 'object' && hay.path && hay.methods && Array.isArray(hay.methods)) { stringToCheck = hay.path; methodsToCheck = hay.methods.map((method) => method.toLowerCase()); } else { throw new Error('Invalid inject object passed to globCheck, expected either a string or {path: string, methods: string[]}. Got instead: ' + JSON.stringify(hay)); } const isMatch = (0, picomatch_1.default)(stringToCheck, picoOptions); if (isMatch(needle)) { if (methodsToCheck) { // when an object this only return true if the method is found if (methodsToCheck.includes(currentMethod.toLowerCase())) { resp = true; } } else { resp = true; } } }); return resp; } /** * Map filenames to routes so that exclude paths can be * calculated from the input filename */ mapPathIndex(yaml, inputPath) { const indexRoute = upath_1.default.dirname(inputPath); const index = js_yaml_1.default.load(yaml); Object.entries(index).forEach(([route, methods]) => { Object.values(methods).forEach((methodToFileRef) => { if (methodToFileRef && methodToFileRef.$ref) { const fullPath = `${indexRoute}/${methodToFileRef.$ref.replace('./', '')}`; this.fileToRouteMap[fullPath] = route; } }); }); } mapChannelIndex(yaml, inputPath) { const indexRoute = upath_1.default.dirname(inputPath); const index = js_yaml_1.default.load(yaml); for (const channel in index) { if (index[channel].$ref) { const fullPath = `${indexRoute}/${index[channel].$ref.replace('./', '')}`; this.fileToRouteMap[fullPath] = channel; } } } convertRootRefToRelative(content, relativePathToRoot, injector) { // todo abstract and unit test const replacer = (_, ref, rootRef) => { const newPath = `${upath_1.default.dirname(rootRef)}/index.yml#/${upath_1.default.basename(rootRef)}`; return `${ref}${relativePathToRoot}/${newPath}`; }; return injector.injectRefs(content.replaceAll(/(\$ref[ '"]*:[ '"]*)#\/([^ '"$]*)/g, replacer), relativePathToRoot); } } exports.default = new Injector();