From a2f5dff62d7da3eaf0eaff1d3f6c2e7b23654a53 Mon Sep 17 00:00:00 2001 From: patzick <13100280+patzick@users.noreply.github.com> Date: Tue, 12 Nov 2024 18:35:16 +0100 Subject: [PATCH] fix(openapi-typescript): include location in error messages --- .changeset/three-bobcats-mate.md | 5 ++ packages/openapi-typescript/src/lib/redoc.ts | 51 ++++++++----------- .../src/transform/schema-object.ts | 4 +- .../openapi-typescript/test/invalid.test.ts | 30 +++++++++++ 4 files changed, 60 insertions(+), 30 deletions(-) create mode 100644 .changeset/three-bobcats-mate.md diff --git a/.changeset/three-bobcats-mate.md b/.changeset/three-bobcats-mate.md new file mode 100644 index 00000000..7989f4bc --- /dev/null +++ b/.changeset/three-bobcats-mate.md @@ -0,0 +1,5 @@ +--- +"openapi-typescript": patch +--- + +Improved error messages to contain locations. diff --git a/packages/openapi-typescript/src/lib/redoc.ts b/packages/openapi-typescript/src/lib/redoc.ts index 4b3172e7..0cfc7a8c 100644 --- a/packages/openapi-typescript/src/lib/redoc.ts +++ b/packages/openapi-typescript/src/lib/redoc.ts @@ -6,6 +6,7 @@ import { Source, type Document, lintDocument, + type NormalizedProblem, } from "@redocly/openapi-core"; import { performance } from "node:perf_hooks"; import { Readable } from "node:stream"; @@ -81,6 +82,25 @@ export async function parseSchema(schema: unknown, { absoluteRef, resolver }: Pa throw new Error(`Expected string, object, or Buffer. Got ${Array.isArray(schema) ? "Array" : typeof schema}`); } +function _processProblems(problems: NormalizedProblem[], options: { silent: boolean }) { + if (problems.length) { + let errorMessage: string | undefined = undefined; + for (const problem of problems) { + const problemLocation = problem.location?.[0].pointer; + const problemMessage = problemLocation ? `${problem.message} at ${problemLocation}` : problem.message; + if (problem.severity === "error") { + errorMessage = problemMessage; + error(problemMessage); + } else { + warn(problemMessage, options.silent); + } + } + if (errorMessage) { + throw new Error(errorMessage); + } + } +} + /** * Validate an OpenAPI schema and flatten into a single schema using Redocly CLI */ @@ -127,20 +147,7 @@ export async function validateAndBundle( config: options.redoc.styleguide, externalRefResolver: resolver, }); - if (problems.length) { - let errorMessage: string | undefined = undefined; - for (const problem of problems) { - if (problem.severity === "error") { - errorMessage = problem.message; - error(problem.message); - } else { - warn(problem.message, options.silent); - } - } - if (errorMessage) { - throw new Error(errorMessage); - } - } + _processProblems(problems, options); debug("Linted schema", "lint", performance.now() - redocLintT); // 3. bundle @@ -150,21 +157,7 @@ export async function validateAndBundle( dereference: false, doc: document, }); - if (bundled.problems.length) { - let errorMessage: string | undefined = undefined; - for (const problem of bundled.problems) { - if (problem.severity === "error") { - errorMessage = problem.message; - error(problem.message); - throw new Error(problem.message); - } else { - warn(problem.message, options.silent); - } - } - if (errorMessage) { - throw new Error(errorMessage); - } - } + _processProblems(bundled.problems, options); debug("Bundled schema", "bundle", performance.now() - redocBundleT); return bundled.bundle.parsed; diff --git a/packages/openapi-typescript/src/transform/schema-object.ts b/packages/openapi-typescript/src/transform/schema-object.ts index 60048973..80ec56b5 100644 --- a/packages/openapi-typescript/src/transform/schema-object.ts +++ b/packages/openapi-typescript/src/transform/schema-object.ts @@ -66,7 +66,9 @@ export function transformSchemaObjectWithComposition( } // for any other unexpected type, throw error if (Array.isArray(schemaObject) || typeof schemaObject !== "object") { - throw new Error(`Expected SchemaObject, received ${Array.isArray(schemaObject) ? "Array" : typeof schemaObject}`); + throw new Error( + `Expected SchemaObject, received ${Array.isArray(schemaObject) ? "Array" : typeof schemaObject} at ${options.path}`, + ); } /** diff --git a/packages/openapi-typescript/test/invalid.test.ts b/packages/openapi-typescript/test/invalid.test.ts index bc6b2e5d..2f019a29 100644 --- a/packages/openapi-typescript/test/invalid.test.ts +++ b/packages/openapi-typescript/test/invalid.test.ts @@ -19,4 +19,34 @@ describe("Invalid schemas", () => { test("Other missing required fields", async () => { await expect(() => openapiTS({} as any)).rejects.toThrowError("Unsupported schema format, expected `openapi: 3.x`"); }); + + test("Unresolved $ref error messages", async () => { + const consoleErrorSpy = vi.spyOn(console, "error").mockImplementation(() => {}); + + await expect(() => + openapiTS({ + openapi: "3.1", + info: { title: "test", version: "1.0" }, + components: { + schemas: { + Pet: { + type: "object", + properties: { + category: { $ref: "#/components/schemas/NonExistingSchema" }, + type: { $ref: "#/components/schemas/AnotherSchema" }, + }, + }, + }, + }, + }), + ).rejects.toThrowError("Can't resolve $ref at #/components/schemas/Pet/properties/type"); + + expect(consoleErrorSpy).toHaveBeenCalledTimes(2); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Can't resolve $ref at #/components/schemas/Pet/properties/category"), + ); + expect(consoleErrorSpy).toHaveBeenCalledWith( + expect.stringContaining("Can't resolve $ref at #/components/schemas/Pet/properties/type"), + ); + }); });