Skip to content

Commit a7bf4de

Browse files
committed
feat: refactor API client generation and enhance type handling
This commit refactors the API client generation by removing the config parameter from the `generateApiClient` function, simplifying its signature. Additionally, it introduces a new utility function, `getTypeFromSchema`, which centralizes the logic for converting OpenAPI schema objects into TypeScript type strings, improving type safety and clarity in generated code. The schema generator and Axios method generation have been updated to utilize this new function for better type handling.
1 parent a1d3976 commit a7bf4de

File tree

4 files changed

+154
-101
lines changed

4 files changed

+154
-101
lines changed

src/generator/clientGenerator.ts

Lines changed: 12 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,12 @@
11
import type { OpenAPIV3 } from "openapi-types";
2-
import type { OpenAPIConfig } from "../types/config";
3-
import { camelCase, pascalCase, sanitizePropertyName, sanitizeTypeName, specTitle } from "../utils";
2+
import {
3+
camelCase,
4+
getTypeFromSchema,
5+
pascalCase,
6+
sanitizePropertyName,
7+
sanitizeTypeName,
8+
specTitle,
9+
} from "../utils";
410

511
export interface OperationInfo {
612
method: string;
@@ -93,11 +99,11 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
9399
// Add path and query parameters
94100
urlParams.forEach((p) => {
95101
const safeName = sanitizePropertyName(p.name);
96-
dataProps.push(`${safeName}: ${getTypeFromParam(p)}`);
102+
dataProps.push(`${safeName}: ${getTypeFromSchema(p.schema)}`);
97103
});
98104
queryParams.forEach((p) => {
99105
const safeName = sanitizePropertyName(p.name);
100-
dataProps.push(`${safeName}${p.required ? "" : "?"}: ${getTypeFromParam(p)}`);
106+
dataProps.push(`${safeName}${p.required ? "" : "?"}: ${getTypeFromSchema(p.schema)}`);
101107
});
102108

103109
// Add request body type if it exists
@@ -167,38 +173,17 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
167173
.join("\n ");
168174

169175
const requestParms = hasData
170-
? `props: ${pascalCase(operationId)}Params & { axiosConfig?: AxiosRequestConfig; }`
176+
? `props: T.${pascalCase(operationId)}Params & { axiosConfig?: AxiosRequestConfig; }`
171177
: "props?: { axiosConfig?: AxiosRequestConfig }";
172178

173179
return `
174-
${hasData ? `export type ${pascalCase(operationId)}Params = ${dataType};` : ""}
175180
${jsDocLines.join("\n ")}
176181
export async function ${camelCase(operationId)}(${requestParms}): Promise<${responseType}> {
177182
${methodBody}
178183
}`;
179184
}
180185

