lib/generator/swaggerMocker.ts (347 lines of code) (raw):

import { JsonLoader } from "../swagger/jsonLoader"; import { log } from "../util/logging"; import { buildItemOption, CacheItem, createLeafItem, createTrunkItem, MockerCache, reBuildExample, PayloadCache, } from "./exampleCache"; import Mocker from "./mocker"; import * as util from "./util"; import { ExampleRule, getRuleValidator } from "./exampleRule"; export default class SwaggerMocker { private jsonLoader: JsonLoader; private mocker: Mocker; private spec: any; private mockCache: MockerCache; private exampleCache: PayloadCache; private exampleRule?: ExampleRule; public constructor(jsonLoader: JsonLoader, mockerCache: MockerCache, payloadCache: PayloadCache) { this.jsonLoader = jsonLoader; this.mocker = new Mocker(); this.mockCache = mockerCache; this.exampleCache = payloadCache; } public setRule(exampleRule?: ExampleRule) { this.exampleRule = exampleRule; } public mockForExample(example: any, specItem: any, spec: any, rp: string) { const preHandledStatusCode = ["200", "201", "202", "204"]; // above status code prehandle in exampleGenerator.ts extractResponse() this.spec = spec; if (Object.keys(example.responses).length === 0) { for (const statusCode of Object.keys(specItem.content.responses)) { if (statusCode !== "default") { example.responses[`${statusCode}`] = {}; } } } else { for (const statusCode of Object.keys(specItem.content.responses)) { if (statusCode !== "default" && !preHandledStatusCode.includes(statusCode)) { example.responses[`${statusCode}`] = {}; } } } example.parameters = this.mockRequest(example.parameters, specItem.content.parameters, rp); example.responses = this.mockResponse(example.responses, specItem); } public getMockCachedObj(objName: string, schema: any, isRequest: boolean) { return this.mockCachedObj(objName, schema, undefined, new Set<string>(), isRequest); } private mockResponse(responseExample: any, specItem: any) { for (const statusCode of Object.keys(responseExample)) { const mockedResp = this.mockEachResponse(statusCode, responseExample[statusCode], specItem); responseExample[statusCode] = mockedResp; } return responseExample; } private mockEachResponse(statusCode: string, responseExample: any, specItem: any) { const visited = new Set<string>(); const validator = getRuleValidator(this.exampleRule).onResponseBody; const responseSpec = specItem.content.responses[statusCode]; if (validator && !validator({ schema: responseSpec })) { return undefined; } return { headers: responseExample.hearders || this.mockHeaders(statusCode, specItem), body: "schema" in responseSpec ? this.mockObj( "response body", responseSpec.schema, responseExample.body || {}, visited, false ) || {} : undefined, }; } private mockHeaders(statusCode: string, specItem: any) { if (statusCode !== "201" && statusCode !== "202") { return undefined; } const validator = getRuleValidator(this.exampleRule).onResponseHeader; if (validator && !validator({ schema: specItem })) { return undefined; } const headerAttr = util.getPollingAttr(specItem); if (!headerAttr) { return; } return { [headerAttr]: "https://contoso.com/operationstatus", }; } private mockRequest(paramExample: any, paramSpec: any, rp: string) { const validator = getRuleValidator(this.exampleRule).onParameter; for (const pName of Object.keys(paramSpec)) { const element = paramSpec[pName]; const visited = new Set<string>(); const paramEle = this.getDefSpec(element, visited); if (paramEle.name === "resourceGroupName") { paramExample.resourceGroupName = `rg${rp}`; } else if (paramEle.name === "api-version") { paramExample["api-version"] = this.spec.info.version; } else if ("schema" in paramEle) { // { // "name": "parameters", // "in": "body", // "required": false, // "schema": { // "$ref": "#/definitions/SignalRResource" // } // } if (!validator || validator({ schema: paramEle })) { paramExample[paramEle.name] = this.mockObj( paramEle.name, paramEle.schema, paramExample[paramEle.name] || {}, visited, true ); } } else { if (paramEle.name in paramExample) { continue; } // { // "name": "api-version", // "in": "query", // "required": true, // "type": "string" // } if (!validator || validator({ schema: paramEle })) { paramExample[paramEle.name] = this.mockObj( paramEle.name, element, // use the original schema containing "$ref" which will hit the cached value paramExample[paramEle.name], new Set<string>(), true ); } } } return paramExample; } private removeFromSet(schema: any, visited: Set<string>) { if ("$ref" in schema && visited.has(schema.$ref)) { visited.delete(schema.$ref); } } private getCache(schema: any) { if ("$ref" in schema) { for (const cache of [this.exampleCache, this.mockCache]) { if (cache.has(schema.$ref.split("#")[1])) { return cache.get(schema.$ref.split("#")[1]); } } } return undefined; } private mockObj( objName: string, schema: any, example: any, visited: Set<string>, isRequest: boolean ) { const cache = this.mockCachedObj(objName, schema, example, visited, isRequest); const validator = getRuleValidator(this.exampleRule).onSchema; return reBuildExample(cache, isRequest, schema, validator); } private mockCachedObj( objName: string, schema: any, example: any, visited: Set<string>, isRequest: boolean, discriminatorValue: string | undefined = undefined ) { if (!schema || typeof schema !== "object") { log.warn(`invalid schema.`); return undefined; } // use visited set to avoid circular dependency if ("$ref" in schema && visited.has(schema.$ref)) { return undefined; } const cache = this.getCache(schema); if (cache) { return cache; } const definitionSpec = this.getDefSpec(schema, visited); if (util.isObject(definitionSpec)) { // circular inherit will not be handled const properties = this.getProperties(definitionSpec, visited); example = example || {}; const discriminator = this.getDiscriminator(definitionSpec, visited); if ( discriminator && !discriminatorValue && properties && Object.keys(properties).includes(discriminator) ) { return ( this.mockForDiscriminator(definitionSpec, example, discriminator, isRequest, visited) || undefined ); } else { Object.keys(properties).forEach((key: string) => { // the objName is the discriminator when discriminatorValue is specified. if (key === objName && discriminatorValue) { example[key] = createLeafItem(discriminatorValue, buildItemOption(properties[key])); } else { example[key] = this.mockCachedObj( key, properties[key], example[key], visited, isRequest, discriminatorValue ); } }); } if ("additionalProperties" in definitionSpec && definitionSpec.additionalProperties) { const newKey = util.randomKey(); if (newKey in properties) { console.error(`generate additionalProperties for ${objName} fail`); } else { example[newKey] = this.mockCachedObj( newKey, definitionSpec.additionalProperties, undefined, visited, isRequest, discriminatorValue ); } } } else if (definitionSpec.type === "array") { example = example || []; const arrItem: any = this.mockCachedObj( `${objName}'s item`, definitionSpec.items, example[0], visited, isRequest ); example = this.mocker.mock(definitionSpec, objName, arrItem); } else { /** type === number or integer */ example = example ? example : this.mocker.mock(definitionSpec, objName); } // return value for primary type: string, number, integer, boolean // "aaaa" // removeFromSet: once we try all roads started from present node, we should remove it and backtrack this.removeFromSet(schema, visited); let cacheItem: CacheItem; if (Array.isArray(example)) { const cacheChild: CacheItem[] = []; for (const item of example) { cacheChild.push(item); } cacheItem = createTrunkItem(cacheChild, buildItemOption(definitionSpec)); } else if (typeof example === "object") { const cacheChild: { [index: string]: CacheItem } = {}; for (const [key, item] of Object.entries(example)) { cacheChild[key] = item as CacheItem; } cacheItem = createTrunkItem(cacheChild, buildItemOption(definitionSpec)); } else { cacheItem = createLeafItem(example, buildItemOption(definitionSpec)); } cacheItem.isMocked = true; const requiredProperties = this.getRequiredProperties(definitionSpec); if (requiredProperties && requiredProperties.length > 0) { cacheItem.required = requiredProperties; } this.mockCache.checkAndCache(schema, cacheItem); return cacheItem; } /** * return all required properties of the object, including parent's properties defined by 'allOf' * It will not spread properties' properties. * @param definitionSpec */ private getRequiredProperties(definitionSpec: any) { let requiredProperties: string[] = Array.isArray(definitionSpec.required) ? definitionSpec.required : []; definitionSpec.allOf?.map((item: any) => { requiredProperties = [ ...requiredProperties, ...this.getRequiredProperties(this.getDefSpec(item, new Set<string>())), ]; }); return requiredProperties; } // TODO: handle discriminator without enum options private mockForDiscriminator( schema: any, example: any, discriminator: string, isRequest: boolean, visited: Set<string> ): any { const disDetail = this.getDefSpec(schema, visited); if (disDetail.discriminatorMap && Object.keys(disDetail.discriminatorMap).length > 0) { const properties = this.getProperties(disDetail, new Set<string>()); let discriminatorValue; if (properties[discriminator] && Array.isArray(properties[discriminator].enum)) { discriminatorValue = properties[discriminator].enum[0]; } else { discriminatorValue = Object.keys(disDetail.discriminatorMap)[0]; } const discriminatorSpec = disDetail.discriminatorMap[discriminatorValue]; if (!discriminatorSpec) { this.removeFromSet(schema, visited); return example; } const cacheItem = this.mockCachedObj( discriminator, discriminatorSpec, {}, new Set<string>(), isRequest, discriminatorValue ) || undefined; this.removeFromSet(schema, visited); return cacheItem; } this.removeFromSet(schema, visited); return undefined; } // { // "$ref": "#/parameters/ApiVersionParameter" // }, // to // { // "name": "api-version", // "in": "query", // "required": true, // "type": "string" // } private getDefSpec(schema: any, visited: Set<string>) { if ("$ref" in schema) { visited.add(schema.$ref); } const content = this.jsonLoader.resolveRefObj(schema); if (!content) { return undefined; } return content; } private getProperties(definitionSpec: any, visited: Set<string>) { let properties: any = {}; definitionSpec.allOf?.map((item: any) => { properties = { ...properties, ...this.getProperties(this.getDefSpec(item, visited), visited), }; this.removeFromSet(item, visited); }); return { ...properties, ...definitionSpec.properties, }; } private getDiscriminator(definitionSpec: any, visited: Set<string>) { let discriminator = undefined; if (definitionSpec.discriminator) { return definitionSpec.discriminator; } definitionSpec.allOf?.some((item: any) => { discriminator = this.getDiscriminator(this.getDefSpec(item, visited), visited); this.removeFromSet(item, visited); return !!discriminator; }); return discriminator; } }