Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for converting OpenAPI3 specs to TypeSpec #3663

Merged
merged 45 commits into from
Jul 2, 2024
Merged
Show file tree
Hide file tree
Changes from 35 commits
Commits
Show all changes
45 commits
Select commit Hold shift + click to select a range
4fbd0c9
Add support for converting OpenAPI3 specs to TypeSpec
Jun 24, 2024
2cd4cdf
remove accidentally committed tsp file from openapi3 package
Jun 26, 2024
e1b887b
update pnpm-lock.yaml
Jun 26, 2024
e3097d4
update @typespec/openapi3 to use same version of prettier as peer pac…
Jun 26, 2024
ba5cca5
prevent CLI from being ran on every test file
Jun 26, 2024
d512401
increase printWidth of generated TSP files to 100 from 80
Jun 26, 2024
49d3c04
remove examples from petstore-swagger spec
Jun 26, 2024
67e8657
safer * replacement in response contentTypes
Jun 26, 2024
2fe4f65
add changeset
Jun 26, 2024
e19d9ed
Merge remote-tracking branch 'upstream/main' into oa3-to-ts
Jun 26, 2024
eac2ac4
add proper version info for CLI
Jun 26, 2024
ff8b86e
add some error reporting to CLI
Jun 26, 2024
cea1315
update package description and remove unused cli arg
Jun 26, 2024
cea668a
Merge branch 'main' into oa3-to-ts
chrisradek Jun 26, 2024
3d2861a
Merge branch 'main' into oa3-to-ts
chrisradek Jun 27, 2024
d706b4b
add support for oneOf
Jul 1, 2024
1cd876d
rename compile to convert
Jul 1, 2024
79f41d2
rename openapi3-to-tsp cli to tsp-openapi3
Jul 1, 2024
b595270
missed one file while updating compile -> convert
Jul 1, 2024
fef63e8
enforce snapshots
Jul 1, 2024
b2d1cae
fix arrays of enums and add nullable support
Jul 1, 2024
5674ea6
add generate schema types tests
Jul 1, 2024
ec8c443
update spec snapshots
Jul 1, 2024
e4e0c96
Merge remote-tracking branch 'upstream/main' into oa3-to-ts
Jul 1, 2024
72af3f2
improve array type generation
Jul 1, 2024
3587aa2
merge emitter into generators: emit-main to generate-main
Jul 1, 2024
94458cd
update dependency path
Jul 1, 2024
41a11f1
import HttpVerb instead of defining own type
Jul 1, 2024
9d4318a
update where OpenAPI3 document parser casting occurs
Jul 1, 2024
827e909
remove prettier dep
Jul 1, 2024
0982bb3
expand OpenAPI3Info interface
Jul 1, 2024
fa2df44
format types in test
Jul 1, 2024
5b3043c
Merge remote-tracking branch 'upstream/main' into oa3-to-ts
Jul 1, 2024
eb7472b
Update cli path resolution to match compilers to play nice with testi…
chrisradek Jul 2, 2024
0d38949
rearrange imports
Jul 2, 2024
60f456e
update changelog with new cli name
Jul 2, 2024
6b6b4c1
Merge branch 'main' into oa3-to-ts
chrisradek Jul 2, 2024
b11fbf4
switch from yargs to parseArgs
Jul 2, 2024
1136d93
remove no longer needed ts-node config in tsconfig
Jul 2, 2024
66c6242
remove unneeded dependencies
Jul 2, 2024
f1b119c
formatting in the changelog
Jul 2, 2024
2ddf85a
add tsp-openapi3 docs
Jul 2, 2024
6c6ad1a
Merge branch 'main' into oa3-to-ts
chrisradek Jul 2, 2024
f542951
update doc website
Jul 2, 2024
571f5cd
Merge branch 'main' into oa3-to-ts
chrisradek Jul 2, 2024
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/oa3-to-ts-2024-5-26-13-27-36.md
chrisradek marked this conversation as resolved.
Show resolved Hide resolved
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@typespec/openapi3"
---

Adds support for converting OpenAPI3 specs to TypeSpec via the new openapi3-to-tsp CLI included in the @typespec/openapi3 package.
chrisradek marked this conversation as resolved.
Show resolved Hide resolved
2 changes: 1 addition & 1 deletion docs/emitters/openapi3/reference/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ import TabItem from '@theme/TabItem';

# Overview

TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding
TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec

## Install

Expand Down
2 changes: 1 addition & 1 deletion packages/openapi3/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# @typespec/openapi3

TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding
TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec

## Install

Expand Down
9 changes: 9 additions & 0 deletions packages/openapi3/cmd/tsp-openapi3.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#!/usr/bin/env node
import { main } from "../dist/src/cli/cli.js";

main().catch((error) => {
// eslint-disable-next-line no-console
console.error(error);

process.exit(1);
});
19 changes: 15 additions & 4 deletions packages/openapi3/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
"name": "@typespec/openapi3",
"version": "0.57.0",
"author": "Microsoft Corporation",
"description": "TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding",
"description": "TypeSpec library for emitting OpenAPI 3.0 from the TypeSpec REST protocol binding and converting OpenAPI3 to TypeSpec",
"homepage": "https://typespec.io",
"readme": "https://github.com/microsoft/typespec/blob/main/README.md",
"license": "MIT",
Expand All @@ -16,6 +16,9 @@
"keywords": [
"typespec"
],
"bin": {
"tsp-openapi3": "cmd/tsp-openapi3.js"
},
"type": "module",
"main": "dist/src/index.js",
"tspMain": "lib/main.tsp",
Expand All @@ -34,7 +37,7 @@
},
"scripts": {
"clean": "rimraf ./dist ./temp",
"build": "npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library",
"build": "npm run gen-version && npm run gen-extern-signature && tsc -p . && npm run lint-typespec-library",
"watch": "tsc -p . --watch",
"gen-extern-signature": "tspd --enable-experimental gen-extern-signature .",
"lint-typespec-library": "tsp compile . --warn-as-error --import @typespec/library-linter --no-emit",
Expand All @@ -44,15 +47,20 @@
"test:ci": "vitest run --coverage --reporter=junit --reporter=default",
"lint": "eslint . --max-warnings=0",
"lint:fix": "eslint . --fix",
"regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/openapi3/reference"
"regen-docs": "tspd doc . --enable-experimental --output-dir ../../docs/emitters/openapi3/reference",
"regen-specs": "cross-env RECORD=true vitest run",
"gen-version": "node scripts/generate-version.js"
},
"files": [
"lib/*.tsp",
"dist/**",
"!dist/test/**"
],
"dependencies": {
"yaml": "~2.4.5"
"@readme/openapi-parser": "~2.6.0",
"@typespec/prettier-plugin-typespec": "workspace:~",
chrisradek marked this conversation as resolved.
Show resolved Hide resolved
"yaml": "~2.4.5",
"yargs": "~17.7.2"
chrisradek marked this conversation as resolved.
Show resolved Hide resolved
},
"peerDependencies": {
"@typespec/compiler": "workspace:~",
Expand All @@ -62,6 +70,7 @@
},
"devDependencies": {
"@types/node": "~18.11.19",
"@types/yargs": "~17.0.32",
"@typespec/compiler": "workspace:~",
"@typespec/http": "workspace:~",
"@typespec/library-linter": "workspace:~",
Expand All @@ -72,7 +81,9 @@
"@vitest/coverage-v8": "^1.6.0",
"@vitest/ui": "^1.6.0",
"c8": "^10.1.2",
"cross-env": "~7.0.3",
"rimraf": "~5.0.7",
"ts-node": "~10.9.2",
chrisradek marked this conversation as resolved.
Show resolved Hide resolved
"typescript": "~5.5.3",
"vitest": "^1.6.0"
}
Expand Down
27 changes: 27 additions & 0 deletions packages/openapi3/scripts/generate-version.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
// @ts-check
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
import { join } from "path";
import { fileURLToPath } from "url";
const root = fileURLToPath(new URL("..", import.meta.url).href);
const distDir = join(root, "dist");
const versionTarget = join(distDir, "version.js");

function loadPackageJson() {
const packageJsonPath = join(root, "package.json");
return JSON.parse(readFileSync(packageJsonPath, "utf-8"));
}

function main() {
const pkg = loadPackageJson();

const version = pkg.version;

if (!existsSync(distDir)) {
mkdirSync(distDir, { recursive: true });
}

const versionJs = `export const version = "${version}";`;
writeFileSync(versionTarget, versionJs);
}

