oav/lib/validate.ts

602 строки
20 KiB
TypeScript

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
/* eslint-disable no-console */
import * as fs from "fs";
import * as path from "path";
import * as openapiToolsCommon from "@azure-tools/openapi-tools-common";
import { Suppression } from "@azure/openapi-markdown";
import jsYaml from "js-yaml";
import * as jsonUtils from "./util/jsonUtils";
import * as specResolver from "./validators/specResolver";
import * as umlGeneratorLib from "./umlGenerator";
import * as utils from "./util/utils";
import {
CommonValidationResult,
SpecValidationResult,
SpecValidator,
} from "./validators/specValidator";
import { ModelValidationError } from "./util/modelValidationError";
import { ModelValidator } from "./validators/modelValidator";
import { NodeError } from "./util/validationError";
import { SemanticValidator } from "./validators/semanticValidator";
import { WireFormatGenerator } from "./wireFormatGenerator";
import { XMsExampleExtractor } from "./xMsExampleExtractor";
import ExampleGenerator from "./generator/exampleGenerator";
import { getErrorsFromModelValidation } from "./util/getErrorsFromModelValidation";
import { getSuppressions } from "./validators/suppressions";
import { log } from "./util/logging";
import { getInputFiles } from "./generator/util";
import { LiveValidator } from "../lib/liveValidation/liveValidator"
import { LiveValidationIssue } from "../lib/liveValidation/liveValidator"
export interface Options extends specResolver.Options, umlGeneratorLib.Options {
consoleLogLevel?: unknown;
logFilepath?: unknown;
pretty?: boolean;
}
export const getDocumentsFromCompositeSwagger = async (
suppression: Suppression | undefined,
compositeSpecPath: string,
reportError: openapiToolsCommon.ReportError
): Promise<string[]> => {
try {
const compositeSwagger = await jsonUtils.parseJson(suppression, compositeSpecPath, reportError);
if (
!(
compositeSwagger.documents &&
Array.isArray(compositeSwagger.documents) &&
compositeSwagger.documents.length > 0
)
) {
throw new Error(
`CompositeSwagger - ${compositeSpecPath} must contain a documents property and it must ` +
`be of type array and it must be a non empty array.`
);
}
const docs = compositeSwagger.documents;
const basePath = path.dirname(compositeSpecPath);
const finalDocs: string[] = [];
for (let i = 0; i < docs.length; i++) {
if (docs[i].startsWith(".")) {
docs[i] = docs[i].substring(1);
}
const individualPath = docs[i].startsWith("http") ? docs[i] : basePath + docs[i];
finalDocs.push(individualPath);
}
return finalDocs;
} catch (err) {
log.error(err);
throw err;
}
};
const vsoLogIssueWrapper = (issueType: string, message: string) => {
if (issueType === "error" || issueType === "warning") {
return `##vso[task.logissue type=${issueType}]${message}`;
} else {
return `##vso[task.logissue type=${issueType}]${message}`;
}
};
const validate = async <T>(
options: Options | undefined,
func: (options: Options) => Promise<T>
): Promise<T> => {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
if (options.pretty) {
log.consoleLogLevel = "off";
}
return func(options);
};
type ErrorType = "error" | "warning";
const prettyPrint = <T extends NodeError<T>>(
errors: readonly T[] | undefined,
errorType: ErrorType
) => {
if (errors !== undefined) {
for (const error of errors) {
const yaml = jsYaml.dump(error);
if (process.env["Agent.Id"]) {
// eslint-disable-next-line no-console
console.error(vsoLogIssueWrapper(errorType, errorType));
// eslint-disable-next-line no-console
console.error(vsoLogIssueWrapper(errorType, yaml));
} else {
// eslint-disable-next-line no-console
console.error("\x1b[31m", errorType, ":", "\x1b[0m");
// eslint-disable-next-line no-console
console.error(yaml);
}
}
}
};
export const validateSpec = async (
specPath: string,
options: Options | undefined
): Promise<SpecValidationResult> =>
validate(options, async (o) => {
// As a part of resolving discriminators we replace all the parent references
// with a oneOf array containing references to the parent and its children.
// This breaks the swagger specification 2.0 schema since oneOf is not supported.
// Hence we disable it since it is not required for semantic check.
o.shouldResolveDiscriminator = false;
// parameters in 'x-ms-parameterized-host' extension need not be resolved for semantic
// validation as that would not match the path parameters defined in the path template
// and cause the semantic validation to fail.
o.shouldResolveParameterizedHost = false;
// We shouldn't be resolving nullable types for semantic validation as we'll replace nodes
// with oneOf arrays which are not semantically valid in swagger 2.0 schema.
o.shouldResolveNullableTypes = false;
const validator = new SemanticValidator(specPath, null, o);
try {
await validator.initialize();
log.info(`Semantically validating ${specPath}:\n`);
const validationResults = await validator.validateSpec();
updateEndResultOfSingleValidation(validator);
logDetailedInfo(validator);
if (o.pretty) {
const resolveSpecError = validator.specValidationResult.resolveSpec;
if (resolveSpecError !== undefined || validationResults.errors.length > 0) {
console.log(vsoLogIssueWrapper("error", `Semantically validating ${specPath}:\n`));
} else if (validationResults.warnings && validationResults.warnings.length > 0) {
console.log(vsoLogIssueWrapper("warning", `Semantically validating ${specPath}:\n`));
} else {
console.log(`Semantically validating ${specPath}: without error.\n`);
}
if (resolveSpecError !== undefined) {
prettyPrint([resolveSpecError], "error");
}
if (validationResults.errors.length > 0) {
prettyPrint(validationResults.errors, "error");
}
if (validationResults.warnings && validationResults.warnings.length > 0) {
prettyPrint(validationResults.warnings, "warning");
}
}
return validator.specValidationResult;
} catch (err) {
let outputMsg = err;
if (typeof err === "object") {
outputMsg = jsYaml.dump(err);
}
if (o.pretty) {
if (process.env["Agent.Id"]) {
console.error(vsoLogIssueWrapper("error", `Semantically validating ${specPath}:\n`));
console.error(vsoLogIssueWrapper("error", outputMsg));
} else {
console.error(`Semantically validating ${specPath}:\n`);
console.error("\x1b[31m", "error", ":", "\x1b[0m");
console.error(outputMsg);
}
} else {
log.error(outputMsg);
}
validator.specValidationResult.validityStatus = false;
return validator.specValidationResult;
}
});
export async function validateCompositeSpec(
compositeSpecPath: string,
options: Options
): Promise<readonly SpecValidationResult[]> {
return validate(options, async (o) => {
const suppression = await getSuppressions(compositeSpecPath);
const docs = await getDocumentsFromCompositeSwagger(
suppression,
compositeSpecPath,
openapiToolsCommon.defaultErrorReport
);
o.consoleLogLevel = log.consoleLogLevel;
o.logFilepath = log.filepath;
const promiseFactories = docs.map((doc) => async () => validateSpec(doc, o));
return utils.executePromisesSequentially(promiseFactories);
});
}
export async function validateExamples(
specPath: string,
operationIds: string | undefined,
options?: Options
): Promise<readonly ModelValidationError[]> {
return validate(options, async (o) => {
const validator = new ModelValidator(specPath, null, o);
try {
await validator.initialize();
log.info(`Validating "examples" and "x-ms-examples" in ${specPath}:\n`);
await validator.validateOperations(operationIds);
updateEndResultOfSingleValidation(validator);
logDetailedInfo(validator);
const errors = getErrorsFromModelValidation(validator.specValidationResult);
if (o.pretty) {
if (errors.length > 0) {
console.log(
vsoLogIssueWrapper(
"error",
`Validating "examples" and "x-ms-examples" in ${specPath}:\n`
)
);
prettyPrint(errors, "error");
}
}
return errors;
} catch (e) {
if (o.pretty) {
if (process.env["Agent.Id"]) {
console.log(
vsoLogIssueWrapper(
"error",
`Validating "examples" and "x-ms-examples" in ${specPath}:\n`
)
);
console.error(vsoLogIssueWrapper("error", e));
} else {
console.error(`Validating "examples" and "x-ms-examples" in ${specPath}:\n`);
console.error("\x1b[31m", "error", ":", "\x1b[0m");
console.error(e);
}
} else {
log.error(e);
}
validator.specValidationResult.validityStatus = false;
updateEndResultOfSingleValidation(validator);
return [{ inner: e }];
}
});
}
export async function validateExamplesInCompositeSpec(
compositeSpecPath: string,
options: Options
): Promise<ReadonlyArray<readonly ModelValidationError[]>> {
return validate(options, async (o) => {
o.consoleLogLevel = log.consoleLogLevel;
o.logFilepath = log.filepath;
const suppression = await getSuppressions(compositeSpecPath);
const docs = await getDocumentsFromCompositeSwagger(
suppression,
compositeSpecPath,
openapiToolsCommon.defaultErrorReport
);
const promiseFactories = docs.map((doc) => async () => validateExamples(doc, undefined, o));
return utils.executePromisesSequentially(promiseFactories);
});
}
export async function validateTrafficAgainstSpec(
specPath: string,
trafficPath: string,
options: Options
): Promise<Array<LiveValidationIssue>>{
specPath = path.resolve(__dirname, specPath);
trafficPath = path.resolve(__dirname, trafficPath);
if (!fs.existsSync(specPath)) {
const error = new Error(`Can not find spec file, please check your specPath parameter.`);
log.error(JSON.stringify(error));
throw error;
}
if (!fs.existsSync(trafficPath)) {
const error = new Error(`Can not find traffic file, please check your trafficPath parameter.`);
log.error(JSON.stringify(error));
throw error;
}
try {
const trafficFile = require(trafficPath);
const specFileDirectory = path.dirname(specPath);
const swaggerPathsPattern = path.basename(specPath);
return validate(options, async (o) => {
o.consoleLogLevel = log.consoleLogLevel;
o.logFilepath = log.filepath;
const liveValidationOptions = {
checkUnderFileRoot: false,
directory: specFileDirectory,
swaggerPathsPattern: [swaggerPathsPattern],
git: {
shouldClone: false
}
}
const validator = new LiveValidator(liveValidationOptions);
const errors: Array<LiveValidationIssue> = [];
await validator.initialize();
const result = await validator.validateLiveRequestResponse(trafficFile);
if (!result.requestValidationResult.isSuccessful) {
errors.push(...result.requestValidationResult.errors);
}
if (!result.responseValidationResult.isSuccessful) {
errors.push(...result.responseValidationResult.errors);
}
if (errors.length > 0) {
if (o.pretty) {
prettyPrint(errors, "error");
} else {
for (let error of errors) {
log.error(JSON.stringify(error));
}
}
} else {
log.info('No errors were found.');
}
return errors;
})
} catch (error) {
log.error(JSON.stringify(error));
throw error;
}
}
export async function resolveSpec(
specPath: string,
outputDir: string,
options: Options,
reportError: openapiToolsCommon.ReportError
): Promise<void> {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
const specFileName = path.basename(specPath);
try {
const suppression = await getSuppressions(specPath);
const result = await jsonUtils.parseJson(suppression, specPath, reportError);
const resolver = new specResolver.SpecResolver(specPath, result, options, reportError);
await resolver.resolve(suppression);
const resolvedSwagger = JSON.stringify(resolver.specInJson, null, 2);
if (outputDir !== "./" && !fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
const outputFilepath = `${path.join(outputDir, specFileName)}`;
fs.writeFileSync(`${path.join(outputDir, specFileName)}`, resolvedSwagger, {
encoding: "utf8",
});
console.log(`Saved the resolved spec at "${outputFilepath}".`);
} catch (err) {
log.error(err);
throw err;
}
}
export async function resolveCompositeSpec(
specPath: string,
outputDir: string,
options: Options
): Promise<void> {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
try {
const suppression = await getSuppressions(specPath);
const docs = await getDocumentsFromCompositeSwagger(
suppression,
specPath,
openapiToolsCommon.defaultErrorReport
);
// eslint-disable-next-line require-atomic-updates
options.consoleLogLevel = log.consoleLogLevel;
// eslint-disable-next-line require-atomic-updates
options.logFilepath = log.filepath;
const promiseFactories = docs.map((doc) => async () =>
resolveSpec(doc, outputDir, options, openapiToolsCommon.defaultErrorReport)
);
await utils.executePromisesSequentially(promiseFactories);
} catch (err) {
log.error(err);
throw err;
}
}
export async function generateWireFormat(
specPath: string,
outDir: string,
emitYaml: unknown,
operationIds: string | null,
options: Options
): Promise<void> {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
const wfGenerator = new WireFormatGenerator(specPath, null, outDir, emitYaml);
try {
await wfGenerator.initialize();
log.info(`Generating wire format request and responses for swagger spec: "${specPath}":\n`);
wfGenerator.processOperations(operationIds);
} catch (err) {
log.error(err);
throw err;
}
}
export async function generateWireFormatInCompositeSpec(
compositeSpecPath: string,
outDir: string,
emitYaml: unknown,
options: Options
): Promise<void> {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
try {
const suppression = await getSuppressions(compositeSpecPath);
const docs = await getDocumentsFromCompositeSwagger(
suppression,
compositeSpecPath,
openapiToolsCommon.defaultErrorReport
);
// eslint-disable-next-line require-atomic-updates
options.consoleLogLevel = log.consoleLogLevel;
// eslint-disable-next-line require-atomic-updates
options.logFilepath = log.filepath;
const promiseFactories = docs.map((doc) => async () =>
generateWireFormat(doc, outDir, emitYaml, null, options)
);
await utils.executePromisesSequentially(promiseFactories);
} catch (err) {
log.error(err);
throw err;
}
}
export async function generateUml(
specPath: string,
outputDir: string,
options?: Options
): Promise<void> {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
const specFileName = path.basename(specPath);
const resolverOptions = {
shouldResolveRelativePaths: true,
shouldResolveXmsExamples: false,
shouldResolveAllOf: false,
shouldSetAdditionalPropertiesFalse: false,
shouldResolvePureObjects: false,
shouldResolveDiscriminator: false,
shouldResolveParameterizedHost: false,
shouldResolveNullableTypes: false,
};
try {
const suppression = await getSuppressions(specPath);
const result = await jsonUtils.parseJson(suppression, specPath, openapiToolsCommon.defaultErrorReport);
const resolver = new specResolver.SpecResolver(
specPath,
result,
resolverOptions,
openapiToolsCommon.defaultErrorReport
);
const umlGenerator = new umlGeneratorLib.UmlGenerator(resolver.specInJson, options);
const svgGraph = await umlGenerator.generateDiagramFromGraph();
if (outputDir !== "./" && !fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir);
}
const svgFile = specFileName.replace(path.extname(specFileName), ".svg");
const outputFilepath = `${path.join(outputDir, svgFile)}`;
fs.writeFileSync(`${path.join(outputDir, svgFile)}`, svgGraph, {
encoding: "utf8",
});
console.log(`Saved the uml at "${outputFilepath}". Please open the file in a browser.`);
} catch (err) {
log.error(err);
throw err;
}
}
export function updateEndResultOfSingleValidation<T extends CommonValidationResult>(
validator: SpecValidator<T>
): void {
if (validator.specValidationResult.validityStatus) {
if (!(log.consoleLogLevel === "json" || log.consoleLogLevel === "off")) {
log.info("No Errors were found.");
}
}
if (!validator.specValidationResult.validityStatus) {
process.exitCode = 1;
}
}
export function logDetailedInfo<T extends CommonValidationResult>(
validator: SpecValidator<T>
): void {
if (log.consoleLogLevel === "json") {
console.dir(validator.specValidationResult, { depth: null, colors: true });
}
log.silly("############################");
log.silly(validator.specValidationResult.toString());
log.silly("----------------------------");
}
export async function extractXMsExamples(
specPath: string,
recordings: string,
options: Options
): Promise<openapiToolsCommon.StringMap<unknown>> {
if (!options) {
options = {};
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
const xMsExampleExtractor = new XMsExampleExtractor(specPath, recordings, options);
return xMsExampleExtractor.extract();
}
export async function generateExamples(
specPath: string,
payloadDir?: string,
operationIds?: string,
readme?: string,
tag?: string,
options?: Options
): Promise<any> {
if (!options) {
options = {};
}
const wholeInputFiles: string[] = []
if (readme && tag) {
const inputFiles = await getInputFiles(readme, tag);
if (!inputFiles) {
throw Error("get input files from readme tag failed.")
}
inputFiles.forEach(file => {
if (path.isAbsolute(file)) {
wholeInputFiles.push(file);
}
else {
wholeInputFiles.push(path.join(path.dirname(readme),file));
}
})
}
else if (specPath) {
wholeInputFiles.push(specPath);
}
if (wholeInputFiles.length === 0) {
console.error(`no spec file specified !`)
}
log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel;
log.filepath = options.logFilepath || log.filepath;
for (const file of wholeInputFiles) {
const generator = new ExampleGenerator(file, payloadDir);
if (operationIds) {
const operationIdArray = operationIds.trim().split(",");
for (const operationId of operationIdArray) {
if (operationId) {
await generator.generate(operationId);
}
}
continue;
}
await generator.generateAll();
}
}