diff --git a/packages/compiler/src/core/schema-validator.ts b/packages/compiler/src/core/schema-validator.ts index 52e9ba9b8a..d7cf7266bc 100644 --- a/packages/compiler/src/core/schema-validator.ts +++ b/packages/compiler/src/core/schema-validator.ts @@ -1,7 +1,9 @@ -import Ajv, { DefinedError, Options } from "ajv"; +import Ajv, { ErrorObject, Options } from "ajv"; import { getLocationInYamlScript } from "../yaml/diagnostics.js"; import { YamlScript } from "../yaml/types.js"; import { compilerAssert } from "./diagnostics.js"; +import { createDiagnostic } from "./messages.js"; +import { isPathAbsolute } from "./path-utils.js"; import { Diagnostic, JSONSchemaType, JSONSchemaValidator, NoTarget, SourceFile } from "./types.js"; export interface JSONSchemaValidatorOptions { @@ -13,12 +15,18 @@ export function createJSONSchemaValidator( schema: JSONSchemaType, options: JSONSchemaValidatorOptions = { strict: true } ): JSONSchemaValidator { - const ajv = new (Ajv as any)({ + const ajv: import("ajv").default = new (Ajv as any)({ strict: options.strict, coerceTypes: options.coerceTypes, allErrors: true, } satisfies Options); + ajv.addFormat("absolute-path", { + type: "string", + validate: (path) => { + return !path.startsWith(".") && isPathAbsolute(path); + }, + }); return { validate }; function validate( @@ -34,7 +42,7 @@ export function createJSONSchemaValidator( const diagnostics = []; for (const error of validate.errors ?? []) { - const diagnostic = ajvErrorToDiagnostic(error, target); + const diagnostic = ajvErrorToDiagnostic(config, error, target); diagnostics.push(diagnostic); } @@ -45,9 +53,24 @@ export function createJSONSchemaValidator( const IGNORED_AJV_PARAMS = new Set(["type", "errors"]); function ajvErrorToDiagnostic( - error: DefinedError, + obj: unknown, + error: ErrorObject, unknown>, target: YamlScript | SourceFile | typeof NoTarget ): Diagnostic { + const tspTarget = + target === NoTarget + ? target + : "kind" in target + ? getLocationInYamlScript(target, getErrorPath(error), "key") + : { file: target, pos: 0, end: 0 }; + if (error.params.format === "absolute-path") { + return createDiagnostic({ + code: "config-path-absolute", + format: { path: getErrorValue(obj, error) as any }, + target: tspTarget, + }); + } + const messageLines = [`Schema violation: ${error.message} (${error.instancePath || "/"})`]; for (const [name, value] of Object.entries(error.params).filter( ([name]) => !IGNORED_AJV_PARAMS.has(name) @@ -61,16 +84,11 @@ function ajvErrorToDiagnostic( code: "invalid-schema", message, severity: "error", - target: - target === NoTarget - ? target - : "kind" in target - ? getLocationInYamlScript(target, getErrorPath(error), "key") - : { file: target, pos: 0, end: 0 }, + target: tspTarget, }; } -function getErrorPath(error: DefinedError): string[] { +function getErrorPath(error: ErrorObject, unknown>): string[] { const instancePath = parseJsonPointer(error.instancePath); switch (error.keyword) { case "additionalProperties": @@ -79,6 +97,17 @@ function getErrorPath(error: DefinedError): string[] { return instancePath; } } +function getErrorValue( + obj: any, + error: ErrorObject, unknown> +): unknown { + const path = getErrorPath(error); + let current = obj; + for (const segment of path) { + current = current[segment]; + } + return current; +} /** * Converts a json pointer into a array of reference tokens diff --git a/packages/openapi3/src/lib.ts b/packages/openapi3/src/lib.ts index feca60d2f3..d55c5fd3fc 100644 --- a/packages/openapi3/src/lib.ts +++ b/packages/openapi3/src/lib.ts @@ -70,6 +70,7 @@ const EmitterOptionsSchema: JSONSchemaType = { "output-file": { type: "string", nullable: true, + format: "absolute-path", description: [ "Name of the output file.", " Output file will interpolate the following values:",