boats
Version:
Beautiful Open / Async Template System - Write less yaml with BOATS and Nunjucks.
237 lines (236 loc) • 10.2 kB
JavaScript
;
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();