main();
4 changes: 4 additions & 0 deletions packages/openapi3/src/cli/actions/convert/args.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
export interface ConvertCliArgs {
"output-dir"?: string;
args?: string[];
}
30 changes: 30 additions & 0 deletions packages/openapi3/src/cli/actions/convert/convert.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import oaParser from "@readme/openapi-parser";
import { resolvePath } from "@typespec/compiler";
import { OpenAPI3Document } from "../../../types.js";
import { CliHost } from "../../types.js";
import { handleInternalCompilerError } from "../../utils.js";
import { ConvertCliArgs } from "./args.js";
import { generateMain } from "./generators/generate-main.js";
import { transform } from "./transforms/transforms.js";

export async function convertAction(host: CliHost, args: ConvertCliArgs & { path: string }) {
// attempt to read the file
const fullPath = resolvePath(process.cwd(), args.path);
const model = await parseOpenApiFile(fullPath);
const program = transform(model);
let mainTsp: string;
try {
mainTsp = await generateMain(program);
} catch (err) {
handleInternalCompilerError(err);
}

if (args["output-dir"]) {
await host.mkdirp(args["output-dir"]);
await host.writeFile(resolvePath(args["output-dir"], "main.tsp"), mainTsp);
}
}

function parseOpenApiFile(path: string): Promise<OpenAPI3Document> {
return oaParser.bundle(path) as Promise<OpenAPI3Document>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { TypeSpecDecorator } from "../interfaces.js";

function generateDecorator({ name, args }: TypeSpecDecorator): string {
const hasArgs = args.length;
const stringifiedArguments = hasArgs ? `(${args.map((a) => JSON.stringify(a)).join(", ")})` : "";

return `@${name}${stringifiedArguments}`;
}

export function generateDecorators(decorators: TypeSpecDecorator[]): string[] {
const uniqueDecorators = new Set<string>(decorators.map(generateDecorator));
return Array.from(uniqueDecorators);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
import { formatTypeSpec } from "@typespec/compiler";
import { TypeSpecProgram } from "../interfaces.js";
import { generateModel } from "./generate-model.js";
import { generateOperation } from "./generate-operation.js";
import { generateServiceInformation } from "./generate-service-info.js";

export async function generateMain(program: TypeSpecProgram): Promise<string> {
const content = `
import "@typespec/http";
import "@typespec/openapi";
import "@typespec/openapi3";

using Http;
using OpenAPI;

${generateServiceInformation(program.serviceInfo)}

${program.models.map(generateModel).join("\n\n")}

${program.operations.map(generateOperation).join("\n\n")}
`;

return formatTypeSpec(content, {
printWidth: 100,
tabWidth: 2,
});
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
import { TypeSpecModel, TypeSpecModelProperty } from "../interfaces.js";
import { getDecoratorsForSchema } from "../utils/decorators.js";
import { generateDocs } from "../utils/docs.js";
import { generateDecorators } from "./generate-decorators.js";
import { generateTypeFromSchema } from "./generate-types.js";

export function generateModel(model: TypeSpecModel): string {
const definitions: string[] = [];
const modelDeclaration = generateModelDeclaration(model);

if (model.doc) {
definitions.push(generateDocs(model.doc));
}

definitions.push(...generateDecorators(model.decorators));
definitions.push(modelDeclaration.open);

definitions.push(...model.properties.map(generateModelProperty));

if (model.additionalProperties) {
definitions.push(`...${generateTypeFromSchema(model.additionalProperties)};`);
}

if (modelDeclaration.close) definitions.push(modelDeclaration.close);

return definitions.join("\n");
}

type ModelDeclarationOutput = { open: string; close?: string };

function generateModelDeclaration(model: TypeSpecModel): ModelDeclarationOutput {
const modelName = model.name;
const modelType = model.type ?? "object";

if (model.is) {
return { open: `model ${modelName} is ${model.is};` };
}

if (!model.extends) {
return { open: `model ${modelName} {`, close: "}" };
}

if (modelType === "object") {
return { open: `model ${modelName} extends ${model.extends} {`, close: "}" };
}

switch (modelType) {
case "boolean":
case "integer":
case "number":
case "string":
return { open: `scalar ${modelName} extends ${model.extends};` };
}

return { open: `model ${modelName} {`, close: "}" };
}

function generateModelProperty(property: TypeSpecModelProperty): string {
// Decorators will be a combination of top-level (parameters) and
// schema-level decorators.
const decorators = generateDecorators([
...property.decorators,
...getDecoratorsForSchema(property.schema),
]).join(" ");

const doc = property.doc ? generateDocs(property.doc) : "";

return `${doc}${decorators} ${property.name}${property.isOptional ? "?" : ""}: ${generateTypeFromSchema(property.schema)};`;
}
Loading
Loading