typed-openapi 1.5.0 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/generator.ts CHANGED
@@ -6,10 +6,28 @@ import * as Codegen from "@sinclair/typebox-codegen";
6
6
  import { match } from "ts-pattern";
7
7
  import { type } from "arktype";
8
8
  import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
9
+ import type { NameTransformOptions } from "./types.ts";
9
10
 
10
- type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
11
+ // Default success status codes (2xx and 3xx ranges)
12
+ export const DEFAULT_SUCCESS_STATUS_CODES = [
13
+ 200, 201, 202, 203, 204, 205, 206, 207, 208, 226, 300, 301, 302, 303, 304, 305, 306, 307, 308,
14
+ ] as const;
15
+
16
+ // Default error status codes (4xx and 5xx ranges)
17
+ export const DEFAULT_ERROR_STATUS_CODES = [
18
+ 400, 401, 402, 403, 404, 405, 406, 407, 408, 409, 410, 411, 412, 413, 414, 415, 416, 417, 418, 421, 422, 423, 424,
19
+ 425, 426, 428, 429, 431, 451, 500, 501, 502, 503, 504, 505, 506, 507, 508, 510, 511,
20
+ ] as const;
21
+
22
+ export type ErrorStatusCode = (typeof DEFAULT_ERROR_STATUS_CODES)[number];
23
+
24
+ export type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
11
25
  runtime?: "none" | keyof typeof runtimeValidationGenerator;
12
26
  schemasOnly?: boolean;
27
+ nameTransform?: NameTransformOptions | undefined;
28
+ successStatusCodes?: readonly number[];
29
+ errorStatusCodes?: readonly number[];
30
+ includeClient?: boolean;
13
31
  };
14
32
  type GeneratorContext = Required<GeneratorOptions>;
15
33
 
@@ -60,11 +78,18 @@ const replacerByRuntime = {
60
78
  };
61
79
 
