lib/swagger/jsonLoader.ts (300 lines of code) (raw):

import { dirname as pathDirname } from "path"; import { FilePosition, getInfo, getRootObjectInfo, Json, parseJson, pathJoin, } from "@azure-tools/openapi-tools-common"; import { load as parseYaml } from "js-yaml"; import { default as jsonPointer } from "json-pointer"; import { inject, injectable } from "inversify"; import { xmsExamples, xmsReadonlyRef } from "../util/constants"; import { getLazyBuilder } from "../util/lazyBuilder"; import { TYPES } from "../inversifyUtils"; import { FileLoader, FileLoaderOption } from "./fileLoader"; import { Loader, setDefaultOpts } from "./loader"; export interface JsonLoaderOption extends FileLoaderOption { useJsonParser?: boolean; eraseDescription?: boolean; eraseXmsExamples?: boolean; keepOriginalContent?: boolean; transformRef?: boolean; // TODO implement transformRef: false skipResolveRefKeys?: string[]; supportYaml?: boolean; } export interface FileCache { resolved?: Json; filePath: string; originalContent?: string; skipResolveRef?: boolean; mockName: string; resolveRef?: boolean; } export const $id = "$id"; export class JsonLoaderRefError extends Error { public position?: FilePosition; public url?: string; public ref?: string; public constructor(source: { $ref: string }) { super(`Failed to resolve ref for ${source.$ref}`); const info = getInfo(source); if (info !== undefined) { const rootInfo = getRootObjectInfo(info); this.position = info.position; this.url = rootInfo.url; this.ref = source.$ref; } } } @injectable() export class JsonLoader implements Loader<Json> { private mockNameMap: { [mockName: string]: string } = {}; private globalMockNameId = 0; private loadedFiles: any[] = []; private skipResolveRefKeys: Set<string>; private fileCache = new Map<string, FileCache>(); private loadFile = getLazyBuilder("resolved", async (cache: FileCache) => { const fileString = await this.fileLoader.load(cache.filePath); if (this.opts.keepOriginalContent || cache.resolveRef) { // eslint-disable-next-line require-atomic-updates cache.originalContent = fileString; } let fileContent = this.parseFileContent(cache, fileString); // eslint-disable-next-line require-atomic-updates cache.resolved = fileContent; (fileContent as any)[$id] = cache.mockName; if (cache.skipResolveRef !== true) { fileContent = await this.resolveRef(fileContent, ["$"], fileContent, cache.filePath, false); } this.loadedFiles.push(fileContent); return fileContent; }); private loadFileWithRefSiblings = getLazyBuilder("resolved", async (cache: FileCache) => { const fileString = await this.fileLoader.load(cache.filePath); if (this.opts.keepOriginalContent || cache.resolveRef) { // eslint-disable-next-line require-atomic-updates cache.originalContent = fileString; } let fileContent = this.parseFileContent(cache, fileString); // eslint-disable-next-line require-atomic-updates cache.resolved = fileContent; (fileContent as any)[$id] = cache.mockName; if (cache.skipResolveRef !== true) { fileContent = await this.resolveRef( fileContent, ["$"], fileContent, cache.filePath, false, true ); } this.loadedFiles.push(fileContent); return fileContent; }); public constructor( @inject(TYPES.opts) private opts: JsonLoaderOption, private fileLoader: FileLoader ) { setDefaultOpts(opts, { useJsonParser: true, eraseDescription: true, eraseXmsExamples: true, transformRef: true, supportYaml: false, }); this.skipResolveRefKeys = new Set(opts.skipResolveRefKeys); } private parseFileContent(cache: FileCache, fileString: string): any { if ( this.opts.supportYaml && (cache.filePath.endsWith(".yaml") || cache.filePath.endsWith(".yml")) ) { return parseYaml(fileString, { filename: cache.filePath, json: true, }); } return this.opts.useJsonParser ? parseJson(cache.filePath, fileString) : JSON.parse(fileString); // throw new Error(`Unknown file format while loading file ${cache.filePath}`); } public async load( inputFilePath: string, skipResolveRef?: boolean, keepRefSiblings?: boolean ): Promise<Json> { const filePath = this.fileLoader.relativePath(inputFilePath); let cache = this.fileCache.get(filePath); if (cache === undefined) { cache = { filePath, mockName: this.getNextMockName(filePath), }; this.fileCache.set(filePath, cache); } if (skipResolveRef !== undefined) { cache.skipResolveRef = skipResolveRef; } if (keepRefSiblings) { return this.loadFileWithRefSiblings(cache); } return this.loadFile(cache); } public getFileContentFromCache(filePath: string): Json | undefined { if (this.fileCache !== undefined) { const cache = this.fileCache.get(filePath); return cache?.resolved; } return undefined; } public async resolveFile(mockName: string): Promise<any> { const filePath = this.mockNameMap[mockName]; let cache = this.fileCache.get(filePath); if (cache !== undefined) { return this.loadFile(cache); } // Fallback for load file outside our swagger context const contentString = await this.fileLoader.load(mockName); cache = { filePath: mockName, mockName, }; const content = this.parseFileContent(cache, contentString); return content; } public resolveRefObj<T>(object: T): T { let refObj = object; while (isRefLike(refObj)) { const $ref = refObj.$ref; const idx = $ref.indexOf("#"); const mockName = idx === -1 ? $ref : $ref.substr(0, idx); const refObjPath = idx === -1 ? "" : $ref.substr(idx + 1); const filePath = this.mockNameMap[mockName]; const cache = this.fileCache.get(filePath); if (cache === undefined) { throw new Error(`cache not found for ${filePath}`); } refObj = jsonPointer.get(cache.resolved! as any, refObjPath); (object as any).$ref = $ref; } return refObj; } public resolveMockedFile(fileName: string): any { let refObj; if (!!fileName && fileName.startsWith("_")) { const filePath = this.mockNameMap[fileName]; const cache = this.fileCache.get(filePath); if (cache === undefined) { throw new Error(`cache not found for ${filePath} and mockName ${fileName}`); } refObj = jsonPointer.get(cache.resolved! as any, ""); } return refObj; } public getRealPath(mockName: string): string { return this.mockNameMap[mockName]; } private async resolveRef( object: Json, pathArr: string[], rootObject: Json, relativeFilePath: string, skipResolveChildRef: boolean, keepRefSiblings?: boolean ): Promise<Json> { if (isRefLike(object)) { const refObjResult: any = {}; if (object.readOnly !== undefined) { refObjResult[xmsReadonlyRef] = true; } const ref = object.$ref; const sp = ref.split("#"); if (sp.length > 2) { throw new Error("ref format error multiple #"); } const [refFilePath, refObjPath] = sp; if (refFilePath === "") { // Local reference if (!jsonPointer.has(rootObject as {}, refObjPath)) { throw new JsonLoaderRefError(object); } const mockName = (rootObject as any)[$id]; if (keepRefSiblings) { object.$ref = `${mockName}#${refObjPath}`; return object; } refObjResult.$ref = `${mockName}#${refObjPath}`; return refObjResult; } const refObj = await this.load( pathJoin(pathDirname(relativeFilePath), refFilePath), skipResolveChildRef ); const refMockName = (refObj as any)[$id]; if (refObjPath !== undefined) { if (!jsonPointer.has(refObj as {}, refObjPath)) { throw new JsonLoaderRefError(object); } if (keepRefSiblings) { object.$ref = `${refMockName}#${refObjPath}`; return object; } refObjResult.$ref = `${refMockName}#${refObjPath}`; return refObjResult; } else { if (keepRefSiblings) { object.$ref = refMockName; return object; } refObjResult.$ref = refMockName; return refObjResult; } } if (Array.isArray(object)) { for (let idx = 0; idx < object.length; ++idx) { const item = object[idx]; if (typeof item === "object" && item !== null) { const newRef = await this.resolveRef( item, pathArr.concat([idx.toString()]), rootObject, relativeFilePath, skipResolveChildRef ); if (newRef !== item) { // eslint-disable-next-line require-atomic-updates (object as any)[idx] = newRef; } } } } else if (typeof object === "object" && object !== null) { const obj = object as any; if (this.opts.eraseDescription && typeof obj.description === "string") { delete obj.description; } if (this.opts.eraseXmsExamples && obj[xmsExamples] !== undefined) { delete obj[xmsExamples]; } for (const key of Object.keys(obj)) { const item = obj[key]; if (typeof item === "object" && item !== null) { const newRef = await this.resolveRef( item, pathArr.concat([key]), rootObject, relativeFilePath, skipResolveChildRef || this.skipResolveRefKeys.has(key) ); if (newRef !== item) { obj[key] = newRef; } } } } else { throw new Error("Invalid json"); } return object; } private getNextMockName(filePath: string) { const id = this.globalMockNameId++; const mockName = `_${id.toString(36)}`; this.mockNameMap[mockName] = filePath; return mockName; } } export const isRefLike = (obj: any): obj is { $ref: string; readOnly?: boolean } => typeof obj.$ref === "string";