Skip to content

Commit 6085140

Browse files
committed
feat: configurable status codes
1 parent 2ff5756 commit 6085140

24 files changed

+742
-1038
lines changed

packages/typed-openapi/src/generator.ts

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,16 @@ import { match } from "ts-pattern";
77
import { type } from "arktype";
88
import { wrapWithQuotesIfNeeded } from "./string-utils.ts";
99

10+
// Default success status codes (2xx and 3xx ranges)
11+
export const DEFAULT_SUCCESS_STATUS_CODES = [
12+
200, 201, 202, 203, 204, 205, 206, 207, 208, 226,
13+
300, 301, 302, 303, 304, 305, 306, 307, 308
14+
] as const;
15+
1016
type GeneratorOptions = ReturnType<typeof mapOpenApiEndpoints> & {
1117
runtime?: "none" | keyof typeof runtimeValidationGenerator;
1218
schemasOnly?: boolean;
19+
successStatusCodes?: readonly number[];
1320
};
1421
type GeneratorContext = Required<GeneratorOptions>;
1522

@@ -60,7 +67,11 @@ const replacerByRuntime = {
6067
};
6168

6269
export const generateFile = (options: GeneratorOptions) => {
63-
const ctx = { ...options, runtime: options.runtime ?? "none" } as GeneratorContext;
70+
const ctx = {
71+
...options,
72+
runtime: options.runtime ?? "none",
73+
successStatusCodes: options.successStatusCodes ?? DEFAULT_SUCCESS_STATUS_CODES
74+
} as GeneratorContext;
6475

6576
const schemaList = generateSchemaList(ctx);
6677
const endpointSchemaList = options.schemasOnly ? "" : generateEndpointSchemaList(ctx);
@@ -276,6 +287,13 @@ const generateApiClient = (ctx: GeneratorContext) => {
276287
const byMethods = groupBy(endpointList, "method");
277288
const endpointSchemaList = generateEndpointByMethod(ctx);
278289

290+
// Generate the StatusCode type from the configured success status codes
291+
const generateStatusCodeType = (statusCodes: readonly number[]) => {
292+
return statusCodes.join(" | ");
293+
};
294+
295+
const statusCodeType = generateStatusCodeType(ctx.successStatusCodes);
296+
279297
const apiClientTypes = `
280298
// <ApiClientTypes>
281299
export type EndpointParameters = {
@@ -315,6 +333,9 @@ export type Endpoint<TConfig extends DefaultEndpoint = DefaultEndpoint> = {
315333
316334
export type Fetcher = (method: Method, url: string, parameters?: EndpointParameters | undefined) => Promise<Response>;
317335
336+
// Status code type for success responses
337+
export type StatusCode = ${statusCodeType};
338+
318339
// Error handling types
319340
export type ApiResponse<TSuccess, TAllResponses extends Record<string | number, unknown> = {}> =
320341
(keyof TAllResponses extends never
@@ -325,21 +346,21 @@ export type ApiResponse<TSuccess, TAllResponses extends Record<string | number,
325346
}
326347
: {
327348
[K in keyof TAllResponses]: K extends string
328-
? K extends \`\${infer StatusCode extends number}\`
329-
? StatusCode extends 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308
349+
? K extends \`\${infer TStatusCode extends number}\`
350+
? TStatusCode extends StatusCode
330351
? {
331352
ok: true;
332-
status: StatusCode;
353+
status: TStatusCode;
333354
data: TAllResponses[K];
334355
}
335356
: {
336357
ok: false;
337-
status: StatusCode;
358+
status: TStatusCode;
338359
error: TAllResponses[K];
339360
}
340361
: never
341362
: K extends number
342-
? K extends 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 226 | 300 | 301 | 302 | 303 | 304 | 305 | 306 | 307 | 308
363+
? K extends StatusCode
343364
? {
344365
ok: true;
345366
status: K;
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
import { it, expect } from "vitest";
2+
import type { OpenAPIObject } from "openapi3-ts/oas31";
3+
4+
import { generateFile } from "../src/generator.ts";
5+
import { mapOpenApiEndpoints } from "../src/map-openapi-endpoints.ts";
6+
import { prettify } from "../src/format.ts";
7+
8+
it("should use custom success status codes", async () => {
9+
const openApiDoc: OpenAPIObject = {
10+
openapi: "3.0.0",
11+
info: { title: "Test API", version: "1.0.0" },
12+
paths: {
13+
"/test": {
14+
get: {
15+
operationId: "getTest",
16+
responses: {
17+
200: {
18+
description: "Success",
19+
content: {
20+
"application/json": {
21+
schema: { type: "object", properties: { message: { type: "string" } } }
22+
}
23+
}
24+
},
25+
201: {
26+
description: "Created",
27+
content: {
28+
"application/json": {
29+
schema: { type: "object", properties: { id: { type: "string" } } }
30+
}
31+
}
32+
},
33+
400: {
34+
description: "Bad Request",
35+
content: {
36+
"application/json": {
37+
schema: { type: "object", properties: { error: { type: "string" } } }
38+
}
39+
}
40+
}
41+
}
42+
}
43+
}
44+
}
45+
};
46+
47+
const endpoints = mapOpenApiEndpoints(openApiDoc);
48+
49+
// Test with default success status codes (should include 200 and 201)
50+
const defaultGenerated = await prettify(generateFile(endpoints));
51+
expect(defaultGenerated).toContain("export type StatusCode =");
52+
expect(defaultGenerated).toContain("| 200");
53+
expect(defaultGenerated).toContain("| 201");
54+
55+
// Test with custom success status codes (only 200)
56+
const customGenerated = await prettify(generateFile({
57+
...endpoints,
58+
successStatusCodes: [200] as const
59+
}));
60+
61+
// Should only contain 200 in the StatusCode type
62+
expect(customGenerated).toContain("export type StatusCode = 200;");
63+
expect(customGenerated).not.toContain("| 201");
64+
65+
// The ApiResponse type should use the custom StatusCode
66+
expect(customGenerated).toContain("TStatusCode extends StatusCode");
67+
});

0 commit comments

Comments
 (0)