From 629fa55d55bf665fc835ce41d0d34c60880250ef Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 26 Jan 2026 19:12:57 +0900 Subject: [PATCH 1/2] Gen interface issue --- .../changepack_log_4Fh3KV4lEgHOoBe4DZe3r.json | 1 + .../generate-interface.test.ts.snap | 418 ++++++++++-------- .../src/__tests__/generate-interface.test.ts | 105 +++++ .../wrap-interface-key-guard.test.ts | 48 +- packages/generator/src/generate-interface.ts | 175 ++++++-- packages/generator/src/generate-schema.ts | 93 +++- .../generator/src/wrap-interface-key-guard.ts | 13 +- 7 files changed, 585 insertions(+), 268 deletions(-) create mode 100644 .changepacks/changepack_log_4Fh3KV4lEgHOoBe4DZe3r.json diff --git a/.changepacks/changepack_log_4Fh3KV4lEgHOoBe4DZe3r.json b/.changepacks/changepack_log_4Fh3KV4lEgHOoBe4DZe3r.json new file mode 100644 index 0000000..234ff7b --- /dev/null +++ b/.changepacks/changepack_log_4Fh3KV4lEgHOoBe4DZe3r.json @@ -0,0 +1 @@ +{"changes":{"packages/generator/package.json":"Patch"},"note":"Fix gen interface issue","date":"2026-01-26T10:12:20.057935400Z"} \ No newline at end of file diff --git a/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap b/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap index dfb40b3..6a2ad19 100644 --- a/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap +++ b/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap @@ -6,12 +6,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: {}; + 'openapi.json': { + '/users': {}; getUsers: {}; } } @@ -30,12 +30,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: Array['User']>; }; getUsers: { @@ -45,8 +45,8 @@ declare module "@devup-api/fetch" { } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: DevupObject<'response', 'openapi.json'>['User']; }; createUser: { @@ -58,7 +58,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; name?: string; @@ -78,12 +78,12 @@ declare module "@devup-api/fetch" { type ResponseStatus = "active" | "inactive" interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: { name?: string | null; age?: number | null; @@ -124,12 +124,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: { name?: string | null; age?: number | null; @@ -160,12 +160,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: DevupObject<'response', 'openapi.json'>['User']; }; getUsers: { @@ -177,7 +177,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; nickname?: string | null; @@ -196,12 +196,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; getUsers: { @@ -211,8 +211,8 @@ declare module "@devup-api/fetch" { } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; createUser: { @@ -222,8 +222,8 @@ declare module "@devup-api/fetch" { } interface DevupPutApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; updateUser: { @@ -233,15 +233,15 @@ declare module "@devup-api/fetch" { } interface DevupDeleteApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: {}; + 'openapi.json': { + '/users': {}; deleteUser: {}; } } interface DevupPatchApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; patchUser: { @@ -264,12 +264,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users/{userId}\`]: { + 'openapi.json': { + '/users/{userId}': { params: { userId: string; }; @@ -298,12 +298,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { query: { page?: number; limit: number; @@ -334,12 +334,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users/{userId}/posts\`]: { + 'openapi.json': { + '/users/{userId}/posts': { params: { userId: string; }; @@ -374,12 +374,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body?: { name?: string; email?: string; @@ -410,12 +410,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body: DevupObject<'request', 'openapi.json'>['User']; response?: {}; }; @@ -427,7 +427,7 @@ declare module "@devup-api/fetch" { } interface DevupRequestComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { name?: string; email?: string; @@ -447,12 +447,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: Array['User']>; }; getUsers: { @@ -464,7 +464,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; name?: string; @@ -482,12 +482,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: DevupObject<'response', 'openapi.json'>['User']; }; createUser: { @@ -499,7 +499,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; name?: string; @@ -517,12 +517,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: { message?: string; }; @@ -549,12 +549,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; error?: { error?: string; @@ -574,7 +574,7 @@ declare module "@devup-api/fetch" { interface DevupResponseComponentStruct {} interface DevupErrorComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { Error: { code?: string; message?: string; @@ -590,12 +590,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; error?: { error?: string; @@ -624,12 +624,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body: DevupObject<'request', 'openapi.json'>['CreateUserRequest']; response?: {}; }; @@ -641,7 +641,7 @@ declare module "@devup-api/fetch" { } interface DevupRequestComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { CreateUserRequest: { name?: string; email?: string; @@ -661,12 +661,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: DevupObject<'response', 'openapi.json'>['User']; }; getUsers: { @@ -678,7 +678,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; name?: string; @@ -696,12 +696,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; error: DevupObject<'error', 'openapi.json'>['Error']; }; @@ -717,7 +717,7 @@ declare module "@devup-api/fetch" { interface DevupResponseComponentStruct {} interface DevupErrorComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { Error: { code?: string; message?: string; @@ -733,12 +733,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: Array['User']>; }; getUsers: { @@ -750,7 +750,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; name?: string; @@ -768,12 +768,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users/{userId}\`]: { + 'openapi.json': { + '/users/{userId}': { params: { userId: string; }; @@ -802,12 +802,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; } @@ -827,12 +827,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body?: { name?: string; email?: string; @@ -863,12 +863,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body?: { name?: string; email?: string; @@ -899,12 +899,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: { id: string; name?: string; @@ -933,12 +933,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: { id?: string; name?: string; @@ -967,22 +967,18 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { - response: { - id?: string; -} & { + 'openapi.json': { + '/users': { + response: DevupObject<'response', 'openapi.json'>['Base'] & { extra?: string; }; }; getUsers: { - response: { - id?: string; -} & { + response: DevupObject<'response', 'openapi.json'>['Base'] & { extra?: string; }; }; @@ -992,7 +988,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { Base: { id?: string; }; @@ -1009,7 +1005,7 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupRequestComponentStruct {} @@ -1026,12 +1022,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users/{userId}\`]: { + 'openapi.json': { + '/users/{userId}': { params: { userId: string; }; @@ -1060,12 +1056,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body?: { name?: string; }; @@ -1094,12 +1090,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: {}; + 'openapi.json': { + '/users': {}; getUsers: {}; } } @@ -1118,12 +1114,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users/{userId}/posts/{postId}\`]: { + 'openapi.json': { + '/users/{userId}/posts/{postId}': { params: { userId: string; postId: string; @@ -1149,8 +1145,8 @@ declare module "@devup-api/fetch" { } interface DevupPutApiStruct { - [\`openapi.json\`]: { - [\`/users/{userId}/posts/{postId}\`]: { + 'openapi.json': { + '/users/{userId}/posts/{postId}': { params: { userId: string; }; @@ -1168,7 +1164,7 @@ declare module "@devup-api/fetch" { } interface DevupRequestComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { UpdatePostRequest: { title?: string; content?: string; @@ -1177,7 +1173,7 @@ declare module "@devup-api/fetch" { } interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { Post: { id?: string; title?: string; @@ -1187,7 +1183,7 @@ declare module "@devup-api/fetch" { } interface DevupErrorComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { Error: { code?: string; message?: string; @@ -1203,26 +1199,16 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { - response: ({ - id?: string; -} | { - id?: string; - role?: string; -}); + 'openapi.json': { + '/users': { + response: (DevupObject<'response', 'openapi.json'>['User'] | DevupObject<'response', 'openapi.json'>['Admin']); }; getUsers: { - response: ({ - id?: string; -} | { - id?: string; - role?: string; -}); + response: (DevupObject<'response', 'openapi.json'>['User'] | DevupObject<'response', 'openapi.json'>['Admin']); }; } } @@ -1230,7 +1216,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; }; @@ -1251,24 +1237,16 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { - response: ({ - id?: string; -} | { - name?: string; -}); + 'openapi.json': { + '/users': { + response: (DevupObject<'response', 'openapi.json'>['User'] | DevupObject<'response', 'openapi.json'>['Guest']); }; getUsers: { - response: ({ - id?: string; -} | { - name?: string; -}); + response: (DevupObject<'response', 'openapi.json'>['User'] | DevupObject<'response', 'openapi.json'>['Guest']); }; } } @@ -1276,7 +1254,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { User: { id?: string; }; @@ -1296,12 +1274,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body: unknown; response?: {}; }; @@ -1313,7 +1291,7 @@ declare module "@devup-api/fetch" { } interface DevupRequestComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { CreateUserRequest: { name?: string; }; @@ -1332,12 +1310,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: {}; + 'openapi.json': { + '/users': {}; getUsers: {}; } } @@ -1345,7 +1323,7 @@ declare module "@devup-api/fetch" { interface DevupRequestComponentStruct {} interface DevupResponseComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { UserResponse: { id?: string; }; @@ -1362,12 +1340,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupPostApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { body: unknown; response?: {}; }; @@ -1392,12 +1370,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: unknown; }; getUsers: { @@ -1420,12 +1398,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; error: unknown; }; @@ -1450,12 +1428,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response: Array; }; getUsers: { @@ -1478,12 +1456,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; error: Array; }; @@ -1508,12 +1486,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; error: Array['Error']>; }; @@ -1529,7 +1507,7 @@ declare module "@devup-api/fetch" { interface DevupResponseComponentStruct {} interface DevupErrorComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { Error: { code?: string; message?: string; @@ -1545,12 +1523,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; getUsers: { @@ -1573,12 +1551,12 @@ import type { DevupObject } from "@devup-api/fetch"; declare module "@devup-api/fetch" { interface DevupApiServers { - [\`openapi.json\`]: never + 'openapi.json': never } interface DevupGetApiStruct { - [\`openapi.json\`]: { - [\`/users\`]: { + 'openapi.json': { + '/users': { response?: {}; }; getUsers: { @@ -1592,7 +1570,7 @@ declare module "@devup-api/fetch" { interface DevupResponseComponentStruct {} interface DevupErrorComponentStruct { - [\`openapi.json\`]: { + 'openapi.json': { ServerError: { error?: string; }; @@ -1600,3 +1578,63 @@ declare module "@devup-api/fetch" { } }" `; + +exports[`generateInterface handles inline response with nested $ref containing enums 1`] = ` +"import "@devup-api/fetch"; +import type { DevupObject } from "@devup-api/fetch"; + +declare module "@devup-api/fetch" { + type ItemStatus = "draft" | "published" | "archived" + type OtherStatus = "pending" | "approved" | "rejected" + + interface DevupApiServers { + 'openapi.json': never + } + + interface DevupGetApiStruct { + 'openapi.json': { + '/items': { + response?: { + items?: Array['ItemResponse']>; + page?: number; + }; + }; + listItems: { + response?: { + items?: Array['ItemResponse']>; + page?: number; + }; + }; + '/other-items': { + response?: { + items?: Array['OtherItemResponse']>; + page?: number; + }; + }; + listOtherItems: { + response?: { + items?: Array['OtherItemResponse']>; + page?: number; + }; + }; + } + } + + interface DevupRequestComponentStruct {} + + interface DevupResponseComponentStruct { + 'openapi.json': { + ItemResponse: { + id?: number; + status?: ItemStatus; + }; + OtherItemResponse: { + id?: number; + status?: OtherStatus; + }; + } + } + + interface DevupErrorComponentStruct {} +}" +`; diff --git a/packages/generator/src/__tests__/generate-interface.test.ts b/packages/generator/src/__tests__/generate-interface.test.ts index b0f8b6e..269cd64 100644 --- a/packages/generator/src/__tests__/generate-interface.test.ts +++ b/packages/generator/src/__tests__/generate-interface.test.ts @@ -1794,3 +1794,108 @@ test('generateInterface handles error response $ref that extracts schema name', generateInterface(createSchemas(createDocument(schema as any))), ).toMatchSnapshot() }) + +// Test inline response with nested $ref containing enums +// This ensures enum names are derived from the referenced schema, not context path +test('generateInterface handles inline response with nested $ref containing enums', () => { + const schema = { + paths: { + '/items': { + get: { + operationId: 'listItems', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + $ref: '#/components/schemas/ItemResponse', + }, + }, + page: { type: 'number' }, + }, + }, + }, + }, + }, + }, + }, + }, + '/other-items': { + get: { + operationId: 'listOtherItems', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + items: { + type: 'array', + items: { + $ref: '#/components/schemas/OtherItemResponse', + }, + }, + page: { type: 'number' }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + ItemResponse: { + type: 'object', + properties: { + id: { type: 'number' }, + status: { + $ref: '#/components/schemas/ItemStatus', + }, + }, + }, + OtherItemResponse: { + type: 'object', + properties: { + id: { type: 'number' }, + status: { + $ref: '#/components/schemas/OtherStatus', + }, + }, + }, + ItemStatus: { + type: 'string', + enum: ['draft', 'published', 'archived'], + }, + OtherStatus: { + type: 'string', + enum: ['pending', 'approved', 'rejected'], + }, + }, + }, + } + const result = generateInterface(createSchemas(createDocument(schema as any))) + expect(result).toMatchSnapshot() + // Verify enum names are derived from schema names, not context path + expect(result).toContain('type ItemStatus =') + expect(result).toContain('type OtherStatus =') + // Should NOT contain ResponseItemsStatus (wrong context-based name) + expect(result).not.toContain('type ResponseItemsStatus =') + // Verify that nested $ref uses DevupObject reference instead of inline expansion + expect(result).toContain( + "DevupObject<'response', 'openapi.json'>['ItemResponse']", + ) + expect(result).toContain( + "DevupObject<'response', 'openapi.json'>['OtherItemResponse']", + ) +}) diff --git a/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts b/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts index a822f1f..29c3774 100644 --- a/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts +++ b/packages/generator/src/__tests__/wrap-interface-key-guard.test.ts @@ -15,43 +15,43 @@ test.each([ }) test.each([ - ['/users', '[`/users`]'], - ['/users/{id}', '[`/users/{id}`]'], - ['/api/v1/users', '[`/api/v1/users`]'], - ['/users/{userId}/posts/{postId}', '[`/users/{userId}/posts/{postId}`]'], - ['/api/v1/users/{id}/profile', '[`/api/v1/users/{id}/profile`]'], -] as const)('wrapInterfaceKeyGuard wraps key with backticks when slash present: %s -> %s', (key, expected) => { + ['/users', "'/users'"], + ['/users/{id}', "'/users/{id}'"], + ['/api/v1/users', "'/api/v1/users'"], + ['/users/{userId}/posts/{postId}', "'/users/{userId}/posts/{postId}'"], + ['/api/v1/users/{id}/profile', "'/api/v1/users/{id}/profile'"], +] as const)('wrapInterfaceKeyGuard wraps key with quotes when slash present: %s -> %s', (key, expected) => { expect(wrapInterfaceKeyGuard(key)).toBe(expected) }) test.each([ ['', ''], - ['/', '[`/`]'], - ['//', '[`//`]'], - ['///', '[`///`]'], + ['/', "'/'"], + ['//', "'//'"], + ['///', "'///'"], ] as const)('wrapInterfaceKeyGuard handles edge cases: %s -> %s', (key, expected) => { expect(wrapInterfaceKeyGuard(key)).toBe(expected) }) test.each([ - ['users/123', '[`users/123`]'], - ['test/path/here', '[`test/path/here`]'], - ['a/b/c/d', '[`a/b/c/d`]'], + ['users/123', "'users/123'"], + ['test/path/here', "'test/path/here'"], + ['a/b/c/d', "'a/b/c/d'"], ] as const)('wrapInterfaceKeyGuard wraps key with multiple slashes: %s -> %s', (key, expected) => { expect(wrapInterfaceKeyGuard(key)).toBe(expected) }) test.each([ - ['field"name', '[`field"name`]'], - ["field'name", "[`field'name`]"], - ['field`name', '[`field`name`]'], - ['field-name', '[`field-name`]'], - ['field name', '[`field name`]'], - ['field@name', '[`field@name`]'], - ['field#name', '[`field#name`]'], + ['field"name', `'field"name'`], + ["field'name", `'field\\'name'`], // single quote needs escaping + ['field`name', "'field`name'"], + ['field-name', "'field-name'"], + ['field name', "'field name'"], + ['field@name', "'field@name'"], + ['field#name', "'field#name'"], ['field$name', 'field$name'], // $ is valid in identifiers ['field_name', 'field_name'], // _ is valid in identifiers - ['123field', '[`123field`]'], // cannot start with number + ['123field', "'123field'"], // cannot start with number ] as const)('wrapInterfaceKeyGuard wraps key with forbidden characters: %s -> %s', (key, expected) => { expect(wrapInterfaceKeyGuard(key)).toBe(expected) }) @@ -60,10 +60,10 @@ test.each([ ['name?', 'name?'], // valid identifier with optional marker ['email?', 'email?'], // valid identifier with optional marker ['field_name?', 'field_name?'], // valid identifier with optional marker - ['field-name?', '[`field-name`]?'], // invalid identifier with optional marker - ['field name?', '[`field name`]?'], // invalid identifier with optional marker - ['/users?', '[`/users`]?'], // path with optional marker - ['123field?', '[`123field`]?'], // starts with number, with optional marker + ['field-name?', "'field-name'?"], // invalid identifier with optional marker + ['field name?', "'field name'?"], // invalid identifier with optional marker + ['/users?', "'/users'?"], // path with optional marker + ['123field?', "'123field'?"], // starts with number, with optional marker ] as const)('wrapInterfaceKeyGuard handles optional keys (ending with ?): %s -> %s', (key, expected) => { expect(wrapInterfaceKeyGuard(key)).toBe(expected) }) diff --git a/packages/generator/src/generate-interface.ts b/packages/generator/src/generate-interface.ts index de63a34..df7383e 100644 --- a/packages/generator/src/generate-interface.ts +++ b/packages/generator/src/generate-interface.ts @@ -69,47 +69,70 @@ function generateSchemaInterface( const convertCaseType = options?.convertCase ?? 'camel' // Helper function to collect schema names from a schema object + // Recursively traverses into referenced schemas to find all nested $refs const collectSchemaNames = ( schemaObj: OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, targetSet: Set, + visited: Set = new Set(), ): void => { if ('$ref' in schemaObj) { const schemaName = extractSchemaNameFromRef(schemaObj.$ref) if (schemaName) { + // Avoid infinite recursion for circular references + if (visited.has(schemaName)) { + return + } targetSet.add(schemaName) + visited.add(schemaName) + + // Recursively collect from the referenced schema + const referencedSchema = schema.components?.schemas?.[schemaName] + if (referencedSchema) { + collectSchemaNames( + referencedSchema as + | OpenAPIV3_1.SchemaObject + | OpenAPIV3_1.ReferenceObject, + targetSet, + visited, + ) + } } return } - const schema = schemaObj as OpenAPIV3_1.SchemaObject + const schemaObjTyped = schemaObj as OpenAPIV3_1.SchemaObject // Check allOf, anyOf, oneOf - if (schema.allOf) { - schema.allOf.forEach((s) => { - collectSchemaNames(s, targetSet) + if (schemaObjTyped.allOf) { + schemaObjTyped.allOf.forEach((s) => { + collectSchemaNames(s, targetSet, visited) }) } - if (schema.anyOf) { - schema.anyOf.forEach((s) => { - collectSchemaNames(s, targetSet) + if (schemaObjTyped.anyOf) { + schemaObjTyped.anyOf.forEach((s) => { + collectSchemaNames(s, targetSet, visited) }) } - if (schema.oneOf) { - schema.oneOf.forEach((s) => { - collectSchemaNames(s, targetSet) + if (schemaObjTyped.oneOf) { + schemaObjTyped.oneOf.forEach((s) => { + collectSchemaNames(s, targetSet, visited) }) } // Check properties - if (schema.properties) { - Object.values(schema.properties).forEach((prop) => { - collectSchemaNames(prop, targetSet) + if (schemaObjTyped.properties) { + Object.values(schemaObjTyped.properties).forEach((prop) => { + collectSchemaNames(prop, targetSet, visited) }) } // Check items (for arrays) - if (schema.type === 'array' && 'items' in schema && schema.items) { - collectSchemaNames(schema.items, targetSet) + if ( + schemaObjTyped.type === 'array' && + 'items' in schemaObjTyped && + schemaObjTyped.items + ) { + collectSchemaNames(schemaObjTyped.items, targetSet, visited) } } @@ -333,6 +356,9 @@ function generateSchemaInterface( { defaultNonNullable: responseDefaultNonNullable, context: inlineContext, + serverName, + componentType: 'response', + usedSchemaNames: responseSchemaNames, }, ) // Merge enums @@ -374,6 +400,9 @@ function generateSchemaInterface( { defaultNonNullable: responseDefaultNonNullable, context: inlineContext, + serverName, + componentType: 'response', + usedSchemaNames: responseSchemaNames, }, ) // Merge enums @@ -395,6 +424,9 @@ function generateSchemaInterface( { defaultNonNullable: responseDefaultNonNullable, context: inlineContext, + serverName, + componentType: 'response', + usedSchemaNames: responseSchemaNames, }, ) // Merge enums @@ -467,6 +499,9 @@ function generateSchemaInterface( { defaultNonNullable: responseDefaultNonNullable, context: inlineContext, + serverName, + componentType: 'error', + usedSchemaNames: errorSchemaNames, }, ) // Merge enums @@ -508,6 +543,9 @@ function generateSchemaInterface( { defaultNonNullable: responseDefaultNonNullable, context: inlineContext, + serverName, + componentType: 'error', + usedSchemaNames: errorSchemaNames, }, ) // Merge enums @@ -529,6 +567,9 @@ function generateSchemaInterface( { defaultNonNullable: responseDefaultNonNullable, context: inlineContext, + serverName, + componentType: 'error', + usedSchemaNames: errorSchemaNames, }, ) // Merge enums @@ -568,49 +609,103 @@ function generateSchemaInterface( } // Extract components schemas + // Generate separately for each context (request/response/error) to: + // 1. Apply correct defaultNonNullable per context + // 2. Use appropriate usedSchemaNames for nested $ref resolution const requestComponents: Record = {} const responseComponents: Record = {} const errorComponents: Record = {} if (schema.components?.schemas) { + const requestDefaultNonNullable = + options?.requestDefaultNonNullable ?? false + const responseDefaultNonNullable = + options?.responseDefaultNonNullable ?? true + for (const [schemaName, schemaObj] of Object.entries( schema.components.schemas, )) { if (schemaObj) { - const requestDefaultNonNullable = - options?.requestDefaultNonNullable ?? false - const responseDefaultNonNullable = - options?.responseDefaultNonNullable ?? true - - // Determine which defaultNonNullable to use based on where schema is used - let defaultNonNullable = responseDefaultNonNullable - if (requestSchemaNames.has(schemaName)) { - defaultNonNullable = requestDefaultNonNullable - } - - // Create a fresh context for each schema with the schema name - const schemaContext = createSchemaContext(schemaName) - - const { type: schemaType } = getTypeFromSchema( - schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, - schema, - { defaultNonNullable, context: schemaContext }, - ) - - // Merge enums from this schema into the main context - for (const [enumName, enumDef] of schemaContext.enums) { - if (!enumContext.enums.has(enumName)) { - enumContext.enums.set(enumName, enumDef) + // Skip enum schemas - they are defined as top-level type aliases + // and referenced directly by type name (e.g., Gender instead of DevupObject<...>['Gender']) + const typedSchemaObj = schemaObj as OpenAPIV3_1.SchemaObject + if ('enum' in typedSchemaObj && typedSchemaObj.enum) { + // Still need to collect enum definition for top-level type alias + const schemaContext = createSchemaContext(schemaName) + getTypeFromSchema(typedSchemaObj, schema, { context: schemaContext }) + for (const [enumName, enumDef] of schemaContext.enums) { + if (!enumContext.enums.has(enumName)) { + enumContext.enums.set(enumName, enumDef) + } } + continue } - // Keep original schema name as-is + // Generate for request context if (requestSchemaNames.has(schemaName)) { + const schemaContext = createSchemaContext(schemaName) + const { type: schemaType } = getTypeFromSchema( + schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, + schema, + { + defaultNonNullable: requestDefaultNonNullable, + context: schemaContext, + serverName, + componentType: 'request', + usedSchemaNames: requestSchemaNames, + }, + ) + // Merge enums + for (const [enumName, enumDef] of schemaContext.enums) { + if (!enumContext.enums.has(enumName)) { + enumContext.enums.set(enumName, enumDef) + } + } requestComponents[schemaName] = schemaType } + + // Generate for response context if (responseSchemaNames.has(schemaName)) { + const schemaContext = createSchemaContext(schemaName) + const { type: schemaType } = getTypeFromSchema( + schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, + schema, + { + defaultNonNullable: responseDefaultNonNullable, + context: schemaContext, + serverName, + componentType: 'response', + usedSchemaNames: responseSchemaNames, + }, + ) + // Merge enums + for (const [enumName, enumDef] of schemaContext.enums) { + if (!enumContext.enums.has(enumName)) { + enumContext.enums.set(enumName, enumDef) + } + } responseComponents[schemaName] = schemaType } + + // Generate for error context if (errorSchemaNames.has(schemaName)) { + const schemaContext = createSchemaContext(schemaName) + const { type: schemaType } = getTypeFromSchema( + schemaObj as OpenAPIV3_1.SchemaObject | OpenAPIV3_1.ReferenceObject, + schema, + { + defaultNonNullable: responseDefaultNonNullable, + context: schemaContext, + serverName, + componentType: 'error', + usedSchemaNames: errorSchemaNames, + }, + ) + // Merge enums + for (const [enumName, enumDef] of schemaContext.enums) { + if (!enumContext.enums.has(enumName)) { + enumContext.enums.set(enumName, enumDef) + } + } errorComponents[schemaName] = schemaType } } diff --git a/packages/generator/src/generate-schema.ts b/packages/generator/src/generate-schema.ts index 946f6bb..d6a6e1c 100644 --- a/packages/generator/src/generate-schema.ts +++ b/packages/generator/src/generate-schema.ts @@ -139,6 +139,18 @@ function resolveSchemaRef< return null } +/** + * Options for component reference handling + */ +export interface ComponentRefOptions { + /** Server name for DevupObject reference */ + serverName?: string + /** Component type: 'request' | 'response' | 'error' */ + componentType?: 'request' | 'response' | 'error' + /** Set of schema names that should be referenced as components */ + usedSchemaNames?: Set +} + /** * Convert OpenAPI schema to TypeScript type representation */ @@ -149,7 +161,7 @@ export function getTypeFromSchema( defaultNonNullable?: boolean context?: SchemaProcessingContext propertyName?: string - }, + } & ComponentRefOptions, ): { type: unknown; default?: unknown } { const defaultNonNullable = options?.defaultNonNullable ?? false const context = options?.context @@ -162,17 +174,62 @@ export function getTypeFromSchema( try { // Handle $ref if ('$ref' in schema) { + // Extract schema name from $ref + const refSchemaName = schema.$ref.startsWith('#/components/schemas/') + ? schema.$ref.replace('#/components/schemas/', '') + : undefined + const resolved = resolveSchemaRef( schema.$ref, document, ) - if (resolved) { - return getTypeFromSchema(resolved, document, { - ...options, - propertyName: undefined, // Don't double-push property name - }) + + // If $ref cannot be resolved, return unknown + if (!resolved) { + return { type: 'unknown', default: undefined } } - return { type: 'unknown', default: undefined } + + // If this $ref points to a used component schema, decide how to reference it + if ( + refSchemaName && + options?.serverName && + options?.componentType && + options?.usedSchemaNames?.has(refSchemaName) + ) { + // Check if the referenced schema is an enum + // Enums are defined at top-level as type aliases, so use direct type name + if ('enum' in resolved && resolved.enum) { + // Return the enum type name directly (e.g., "Gender" instead of DevupObject<...>['Gender']) + return { + type: refSchemaName, + default: undefined, + } + } + + // For object schemas, return a component reference marker + return { + type: { + __componentRef: true, + schemaName: refSchemaName, + serverName: options.serverName, + componentType: options.componentType, + }, + default: undefined, + } + } + + // Create updated context with the referenced schema name + // Reset propertyPath since we're entering a new schema context + const updatedContext = + context && refSchemaName + ? { ...context, schemaName: refSchemaName, propertyPath: [] } + : context + + return getTypeFromSchema(resolved, document, { + ...options, + context: updatedContext, + propertyName: undefined, // Don't double-push property name + }) } const schemaObj = schema as OpenAPIV3_1.SchemaObject @@ -527,6 +584,23 @@ function isNullableObject( ) } +/** + * Check if a value is a component reference marker + */ +function isComponentRef(value: unknown): value is { + __componentRef: true + schemaName: string + serverName: string + componentType: 'request' | 'response' | 'error' +} { + return ( + typeof value === 'object' && + value !== null && + '__componentRef' in value && + (value as Record).__componentRef === true + ) +} + /** * Format a type value to TypeScript type string */ @@ -535,6 +609,11 @@ export function formatTypeValue(value: unknown, indent: number = 0): string { return value } + // Handle component reference marker + if (isComponentRef(value)) { + return `DevupObject<'${value.componentType}', '${value.serverName}'>['${value.schemaName}']` + } + // Handle array type marker if (isArrayType(value)) { const itemsFormatted = formatTypeValue(value.items, indent) diff --git a/packages/generator/src/wrap-interface-key-guard.ts b/packages/generator/src/wrap-interface-key-guard.ts index e537537..31f9e1c 100644 --- a/packages/generator/src/wrap-interface-key-guard.ts +++ b/packages/generator/src/wrap-interface-key-guard.ts @@ -13,13 +13,12 @@ export function wrapInterfaceKeyGuard(key: string): string { // TypeScript identifier pattern: starts with letter/underscore/dollar, followed by letters/numbers/underscore/dollar const isValidIdentifier = /^[a-zA-Z_$][a-zA-Z0-9_$]*$/.test(baseKey) - if ( - !isValidIdentifier || - baseKey.includes('"') || - baseKey.includes("'") || - baseKey.includes('`') - ) { - const wrapped = `[\`${baseKey}\`]` + if (!isValidIdentifier || baseKey.includes("'")) { + // Use single quotes for keys, escape if needed + const escapedKey = baseKey.includes("'") + ? baseKey.replace(/'/g, "\\'") + : baseKey + const wrapped = `'${escapedKey}'` return isOptional ? `${wrapped}?` : wrapped } return key From 69de8f402aa11edced9203e274c3117dfda2e698 Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Mon, 26 Jan 2026 19:17:35 +0900 Subject: [PATCH 2/2] Add testcase --- bunfig.toml | 3 +- .../generate-interface.test.ts.snap | 114 +++++++++++++ .../src/__tests__/generate-interface.test.ts | 155 ++++++++++++++++++ 3 files changed, 271 insertions(+), 1 deletion(-) diff --git a/bunfig.toml b/bunfig.toml index 6f46ac9..39ff80d 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,4 +2,5 @@ coverage = true coveragePathIgnorePatterns = ["node_modules", "**/dist/**"] coverageSkipTestFiles = true -preload = ["bun-test-env-dom"] \ No newline at end of file +preload = ["bun-test-env-dom"] +coverageThreshold = 1.0 diff --git a/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap b/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap index 6a2ad19..9a2d1af 100644 --- a/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap +++ b/packages/generator/src/__tests__/__snapshots__/generate-interface.test.ts.snap @@ -1638,3 +1638,117 @@ declare module "@devup-api/fetch" { interface DevupErrorComponentStruct {} }" `; + +exports[`generateInterface handles circular references in schema collection 1`] = ` +"import "@devup-api/fetch"; +import type { DevupObject } from "@devup-api/fetch"; + +declare module "@devup-api/fetch" { + interface DevupApiServers { + 'openapi.json': never + } + + interface DevupGetApiStruct { + 'openapi.json': { + '/nodes': { + response: Array['TreeNode']>; + }; + listNodes: { + response: Array['TreeNode']>; + }; + } + } + + interface DevupRequestComponentStruct {} + + interface DevupResponseComponentStruct { + 'openapi.json': { + TreeNode: { + id?: number; + name?: string; + children?: Array['TreeNode']>; + }; + } + } + + interface DevupErrorComponentStruct {} +}" +`; + +exports[`generateInterface handles request schema with inline enum 1`] = ` +"import "@devup-api/fetch"; +import type { DevupObject } from "@devup-api/fetch"; + +declare module "@devup-api/fetch" { + type CreateItemRequestPriority = "low" | "medium" | "high" + + interface DevupApiServers { + 'openapi.json': never + } + + interface DevupPostApiStruct { + 'openapi.json': { + '/items': { + body: DevupObject<'request', 'openapi.json'>['CreateItemRequest']; + response?: {}; + }; + createItem: { + body: DevupObject<'request', 'openapi.json'>['CreateItemRequest']; + response?: {}; + }; + } + } + + interface DevupRequestComponentStruct { + 'openapi.json': { + CreateItemRequest: { + name?: string; + priority?: CreateItemRequestPriority; + }; + } + } + + interface DevupResponseComponentStruct {} + + interface DevupErrorComponentStruct {} +}" +`; + +exports[`generateInterface handles error schema with inline enum 1`] = ` +"import "@devup-api/fetch"; +import type { DevupObject } from "@devup-api/fetch"; + +declare module "@devup-api/fetch" { + type ErrorResponseCode = "INVALID_INPUT" | "NOT_FOUND" | "UNAUTHORIZED" + + interface DevupApiServers { + 'openapi.json': never + } + + interface DevupGetApiStruct { + 'openapi.json': { + '/items': { + response?: {}; + error: DevupObject<'error', 'openapi.json'>['ErrorResponse']; + }; + getItems: { + response?: {}; + error: DevupObject<'error', 'openapi.json'>['ErrorResponse']; + }; + } + } + + interface DevupRequestComponentStruct {} + + interface DevupResponseComponentStruct {} + + interface DevupErrorComponentStruct { + 'openapi.json': { + ErrorResponse: { + message?: string; + code?: ErrorResponseCode; + }; + } + } +}" +`; diff --git a/packages/generator/src/__tests__/generate-interface.test.ts b/packages/generator/src/__tests__/generate-interface.test.ts index 269cd64..bf5782b 100644 --- a/packages/generator/src/__tests__/generate-interface.test.ts +++ b/packages/generator/src/__tests__/generate-interface.test.ts @@ -1899,3 +1899,158 @@ test('generateInterface handles inline response with nested $ref containing enum "DevupObject<'response', 'openapi.json'>['OtherItemResponse']", ) }) + +// Test circular reference handling in collectSchemaNames (line 83 coverage) +test('generateInterface handles circular references in schema collection', () => { + const schema = { + paths: { + '/nodes': { + get: { + operationId: 'listNodes', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'array', + items: { + $ref: '#/components/schemas/TreeNode', + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + TreeNode: { + type: 'object', + properties: { + id: { type: 'number' }, + name: { type: 'string' }, + // Circular reference - children contains TreeNode + children: { + type: 'array', + items: { + $ref: '#/components/schemas/TreeNode', + }, + }, + }, + }, + }, + }, + } + const result = generateInterface(createSchemas(createDocument(schema as any))) + expect(result).toMatchSnapshot() + // Should not cause infinite loop and should generate valid output + expect(result).toContain('TreeNode') +}) + +// Test request schema with inline enum (lines 659-661 coverage) +test('generateInterface handles request schema with inline enum', () => { + const schema = { + paths: { + '/items': { + post: { + operationId: 'createItem', + requestBody: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/CreateItemRequest', + }, + }, + }, + }, + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { type: 'object', properties: {} }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + CreateItemRequest: { + type: 'object', + properties: { + name: { type: 'string' }, + // Inline enum - not a $ref, should be collected during schema processing + priority: { + type: 'string', + enum: ['low', 'medium', 'high'], + }, + }, + }, + }, + }, + } + const result = generateInterface(createSchemas(createDocument(schema as any))) + expect(result).toMatchSnapshot() + // Inline enum should generate type alias based on context + expect(result).toContain('type CreateItemRequestPriority =') + expect(result).toContain('"low"') +}) + +// Test error schema with inline enum (lines 705-707 coverage) +test('generateInterface handles error schema with inline enum', () => { + const schema = { + paths: { + '/items': { + get: { + operationId: 'getItems', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { type: 'object', properties: {} }, + }, + }, + }, + '400': { + description: 'Error', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/ErrorResponse', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + ErrorResponse: { + type: 'object', + properties: { + message: { type: 'string' }, + // Inline enum - not a $ref, should be collected during schema processing + code: { + type: 'string', + enum: ['INVALID_INPUT', 'NOT_FOUND', 'UNAUTHORIZED'], + }, + }, + }, + }, + }, + } + const result = generateInterface(createSchemas(createDocument(schema as any))) + expect(result).toMatchSnapshot() + // Inline enum should generate type alias based on context + expect(result).toContain('type ErrorResponseCode =') + expect(result).toContain('"INVALID_INPUT"') +})