Skip to content

Commit 9aba9a6

Browse files
committed
feat: improve identifier sanitization across generators
1 parent bfee432 commit 9aba9a6

File tree

3 files changed

+54
-17
lines changed

3 files changed

+54
-17
lines changed

src/generator/clientGenerator.ts

Lines changed: 23 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ export interface OperationInfo {
1111
responses: OpenAPIV3.ResponsesObject;
1212
}
1313

14+
function sanitizeOperationId(operationId: string): string {
15+
return operationId.replace(/[^a-zA-Z0-9_]/g, '_');
16+
}
17+
18+
function sanitizePropertyName(name: string): string {
19+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
20+
}
21+
1422
function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document): string {
15-
const { method, path, operationId, summary, description, parameters, requestBody, responses } = operation;
23+
const { method, path, operationId: rawOperationId, summary, description, parameters, requestBody, responses } = operation;
24+
const operationId = rawOperationId || `${method.toLowerCase()}${path.replace(/\W+/g, '_')}`;
25+
const sanitizedOperationId = sanitizeOperationId(operationId);
1626

1727
// Generate JSDoc
1828
const jsDocLines = ['/**'];
@@ -36,7 +46,7 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
3646
const responseObj = response as OpenAPIV3.ResponseObject;
3747
const desc = 'description' in responseObj ? responseObj.description : '';
3848
const contentType = responseObj.content?.['application/json']?.schema;
39-
const typeName = `${operationId}Response${code}`;
49+
const typeName = `${sanitizedOperationId}Response${code}`;
4050

4151
if (contentType) {
4252
if (desc) {
@@ -68,23 +78,25 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
6878

6979
// Add path and query parameters
7080
urlParams.forEach(p => {
71-
dataProps.push(`${p.name}: ${getTypeFromParam(p)}`);
81+
const safeName = sanitizePropertyName(p.name);
82+
dataProps.push(`${safeName}: ${getTypeFromParam(p)}`);
7283
});
7384
queryParams.forEach(p => {
74-
dataProps.push(`${p.name}${p.required ? '' : '?'}: ${getTypeFromParam(p)}`);
85+
const safeName = sanitizePropertyName(p.name);
86+
dataProps.push(`${safeName}${p.required ? '' : '?'}: ${getTypeFromParam(p)}`);
7587
});
7688

7789
// Add request body type if it exists
7890
const hasData = (parameters && parameters.length > 0) || operation.requestBody;
7991
const dataType = hasData
8092
? requestBody
81-
? `${operationId}Request & { ${dataProps.join('; ')} }`
93+
? `${sanitizedOperationId}Request & { ${dataProps.join('; ')} }`
8294
: `{ ${dataProps.join('; ')} }`
8395
: 'undefined';
8496

8597
// Get response type from 2xx response
8698
const successResponse = Object.entries(responses).find(([code]) => code.startsWith('2'));
87-
const responseType = successResponse ? `${operationId}Response${successResponse[0]}` : 'any';
99+
const responseType = successResponse ? `${sanitizedOperationId}Response${successResponse[0]}` : 'any';
88100

89101
const urlWithParams = urlParams.length > 0
90102
? path.replace(/{(\w+)}/g, '${data.$1}')
@@ -116,7 +128,7 @@ function generateAxiosMethod(operation: OperationInfo, spec: OpenAPIV3.Document)
116128

117129
return `
118130
${jsDocLines.join('\n ')}
119-
async ${operationId}(data${hasData ? `: ${dataType}` : '?: undefined'}, headers?: Record<string, string>): Promise<AxiosResponse<${responseType}>> {
131+
async ${sanitizedOperationId}(data${hasData ? `: ${dataType}` : '?: undefined'}, headers?: Record<string, string>): Promise<AxiosResponse<${responseType}>> {
120132
${methodBody}
121133
}`;
122134
}
@@ -150,7 +162,7 @@ export function generateApiClient(spec: OpenAPIV3.Document): string {
150162
operations.push({
151163
method: method.toUpperCase(),
152164
path,
153-
operationId: operation.operationId || `${method}${path.replace(/\W+/g, '_')}`,
165+
operationId: sanitizeOperationId(operation.operationId || `${method}${path.replace(/\W+/g, '_')}`),
154166
summary: operation.summary,
155167
description: operation.description,
156168
parameters: [...(pathItem.parameters || []), ...(operation.parameters || [])] as OpenAPIV3.ParameterObject[],
@@ -174,11 +186,13 @@ export function generateApiClient(spec: OpenAPIV3.Document): string {
174186
}
175187
});
176188

189+
const title = spec.info.title.toLowerCase().replace(/\s+/g, '-');
190+
177191
// Generate the client class
178192
return `import axios, { AxiosInstance, AxiosResponse } from 'axios';
179193
import {
180194
${Array.from(usedTypes).join(',\n ')}
181-
} from './types';
195+
} from './${title}.schema';
182196
183197
export class ApiClient {
184198
private axios: AxiosInstance;

src/generator/reactQueryGenerator.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,17 @@
11
import { OpenAPIV3 } from 'openapi-types';
22
import { OperationInfo } from './clientGenerator';
33

4+
function sanitizeOperationId(operationId: string): string {
5+
return operationId.replace(/[^a-zA-Z0-9_]/g, '_');
6+
}
7+
8+
function sanitizePropertyName(name: string): string {
9+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
10+
}
11+
412
function generateQueryOptions(operation: OperationInfo, spec: OpenAPIV3.Document): string {
5-
const { operationId, parameters, requestBody } = operation;
13+
const { operationId: rawOperationId, parameters, requestBody } = operation;
14+
const operationId = sanitizeOperationId(rawOperationId);
615

716
const hasData = (parameters && parameters.length > 0) || operation.requestBody;
817

@@ -60,7 +69,7 @@ export function generateReactQuery(spec: OpenAPIV3.Document): string {
6069
operations.push({
6170
method: method.toUpperCase(),
6271
path,
63-
operationId: operation.operationId || `${method}${path.replace(/\W+/g, '_')}`,
72+
operationId: sanitizeOperationId(operation.operationId || `${method}${path.replace(/\W+/g, '_')}`),
6473
summary: operation.summary,
6574
description: operation.description,
6675
parameters: [...(pathItem.parameters || []), ...(operation.parameters || [])] as OpenAPIV3.ParameterObject[],
@@ -70,8 +79,10 @@ export function generateReactQuery(spec: OpenAPIV3.Document): string {
7079
});
7180
});
7281

82+
const title = spec.info.title.toLowerCase().replace(/\s+/g, '-');
83+
7384
return `import { queryOptions, skipToken } from '@tanstack/react-query';
74-
import type { ApiClient } from './client';
85+
import type { ApiClient } from './${title}.client';
7586
7687
const hasDefinedProps = <T extends { [P in K]?: any }, K extends PropertyKey>(
7788
obj: T,

src/generator/schemaGenerator.ts

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,17 @@ interface SchemaContext {
88
/**
99
* Converts OpenAPI schema type to TypeScript type
1010
*/
11+
function sanitizePropertyName(name: string): string {
12+
// If property name has special characters, wrap it in quotes
13+
return /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(name) ? name : `'${name}'`;
14+
}
15+
1116
function getTypeFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, context: SchemaContext): string {
1217
if (!schema) return 'any';
1318

1419
if ('$ref' in schema) {
1520
const refType = schema.$ref.split('/').pop()!;
16-
return refType;
21+
return sanitizeString(refType);
1722
}
1823

1924
// Handle enum types properly
@@ -38,7 +43,8 @@ function getTypeFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceO
3843
.map(([key, prop]) => {
3944
const isRequired = schema.required?.includes(key);
4045
const propertyType = getTypeFromSchema(prop as OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, context);
41-
return ` ${key}${isRequired ? '' : '?'}: ${propertyType};`;
46+
const safeName = sanitizePropertyName(key);
47+
return ` ${safeName}${isRequired ? '' : '?'}: ${propertyType};`;
4248
})
4349
.join('\n');
4450
return `{\n${properties}\n}`;
@@ -55,11 +61,13 @@ function getTypeFromSchema(schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceO
5561
}
5662
}
5763

58-
function generateTypeDefinition(name: string, schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, context: SchemaContext): string {
64+
function generateTypeDefinition(badName: string, schema: OpenAPIV3.SchemaObject | OpenAPIV3.ReferenceObject, context: SchemaContext): string {
5965
const description = !('$ref' in schema) && schema.description
6066
? `/**\n * ${schema.description}\n */\n`
6167
: '';
6268

69+
const name = sanitizeString(badName)
70+
6371
const typeValue = getTypeFromSchema(schema, context);
6472

6573
// Use 'type' for primitives, unions, and simple types
@@ -71,6 +79,10 @@ function generateTypeDefinition(name: string, schema: OpenAPIV3.SchemaObject | O
7179
: `${description}export type ${name} = ${typeValue}\n\n`;
7280
}
7381

82+
function sanitizeString(operationId: string): string {
83+
return operationId.replace(/[^a-zA-Z0-9_]/g, '_');
84+
}
85+
7486
/**
7587
* Generates TypeScript interface definitions from OpenAPI schemas
7688
*/
@@ -103,7 +115,7 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
103115
const content = (operationObject.requestBody as OpenAPIV3.RequestBodyObject).content;
104116
const jsonContent = content['application/json'] || content['multipart/form-data']
105117
if (jsonContent?.schema) {
106-
const typeName = `${operationObject.operationId}Request`;
118+
const typeName = sanitizeString(`${operationObject.operationId}Request`);
107119
output += generateTypeDefinition(typeName, jsonContent.schema as OpenAPIV3.SchemaObject, context);
108120
}
109121
}
@@ -114,7 +126,7 @@ export function generateTypeDefinitions(spec: OpenAPIV3.Document): string {
114126
const responseObj = response as OpenAPIV3.ResponseObject;
115127
const content = responseObj.content?.['application/json'];
116128
if (content?.schema) {
117-
const typeName = `${operationObject.operationId}Response${code}`;
129+
const typeName = sanitizeString(`${operationObject.operationId || `${method.toLowerCase()}${path.replace(/\W+/g, '_')}`}Response${code}`);
118130
output += generateTypeDefinition(typeName, content.schema as OpenAPIV3.SchemaObject, context);
119131
}
120132
}

0 commit comments

Comments
 (0)