lib/validate.ts (363 lines of code) (raw):

// 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 path from "path"; import * as fs from "fs"; import jsYaml from "js-yaml"; import { flatMap, StringMap } from "@azure-tools/openapi-tools-common"; import * as utils from "./util/utils"; import { NewModelValidator as ModelValidator, SwaggerExampleErrorDetail, } from "./swaggerValidator/modelValidator"; import { NodeError } from "./util/validationError"; import * as XMsExampleExtractor from "./xMsExampleExtractor"; import ExampleGenerator from "./generator/exampleGenerator"; import { log } from "./util/logging"; import { SemanticValidator } from "./swaggerValidator/semanticValidator"; import { ErrorCodeConstants } from "./util/errorDefinitions"; import { TrafficValidationIssue, TrafficValidationOptions, TrafficValidator, } from "./swaggerValidator/trafficValidator"; import { ReportGenerator, loadErrorDefinitions } from "./report/generateReport"; export interface Options extends XMsExampleExtractor.Options { consoleLogLevel?: unknown; logFilepath?: unknown; pretty?: boolean; } 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, yaml)); } else { // eslint-disable-next-line no-console console.error(yaml); } } } }; const prettyPrintInfo = <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, yaml)); } else { // eslint-disable-next-line no-console console.error(yaml); } } } }; export const validateSpec = async (specPath: string, options: Options | undefined) => validate(options, async (o) => { const validator = new SemanticValidator(specPath, null); try { await validator.initialize(); log.info(`Semantically validating ${specPath}:\n`); const validationResults = await validator.validateSpec(); if (o.pretty) { if (validationResults.errors.length > 0) { logMessage(`Semantically validating ${specPath}`, "error"); } else { logMessage(`Semantically validating ${specPath} without error`, "info"); } if (validationResults.errors.length > 0) { logMessage(`Errors reported:`, "error"); prettyPrint(validationResults.errors, "error"); } } else { if (validationResults.errors.length > 0) { logMessage(`Errors reported:`, "error"); for (const error of validationResults.errors) { // eslint-disable-next-line no-console log.error(error); } } else { logMessage(`Semantically validating ${specPath} without error`, "info"); } } return validator.specValidationResult; } catch (err) { let outputMsg = err; if (typeof err === "object") { outputMsg = jsYaml.dump(err); } if (o.pretty) { logMessage(`Semantically validating ${specPath}`); logMessage(`${outputMsg}`, "error"); } else { log.error(`Detail error:${(err as any)?.message}.ErrorStack:${(err as any)?.stack}`); } validator.specValidationResult.validityStatus = false; return validator.specValidationResult; } }); export async function validateExamples( specPath: string, operationIds: string | undefined, options?: Options ): Promise<SwaggerExampleErrorDetail[]> { return validate(options, async (o) => { try { const validator = new ModelValidator(specPath); await validator.initialize(); log.info(`Validating "examples" and "x-ms-examples" in ${specPath}:\n`); await validator.validateOperations(operationIds); const errors = validator.result; if (o.pretty) { if (errors.length > 0) { logMessage(`Validating "examples" and "x-ms-examples" in ${specPath}`, "error"); logMessage("Error reported:"); prettyPrint(errors, "error"); } else { logMessage("Validation completes without errors.", "info"); } } else { if (errors.length > 0) { logMessage("Error reported:"); for (const error of errors) { log.error(error); } } else { logMessage("Validation completes without errors.", "info"); } } return errors; } catch (e) { logMessage(`Validating x-ms-examples in ${specPath}`, "error"); logMessage("Unexpected runtime exception:"); if (o.pretty) { logMessage(`Detail error:${(e as any)?.message}.ErrorStack:${(e as any)?.stack}`, "error"); } else { log.error(`Detail error:${(e as any)?.message}.ErrorStack:${(e as any)?.stack}`); } const error: SwaggerExampleErrorDetail = { inner: e, message: "Unexpected internal error", code: ErrorCodeConstants.INTERNAL_ERROR as any, }; return [error]; } }); } export async function validateTraffic( specPath: string, trafficPath: string, options: TrafficValidationOptions ): Promise<TrafficValidationIssue[]> { return validate(options, async (o) => { o.consoleLogLevel = log.consoleLogLevel; o.logFilepath = log.filepath; const validator = new TrafficValidator(specPath, trafficPath); const trafficValidationResult: TrafficValidationIssue[] = []; try { await validator.initialize(); const result = await validator.validate(); trafficValidationResult.push(...result); } catch (err) { const msg = `Detail error message:${(err as any)?.message}. ErrorStack:${ (err as any)?.Stack }`; log.error(msg); trafficValidationResult.push({ payloadFilePath: specPath, runtimeExceptions: [ { code: ErrorCodeConstants.RUNTIME_ERROR, message: msg, }, ], }); } if (options.jsonReportPath) { const errorDefinitions = await loadErrorDefinitions(); const report = { coveredSpecFiles: validator.operationCoverageResult.map((item) => options.overrideLinkInReport ? `${options.specLinkPrefix}/${item.spec?.substring( item.spec?.indexOf("specification") )}` : `${item.spec}` ), allOperations: validator.operationCoverageResult .map((item) => item.totalOperations) .reduce((a, b) => a + b, 0), coveredOperations: validator.operationCoverageResult .map((item) => item.coveredOperations) .reduce((a, b) => a + b, 0), unCoveredOperationsList: validator.operationCoverageResult.map((item) => { return { spec: item.spec, operationIds: item.unCoveredOperationsList.map((opeartion) => opeartion.operationId), }; }), passedOperationsList: validator.operationCoverageResult.map((item) => { return { spec: item.spec, operationIds: item.coveredOperationsList .map((opeartion) => opeartion.operationId) .filter((id) => { return !trafficValidationResult.some( (error) => error.operationInfo?.operationId === id ); }), }; }), failedOperations: validator.operationCoverageResult .map((item) => item.validationFailOperations) .reduce((a, b) => a + b, 0), errors: Array.from( flatMap( trafficValidationResult, (item) => item.errors?.map((it) => { const errorDef = errorDefinitions.get(it.code); const specFilePath = item.specFilePath || ""; const overrideLinkInReport = options.overrideLinkInReport || false; const specLinkPrefix = options.specLinkPrefix || ""; return { spec: specFilePath, errorCode: it.code, errorLink: errorDef?.link, errorMessage: it.message, issueSource: it.issueSource, operationId: item.operationInfo?.operationId, schemaPathWithPosition: overrideLinkInReport ? `${specLinkPrefix}/${specFilePath.substring( specFilePath.indexOf("specification") )}#L${it.source.position.line}` : `${specFilePath}#L${it.source.position.line}`, }; }) ?? [] ) ), }; fs.writeFileSync(options.jsonReportPath, JSON.stringify(report, null, 2)); } if (options.reportPath) { const generator = new ReportGenerator( trafficValidationResult, validator.operationCoverageResult, validator.operationUndefinedResult, options ); await generator.generateHtmlReport(); } else if (trafficValidationResult.length > 0) { if (o.pretty) { prettyPrintInfo(trafficValidationResult, "error"); } else { for (const error of trafficValidationResult) { const errorInfo = JSON.stringify(error); log.error(errorInfo); } } } else { log.info("No errors were found."); } return trafficValidationResult; }); } export async function extractXMsExamples( specPath: string, recordings: string, options: Options ): Promise<StringMap<unknown>> { if (!options) { options = {}; } log.consoleLogLevel = options.consoleLogLevel || log.consoleLogLevel; log.filepath = options.logFilepath || log.filepath; const xMsExampleExtractor = new XMsExampleExtractor.XMsExampleExtractor( specPath, recordings, options ); return xMsExampleExtractor.extract(); } export async function generateExamples( specPath: string, payloadDir?: string, operationIds?: string, readme?: string, tag?: string, generationRule?: "Max" | "Min", options?: Options ): Promise<any> { if (!options) { options = {}; } const wholeInputFiles: string[] = []; if (readme && tag) { const inputFiles = await utils.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, generationRule); if (operationIds) { const operationIdArray = operationIds.trim().split(","); for (const operationId of operationIdArray) { if (operationId) { await generator.generate(operationId); } } continue; } await generator.generateAll(); } } const logMessage = (message: string, level?: string) => { const logLevel = level || "error"; if (process.env["Agent.Id"]) { if (level === "error") { console.error(vsoLogIssueWrapper(`${logLevel}`, `${message}\n`)); } else { console.info(vsoLogIssueWrapper(`${logLevel}`, `${message}\n`)); } } else { if (level === "error") { console.error(`${message}\n`); } else { console.info(`${message}\n`); } } };