oav/lib/validate.ts

472 строки
16 KiB
TypeScript

// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License. See License.txt in the project root for license information.
import { Suppression } from "@azure/openapi-markdown"
import * as jsonParser from "@ts-common/json-parser"
import { StringMap } from "@ts-common/string-map"
import * as fs from "fs"
import jsYaml from "js-yaml"
import * as path from "path"
import * as umlGeneratorLib from "./umlGenerator"
import { getErrorsFromModelValidation } from "./util/getErrorsFromModelValidation"
import * as jsonUtils from "./util/jsonUtils"
import { log } from "./util/logging"
import { ModelValidationError } from "./util/modelValidationError"
import * as utils from "./util/utils"
import { NodeError } from "./util/validationError"
import { ModelValidator } from "./validators/modelValidator"
import { SemanticValidator } from "./validators/semanticValidator"
import * as specResolver from "./validators/specResolver"
import {
CommonValidationResult,
SpecValidationResult,
SpecValidator
} from "./validators/specValidator"
import { getSuppressions } from "./validators/suppressions"
import { WireFormatGenerator } from "./wireFormatGenerator"
import { XMsExampleExtractor } from "./xMsExampleExtractor"
export interface Options extends specResolver.Options, umlGeneratorLib.Options {
consoleLogLevel?: unknown
logFilepath?: unknown
pretty?: boolean
}
export async function getDocumentsFromCompositeSwagger(
suppression: Suppression | undefined,
compositeSpecPath: string,
reportError: jsonParser.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}`
}
}
async function validate<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: ReadonlyArray<T> | undefined,
errorType: ErrorType
) => {
if (errors !== undefined) {
for (const error of errors) {
const yaml = jsYaml.dump(error)
if (process.env["Agent.Id"]) {
/* tslint:disable-next-line:no-console no-string-literal */
console.error(vsoLogIssueWrapper(errorType, errorType))
/* tslint:disable-next-line:no-console no-string-literal */
console.error(vsoLogIssueWrapper(errorType, yaml))
} else {
/* tslint:disable-next-line:no-console no-string-literal */
console.error("\x1b[31m", errorType, ":", "\x1b[0m")
/* tslint:disable-next-line:no-console no-string-literal */
console.error(yaml)
}
}
}
}
export async function validateSpec(
specPath: string,
options: Options | undefined
): Promise<SpecValidationResult> {
return 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) {
/* tslint:disable-next-line:no-console no-string-literal */
console.log(vsoLogIssueWrapper("error", `Semantically validating ${specPath}:\n`))
} else if (validationResults.warnings && validationResults.warnings.length > 0) {
/* tslint:disable-next-line:no-console no-string-literal */
console.log(vsoLogIssueWrapper("warning", `Semantically validating ${specPath}:\n`))
} else {
/* tslint:disable-next-line:no-console no-string-literal */
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) {
// console.log(err)
log.error(err)
validator.specValidationResult.validityStatus = false
return validator.specValidationResult
}
})
}
export async function validateCompositeSpec(
compositeSpecPath: string,
options: Options
): Promise<ReadonlyArray<SpecValidationResult>> {
return validate(options, async o => {
const suppression = await getSuppressions(compositeSpecPath)
const docs = await getDocumentsFromCompositeSwagger(
suppression,
compositeSpecPath,
jsonParser.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<ReadonlyArray<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) {
/* tslint:disable-next-line:no-console no-string-literal */
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`
)
)
/* tslint:disable-next-line:no-console no-string-literal */
console.error(vsoLogIssueWrapper("error", e))
} else {
console.error(
`Validating "examples" and "x-ms-examples" in ${specPath}:\n`
)
/* tslint:disable-next-line:no-console no-string-literal */
console.error("\x1b[31m", "error", ":", "\x1b[0m")
/* tslint:disable-next-line:no-console no-string-literal */
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<ReadonlyArray<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,
jsonParser.defaultErrorReport
)
const promiseFactories = docs.map(doc => async () => validateExamples(doc, undefined, o))
return utils.executePromisesSequentially(promiseFactories)
})
}
export async function resolveSpec(
specPath: string,
outputDir: string,
options: Options,
reportError: jsonParser.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"
})
/* tslint:disable-next-line */
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,
jsonParser.defaultErrorReport
)
options.consoleLogLevel = log.consoleLogLevel
options.logFilepath = log.filepath
const promiseFactories = docs.map(doc => async () =>
resolveSpec(doc, outputDir, options, jsonParser.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,
jsonParser.defaultErrorReport
)
options.consoleLogLevel = log.consoleLogLevel
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, jsonParser.defaultErrorReport)
const resolver = new specResolver.SpecResolver(
specPath,
result,
resolverOptions,
jsonParser.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"
})
/* tslint:disable-next-line */
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") {
/* tslint:disable-next-line */
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<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()
}