oav/lib/generator/swaggerMocker.ts

392 строки
12 KiB
TypeScript

import { JsonLoader } from "../swagger/jsonLoader";
import {
buildItemOption,
CacheItem,
createLeafItem,
createTrunkItem,
MockerCache,
reBuildExample,
PayloadCache,
} from "./exampleCache";
import Mocker from "./mocker";
import * as util from "./util";
import { ExampleRule, getRuleValidator } from "./exampleRule";
import { log } from "../util/logging";
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) {
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}`] = {};
}
}
}
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]: "LocationURl",
};
}
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
}
}