lib/generator/exampleCache.ts (257 lines of code) (raw):

import { RuleValidatorFunc } from "./exampleRule"; /* tslint:disable:max-classes-per-file */ interface BaseCache { get(modelName: string): CacheItem | undefined; set(modelName: string, example: CacheItem): void; has(modelName: string): boolean; } const isBaseResource = (cacheKey: string) => { const pieces = cacheKey.split("/"); if (pieces.length < 2) { return false; } return ["resource", "proxyresource", "trackedresource", "azureentityresource"].some( (r) => r === pieces[pieces.length - 1].toLowerCase() ); }; export class MockerCache implements BaseCache { private caches = new Map<string, CacheItem>(); public get(modelName: string) { if (this.has(modelName)) { return this.caches.get(modelName); } return undefined; } public set(modelName: string, example: CacheItem) { if (!this.has(modelName)) { this.caches.set(modelName, example); } } public has(modelName: string) { return this.caches.has(modelName); } public checkAndCache(schema: any, example: CacheItem) { if (!schema || !example) { return; } const cacheKey = schema.$ref && schema.$ref.includes("#") ? schema.$ref.split("#")[1] : undefined; if (cacheKey && !isBaseResource(cacheKey) && !this.has(cacheKey)) { this.set(cacheKey, example); } } } export class PayloadCache implements BaseCache { private requestCaches = new Map<string, CacheItem>(); private responseCaches = new Map<string, CacheItem>(); private mergedCaches = new Map<string, CacheItem>(); private hasByDirection(modelName: string, isRequest: boolean) { const cache = isRequest ? this.requestCaches : this.responseCaches; return cache.has(modelName); } private setByDirection(modelName: string, example: CacheItem, isRequest: boolean) { const cache = isRequest ? this.requestCaches : this.responseCaches; if (!cache.has(modelName)) { cache.set(modelName, example); } } private getByDirection(modelName: string, isRequest: boolean) { const cache = isRequest ? this.requestCaches : this.responseCaches; if (cache.has(modelName)) { return cache.get(modelName); } return undefined; } public get(modelName: string) { if (this.mergedCaches.has(modelName)) { return this.mergedCaches.get(modelName); } return undefined; } public set(key: string, value: CacheItem) { if (!this.mergedCaches.has(key)) { this.mergedCaches.set(key, value); } } public has(modelName: string) { return this.mergedCaches.has(modelName); } public checkAndCache(schema: any, example: CacheItem | undefined, isRequest: boolean) { if (!schema || !example) { return; } const cacheKey = schema.$ref && schema.$ref.includes("#") ? schema.$ref.split("#")[1] : undefined; if (cacheKey && !isBaseResource(cacheKey) && !this.hasByDirection(cacheKey, isRequest)) { this.setByDirection(cacheKey, example, isRequest); } } /** * picking value priority : non-mocked value > mocked value , target value > source value * @param target The target item that to be merged into * @param source The source item that needs to merge */ public mergeItem(target: CacheItem, source: CacheItem): CacheItem { const result = target; if (!source || !target) { return target ? target : source; } if (Array.isArray(result.child) && Array.isArray(source.child)) { const resultArr = result.child as CacheItem[]; const sourceArr = source.child as CacheItem[]; if (resultArr.length === 0 || sourceArr.length === 0) { return resultArr.length === 0 ? source : result; } // only when source is not mocked and target is mocked , choose source cache. if (resultArr[0].isMocked && !sourceArr[0].isMocked) { return source; } for (let i = 0; i < resultArr.length; i++) { if (i < sourceArr.length) { resultArr[i] = this.mergeItem(resultArr[i], sourceArr[i]); } } } else if (source.child && result.child) { const resultObj = result.child as CacheItemObject; const sourceObj = source.child as CacheItemObject; for (const key of Object.keys(sourceObj)) { if (!resultObj[key]) { resultObj[key] = sourceObj[key]; } else { resultObj[key] = this.mergeItem(resultObj[key], sourceObj[key]); } } } else { return result.isMocked && !source.isMocked ? source : result; } return result; } /** * 1 for each request cache , if exists in response cache, merge it with response cache and put into merged cache . * 2 for each response cache, if not exists in merged cache, then put into merged cache. */ public mergeCache() { for (const [key, requestCache] of this.requestCaches.entries()) { if (this.hasByDirection(key, false) && !requestCache.isLeaf) { const responseCache = this.getByDirection(key, false); if (responseCache) { if (responseCache.isLeaf) { console.error(`the response cache and request cache is inconsistent! key:${key}`); } else { const mergedCache = this.mergeItem(requestCache, responseCache); this.set(key, mergedCache); continue; } } } this.set(key, requestCache); } for (const [key, responseCache] of this.responseCaches.entries()) { if (!this.hasByDirection(key, true)) { this.set(key, responseCache); } } this.requestCaches.clear(); this.responseCaches.clear(); } } type CacheItemValue = string | number | object | boolean; interface CacheItemObject { [index: string]: CacheItem; } type CacheItemChild = CacheItemObject | CacheItem[]; interface CacheItemOptions { isReadonly?: boolean; isXmsSecret?: boolean; isRequired?: boolean; isWriteOnly?: boolean; } export interface CacheItem { value?: CacheItemValue; child?: CacheItemChild; options?: CacheItemOptions; isLeaf: boolean; required?: string[]; isMocked?: boolean; } export const buildItemOption = (schema: any) => { if (schema) { const isReadonly = !!schema.readOnly; const isXmsSecret = !!schema["x-ms-secret"]; const isRequired = !!schema.required; const isWriteOnly = schema["x-ms-mutability"] ? schema["x-ms-mutability"].indexOf("read") === -1 : false; if (!isReadonly && !isXmsSecret && !isRequired && !isWriteOnly) { return undefined; } let option: CacheItemOptions = {}; if (isReadonly) { option = { isReadonly: true }; } if (isXmsSecret) { option = { ...option, isXmsSecret: true }; } if (isWriteOnly) { option = { ...option, isWriteOnly: true }; } if (schema.required === true) { option = { ...option, isRequired: true }; } return option; } return undefined; }; export const createLeafItem = ( itemValue: CacheItemValue, option: CacheItemOptions | undefined = undefined ): CacheItem => { const item = { isLeaf: true, value: itemValue, } as CacheItem; if (option) { item.options = option; } return item; }; export const createTrunkItem = ( itemValue: CacheItemChild, option: CacheItemOptions | undefined ): CacheItem => { const item = { isLeaf: false, child: itemValue, } as CacheItem; if (option) { item.options = option; } return item; }; export const reBuildExample = ( cache: CacheItem | undefined, isRequest: boolean, schema: any, validator: RuleValidatorFunc | undefined ): any => { if (!cache) { return undefined; } if (validator && !validator({ schemaCache: cache, isRequest })) { return undefined; } if (cache.isLeaf) { return cache.value; } if (Array.isArray(cache.child)) { const result = []; for (const item of cache.child) { if (validator && !validator({ schemaCache: item, isRequest, schema })) { continue; } result.push(reBuildExample(item, isRequest, schema, validator)); } return result; } else if (cache.child) { const result: any = {}; for (const key of Object.keys(cache.child)) { if (!validator || validator({ schemaCache: cache, propertyName: key, isRequest, schema })) { const value = reBuildExample(cache.child[key], isRequest, schema, validator); if (value !== undefined) { result[key] = value; } } } return result; } return undefined; };