62
80
  export const generateFile = (options: GeneratorOptions) => {
63
- const ctx = { ...options, runtime: options.runtime ?? "none" } as GeneratorContext;
81
+ const ctx = {
82
+ ...options,
83
+ runtime: options.runtime ?? "none",
84
+ successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES,
85
+ errorStatusCodes: options.errorStatusCodes ?? DEFAULT_ERROR_STATUS_CODES,
86
+ includeClient: options.includeClient ?? true,
87
+ } as GeneratorContext;
64
88
 
65
89
  const schemaList = generateSchemaList(ctx);
66
90
  const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
67
- const apiClient = options.schemasOnly ? "" : generateApiClient(ctx);
91
+ const endpointByMethod = options.schemasOnly ? "" : generateEndpointByMethod(ctx);
92
+ const apiClient = options.schemasOnly || !ctx.includeClient ? "" : generateApiClient(ctx);
68
93
 
69
94
  const transform =
70
95
  ctx.runtime === "none"
@@ -96,6 +121,7 @@ export const generateFile = (options: GeneratorOptions) => {
96
121
 
97
122
  const file = `
98
123
  ${transform(schemaList + endpointSchemaList)}
124
+ ${endpointByMethod}
99
125
  ${apiClient}
100
126
  `;
101
127
 
@@ -151,6 +177,24 @@ const responseHeadersObjectToString = (responseHeaders: Record<string, AnyBox>,
151
177
  return str + "}";
152
178
  };
153
179
 
180
+ const generateResponsesObject = (responses: Record<string, AnyBox>, ctx: GeneratorContext) => {
181
+ let str = "{";
182
+ for (const [statusCode, responseType] of Object.entries(responses)) {
183
+ const value =
184
+ ctx.runtime === "none"
185
+ ? responseType.recompute((box) => {
186
+ if (Box.isReference(box) && !box.params.generics && box.value !== "null") {
187
+ box.value = `Schemas.${box.value}`;
188
+ }
189
+
190
+ return box;
191
+ }).value
192
+ : responseType.value;
193
+ str += `${wrapWithQuotesIfNeeded(statusCode)}: ${value},\n`;
194
+ }
195
+ return str + "}";
196
+ };
197
+
154
198
  const generateEndpointSchemaList = (ctx: GeneratorContext) => {
155
199
  let file = `
156
200
  ${ctx.runtime === "none" ? "export namespace Endpoints {" : ""}
@@ -197,6 +241,7 @@ const generateEndpointSchemaList = (ctx: GeneratorContext) => {
197
241
  }).value
198
242
  : endpoint.response.value
199
243
  },
244
+ ${endpoint.responses ? `responses: ${generateResponsesObject(endpoint.responses, ctx)},` : ""}
200
245
  ${
201
246
  endpoint.responseHeaders
202
247
  ? `responseHeaders: ${responseHeadersObjectToString(endpoint.responseHeaders, ctx)},`
@@ -225,9 +270,11 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
225
270
  ${Object.entries(byMethods)
226
271
  .map(([method, list]) => {
227
272
  return `${method}: {
228
- ${list.map(
229
- (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
230
- )}
273
+ ${list
274
+ .map(
275
+ (endpoint) => `"${endpoint.path}": ${ctx.runtime === "none" ? "Endpoints." : ""}${endpoint.meta.alias}`,
276
+ )
277
+ .join(",\n")}
231
278
  }`;
232
279
  })
233
280
  .join(",\n")}
@@ -249,9 +296,12 @@ const generateEndpointByMethod = (ctx: GeneratorContext) => {
249
296
  };
250
297
 
251
298
  const generateApiClient = (ctx: GeneratorContext) => {
299
+ if (!ctx.includeClient) {
300
+ return "";
301
+ }
302
+
252
303
  const { endpointList } = ctx;
253
304
  const byMethods = groupBy(endpointList, "method");
254
- const endpointSchemaList = generateEndpointByMethod(ctx);
255
305
 
256
306
  const apiClientTypes = `
257
307
  // <ApiClientTypes>
@@ -270,6 +320,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
270
320
  export type DefaultEndpoint = {
271
321
  parameters?: EndpointParameters | undefined;
272
322
  response: unknown;
323
+ responses?: Record<string, unknown>;
273
324
  responseHeaders?: Record<string, unknown>;
274
325
  };
275
326
 
@@ -285,11 +336,64 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
285
336
  areParametersRequired: boolean;
286
337
  };
287
338
  response: TConfig["response"];
339
+ responses?: TConfig["responses"];
288
340
  responseHeaders?: TConfig["responseHeaders"]
289
341
  };
290
342
 
291
343
  export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
292
344
 
345
+ export const successStatusCodes = [${ctx.successStatusCodes.join(",")}] as const;
346
+ export type SuccessStatusCode = typeof successStatusCodes[number];
347
+
348
+ export const errorStatusCodes = [${ctx.errorStatusCodes.join(",")}] as const;
349
+ export type ErrorStatusCode = typeof errorStatusCodes[number];
350
+
351
+ // Error handling types
352
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
353
+ interface SuccessResponse<TSuccess, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
354
+ ok: true;
355
+ status: TStatusCode;
356
+ data: TSuccess;
357
+ /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
358
+ json: () => Promise<TSuccess>;
359
+ }
360
+
361
+ /** @see https://developer.mozilla.org/en-US/docs/Web/API/Response */
362
+ interface ErrorResponse<TData, TStatusCode> extends Omit<Response, "ok" | "status" | "json"> {
363
+ ok: false;
364
+ status: TStatusCode;
365
+ data: TData;
366
+ /** [MDN Reference](https://developer.mozilla.org/en-US/docs/Web/API/Response/json) */
367
+ json: () => Promise<TData>;
368
+ }
369
+
370
+ export type TypedApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
371
+ (keyof TAllResponses extends never
372
+ ? SuccessResponse<TSuccess, number>
373
+ : {
374
+ [K in keyof TAllResponses]: K extends string
375
+ ? K extends \`\${infer TStatusCode extends number}\`
376
+ ? TStatusCode extends SuccessStatusCode
377
+ ? SuccessResponse<TSuccess, TStatusCode>
378
+ : ErrorResponse<TAllResponses[K], TStatusCode>
379
+ : never
380
+ : K extends number
381
+ ? K extends SuccessStatusCode
382
+ ? SuccessResponse<TSuccess, K>
383
+ : ErrorResponse<TAllResponses[K], K>
384
+ : never;
385
+ }[keyof TAllResponses]);
386
+
387
+ export type SafeApiResponse<TEndpoint> = TEndpoint extends { response: infer TSuccess; responses: infer TResponses }
388
+ ? TResponses extends Record<string, unknown>
389
+ ? TypedApiResponse<TSuccess, TResponses>
390
+ : SuccessResponse<TSuccess, number>
391
+ : TEndpoint extends { response: infer TSuccess }
392
+ ? SuccessResponse<TSuccess, number>
393
+ : never;
394
+
395
+ export type InferResponseByStatus<TEndpoint, TStatusCode> = Extract<SafeApiResponse<TEndpoint>, { status: TStatusCode }>
396
+
293
397
  type RequiredKeys<T> = {
294
398
  [P in keyof T]-?: undefined extends T[P] ? never : P;
295
399
  }[keyof T];
@@ -300,9 +404,23 @@ type MaybeOptionalArg<T> = RequiredKeys<T> extends never ? [config?: T] : [confi
300
404
  `;
301
405
 
302
406
  const apiClient = `
407
+ // <TypedResponseError>
408
+ export class TypedResponseError extends Error {
409
+ response: ErrorResponse<unknown, ErrorStatusCode>;
410
+ status: number;
411
+ constructor(response: ErrorResponse<unknown, ErrorStatusCode>) {
412
+ super(\`HTTP \${response.status}: \${response.statusText}\`);
413
+ this.name = 'TypedResponseError';
414
+ this.response = response;
415
+ this.status = response.status;
416
+ }
417
+ }
418
+ // </TypedResponseError>
303
419
  // <ApiClient>
304
420
  export class ApiClient {
305
421
  baseUrl: string = "";
422
+ successStatusCodes = successStatusCodes;
423
+ errorStatusCodes = errorStatusCodes;
306
424
 
307
425
  constructor(public fetcher: Fetcher) {}
308
426
 
@@ -331,16 +449,53 @@ export class ApiClient {
331
449
  ...params: MaybeOptionalArg<${match(ctx.runtime)
332
450
  .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
333
451
  .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
334
- .otherwise(() => `TEndpoint["parameters"]`)}>
452
+ .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse?: false; throwOnStatusError?: boolean }>
335
453
  ): Promise<${match(ctx.runtime)
336
454
  .with("zod", "yup", () => infer(`TEndpoint["response"]`))
337
455
  .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["response"]`)
338
- .otherwise(() => `TEndpoint["response"]`)}> {
339
- return this.fetcher("${method}", this.baseUrl + path, params[0])
340
- .then(response => this.parseResponse(response))${match(ctx.runtime)
456
+ .otherwise(() => `TEndpoint["response"]`)}>;
457
+
458
+ ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
459
+ path: Path,
460
+ ...params: MaybeOptionalArg<${match(ctx.runtime)
461
+ .with("zod", "yup", () => infer(`TEndpoint["parameters"]`))
462
+ .with("arktype", "io-ts", "typebox", "valibot", () => infer(`TEndpoint`) + `["parameters"]`)
463
+ .otherwise(() => `TEndpoint["parameters"]`)} & { withResponse: true; throwOnStatusError?: boolean }>
464
+ ): Promise<SafeApiResponse<TEndpoint>>;
465
+
466
+ ${method}<Path extends keyof ${capitalizedMethod}Endpoints, TEndpoint extends ${capitalizedMethod}Endpoints[Path]>(
467
+ path: Path,
468
+ ...params: MaybeOptionalArg<any>
469
+ ): Promise<any> {
470
+ const requestParams = params[0];
471
+ const withResponse = requestParams?.withResponse;
472
+ const { withResponse: _, throwOnStatusError = withResponse ? false : true, ...fetchParams } = requestParams || {};
473
+
474
+ const promise = this.fetcher("${method}", this.baseUrl + path, Object.keys(fetchParams).length ? requestParams : undefined)
475
+ .then(async (response) => {
476
+ const data = await this.parseResponse(response);
477
+ const typedResponse = Object.assign(response, {
478
+ data: data,
479
+ json: () => Promise.resolve(data)
480
+ }) as SafeApiResponse<TEndpoint>;
481
+
482
+ if (throwOnStatusError && errorStatusCodes.includes(response.status as never)) {
483
+ throw new TypedResponseError(typedResponse as never);
484
+ }
485
+
486
+ return withResponse ? typedResponse : data;
487
+ });
488
+
489
+ return promise ${match(ctx.runtime)
341
490
  .with("zod", "yup", () => `as Promise<${infer(`TEndpoint["response"]`)}>`)
342
- .with("arktype", "io-ts", "typebox", "valibot", () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`)
343
- .otherwise(() => `as Promise<TEndpoint["response"]>`)};
491
+ .with(
492
+ "arktype",
493
+ "io-ts",
494
+ "typebox",
495
+ "valibot",
496
+ () => `as Promise<${infer(`TEndpoint`) + `["response"]`}>`,
497
+ )
498
+ .otherwise(() => `as Promise<TEndpoint["response"]>`)}
344
499
  }
345
500
  // </ApiClient.${method}>
346
501
  `
@@ -371,11 +526,8 @@ export class ApiClient {
371
526
  () => inferByRuntime[ctx.runtime](`TEndpoint`) + `["parameters"]`,
372
527
  )
373
528
  .otherwise(() => `TEndpoint extends { parameters: infer Params } ? Params : never`)}>)
374
- : Promise<Omit<Response, "json"> & {
375
- /** [MDN Reference](https://developer.mozilla.org/docs/Web/API/Request/json) */
376
- json: () => Promise<TEndpoint extends { response: infer Res } ? Res : never>;
377
- }> {
378
- return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters);
529
+ : Promise<SafeApiResponse<TEndpoint>> {
530
+ return this.fetcher(method, this.baseUrl + (path as string), params[0] as EndpointParameters) as Promise<SafeApiResponse<TEndpoint>>;
379
531
  }
380
532
  // </ApiClient.request>
381
533
  }
@@ -393,10 +545,25 @@ export function createApiClient(fetcher: Fetcher, baseUrl?: string) {
393
545
  api.get("/users").then((users) => console.log(users));
394
546
  api.post("/users", { body: { name: "John" } }).then((user) => console.log(user));
395
547
  api.put("/users/:id", { path: { id: 1 }, body: { name: "John" } }).then((user) => console.log(user));
548
+
549
+ // With error handling
550
+ const result = await api.get("/users/{id}", { path: { id: "123" }, withResponse: true });
551
+ if (result.ok) {
552
+ // Access data directly
553
+ const user = result.data;
554
+ console.log(user);
555
+
556
+ // Or use the json() method for compatibility
557
+ const userFromJson = await result.json();
558
+ console.log(userFromJson);
559
+ } else {
560
+ const error = result.data;
561
+ console.error(\`Error \${result.status}:\`, error);
562
+ }
396
563
  */
397
564
 
398
- // </ApiClient
565
+ // </ApiClient>
399
566
  `;
400
567
 
401
- return endpointSchemaList + apiClientTypes + apiClient;
568
+ return apiClientTypes + apiClient;
402
569
  };
@@ -8,11 +8,13 @@ import { createRefResolver } from "./ref-resolver.ts";
8
8
  import { tsFactory } from "./ts-factory.ts";
9
9
  import { AnyBox, BoxRef, OpenapiSchemaConvertContext } from "./types.ts";
10
10
  import { pathToVariableName } from "./string-utils.ts";
11
+ import { NameTransformOptions } from "./types.ts";
11
12
  import { match, P } from "ts-pattern";
13
+ import { sanitizeName } from "./sanitize-name.ts";
12
14
 
13
15
  const factory = tsFactory;
14
16
 
15
- export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
17
+ export const mapOpenApiEndpoints = (doc: OpenAPIObject, options?: { nameTransform?: NameTransformOptions }) => {
16
18
  const refs = createRefResolver(doc, factory);
17
19
  const ctx: OpenapiSchemaConvertContext = { refs, factory };
18
20
  const endpointList = [] as Array<Endpoint>;
@@ -22,6 +24,10 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
22
24
  Object.entries(pathItem).forEach(([method, operation]) => {
23
25
  if (operation.deprecated) return;
24
26
 
27
+ let alias = getAlias({ path, method, operation } as Endpoint);
28
+ if (options?.nameTransform?.transformEndpointName) {
29
+ alias = options.nameTransform.transformEndpointName({ alias, path, method: method as Method, operation });
30
+ }
25
31
  const endpoint = {
26
32
  operation,
27
33
  method: method as Method,
@@ -29,7 +35,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
29
35
  requestFormat: "json",
30
36
  response: openApiSchemaToTs({ schema: {}, ctx }),
31
37
  meta: {
32
- alias: getAlias({ path, method, operation } as Endpoint),
38
+ alias,
33
39
  areParametersRequired: false,
34
40
  hasParameters: false,
35
41
  },
@@ -84,7 +90,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
84
90
 
85
91
  if (matchingMediaType && content[matchingMediaType]) {
86
92
  params.body = openApiSchemaToTs({
87
- schema: content[matchingMediaType]?.schema ?? {} ?? {},
93
+ schema: content[matchingMediaType]?.schema ?? {},
88
94
  ctx,
89
95
  });
90
96
  }
@@ -124,14 +130,56 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
124
130
 
125
131
  // Match the first 2xx-3xx response found, or fallback to default one otherwise
126
132
  let responseObject: ResponseObject | undefined;
133
+ const allResponses: Record<string, AnyBox> = {};
134
+
127
135
  Object.entries(operation.responses ?? {}).map(([status, responseOrRef]) => {
128
136
  const statusCode = Number(status);
129
- if (statusCode >= 200 && statusCode < 300) {
130
- responseObject = refs.unwrap<ResponseObject>(responseOrRef);
137
+ const responseObj = refs.unwrap<ResponseObject>(responseOrRef);
138
+
139
+ // Collect all responses for error handling
140
+ const content = responseObj?.content;
141
+ if (content) {
142
+ const matchingMediaType = Object.keys(content).find(isResponseMediaType);
143
+ if (matchingMediaType && content[matchingMediaType]) {
144
+ allResponses[status] = openApiSchemaToTs({
145
+ schema: content[matchingMediaType]?.schema ?? {},
146
+ ctx,
147
+ });
148
+ } else {
149
+ // If no JSON content, use unknown type
150
+ allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
151
+ }
152
+ } else {
153
+ // If no content defined, use unknown type
154
+ allResponses[status] = openApiSchemaToTs({ schema: {}, ctx });
155
+ }
156
+
157
+ // Keep the current logic for the main response (first 2xx-3xx)
158
+ if (statusCode >= 200 && statusCode < 300 && !responseObject) {
159
+ responseObject = responseObj;
131
160
  }
132
161
  });
162
+
133
163
  if (!responseObject && operation.responses?.default) {
134
164
  responseObject = refs.unwrap(operation.responses.default);
165
+ // Also add default to all responses if not already covered
166
+ if (!allResponses["default"]) {
167
+ const content = responseObject?.content;
168
+ if (content) {
169
+ const matchingMediaType = Object.keys(content).find(isResponseMediaType);
170
+ if (matchingMediaType && content[matchingMediaType]) {
171
+ allResponses["default"] = openApiSchemaToTs({
172
+ schema: content[matchingMediaType]?.schema ?? {},
173
+ ctx,
174
+ });
175
+ }
176
+ }
177
+ }
178
+ }
179
+
180
+ // Set the responses collection
181
+ if (Object.keys(allResponses).length > 0) {
182
+ endpoint.responses = allResponses;
135
183
  }
136
184
 
137
185
  const content = responseObject?.content;
@@ -139,7 +187,7 @@ export const mapOpenApiEndpoints = (doc: OpenAPIObject) => {
139
187
  const matchingMediaType = Object.keys(content).find(isResponseMediaType);
140
188
  if (matchingMediaType && content[matchingMediaType]) {
141
189
  endpoint.response = openApiSchemaToTs({
142
- schema: content[matchingMediaType]?.schema ?? {} ?? {},
190
+ schema: content[matchingMediaType]?.schema ?? {},
143
191
  ctx,
144
192
  });
145
193
  }
@@ -180,10 +228,13 @@ const isAllowedParamMediaTypes = (
180
228
 
181
229
  const isResponseMediaType = (mediaType: string) => mediaType === "application/json";
182
230
  const getAlias = ({ path, method, operation }: Endpoint) =>
183
- (method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__");
231
+ sanitizeName(
232
+ (method + "_" + capitalize(operation.operationId ?? pathToVariableName(path))).replace(/-/g, "__"),
233
+ "endpoint",
234
+ );
184
235
 
185
236
  type MutationMethod = "post" | "put" | "patch" | "delete";
186
- type Method = "get" | "head" | "options" | MutationMethod;
237
+ export type Method = "get" | "head" | "options" | MutationMethod;
187
238
 
188
239
  export type EndpointParameters = {
189
240
  body?: Box<BoxRef>;
@@ -197,6 +248,7 @@ type RequestFormat = "json" | "form-data" | "form-url" | "binary" | "text";
197
248
  type DefaultEndpoint = {
198
249
  parameters?: EndpointParameters | undefined;
199
250
  response: AnyBox;
251
+ responses?: Record<string, AnyBox>;
200
252
  responseHeaders?: Record<string, AnyBox>;
201
253
  };
202
254
 
@@ -212,5 +264,6 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
212
264
  areParametersRequired: boolean;
213
265
  };
214
266
  response: TConfig["response"];
267
+ responses?: TConfig["responses"];
215
268
  responseHeaders?: TConfig["responseHeaders"];
216
269
  };
@@ -1,2 +1 @@
1
- export { prettify } from "./format.ts";
2
1
  export { generateClientFiles } from "./generate-client-files.ts";
@@ -0,0 +1 @@
1
+ export { prettify } from "./format.ts";
@@ -5,8 +5,10 @@ import { Box } from "./box.ts";
5
5
  import { isReferenceObject } from "./is-reference-object.ts";
6
6
  import { openApiSchemaToTs } from "./openapi-schema-to-ts.ts";
7
7
  import { normalizeString } from "./string-utils.ts";
8
+ import { NameTransformOptions } from "./types.ts";
8
9
  import { AnyBoxDef, GenericFactory, type LibSchemaObject } from "./types.ts";
9
10
  import { topologicalSort } from "./topological-sort.ts";
11
+ import { sanitizeName } from "./sanitize-name.ts";
10
12
 
11
13
  const autocorrectRef = (ref: string) => (ref[1] === "/" ? ref : "#/" + ref.slice(1));
12
14
  const componentsWithSchemas = ["schemas", "responses", "parameters", "requestBodies", "headers"];
@@ -26,7 +28,11 @@ export type RefInfo = {
26
28
  kind: "schemas" | "responses" | "parameters" | "requestBodies" | "headers";
27
29
  };
28
30
 
29
- export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) => {
31
+ export const createRefResolver = (
32
+ doc: OpenAPIObject,
33
+ factory: GenericFactory,
34
+ nameTransform?: NameTransformOptions,
35
+ ) => {
30
36
  // both used for debugging purpose
31
37
  const nameByRef = new Map<string, string>();
32
38
  const refByName = new Map<string, string>();
@@ -48,7 +54,11 @@ export const createRefResolver = (doc: OpenAPIObject, factory: GenericFactory) =
48
54
 
49
55
  // "#/components/schemas/Something.jsonld" -> "Something.jsonld"
50
56
  const name = split[split.length - 1]!;
51
- const normalized = normalizeString(name);
57
+ let normalized = normalizeString(name);
58
+ if (nameTransform?.transformSchemaName) {
59
+ normalized = nameTransform.transformSchemaName(normalized);
60
+ }
61
+ normalized = sanitizeName(normalized, "schema");
52
62
 
53
63
  nameByRef.set(correctRef, normalized);
54
64
  refByName.set(normalized, correctRef);
@@ -0,0 +1,79 @@
1
+ const reservedWords = new Set([
2
+ // TS keywords and built-ins
3
+ "import",
4
+ "package",
5
+ "namespace",
6
+ "Record",
7
+ "Partial",
8
+ "Required",
9
+ "Readonly",
10
+ "Pick",
11
+ "Omit",
12
+ "String",
13
+ "Number",
14
+ "Boolean",
15
+ "Object",
16
+ "Array",
17
+ "Function",
18
+ "any",
19
+ "unknown",
20
+ "never",
21
+ "void",
22
+ "extends",
23
+ "super",
24
+ "class",
25
+ "interface",
26
+ "type",
27
+ "enum",
28
+ "const",
29
+ "let",
30
+ "var",
31
+ "if",
32
+ "else",
33
+ "for",
34
+ "while",
35
+ "do",
36
+ "switch",
37
+ "case",
38
+ "default",
39
+ "break",
40
+ "continue",
41
+ "return",
42
+ "try",
43
+ "catch",
44
+ "finally",
45
+ "throw",
46
+ "new",
47
+ "delete",
48
+ "in",
49
+ "instanceof",
50
+ "typeof",
51
+ "void",
52
+ "with",
53
+ "yield",
54
+ "await",
55
+ "static",
56
+ "public",
57
+ "private",
58
+ "protected",
59
+ "abstract",
60
+ "as",
61
+ "asserts",
62
+ "from",
63
+ "get",
64
+ "set",
65
+ "module",
66
+ "require",
67
+ "keyof",
68
+ "readonly",
69
+ "global",
70
+ "symbol",
71
+ "bigint",
72
+ ]);
73
+
74
+ export function sanitizeName(name: string, type: "schema" | "endpoint") {
75
+ let n = name.replace(/[\W/]+/g, "_");
76
+ if (/^\d/.test(n)) n = "_" + n;
77
+ if (reservedWords.has(n)) n = (type === "schema" ? "Schema_" : "Endpoint_") + n;
78
+ return n;
79
+ }