lib/liveValidation/liveValidatorLoader.ts (297 lines of code) (raw):
import { copyInfo, StringMap } from "@azure-tools/openapi-tools-common";
import { inject, injectable } from "inversify";
import { TYPES } from "../inversifyUtils";
import { JsonLoader } from "../swagger/jsonLoader";
import { Loader, setDefaultOpts } from "../swagger/loader";
import { SwaggerLoader, SwaggerLoaderOption } from "../swagger/swaggerLoader";
import {
LoggingFn,
Operation,
Parameter,
PathParameter,
QueryParameter,
Response,
Schema,
SwaggerSpec,
} from "../swagger/swaggerTypes";
import {
SchemaValidateFunction,
SchemaValidator,
SchemaValidatorOption,
} from "../swaggerValidator/schemaValidator";
import { allOfTransformer } from "../transform/allOfTransformer";
import { getTransformContext, TransformContext } from "../transform/context";
import { discriminatorTransformer } from "../transform/discriminatorTransformer";
import { noAdditionalPropertiesTransformer } from "../transform/noAdditionalPropertiesTransformer";
import { nullableTransformer } from "../transform/nullableTransformer";
import { pathRegexTransformer } from "../transform/pathRegexTransformer";
import { pureObjectTransformer } from "../transform/pureObjectTransformer";
import { referenceFieldsTransformer } from "../transform/referenceFieldsTransformer";
import { resolveNestedDefinitionTransformer } from "../transform/resolveNestedDefinitionTransformer";
import { schemaV4ToV7Transformer } from "../transform/schemaV4ToV7Transformer";
import { applyGlobalTransformers, applySpecTransformers } from "../transform/transformer";
import { traverseSwaggerAsync } from "../transform/traverseSwagger";
import { xmsPathsTransformer } from "../transform/xmsPathsTransformer";
import { getLazyBuilder } from "../util/lazyBuilder";
import { waitUntilLowLoad } from "../util/utils";
export interface LiveValidatorLoaderOption extends SwaggerLoaderOption, SchemaValidatorOption {
transformToNewSchemaFormat?: boolean;
}
@injectable()
export class LiveValidatorLoader implements Loader<SwaggerSpec> {
private transformContext: TransformContext;
public logging: LoggingFn;
public getResponseValidator = getLazyBuilder(
"_validate",
(response: Response): Promise<SchemaValidateFunction> => {
const schema: Schema = { properties: {} };
if (response.schema !== undefined && (response.schema.type as string) !== "file") {
schema.properties!.body = response.schema;
copyInfo(response, response.schema);
this.addRequiredToSchema(schema, "body");
}
if (response.headers !== undefined) {
const headerSchema: Schema = { properties: {}, required: [] };
copyInfo(response.headers, headerSchema);
schema.properties!.headers = headerSchema;
for (const headerName of Object.keys(response.headers)) {
// Per swagger 2.0 spec, header is optional by default
const name = headerName.toLowerCase();
const sch = response.headers[headerName];
headerSchema.properties![name] = sch;
addParamTransform(response, { type: sch.type, name, in: "header" });
}
if (headerSchema.required?.length === 0) {
headerSchema.required = undefined;
}
} else {
schema.properties!.headers = {};
}
return this.schemaValidator.compileAsync(schema);
}
);
public getRequestValidator = getLazyBuilder(
"_validate",
(operation: Operation): Promise<SchemaValidateFunction> => {
const schema: Schema = {
properties: {
query: { properties: {} },
headers: { properties: {} },
path: { properties: {} },
formData: { properties: {} },
},
};
this.addParamToSchema(schema, operation._path.parameters, operation);
this.addParamToSchema(schema, operation.parameters, operation);
return this.schemaValidator.compileAsync(schema);
}
);
public constructor(
@inject(TYPES.opts) private opts: LiveValidatorLoaderOption,
private jsonLoader: JsonLoader,
private swaggerLoader: SwaggerLoader,
@inject(TYPES.schemaValidator) private schemaValidator: SchemaValidator
) {
setDefaultOpts(opts, {
transformToNewSchemaFormat: false,
});
this.setTransformContext();
}
public setTransformContext() {
this.transformContext = getTransformContext(
this.jsonLoader,
this.schemaValidator,
[
xmsPathsTransformer,
resolveNestedDefinitionTransformer,
this.opts.transformToNewSchemaFormat ? schemaV4ToV7Transformer : undefined,
referenceFieldsTransformer,
pathRegexTransformer,
discriminatorTransformer,
allOfTransformer,
noAdditionalPropertiesTransformer,
nullableTransformer,
pureObjectTransformer,
],
this.logging
);
}
public async load(specFilePath: string, keepRefSiblings?: boolean): Promise<SwaggerSpec> {
const spec = await this.swaggerLoader.load(specFilePath, keepRefSiblings);
applySpecTransformers(spec, this.transformContext);
return spec;
}
public getResolvedJsonLoader() {
return this.swaggerLoader.getResolvedJsonLoader();
}
public transformLoadedSpecs() {
applyGlobalTransformers(this.transformContext);
}
public async buildAjvValidator(spec: SwaggerSpec, options?: { inBackground?: boolean }) {
return traverseSwaggerAsync(spec, {
onOperation: async (operation) => {
await this.getRequestValidator(operation);
if (options?.inBackground) {
await waitUntilLowLoad();
}
},
onResponse: async (response) => {
await this.getResponseValidator(response);
if (options?.inBackground) {
await waitUntilLowLoad();
}
},
});
}
private addRequiredToSchema(schema: Schema, requiredName: string) {
if (schema.required === undefined) {
schema.required = [];
}
if (!schema.required.includes(requiredName)) {
schema.required.push(requiredName);
}
}
private addParamToSchema(schema: Schema, params: Parameter[] | undefined, operation: Operation) {
if (params === undefined) {
return;
}
const properties = schema.properties!;
for (const p of params) {
const param = this.jsonLoader.resolveRefObj(p);
switch (param.in) {
case "body":
// ignore validation in case of file type
if (param.schema?.type === "file") {
break;
}
properties.body = param.schema ?? {};
if (param.required) {
copyInfo(param, schema);
this.addRequiredToSchema(schema, "body");
} else {
operation._bodyTransform = bodyTransformIfNotRequiredAndEmpty;
}
break;
case "header":
const name = param.name.toLowerCase();
if (param.required) {
this.addRequiredToSchema(properties.headers, name);
}
param.required = undefined;
properties.headers.properties![name] = param as Schema;
addParamTransform(operation, param);
break;
case "query":
if (shouldSkipQueryParam(param)) {
break;
}
if (param.required) {
this.addRequiredToSchema(properties.query, param.name);
}
// Remove param.required as it have different meaning in swagger and json schema
param.required = undefined;
properties.query.properties![param.name] = param as Schema;
addParamTransform(operation, param);
break;
case "path":
if (shouldSkipPathParam(param)) {
break;
}
// if (!param.required) {
// throw new Error("Path property mush be required");
// }
this.addRequiredToSchema(properties.path, param.name);
param.required = undefined;
properties.path.properties![param.name] = param as Schema;
addParamTransform(operation, param);
break;
case "formData":
// ignore validation in case of file type
if (param.type === "file") {
break;
}
if (param.required) {
this.addRequiredToSchema(properties.formData, param.name);
}
// Remove param.required as it have different meaning in swagger and json schema
param.required = undefined;
properties.formData.properties![param.name] = param as Schema;
addParamTransform(operation, param);
break;
default:
throw new Error(`Not Supported parameter in: ${param}`);
}
}
}
}
const skipPathParamProperties: StringMap<boolean | string> = {
in: "path",
name: true,
type: "string",
description: true,
required: true,
};
const shouldSkipPathParam = (param: PathParameter) => {
for (const key of Object.keys(param)) {
const val = skipPathParamProperties[key];
if (val !== true && val !== (param as any)[key]) {
return false;
}
}
return true;
};
const skipQueryParamProperties: StringMap<boolean | string> = {
in: "query",
name: true,
type: "string",
description: true,
required: true,
};
const shouldSkipQueryParam = (param: QueryParameter) => {
if (param.name !== "api-version") {
return false;
}
for (const key of Object.keys(param)) {
const val = skipQueryParamProperties[key];
if (val !== true && val !== (param as any)[key]) {
return false;
}
}
return true;
};
const paramTransformInteger = (data: string) => {
const val = Number(data);
return isNaN(val) ? data : val;
};
const paramTransformBoolean = (data: string) => {
const val = data.toLowerCase();
return val === "true" ? true : val === "false" ? false : data;
};
const parameterTransform = {
number: paramTransformInteger,
integer: paramTransformInteger,
boolean: paramTransformBoolean,
};
const bodyTransformIfNotRequiredAndEmpty = (body: any) => {
if (body && Object.keys(body).length === 0 && body.constructor === Object) {
return undefined;
} else {
return body;
}
};
const addParamTransform = (it: Operation | Response, param: Parameter) => {
const transform = parameterTransform[param.type! as keyof typeof parameterTransform];
if (transform === undefined) {
return;
}
if (param.in === "query") {
const op = it as Operation;
if (op._queryTransform === undefined) {
op._queryTransform = {};
}
op._queryTransform[param.name] = transform;
} else if (param.in === "header") {
if (it._headerTransform === undefined) {
it._headerTransform = {};
}
it._headerTransform[param.name.toLowerCase()] = transform;
} else if (param.in === "path") {
const op = it as Operation;
if (op._pathTransform === undefined) {
op._pathTransform = {};
}
op._pathTransform[param.name] = transform;
}
};