More updates

This commit is contained in:
Steve Faulkner 2020-07-23 11:17:20 -05:00
parent 9db8d11801
commit cfe9bd8303
3 changed files with 107 additions and 55 deletions

21
package-lock.json generated
View File

@ -7582,6 +7582,27 @@
"resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-12.11.1.tgz",
"integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A==" "integrity": "sha512-TJtwsqZ39pqcljJpajeoofYRfeZ7/I/OMUQ5pR4q5wOKf2ocrUvBAZUMhWsOvKx3dVc/aaV5GluBivt0sWqA5A=="
}, },
"@types/node-fetch": {
"version": "2.5.7",
"resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.5.7.tgz",
"integrity": "sha512-o2WVNf5UhWRkxlf6eq+jMZDu7kjgpgJfl4xVNlvryc95O/6F2ld8ztKX+qu+Rjyet93WAWm5LjeX9H5FGkODvw==",
"requires": {
"@types/node": "*",
"form-data": "^3.0.0"
},
"dependencies": {
"form-data": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.0.tgz",
"integrity": "sha512-CKMFDglpbMi6PyN+brwB9Q/GOw0eAnsrEZDgcsH5Krhz5Od/haKHAX0NmQfha2zPPz0JpWzA7GJHGSnvCRLWsg==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.8",
"mime-types": "^2.1.12"
}
}
}
},
"@types/normalize-package-data": { "@types/normalize-package-data": {
"version": "2.4.0", "version": "2.4.0",
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz", "resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.0.tgz",

View File

@ -34,6 +34,7 @@
"@nteract/transform-vega": "7.0.6", "@nteract/transform-vega": "7.0.6",
"@octokit/rest": "17.9.2", "@octokit/rest": "17.9.2",
"@phosphor/widgets": "1.9.3", "@phosphor/widgets": "1.9.3",
"@types/node-fetch": "2.5.7",
"@uifabric/react-cards": "0.109.110", "@uifabric/react-cards": "0.109.110",
"@uifabric/styling": "7.13.7", "@uifabric/styling": "7.13.7",
"abort-controller": "3.0.0", "abort-controller": "3.0.0",
@ -188,7 +189,8 @@
"build:contracts": "npm run compile:contracts", "build:contracts": "npm run compile:contracts",
"strictEligibleFiles": "node ./strict-migration-tools/index.js", "strictEligibleFiles": "node ./strict-migration-tools/index.js",
"autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js", "autoAddStrictEligibleFiles": "node ./strict-migration-tools/autoAdd.js",
"compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks" "compile:fullStrict": "tsc -p ./tsconfig.json --strictNullChecks",
"generateARMClient": "ts-node --compiler-options '{\"module\":\"commonjs\"}' utils/armClientGenerator/generator.ts"
}, },
"repository": { "repository": {
"type": "git", "type": "git",

View File

@ -1,33 +1,51 @@
/// <reference types="node" /> /// <reference types="node" />
import { writeFileSync } from "fs";
import fetch from "node-fetch";
const { writeFileSync } = require("fs"); /*
const schema = require("./schema.json"); Open API TypeScript Client Generator
const file: string[] = [""]; This is a quickly made bespoke Open API client generator.
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.
Results of this file should be checked into the repo.
*/
// Array of strings to use for eventual output
const output: 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";
// Buckets for grouping operations based on their name
const namespaces: { [key: string]: string[] } = {}; const namespaces: { [key: string]: string[] } = {};
// Mapping for OpenAPI types to TypeScript types
const propertyMap: { [key: string]: string } = { const propertyMap: { [key: string]: string } = {
integer: "number" integer: "number"
}; };
// Converts a Open API reference: "#/definitions/Foo" to a type name: Foo
function refToType(path: string | undefined) { function refToType(path: string | undefined) {
// Handles refs pointing to other files. We don't support that yet. // References must be in the same file. Bail to `unknown` types for remote references
if (path && path.startsWith("#")) { if (path && path.startsWith("#")) {
return path.split("/").pop(); return path.split("/").pop();
} }
return "unknown"; return "unknown";
} }
// Converts "Something_Foo" -> "somethingFoo"
function camelize(str: string) { function camelize(str: string) {
return str return str
.replace(/(?:^\w|[A-Z]|\b\w)/g, function(word: any, index: any) { .replace(/(?:^\w|[A-Z]|\b\w)/g, function(word: string, index: number) {
return index === 0 ? word.toLowerCase() : word.toUpperCase(); return index === 0 ? word.toLowerCase() : word.toUpperCase();
}) })
.replace(/\s+/g, ""); .replace(/\s+/g, "");
} }
function bodyParam(parameter: any) { // Converts a body paramter to the equivalent typescript function parameter type
function bodyParam(parameter: { schema: { $ref: string } }) {
if (!parameter) { if (!parameter) {
return ""; return "";
} }
@ -35,18 +53,22 @@ function bodyParam(parameter: any) {
} }
// eslint-disable-next-line @typescript-eslint/no-explicit-any // eslint-disable-next-line @typescript-eslint/no-explicit-any
function parametersFromPath(path: any) { function parametersFromPath(path: string) {
// TODO: Remove any. String.matchAll is a real thing. // TODO: Remove any. String.matchAll is a real thing.
const matches = path.matchAll(/{(\w+)}/g); // eslint-disable-next-line @typescript-eslint/no-explicit-any
const matches = (path as any).matchAll(/{(\w+)}/g);
return Array.from(matches) return Array.from(matches)
.map((match: string[]) => `${match[1]}: string`) .map((match: string[]) => `${match[1]}: string`)
.join(",\n"); .join(",\n");
} }
function responseType(operation: any) { 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) {
if (operation.responses) { if (operation.responses) {
return Object.keys(operation.responses) return Object.keys(operation.responses)
.map((responseCode: any) => { .map((responseCode: string) => {
if (!operation.responses[responseCode].schema) { if (!operation.responses[responseCode].schema) {
return "void"; return "void";
} }
@ -58,40 +80,44 @@ function responseType(operation: any) {
} }
async function main() { async function main() {
for (const interface in schema.definitions) { const response = await fetch(schemaURL);
const properties = schema.definitions[interface].properties; const schema = await response.json();
// STEP 1: Convert all definitions to TypeScript types and interfaces
for (const definition in schema.definitions) {
const properties = schema.definitions[definition].properties;
if (properties) { if (properties) {
if (schema.definitions[interface].allOf) { if (schema.definitions[definition].allOf) {
const baseTypes = schema.definitions[interface].allOf const baseTypes = schema.definitions[definition].allOf
.map((allof: { $ref: string }) => refToType(allof.$ref)) .map((allof: { $ref: string }) => refToType(allof.$ref))
.join(" & "); .join(" & ");
file.push(`type ${interface} = ${baseTypes} & {`); output.push(`type ${definition} = ${baseTypes} & {`);
} else { } else {
file.push(`interface ${interface} {`); output.push(`interface ${definition} {`);
} }
for (const prop in schema.definitions[interface].properties) { for (const prop in schema.definitions[definition].properties) {
const property = schema.definitions[interface].properties[prop]; const property = schema.definitions[definition].properties[prop];
if (property) { if (property) {
if (property.$ref) { if (property.$ref) {
const type = refToType(property.$ref); const type = refToType(property.$ref);
file.push(` output.push(`
/* ${property.description} */ /* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${type} ${property.readOnly ? "readonly " : ""}${prop}: ${type}
`); `);
} else if (property.type === "array") { } else if (property.type === "array") {
const type = refToType(property.items.$ref); const type = refToType(property.items.$ref);
file.push(` output.push(`
/* ${property.description} */ /* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${type}[] ${property.readOnly ? "readonly " : ""}${prop}: ${type}[]
`); `);
} else if (property.type === "object") { } else if (property.type === "object") {
const type = refToType(property.$ref); const type = refToType(property.$ref);
file.push(` output.push(`
/* ${property.description} */ /* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${type} ${property.readOnly ? "readonly " : ""}${prop}: ${type}
`); `);
} else { } else {
file.push(` output.push(`
/* ${property.description} */ /* ${property.description} */
${property.readOnly ? "readonly " : ""}${prop}: ${ ${property.readOnly ? "readonly " : ""}${prop}: ${
propertyMap[property.type] ? propertyMap[property.type] : property.type propertyMap[property.type] ? propertyMap[property.type] : property.type
@ -99,48 +125,50 @@ async function main() {
} }
} }
} }
file.push(`}`); output.push(`}`);
file.push("\n\n"); output.push("\n\n");
} else { } else {
const definition = schema.definitions[interface]; const def = schema.definitions[definition];
if (definition.enum) { if (def.enum) {
file.push(` output.push(`
/* ${definition.description} */ /* ${def.description} */
type ${interface} = ${definition.enum.map((v: string) => `"${v}"`).join(" | ")}`); type ${definition} = ${def.enum.map((v: string) => `"${v}"`).join(" | ")}`);
file.push("\n"); output.push("\n");
} else if (definition.type === "string") { } else if (def.type === "string") {
file.push(` output.push(`
/* ${definition.description} */ /* ${def.description} */
type ${interface} = string type ${definition} = string
`); `);
} else if (definition.type === "array") { } else if (def.type === "array") {
const type = refToType(definition.items.$ref); const type = refToType(def.items.$ref);
file.push(` output.push(`
/* ${definition.description} */ /* ${def.description} */
type ${interface} = ${type}[] type ${definition} = ${type}[]
`); `);
} else if (definition.type === "object" && definition.additionalProperties) { } else if (def.type === "object" && def.additionalProperties) {
file.push(` output.push(`
/* ${definition.description} */ /* ${def.description} */
type ${interface} = { [key: string]: ${definition.additionalProperties.type}} type ${definition} = { [key: string]: ${def.additionalProperties.type}}
`); `);
} else if (definition.type === "object" && definition.allOf) { } else if (def.type === "object" && def.allOf) {
const type = refToType(definition.allOf[0].$ref); const type = refToType(def.allOf[0].$ref);
file.push(` output.push(`
/* ${definition.description} */ /* ${def.description} */
type ${interface} = ${type} type ${definition} = ${type}
`); `);
} else { } else {
console.log("UNHANDLED MODEL:", interface, schema.definitions[interface]); console.log("UNHANDLED MODEL:", def, schema.definitions[def]);
} }
} }
} }
// STEP 2: Convert all paths and operations to simple fetch functions.
// Functions are grouped into objects based on resource types
for (const path in schema.paths) { for (const path in schema.paths) {
for (const method in schema.paths[path]) { for (const method in schema.paths[path]) {
const operation = schema.paths[path][method]; const operation = schema.paths[path][method];
const bodyParameter = operation.parameters.find( const bodyParameter = operation.parameters.find(
(parameter: any) => parameter.in === "body" && parameter.required === true (parameter: { in: string; required: boolean }) => parameter.in === "body" && parameter.required === true
); );
const [namespace, operationName] = operation.operationId.split("_"); const [namespace, operationName] = operation.operationId.split("_");
if (namespaces[namespace] === undefined) { if (namespaces[namespace] === undefined) {
@ -160,13 +188,14 @@ async function main() {
} }
} }
// Write all grouped fetch functions to objects
for (const namespace in namespaces) { for (const namespace in namespaces) {
file.push(`export const ${namespace} = {`); output.push(`export const ${namespace} = {`);
file.push(namespaces[namespace].join(",\n")); output.push(namespaces[namespace].join(",\n"));
file.push(`}\n`); output.push(`}\n`);
} }
writeFileSync("./models.ts", file.join("")); writeFileSync("./client.ts", output.join(""));
} }
main().catch(e => { main().catch(e => {