Split up generators

This commit is contained in:
Steve Faulkner
2020-07-23 16:35:05 -05:00
parent cfe9bd8303
commit f1812077e9
25 changed files with 2983 additions and 2859 deletions

View File

@@ -1,11 +1,13 @@
/// <reference types="node" />
import { writeFileSync } from "fs";
import * as path from "path";
import fetch from "node-fetch";
import mkdirp from "mkdirp";
/*
Open API TypeScript Client Generator
This is a quickly made bespoke Open API client generator.
This is a bespoke Open API client generator not intended for general public use.
It is not designed to handle the full OpenAPI spec.
Many other more general purpose generators exist, but their output is very verbose and overly complex for our use case.
But it does work well enough to generate a fully typed tree-shakeable client for the Cosmos resource provider.
@@ -13,11 +15,14 @@ Results of this file should be checked into the repo.
*/
// Array of strings to use for eventual output
const output: string[] = [""];
const outputTypes: string[] = [""];
const schemaURL =
"https://raw.githubusercontent.com/Azure/azure-rest-api-specs/master/specification/cosmos-db/resource-manager/Microsoft.DocumentDB/preview/2020-06-01-preview/cosmos-db.json";
const outputDir = path.join(__dirname, "../../src/Utils/arm/");
mkdirp.sync(outputDir);
// Buckets for grouping operations based on their name
const namespaces: { [key: string]: string[] } = {};
@@ -27,10 +32,11 @@ const propertyMap: { [key: string]: string } = {
};
// Converts a Open API reference: "#/definitions/Foo" to a type name: Foo
function refToType(path: string | undefined) {
function refToType(path: string | undefined, namespace?: string) {
// References must be in the same file. Bail to `unknown` types for remote references
if (path && path.startsWith("#")) {
return path.split("/").pop();
const type = path.split("/").pop();
return namespace ? `${namespace}.${type}` : type;
}
return "unknown";
}
@@ -45,11 +51,11 @@ function camelize(str: string) {
}
// Converts a body paramter to the equivalent typescript function parameter type
function bodyParam(parameter: { schema: { $ref: string } }) {
function bodyParam(parameter: { schema: { $ref: string } }, namespace: string) {
if (!parameter) {
return "";
}
return `,body: ${refToType(parameter.schema.$ref)}`;
return `,body: ${refToType(parameter.schema.$ref, namespace)}`;
}
// eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -65,14 +71,14 @@ function parametersFromPath(path: string) {
type Operation = { responses: { [key: string]: { schema: { $ref: string } } } };
// Converts OpenAPI response definition to TypeScript return type. Uses unions if possible. Bails to unknown
function responseType(operation: Operation) {
function responseType(operation: Operation, namespace: string) {
if (operation.responses) {
return Object.keys(operation.responses)
.map((responseCode: string) => {
if (!operation.responses[responseCode].schema) {
return "void";
}
return refToType(operation.responses[responseCode].schema.$ref);
return refToType(operation.responses[responseCode].schema.$ref, namespace);
})
.join(" | ");
}
@@ -91,33 +97,33 @@ async function main() {
const baseTypes = schema.definitions[definition].allOf
.map((allof: { $ref: string }) => refToType(allof.$ref))
.join(" & ");
output.push(`type ${definition} = ${baseTypes} & {`);
outputTypes.push(`export type ${definition} = ${baseTypes} & {`);
} else {
output.push(`interface ${definition} {`);
outputTypes.push(`export interface ${definition} {`);
}
for (const prop in schema.definitions[definition].properties) {
const property = schema.definitions[definition].properties[prop];
if (property) {
if (property.$ref) {
const type = refToType(property.$ref);
output.push(`
outputTypes.push(`
/* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}
`);
} else if (property.type === "array") {
const type = refToType(property.items.$ref);
output.push(`
outputTypes.push(`
/* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}[]
`);
} else if (property.type === "object") {
const type = refToType(property.$ref);
output.push(`
outputTypes.push(`
/* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}
`);
} else {
output.push(`
outputTypes.push(`
/* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${
propertyMap[property.type] ? propertyMap[property.type] : property.type
@@ -125,36 +131,36 @@ async function main() {
}
}
}
output.push(`}`);
output.push("\n\n");
outputTypes.push(`}`);
outputTypes.push("\n\n");
} else {
const def = schema.definitions[definition];
if (def.enum) {
output.push(`
outputTypes.push(`
/* ${def.description} */
type ${definition} = ${def.enum.map((v: string) => `"${v}"`).join(" | ")}`);
output.push("\n");
export type ${definition} = ${def.enum.map((v: string) => `"${v}"`).join(" | ")}`);
outputTypes.push("\n");
} else if (def.type === "string") {
output.push(`
outputTypes.push(`
/* ${def.description} */
type ${definition} = string
export type ${definition} = string
`);
} else if (def.type === "array") {
const type = refToType(def.items.$ref);
output.push(`
outputTypes.push(`
/* ${def.description} */
type ${definition} = ${type}[]
export type ${definition} = ${type}[]
`);
} else if (def.type === "object" && def.additionalProperties) {
output.push(`
outputTypes.push(`
/* ${def.description} */
type ${definition} = { [key: string]: ${def.additionalProperties.type}}
export type ${definition} = { [key: string]: ${def.additionalProperties.type}}
`);
} else if (def.type === "object" && def.allOf) {
const type = refToType(def.allOf[0].$ref);
output.push(`
outputTypes.push(`
/* ${def.description} */
type ${definition} = ${type}
export type ${definition} = ${type}
`);
} else {
console.log("UNHANDLED MODEL:", def, schema.definitions[def]);
@@ -176,10 +182,10 @@ async function main() {
}
namespaces[namespace].push(`
/* ${operation.description} */
async ${camelize(operationName)} (
export async function ${camelize(operationName)} (
${parametersFromPath(path)}
${bodyParam(bodyParameter)}
) : Promise<${responseType(operation)}> {
${bodyParam(bodyParameter, "Types")}
) : Promise<${responseType(operation, "Types")}> {
return window.fetch(\`https://management.azure.com${path.replace(/{/g, "${")}\`, { method: "${method}", ${
bodyParameter ? "body: JSON.stringify(body)" : ""
} }).then((response) => response.json())
@@ -190,12 +196,22 @@ async function main() {
// Write all grouped fetch functions to objects
for (const namespace in namespaces) {
output.push(`export const ${namespace} = {`);
output.push(namespaces[namespace].join(",\n"));
output.push(`}\n`);
const outputClient: string[] = [""];
outputClient.push(`import * as Types from "./types"\n\n`);
outputClient.push(namespaces[namespace].join("\n\n"));
writeOutputFile(`./${namespace}.ts`, outputClient);
}
writeFileSync("./client.ts", output.join(""));
writeOutputFile("./types.ts", outputTypes);
}
function writeOutputFile(outputPath: string, components: string[]) {
components.unshift(`/*
AUTOGENERATED FILE
Do not manually edit
Run "npm run generateARMClients" to regenerate
*/\n\n`);
writeFileSync(path.join(outputDir, outputPath), components.join(""));
}
main().catch(e => {