Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions .chronus/changes/chore-parser-migration-2025-9-10-15-16-40.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: internal
packages:
- "@typespec/openapi3"
---

[deps] swaps outdated and unmaintained openapi-types and swagger parser dependencies for drop in replacements
5 changes: 3 additions & 2 deletions packages/openapi3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -62,9 +62,10 @@
"!dist/test/**"
],
"dependencies": {
"@apidevtools/swagger-parser": "~12.0.0",
"@scalar/json-magic": "^0.6.1",
"@scalar/openapi-parser": "^0.22.3",
"@scalar/openapi-types": "^0.5.0",
"@typespec/asset-emitter": "workspace:^",
"openapi-types": "~12.1.3",
"yaml": "~2.8.0"
},
"peerDependencies": {
Expand Down
16 changes: 12 additions & 4 deletions packages/openapi3/src/cli/actions/convert/convert-file.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,7 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { bundle } from "@scalar/json-magic/bundle";
import { fetchUrls, parseJson, parseYaml } from "@scalar/json-magic/bundle/plugins/browser";
import { readFiles } from "@scalar/json-magic/bundle/plugins/node";
import { OpenAPI } from "@scalar/openapi-types";
import { formatTypeSpec, resolvePath } from "@typespec/compiler";
import { OpenAPI3Document } from "../../../types.js";
import { CliHost } from "../../types.js";
Expand All @@ -11,9 +14,14 @@ import { createContext } from "./utils/context.js";
export async function convertAction(host: CliHost, args: ConvertCliArgs) {
// attempt to read the file
const fullPath = resolvePath(process.cwd(), args.path);
const parser = new OpenAPIParser();
const model = await parser.bundle(fullPath);
const context = createContext(parser, model as OpenAPI3Document, console, args.namespace);
const data = (await bundle(fullPath, {
plugins: [readFiles(), fetchUrls(), parseYaml(), parseJson()],
treeShake: false,
})) as OpenAPI.Document;
if (!data) {
throw new Error("Failed to load OpenAPI document");
}
const context = createContext(data as OpenAPI3Document, console, args.namespace);
const program = transform(context);
let mainTsp: string;
try {
Expand Down
19 changes: 13 additions & 6 deletions packages/openapi3/src/cli/actions/convert/convert.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { AnyObject, dereference } from "@scalar/openapi-parser";
import { formatTypeSpec } from "@typespec/compiler";
import { OpenAPI3Document } from "../../../types.js";
import { generateMain } from "./generators/generate-main.js";
Expand All @@ -20,14 +20,21 @@ export async function convertOpenAPI3Document(
document: OpenAPI3Document,
{ disableExternalRefs, namespace }: ConvertOpenAPI3DocumentOptions = {},
) {
const parser = new OpenAPIParser();
const bundleOptions = disableExternalRefs
const dereferenceOptions = disableExternalRefs
? {
resolve: { external: false, http: false, file: false },
onDereference: (data: { schema: AnyObject; ref: string }): void => {
if (data.ref.startsWith("#")) {
return;
}
throw new Error(`External $ref pointers are disabled, but found $ref: ${data.ref}`);
},
}
: {};
await parser.bundle(document as any, bundleOptions);
const context = createContext(parser, document, console, namespace);
const { specification } = await dereference(document, dereferenceOptions);
if (!specification) {
throw new Error("Failed to dereference OpenAPI document");
}
const context = createContext(document, console, namespace);
const program = transform(context);
const content = generateMain(program, context);
try {
Expand Down
45 changes: 36 additions & 9 deletions packages/openapi3/src/cli/actions/convert/utils/context.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import type OpenAPIParser from "@apidevtools/swagger-parser";
import { OpenAPI3Document, OpenAPI3Encoding, OpenAPI3Schema, Refable } from "../../../../types.js";
import {
OpenAPI3Document,
OpenAPI3Encoding,
OpenAPI3Schema,
OpenAPIDocument3_1,
Refable,
} from "../../../../types.js";
import { Logger } from "../../../types.js";
import { SchemaToExpressionGenerator } from "../generators/generate-types.js";
import { generateNamespaceName } from "./generate-namespace-name.js";
Expand Down Expand Up @@ -47,12 +52,7 @@ export interface Context {
): string;
}

export type Parser = {
$refs: OpenAPIParser["$refs"];
};

export function createContext(
parser: Parser,
openApi3Doc: OpenAPI3Document,
logger?: Logger,
namespace?: string,
Expand Down Expand Up @@ -96,8 +96,35 @@ export function createContext(
getSchemaByRef(ref) {
return this.getByRef(ref);
},
getByRef(ref) {
return parser.$refs.get(ref) as any;
getByRef<T>(ref: string): T | undefined {
const splitRef = ref.split("/");
const componentKind = splitRef[2]; // #/components/schemas/Pet -> components
const componentName = splitRef[3]; // #/components/schemas/Pet -> Pet
switch (componentKind) {
case "schemas":
return openApi3Doc.components?.schemas?.[componentName] as T;
case "responses":
return openApi3Doc.components?.responses?.[componentName] as T;
case "parameters":
return openApi3Doc.components?.parameters?.[componentName] as T;
case "examples":
return openApi3Doc.components?.examples?.[componentName] as T;
case "requestBodies":
return openApi3Doc.components?.requestBodies?.[componentName] as T;
case "headers":
return openApi3Doc.components?.headers?.[componentName] as T;
case "securitySchemes":
return openApi3Doc.components?.securitySchemes?.[componentName] as T;
case "links":
return openApi3Doc.components?.links?.[componentName] as T;
case "callbacks":
return (openApi3Doc as unknown as OpenAPIDocument3_1).components?.callbacks?.[
componentName
] as T;
default:
this.logger.warn(`Unsupported component kind in $ref: ${ref}`);
return undefined;
}
},
registerMultipartSchema(ref: string, encoding?: Record<string, OpenAPI3Encoding>) {
multipartSchemas.set(ref, encoding ?? null);
Expand Down
18 changes: 10 additions & 8 deletions packages/openapi3/test/tsp-openapi3/context.test.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { OpenAPI } from "openapi-types";
import { dereference } from "@scalar/openapi-parser";
import { OpenAPI } from "@scalar/openapi-types";
import { beforeAll, describe, expect, it } from "vitest";
import { createContext } from "../../src/cli/actions/convert/utils/context.js";
import { OpenAPI3Document } from "../../src/types.js";

describe("tsp-openapi: Context methods", () => {
let parser: OpenAPIParser;
let doc: OpenAPI.Document<{}>;

beforeAll(async () => {
parser = new OpenAPIParser();
doc = await parser.bundle({
const { specification } = await dereference({
openapi: "3.0.0",
info: { title: "Test", version: "1.0.0" },
paths: {},
});
if (!specification) {
throw new Error("Failed to dereference OpenAPI document");
}
doc = specification;
});

it("should add a component encoding to the registry", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const reference = "#/components/schemas/MySchema";
const encoding = {
myProperty: {
Expand All @@ -31,15 +33,15 @@ describe("tsp-openapi: Context methods", () => {
});

it("should consider a component without encoding to be registered", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const reference = "#/components/schemas/MySchema";
context.registerMultipartSchema(reference);
expect(context.getMultipartSchemaEncoding(reference)).toBeUndefined();
expect(context.isSchemaReferenceRegisteredForMultipartForm(reference)).toBe(true);
});

it("should NOT consider a component to be registered when it's not", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const reference = "#/components/schemas/MySchema";
expect(context.getMultipartSchemaEncoding(reference)).toBeUndefined();
expect(context.isSchemaReferenceRegisteredForMultipartForm(reference)).toBe(false);
Expand Down
10 changes: 6 additions & 4 deletions packages/openapi3/test/tsp-openapi3/generate-type.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { dereference } from "@scalar/openapi-parser";
import { formatTypeSpec } from "@typespec/compiler";
import { strictEqual } from "node:assert";
import { beforeAll, describe, it } from "vitest";
Expand Down Expand Up @@ -302,13 +302,15 @@ const testScenarios: TestScenario[] = [
describe("tsp-openapi: generate-type", () => {
let context: Context;
beforeAll(async () => {
const parser = new OpenAPIParser();
const doc = await parser.bundle({
const { specification } = await dereference({
openapi: "3.0.0",
info: { title: "Test", version: "1.0.0" },
paths: {},
});
context = createContext(parser, doc as OpenAPI3Document);
if (!specification) {
throw new Error("Failed to dereference OpenAPI document");
}
context = createContext(specification as OpenAPI3Document);
});
testScenarios.forEach((t) =>
it(`${generateScenarioName(t)}`, async () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { dereference } from "@scalar/openapi-parser";
import { formatTypeSpec } from "@typespec/compiler";
import { strictEqual } from "node:assert";
import { beforeAll, describe, it } from "vitest";
Expand All @@ -15,13 +15,12 @@ describe("tsp-openapi: HTTP part generation methods", () => {
let context: Context;

beforeAll(async () => {
const parser = new OpenAPIParser();
const doc = await parser.bundle({
const { specification } = await dereference({
openapi: "3.0.0",
info: { title: "Test", version: "1.0.0" },
paths: {},
});
context = createContext(parser, doc as OpenAPI3Document);
context = createContext(specification as OpenAPI3Document);
});

describe("basic HTTP part wrapping", () => {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
import { beforeEach, describe, expect, it, vi } from "vitest";
import { transformPaths } from "../../src/cli/actions/convert/transforms/transform-paths.js";
import { createContext, Parser } from "../../src/cli/actions/convert/utils/context.js";
import { createContext } from "../../src/cli/actions/convert/utils/context.js";
import { convertOpenAPI3Document } from "../../src/index.js";

describe("Convert OpenAPI3 with missing operationId", () => {
const mockParser: Parser = {
$refs: {
get: () => undefined,
} as any,
};

// Mock logger to capture warnings
const mockLogger = {
trace: vi.fn(),
Expand Down Expand Up @@ -65,7 +59,7 @@ describe("Convert OpenAPI3 with missing operationId", () => {
},
};

const context = createContext(mockParser, openApiDoc as any, mockLogger);
const context = createContext(openApiDoc as any, mockLogger);
const operations = transformPaths(openApiDoc.paths, context);

// Should have 3 operations
Expand Down Expand Up @@ -110,7 +104,7 @@ describe("Convert OpenAPI3 with missing operationId", () => {
},
};

const context = createContext(mockParser, openApiDoc as any, mockLogger);
const context = createContext(openApiDoc as any, mockLogger);
const operations = transformPaths(openApiDoc.paths, context);

// Should have 2 operations
Expand Down
20 changes: 11 additions & 9 deletions packages/openapi3/test/tsp-openapi3/single-anyof-oneof.test.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { OpenAPI } from "openapi-types";
import { dereference } from "@scalar/openapi-parser";
import { OpenAPI } from "@scalar/openapi-types";
import { beforeAll, describe, expect, it } from "vitest";
import { generateDataType } from "../../src/cli/actions/convert/generators/generate-model.js";
import { TypeSpecDataTypes, TypeSpecModel } from "../../src/cli/actions/convert/interfaces.js";
Expand All @@ -8,12 +8,10 @@ import { createContext } from "../../src/cli/actions/convert/utils/context.js";
import { OpenAPI3Document } from "../../src/types.js";

describe("tsp-openapi: single anyOf/oneOf inline schema should produce model", () => {
let parser: OpenAPIParser;
let doc: OpenAPI.Document<{}>;

beforeAll(async () => {
parser = new OpenAPIParser();
doc = await parser.bundle({
const { specification } = await dereference({
openapi: "3.1.0",
info: { title: "repro API", version: "1.0.0", description: "API for repro" },
servers: [{ url: "http://localhost:3000" }],
Expand Down Expand Up @@ -87,10 +85,14 @@ describe("tsp-openapi: single anyOf/oneOf inline schema should produce model", (
},
},
});
if (!specification) {
throw new Error("Failed to dereference OpenAPI document");
}
doc = specification;
});

it("should generate a model for anyOf with single inline schema", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const types: TypeSpecDataTypes[] = [];
transformComponentSchemas(context, types);

Expand All @@ -115,7 +117,7 @@ describe("tsp-openapi: single anyOf/oneOf inline schema should produce model", (
});

it("should generate a model for oneOf with single inline schema", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const types: TypeSpecDataTypes[] = [];
transformComponentSchemas(context, types);

Expand All @@ -132,7 +134,7 @@ describe("tsp-openapi: single anyOf/oneOf inline schema should produce model", (
});

it("should generate a model for anyOf with single inline schema + null", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const types: TypeSpecDataTypes[] = [];
transformComponentSchemas(context, types);

Expand All @@ -152,7 +154,7 @@ describe("tsp-openapi: single anyOf/oneOf inline schema should produce model", (
});

it("should generate a model for oneOf with single inline schema + null", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
const types: TypeSpecDataTypes[] = [];
transformComponentSchemas(context, types);

Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,16 @@
import OpenAPIParser from "@apidevtools/swagger-parser";
import { OpenAPI } from "openapi-types";
import { dereference } from "@scalar/openapi-parser";
import { OpenAPI } from "@scalar/openapi-types";
import { beforeAll, describe, expect, it } from "vitest";
import { TypeSpecModel } from "../../src/cli/actions/convert/interfaces.js";
import { transformComponentSchemas } from "../../src/cli/actions/convert/transforms/transform-component-schemas.js";
import { createContext } from "../../src/cli/actions/convert/utils/context.js";
import { OpenAPI3Document } from "../../src/types.js";

describe("tsp-openapi: transform component schemas", () => {
let parser: OpenAPIParser;
let doc: OpenAPI.Document<{}>;

beforeAll(async () => {
parser = new OpenAPIParser();
doc = await parser.bundle({
const { specification } = await dereference({
openapi: "3.0.0",
info: { title: "Test", version: "1.0.0" },
paths: {
Expand Down Expand Up @@ -52,10 +50,14 @@ describe("tsp-openapi: transform component schemas", () => {
},
},
});
if (!specification) {
throw new Error("Failed to dereference OpenAPI document");
}
doc = specification;
});

it("adds the encoding to the model when available", () => {
const context = createContext(parser, doc as OpenAPI3Document);
const context = createContext(doc as OpenAPI3Document);
context.registerMultipartSchema("#/components/schemas/MyModel", {
id: { contentType: "text/plain" },
name: { contentType: "text/plain" },
Expand Down
Loading
Loading