| /*! |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */ |
| |
| const ts = require("typescript"); |
| const fs = require("fs"); |
| |
| /* This library does three things to make openapi-typescript generation easier to use. |
| * 1. Creates capitalized exports for Paths and Operations |
| * 2. Alias Variables based either on the Path name or the Operation ID, such as: |
| * export type ListProjectsVariables = operations['listProjects']['parameters']['query']; |
| * 3. Aliases the returned data types, such as: |
| * export type ConnectionArgument = components['schemas']['ConnectionArgument']; |
| */ |
| |
| /* Finds all words, capitalizes them, and removes all other characters. */ |
| const toPascalCase = (str) => |
| (str.match(/[a-zA-Z0-9]+/g) || []) |
| .map((w) => `${w.charAt(0).toUpperCase()}${w.slice(1)}`) |
| .join(""); |
| |
| /* Adds a prefix to a type prop as necessary. |
| * ('', 'components') => 'components' |
| */ |
| const prefixPath = (rootPrefix, prop) => |
| rootPrefix ? `${rootPrefix}['${prop}']` : prop; |
| |
| // Recursively find child nodes by name. |
| const findNode = (node, ...names) => { |
| if (!node || names.length === 0) return node; |
| |
| const children = node.members || node.type.members; |
| |
| if (!children) { |
| return undefined; |
| } |
| |
| const child = children.find((c) => c.name?.text === names[0]); |
| |
| return findNode(child, ...names.slice(1)); |
| }; |
| |
| // Generate Variable Type Aliases for a given path or operation |
| const generateVariableAliases = (node, operationPath, operationName) => { |
| const variableTypes = []; |
| const hasPath = !!findNode(node, "parameters", "path"); |
| const hasQuery = !!findNode(node, "parameters", "query"); |
| const hasBody = !!findNode( |
| node, |
| "requestBody", |
| "content", |
| "application/json" |
| ); |
| |
| if (hasPath) variableTypes.push(`${operationPath}['parameters']['path']`); |
| if (hasQuery) variableTypes.push(`${operationPath}['parameters']['query']`); |
| if (hasBody) |
| variableTypes.push( |
| `${operationPath}['requestBody']['content']['application/json']` |
| ); |
| |
| if (variableTypes.length === 0) return ""; |
| const typeName = `${toPascalCase(operationName)}Variables`; |
| return [ |
| typeName, |
| `export type ${typeName} = CamelCasedPropertiesDeep<${variableTypes.join( |
| " & " |
| )}>;`, |
| ]; |
| }; |
| |
| // Generate Type Aliases |
| const generateAliases = (rootNode, writeText, prefix = "") => { |
| // Loop through the root AST nodes of the file |
| ts.forEachChild(rootNode, (node) => { |
| // Response Data Types |
| if (ts.isInterfaceDeclaration(node) && node.name?.text === "components") { |
| const schemaMemberNames = findNode(node, "schemas").type.members.map( |
| (n) => n.name?.text |
| ); |
| |
| const types = schemaMemberNames.map((n) => [ |
| `${n}`, |
| `export type ${n} = CamelCasedPropertiesDeep<${prefixPath( |
| prefix, |
| "components" |
| )}['schemas']['${n}']>;`, |
| ]); |
| if (types.length) { |
| writeText.push(["comment", `Types for returned data ${prefix}`]); |
| writeText.push(...types); |
| } |
| } |
| |
| // Paths referencing an operation are skipped |
| if (node.name?.text === "paths") { |
| if (!prefix) { |
| writeText.push(["comment", "Alias paths to PascalCase."]); |
| writeText.push(["Paths", "export type Paths = paths;"]); |
| } |
| |
| const types = []; |
| |
| (node.members || node.type.members).forEach((path) => { |
| const methodNames = path.type.members.map((m) => m.name.text); |
| const methodTypes = methodNames.map((m) => |
| generateVariableAliases( |
| findNode(path, m), |
| `${prefixPath(prefix, "paths")}['${path.name?.text}']['${m}']`, |
| `${path.name.text}${toPascalCase(m)}` |
| ) |
| ); |
| types.push(...methodTypes.filter((m) => !!m)); |
| }); |
| |
| if (types.length) { |
| writeText.push([ |
| "comment", |
| `Types for path operation variables ${prefix}`, |
| ]); |
| writeText.push(...types); |
| } |
| } |
| |
| // operationIds are defined |
| if (node.name?.text === "operations") { |
| if (!prefix) { |
| writeText.push(["comment", "Alias operations to PascalCase."]); |
| writeText.push(["Operations", "export type Operations = operations;"]); |
| } |
| |
| const types = (node.members || node.type.members).map((operation) => |
| generateVariableAliases( |
| operation, |
| `${prefixPath(prefix, "operations")}['${operation.name.text}']`, |
| operation.name.text |
| ) |
| ); |
| if (types.length) { |
| writeText.push(["comment", `Types for operation variables ${prefix}`]); |
| writeText.push(...types); |
| writeText.push("\n"); |
| } |
| } |
| |
| // recursively call this for any externals |
| if (ts.isInterfaceDeclaration(node) && node.name?.text === "external") { |
| node.members.forEach((external) => { |
| generateAliases( |
| external.type, |
| writeText, |
| `external['${external.name.text}']` |
| ); |
| }); |
| } |
| }); |
| }; |
| |
| const license = `/*! |
| * Licensed to the Apache Software Foundation (ASF) under one |
| * or more contributor license agreements. See the NOTICE file |
| * distributed with this work for additional information |
| * regarding copyright ownership. The ASF licenses this file |
| * to you under the Apache License, Version 2.0 (the |
| * "License"); you may not use this file except in compliance |
| * with the License. You may obtain a copy of the License at |
| * |
| * http://www.apache.org/licenses/LICENSE-2.0 |
| * |
| * Unless required by applicable law or agreed to in writing, |
| * software distributed under the License is distributed on an |
| * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| * KIND, either express or implied. See the License for the |
| * specific language governing permissions and limitations |
| * under the License. |
| */`; |
| |
| function generate(file) { |
| // Create a Program to represent the project, then pull out the |
| // source file to parse its AST. |
| const program = ts.createProgram([file], { allowJs: true }); |
| const sourceFile = program.getSourceFile(file); |
| const writeText = []; |
| writeText.push(["block", license]); |
| writeText.push(["comment", "eslint-disable"]); |
| // eslint-disable-next-line quotes |
| writeText.push([ |
| "block", |
| `import type { CamelCasedPropertiesDeep } from 'type-fest';`, |
| ]); |
| writeText.push(["block", sourceFile.text]); |
| generateAliases(sourceFile, writeText); |
| |
| const finalText = writeText |
| // Deduplicate types |
| .map((pair) => { |
| // keep all comments and code blocks |
| if (pair[0] === "comment" || pair[0] === "block") return pair; |
| // return the first instance of this key only |
| const firstInstance = writeText.find((p) => p[0] === pair[0]); |
| return firstInstance === pair |
| ? pair |
| : ["comment", `Duplicate removed: ${pair[1]}`]; |
| }) |
| // Remove undefined created above |
| .filter((p) => !!p) |
| // Escape comments and flatten. |
| .map((pair) => (pair[0] === "comment" ? `\n/* ${pair[1]} */` : pair[1])) |
| .join("\n"); |
| |
| fs.writeFileSync(file, finalText, (err) => { |
| if (err) { |
| console.error(err); |
| } |
| }); |
| } |
| |
| generate(process.argv[2]); |