181-
function getTypeFromParam(param: OpenAPIV3.ParameterObject): string {
182-
if ("schema" in param) {
183-
const schema = param.schema as OpenAPIV3.SchemaObject;
184-
switch (schema.type) {
185-
case "string":
186-
return "string";
187-
case "integer":
188-
case "number":
189-
return "number";
190-
case "boolean":
191-
return "boolean";
192-
case "array":
193-
return "Array<any>"; // You might want to make this more specific
194-
default:
195-
return "any";
196-
}
197-
}
198-
return "any";
199-
}
200-
201-
export function generateApiClient(spec: OpenAPIV3.Document, config: OpenAPIConfig): string {
186+
export function generateApiClient(spec: OpenAPIV3.Document): string {
202187
const operations: OperationInfo[] = [];
203188

204189
const resolveParameters = (

src/generator/schemaGenerator.ts

Lines changed: 48 additions & 73 deletions
Original file line numberDiff line numberDiff line change
@@ -1,79 +1,17 @@
11
import type { OpenAPIV3 } from "openapi-types";
2-
import { sanitizePropertyName, sanitizeTypeName } from "../utils";
2+
import { getTypeFromSchema, pascalCase, sanitizePropertyName, sanitizeTypeName } from "../utils";
33

44
interface SchemaContext {
55
schemas: { [key: string]: OpenAPIV3.SchemaObject };
66
generatedTypes: Set<string>;
77
}
88

9-
/**
10-
* Converts OpenAPI schema type to TypeScript type
11-
*/
12-
function getTypeFromSchema(
13-
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
14-
context: SchemaContext
15-
): string {
16-
if (!schema) return "any";
17-
18-
if ("$ref" in schema) {
19-
const refType = schema.$ref.split("/").pop();
20-
return sanitizeTypeName(refType as string);
21-
}
22-
const nullable = schema.nullable ? " | null" : "";
23-
24-
// Handle enum types properly
25-
if (schema.enum) {
26-
return schema.enum.map((e) => (typeof e === "string" ? `'${e}'` : e)).join(" | ") + nullable;
27-
}
28-
29-
switch (schema.type) {
30-
case "string":
31-
if ("format" in schema && schema.format === "binary") {
32-
return `string | { name?: string; type?: string; uri: string }${nullable}`;
33-
}
34-
35-
return `string${nullable}`;
36-
case "number":
37-
case "integer":
38-
return `number${nullable}`;
39-
case "boolean":
40-
return `boolean${nullable}`;
41-
case "array": {
42-
const itemType = getTypeFromSchema(schema.items, context);
43-
return `Array<${itemType}>${nullable}`;
44-
}
45-
case "object":
46-
if (schema.properties) {
47-
const properties = Object.entries(schema.properties)
48-
.map(([key, prop]) => {
49-
const isRequired = schema.required?.includes(key);
50-
const propertyType = getTypeFromSchema(prop, context);
51-
const safeName = sanitizePropertyName(key);
52-
return ` ${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
53-
})
54-
.join("\n");
55-
return `{${properties}\n}${nullable}`;
56-
}
57-
if (schema.additionalProperties) {
58-
const valueType =
59-
typeof schema.additionalProperties === "boolean"
60-
? "any"
61-
: getTypeFromSchema(schema.additionalProperties, context);
62-
return `Record<string, ${valueType}>${nullable}`;
63-
}
64-
return `Record<string, any>${nullable}`;
65-
default:
66-
return `any${nullable}`;
67-
}
68-
}
69-
709
function generateTypeDefinition(
7110
name: string,
72-
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject,
73-
context: SchemaContext
11+
schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject
7412
): string {
7513
const description = !("$ref" in schema) && schema.description ? `/**\n * ${schema.description}\n */\n` : "";
76-
const typeValue = getTypeFromSchema(schema, context);
14+
const typeValue = getTypeFromSchema(schema);
7715

7816
// Use 'type' for primitives, unions, and simple types
7917
// Use 'interface' only for complex objects with properties
@@ -98,7 +36,7 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
9836
// Generate types for all schema definitions
9937
for (const [name, schema] of Object.entries(context.schemas)) {
10038
if (context.generatedTypes.has(name)) continue;
101-
output += generateTypeDefinition(name, schema, context);
39+
output += generateTypeDefinition(name, schema);
10240
context.generatedTypes.add(name);
10341
}
10442

@@ -110,36 +48,73 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
11048

11149
const operationObject = operation as OpenAPIV3.OperationObject;
11250
if (!operationObject) continue;
113-
const operationId = `${sanitizeTypeName(operationObject.operationId || `${path.replace(/\W+/g, "_")}`)}`;
51+
const { operationId: badOperationId, requestBody, responses, parameters } = operationObject;
52+
const operationId = `${sanitizeTypeName(badOperationId || `${path.replace(/\W+/g, "_")}`)}`;
11453

11554
// Generate request body type
116-
if (operationObject.requestBody) {
117-
const content = (operationObject.requestBody as OpenAPIV3.RequestBodyObject).content;
55+
if (requestBody) {
56+
const content = (requestBody as OpenAPIV3.RequestBodyObject).content;
11857
const jsonContent =
11958
content["application/ld+json"] ??
12059
content["application/json"] ??
12160
content["multipart/form-data"] ??
12261
content["application/octet-stream"];
12362
if (jsonContent?.schema) {
12463
const typeName = `${operationId}Request`;
125-
output += generateTypeDefinition(typeName, jsonContent.schema as OpenAPIV3.SchemaObject, context);
64+
output += generateTypeDefinition(typeName, jsonContent.schema as OpenAPIV3.SchemaObject);
12665
}
12766
}
12867

12968
// Generate response types
130-
if (operationObject.responses) {
131-
for (const [code, response] of Object.entries(operationObject.responses)) {
69+
if (responses) {
70+
for (const [code, response] of Object.entries(responses)) {
13271
const responseObj = response as OpenAPIV3.ResponseObject;
13372
const content =
13473
responseObj.content?.["application/ld+json"] ??
13574
responseObj.content?.["application/json"] ??
13675
responseObj.content?.["application/octet-stream"];
13776
if (content?.schema) {
13877
const typeName = `${operationId}Response${code}`;
139-
output += generateTypeDefinition(typeName, content.schema as OpenAPIV3.SchemaObject, context);
78+
output += generateTypeDefinition(typeName, content.schema as OpenAPIV3.SchemaObject);
14079
}
14180
}
14281
}
82+
83+
// Build data type parts
84+
const dataProps: string[] = [];
85+
86+
const urlParams = (parameters?.filter((p) => "in" in p && p.in === "path") ||
87+
[]) as OpenAPIV3.ParameterObject[];
88+
const queryParams = (parameters?.filter((p) => "in" in p && p.in === "query") ||
89+
[]) as OpenAPIV3.ParameterObject[];
90+
91+
// Add path and query parameters
92+
urlParams.forEach((p) => {
93+
const safeName = sanitizePropertyName(p.name);
94+
dataProps.push(`${safeName}: ${getTypeFromSchema(p.schema)}`);
95+
});
96+
queryParams.forEach((p) => {
97+
const safeName = sanitizePropertyName(p.name);
98+
dataProps.push(`${safeName}${p.required ? "" : "?"}: ${getTypeFromSchema(p.schema)}`);
99+
});
100+
101+
// Add request body type if it exists
102+
const hasData = (parameters && parameters.length > 0) || requestBody;
103+
104+
let dataType = "undefined";
105+
const namedType = pascalCase(operationId);
106+
if (hasData) {
107+
if (requestBody && dataProps.length > 0) {
108+
dataType = `${namedType}Request & { ${dataProps.join("; ")} }`;
109+
} else if (requestBody) {
110+
dataType = `${namedType}Request`;
111+
} else if (dataProps.length > 0) {
112+
dataType = `{ ${dataProps.join("; ")} }`;
113+
} else {
114+
dataType = "Record<string, never>";
115+
}
116+
output += `\n\nexport type ${pascalCase(operationId)}Params = ${dataType};\n\n`;
117+
}
143118
}
144119
}
145120
}

src/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ export async function codegenerate(config: OpenAPIConfig): Promise<void> {
6262
await writeFile(resolve(config.exportDir, `${title}.schema.ts`), typeDefinitions, "utf-8");
6363

6464
// Generate and write API client
65-
const clientCode = generateApiClient(spec, config);
65+
const clientCode = generateApiClient(spec);
6666
await writeFile(resolve(config.exportDir, `${title}.client.ts`), clientCode, "utf-8");
6767

6868
// Generate and write React Query options

src/utils.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -52,3 +52,96 @@ export function sanitizeTypeName(name: string): string {
5252
export function specTitle(spec: OpenAPIV3.Document): string {
5353
return camelCase(spec.info.title.toLowerCase().replace(/\s+/g, "-"));
5454
}
55+
56+
/**
57+
* Converts an OpenAPI schema object into a TypeScript type string.
58+
*
59+
* Handles:
60+
* - References ($ref) by extracting the type name
61+
* - Nullable types by appending "| null"
62+
* - Enums by creating union types of the values
63+
* - Basic types (string, number, boolean)
64+
* - Binary format strings as a union with file metadata object
65+
* - Arrays by recursively getting the item type
66+
* - Objects with properties by creating interfaces
67+
* - Objects with additionalProperties as Records
68+
* - Fallback to "any" for unknown types
69+
*
70+
* @param param - The OpenAPI schema/parameter object to convert
71+
* @returns The TypeScript type as a string
72+
*/
73+
export function getTypeFromSchema(
74+
schema: OpenAPIV3.ParameterObject | OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject | undefined
75+
): string | undefined {
76+
if (!schema) return undefined;
77+
// Handle $ref by extracting the referenced type name
78+
if ("$ref" in schema) {
79+
const refType = schema.$ref.split("/").pop();
80+
return sanitizeTypeName(refType as string);
81+
}
82+
83+
// Add "| null" for nullable types
84+
const nullable = "nullable" in schema && schema.nullable ? " | null" : "";
85+
86+
// Handle enums as union types
87+
if ("enum" in schema && schema.enum) {
88+
return schema.enum.map((e) => (typeof e === "string" ? `'${e}'` : e)).join(" | ") + nullable;
89+
}
90+
91+
// Handle types based on the "type" property
92+
if ("type" in schema) {
93+
switch (schema.type) {
94+
case "string":
95+
// Special case for binary format strings
96+
if ("format" in schema && schema.format === "binary") {
97+
return `string | { name?: string; type?: string; uri: string }${nullable}`;
98+
}
99+
return `string${nullable}`;
100+
101+
case "number":
102+
case "integer":
103+
return `number${nullable}`;
104+
105+
case "boolean":
106+
return `boolean${nullable}`;
107+
108+
case "array": {
109+
// Recursively get the array item type
110+
const itemType = getTypeFromSchema(schema.items);
111+
return `Array<${itemType}>${nullable}`;
112+
}
113+
114+
case "object":
115+
// Handle objects with defined properties
116+
if (schema.properties) {
117+
const properties = Object.entries(schema.properties)
118+
.map(([key, prop]) => {
119+
const isRequired = schema.required?.includes(key);
120+
const propertyType = getTypeFromSchema(prop);
121+
const safeName = sanitizePropertyName(key);
122+
return ` ${safeName}${isRequired ? "" : "?"}: ${propertyType};`;
123+
})
124+
.join("\n");
125+
return `{${properties}\n}${nullable}`;
126+
}
127+
128+
// Handle objects with additionalProperties
129+
if (schema.additionalProperties) {
130+
const valueType =
131+
typeof schema.additionalProperties === "boolean"
132+
? "any"
133+
: getTypeFromSchema(schema.additionalProperties);
134+
return `Record<string, ${valueType}>${nullable}`;
135+
}
136+
137+
// Default object type when no properties specified
138+
return `Record<string, any>${nullable}`;
139+
140+
default:
141+
return `any${nullable}`;
142+
}
143+
}
144+
145+
// Fallback for schemas without a type
146+
return "any";
147+
}

0 commit comments

Comments
 (0)