From 13b485b57afdcb821168d5c5b6d8f86e1b20d83a Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 4 Jan 2026 18:06:00 +0900 Subject: [PATCH 1/2] Add typing test --- bun.lock | 71 +- bunfig.toml | 2 +- package.json | 1 + packages/core/src/__tests__/types.test.ts | 408 +++++++++++ packages/fetch/src/__tests__/types.test.ts | 365 ++++++++++ .../generator/src/__tests__/types.test.ts | 632 ++++++++++++++++++ packages/react-query/setup.ts | 27 - 7 files changed, 1456 insertions(+), 50 deletions(-) create mode 100644 packages/core/src/__tests__/types.test.ts create mode 100644 packages/fetch/src/__tests__/types.test.ts create mode 100644 packages/generator/src/__tests__/types.test.ts delete mode 100644 packages/react-query/setup.ts diff --git a/bun.lock b/bun.lock index deb955d..86b3bac 100644 --- a/bun.lock +++ b/bun.lock @@ -9,6 +9,7 @@ "@testing-library/react": "^16.3.1", "@testing-library/react-hooks": "^8.0.1", "@types/bun": "latest", + "bun-test-env-dom": "^1.0.3", "husky": "^9", "react": "^19.2.3", "react-dom": "^19.2.3", @@ -97,7 +98,7 @@ }, "packages/fetch": { "name": "@devup-api/fetch", - "version": "0.1.11", + "version": "0.1.13", "dependencies": { "@devup-api/core": "workspace:*", }, @@ -108,10 +109,10 @@ }, "packages/generator": { "name": "@devup-api/generator", - "version": "0.1.9", + "version": "0.1.12", "dependencies": { - "@devup-api/core": "workspace:*", - "@devup-api/utils": "workspace:*", + "@devup-api/core": "workspace:^", + "@devup-api/utils": "workspace:^", }, "devDependencies": { "@types/node": "^25.0", @@ -121,12 +122,12 @@ }, "packages/next-plugin": { "name": "@devup-api/next-plugin", - "version": "0.1.6", + "version": "0.1.8", "dependencies": { - "@devup-api/core": "workspace:*", - "@devup-api/generator": "workspace:*", - "@devup-api/utils": "workspace:*", - "@devup-api/webpack-plugin": "workspace:*", + "@devup-api/core": "workspace:^", + "@devup-api/generator": "workspace:^", + "@devup-api/utils": "workspace:^", + "@devup-api/webpack-plugin": "workspace:^", }, "devDependencies": { "@types/node": "^25.0", @@ -140,7 +141,7 @@ }, "packages/react-query": { "name": "@devup-api/react-query", - "version": "0.1.3", + "version": "0.1.5", "dependencies": { "@devup-api/fetch": "workspace:*", "@tanstack/react-query": ">=5.90", @@ -159,11 +160,11 @@ }, "packages/rsbuild-plugin": { "name": "@devup-api/rsbuild-plugin", - "version": "0.1.6", + "version": "0.1.8", "dependencies": { - "@devup-api/core": "workspace:*", - "@devup-api/generator": "workspace:*", - "@devup-api/utils": "workspace:*", + "@devup-api/core": "workspace:^", + "@devup-api/generator": "workspace:^", + "@devup-api/utils": "workspace:^", }, "devDependencies": { "@types/node": "^25.0", @@ -185,11 +186,11 @@ }, "packages/vite-plugin": { "name": "@devup-api/vite-plugin", - "version": "0.1.6", + "version": "0.1.8", "dependencies": { - "@devup-api/core": "workspace:*", - "@devup-api/generator": "workspace:*", - "@devup-api/utils": "workspace:*", + "@devup-api/core": "workspace:^", + "@devup-api/generator": "workspace:^", + "@devup-api/utils": "workspace:^", }, "devDependencies": { "@types/node": "^25.0", @@ -202,11 +203,11 @@ }, "packages/webpack-plugin": { "name": "@devup-api/webpack-plugin", - "version": "0.1.6", + "version": "0.1.8", "dependencies": { - "@devup-api/core": "workspace:*", - "@devup-api/generator": "workspace:*", - "@devup-api/utils": "workspace:*", + "@devup-api/core": "workspace:^", + "@devup-api/generator": "workspace:^", + "@devup-api/utils": "workspace:^", }, "devDependencies": { "@types/node": "^25.0", @@ -219,6 +220,8 @@ }, }, "packages": { + "@adobe/css-tools": ["@adobe/css-tools@4.4.4", "", {}, "sha512-Elp+iwUx5rN5+Y8xLt5/GRoG20WGoDCQ/1Fb+1LiGtvwbDavuSk0jhD/eZdckHAuzcDzccnkv+rEjyWfRx18gg=="], + "@babel/code-frame": ["@babel/code-frame@7.27.1", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg=="], "@babel/compat-data": ["@babel/compat-data@7.28.5", "", {}, "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA=="], @@ -361,6 +364,8 @@ "@esbuild/win32-x64": ["@esbuild/win32-x64@0.27.2", "", { "os": "win32", "cpu": "x64" }, "sha512-sRdU18mcKf7F+YgheI/zGf5alZatMUTKj/jNS6l744f9u3WFu4v7twcUI9vu4mknF4Y9aDlblIie0IM+5xxaqQ=="], + "@happy-dom/global-registrator": ["@happy-dom/global-registrator@20.0.11", "", { "dependencies": { "@types/node": "^20.0.0", "happy-dom": "^20.0.11" } }, "sha512-GqNqiShBT/lzkHTMC/slKBrvN0DsD4Di8ssBk4aDaVgEn+2WMzE6DXxq701ndSXj7/0cJ8mNT71pM7Bnrr6JRw=="], + "@img/colour": ["@img/colour@1.0.0", "", {}, "sha512-A5P/LfWGFSl6nsckYtjw9da+19jB8hkJ6ACTGcDfEJ0aE+l2n2El7dsVM7UVHZQ9s2lmYMWlrS21YLy2IR1LUw=="], "@img/sharp-darwin-arm64": ["@img/sharp-darwin-arm64@0.34.5", "", { "optionalDependencies": { "@img/sharp-libvips-darwin-arm64": "1.2.4" }, "os": "darwin", "cpu": "arm64" }, "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w=="], @@ -545,10 +550,14 @@ "@testing-library/dom": ["@testing-library/dom@10.4.1", "", { "dependencies": { "@babel/code-frame": "^7.10.4", "@babel/runtime": "^7.12.5", "@types/aria-query": "^5.0.1", "aria-query": "5.3.0", "dom-accessibility-api": "^0.5.9", "lz-string": "^1.5.0", "picocolors": "1.1.1", "pretty-format": "^27.0.2" } }, "sha512-o4PXJQidqJl82ckFaXUeoAW+XysPLauYI43Abki5hABd853iMhitooc6znOnczgbTYmEP6U6/y1ZyKAIsvMKGg=="], + "@testing-library/jest-dom": ["@testing-library/jest-dom@6.9.1", "", { "dependencies": { "@adobe/css-tools": "^4.4.0", "aria-query": "^5.0.0", "css.escape": "^1.5.1", "dom-accessibility-api": "^0.6.3", "picocolors": "^1.1.1", "redent": "^3.0.0" } }, "sha512-zIcONa+hVtVSSep9UT3jZ5rizo2BsxgyDYU7WFD5eICBE7no3881HGeb/QkGfsJs6JTkY1aQhT7rIPC7e+0nnA=="], + "@testing-library/react": ["@testing-library/react@16.3.1", "", { "dependencies": { "@babel/runtime": "^7.12.5" }, "peerDependencies": { "@testing-library/dom": "^10.0.0", "@types/react": "^18.0.0 || ^19.0.0", "@types/react-dom": "^18.0.0 || ^19.0.0", "react": "^18.0.0 || ^19.0.0", "react-dom": "^18.0.0 || ^19.0.0" }, "optionalPeers": ["@types/react", "@types/react-dom"] }, "sha512-gr4KtAWqIOQoucWYD/f6ki+j5chXfcPc74Col/6poTyqTmn7zRmodWahWRCp8tYd+GMqBonw6hstNzqjbs6gjw=="], "@testing-library/react-hooks": ["@testing-library/react-hooks@8.0.1", "", { "dependencies": { "@babel/runtime": "^7.12.5", "react-error-boundary": "^3.1.0" }, "peerDependencies": { "@types/react": "^16.9.0 || ^17.0.0", "react": "^16.9.0 || ^17.0.0", "react-dom": "^16.9.0 || ^17.0.0", "react-test-renderer": "^16.9.0 || ^17.0.0" }, "optionalPeers": ["@types/react", "react-dom", "react-test-renderer"] }, "sha512-Aqhl2IVmLt8IovEVarNDFuJDVWVvhnr9/GCU6UUnrYXwgDFF9h2L2o2P9KBni1AST5sT6riAyoukFLyjQUgD/g=="], + "@testing-library/user-event": ["@testing-library/user-event@14.6.1", "", { "peerDependencies": { "@testing-library/dom": ">=7.21.4" } }, "sha512-vq7fv0rnt+QTXgPxr5Hjc210p6YKq2kmdziLgnsZGgLJ9e6VAShx1pACLuRjd/AS/sr7phAR58OIIpf0LlmQNw=="], + "@tybys/wasm-util": ["@tybys/wasm-util@0.10.1", "", { "dependencies": { "tslib": "^2.4.0" } }, "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg=="], "@types/aria-query": ["@types/aria-query@5.0.4", "", {}, "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw=="], @@ -639,6 +648,8 @@ "buffer-from": ["buffer-from@1.1.2", "", {}, "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ=="], + "bun-test-env-dom": ["bun-test-env-dom@1.0.3", "", { "dependencies": { "@happy-dom/global-registrator": ">=20.0", "@testing-library/dom": ">=10.4", "@testing-library/jest-dom": ">=6.9", "@testing-library/react": ">=16.3", "@testing-library/user-event": ">=14.6" } }, "sha512-Ozepvzk1s/bJSxABEjbI+Ztnm3CN1b0vRSvf0Qa0rTnuO7S0wKN2cUTsXdyIJuqE6OnlAhyoe2NGqkdeemz5/Q=="], + "bun-types": ["bun-types@1.3.4", "", { "dependencies": { "@types/node": "*" } }, "sha512-5ua817+BZPZOlNaRgGBpZJOSAQ9RQ17pkwPD0yR7CfJg+r8DgIILByFifDTa+IPDDxzf5VNhtNlcKqFzDgJvlQ=="], "caniuse-lite": ["caniuse-lite@1.0.30001760", "", {}, "sha512-7AAMPcueWELt1p3mi13HR/LHH0TJLT11cnwDJEs3xA4+CK/PLKeO9Kl1oru24htkyUKtkGCvAx4ohB0Ttry8Dw=="], @@ -653,6 +664,8 @@ "core-js": ["core-js@3.47.0", "", {}, "sha512-c3Q2VVkGAUyupsjRnaNX6u8Dq2vAdzm9iuPj5FW0fRxzlxgq9Q39MDq10IvmQSpLgHQNyQzQmOo6bgGHmH3NNg=="], + "css.escape": ["css.escape@1.5.1", "", {}, "sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], "csstype-extra": ["csstype-extra@0.1.21", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-+bNbaI4AB6Sh9MeWlzHe+OPPDttu8oM8g6oyOSfoDmzjn6jdaixBdo6Sbu64apL9WEUfiuXuvDiVfqoSnyolBA=="], @@ -709,6 +722,8 @@ "husky": ["husky@9.1.7", "", { "bin": { "husky": "bin.js" } }, "sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA=="], + "indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], + "jest-worker": ["jest-worker@27.5.1", "", { "dependencies": { "@types/node": "*", "merge-stream": "^2.0.0", "supports-color": "^8.0.0" } }, "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg=="], "jiti": ["jiti@2.6.1", "", { "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ=="], @@ -735,6 +750,8 @@ "mime-types": ["mime-types@2.1.35", "", { "dependencies": { "mime-db": "1.52.0" } }, "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw=="], + "min-indent": ["min-indent@1.0.1", "", {}, "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg=="], + "minimatch": ["minimatch@10.1.1", "", { "dependencies": { "@isaacs/brace-expansion": "^5.0.0" } }, "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ=="], "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], @@ -777,6 +794,8 @@ "react-refresh": ["react-refresh@0.18.0", "", {}, "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw=="], + "redent": ["redent@3.0.0", "", { "dependencies": { "indent-string": "^4.0.0", "strip-indent": "^3.0.0" } }, "sha512-6tDA8g98We0zd0GvVeMT9arEOnTw9qM03L9cJXaCjrip1OO764RDBLBfrB4cwzNGDj5OA5ioymC9GkizgWJDUg=="], + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], "rollup": ["rollup@4.53.5", "", { "dependencies": { "@types/estree": "1.0.8" }, "optionalDependencies": { "@rollup/rollup-android-arm-eabi": "4.53.5", "@rollup/rollup-android-arm64": "4.53.5", "@rollup/rollup-darwin-arm64": "4.53.5", "@rollup/rollup-darwin-x64": "4.53.5", "@rollup/rollup-freebsd-arm64": "4.53.5", "@rollup/rollup-freebsd-x64": "4.53.5", "@rollup/rollup-linux-arm-gnueabihf": "4.53.5", "@rollup/rollup-linux-arm-musleabihf": "4.53.5", "@rollup/rollup-linux-arm64-gnu": "4.53.5", "@rollup/rollup-linux-arm64-musl": "4.53.5", "@rollup/rollup-linux-loong64-gnu": "4.53.5", "@rollup/rollup-linux-ppc64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-gnu": "4.53.5", "@rollup/rollup-linux-riscv64-musl": "4.53.5", "@rollup/rollup-linux-s390x-gnu": "4.53.5", "@rollup/rollup-linux-x64-gnu": "4.53.5", "@rollup/rollup-linux-x64-musl": "4.53.5", "@rollup/rollup-openharmony-arm64": "4.53.5", "@rollup/rollup-win32-arm64-msvc": "4.53.5", "@rollup/rollup-win32-ia32-msvc": "4.53.5", "@rollup/rollup-win32-x64-gnu": "4.53.5", "@rollup/rollup-win32-x64-msvc": "4.53.5", "fsevents": "~2.3.2" }, "bin": { "rollup": "dist/bin/rollup" } }, "sha512-iTNAbFSlRpcHeeWu73ywU/8KuU/LZmNCSxp6fjQkJBD3ivUb8tpDrXhIxEzA05HlYMEwmtaUnb3RP+YNv162OQ=="], @@ -803,6 +822,8 @@ "stackframe": ["stackframe@1.3.4", "", {}, "sha512-oeVtt7eWQS+Na6F//S4kJ2K2VbRlS9D43mAlMyVpVWovy9o+jfgH8O9agzANzaiLjclA0oYzUXEM4PurhSUChw=="], + "strip-indent": ["strip-indent@3.0.0", "", { "dependencies": { "min-indent": "^1.0.0" } }, "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ=="], + "styled-jsx": ["styled-jsx@5.1.6", "", { "dependencies": { "client-only": "0.0.1" }, "peerDependencies": { "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" } }, "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA=="], "supports-color": ["supports-color@8.1.1", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q=="], @@ -847,8 +868,12 @@ "@devup-ui/webpack-plugin/@devup-ui/wasm": ["@devup-ui/wasm@1.0.47", "", {}, "sha512-RPktfdg53bK5BqAyhfs9hA5vzAiH0D63w60S+ACaoIPXpqQaQp2Lh9pl3Mi6E+8KA0Div/hoQCLfYxuAefodrg=="], + "@happy-dom/global-registrator/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], + "@rsbuild/core/@swc/helpers": ["@swc/helpers@0.5.17", "", { "dependencies": { "tslib": "^2.8.0" } }, "sha512-5IKx/Y13RsYd+sauPb2x+U/xZikHjolzfuDgTAl/Tdf3Q8rslRvC19NKDLgAJQ6wsqADk10ntlv08nPFw/gO/A=="], + "@testing-library/jest-dom/dom-accessibility-api": ["dom-accessibility-api@0.6.3", "", {}, "sha512-7ZgogeTnjuHbo+ct10G9Ffp0mif17idi0IyWNVA/wcwcm7NPOD/WEHVP3n7n3MhXqxoIYm8d6MuZohYWIZ4T3w=="], + "esrecurse/estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], "happy-dom/@types/node": ["@types/node@20.19.27", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-N2clP5pJhB2YnZJ3PIHFk5RkygRX5WO/5f0WC08tp0wd+sv0rsJk3MqWn3CbNmT2J505a5336jaQj4ph1AdMug=="], @@ -873,6 +898,8 @@ "@devup-ui/next-plugin/next/@next/swc-win32-x64-msvc": ["@next/swc-win32-x64-msvc@16.0.10", "", { "os": "win32", "cpu": "x64" }, "sha512-E+njfCoFLb01RAFEnGZn6ERoOqhK1Gl3Lfz1Kjnj0Ulfu7oJbuMyvBKNj/bw8XZnenHDASlygTjZICQW+rYW1Q=="], + "@happy-dom/global-registrator/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "happy-dom/@types/node/undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], } } diff --git a/bunfig.toml b/bunfig.toml index 8fe65c6..6f46ac9 100644 --- a/bunfig.toml +++ b/bunfig.toml @@ -2,4 +2,4 @@ coverage = true coveragePathIgnorePatterns = ["node_modules", "**/dist/**"] coverageSkipTestFiles = true -preload = ["./packages/react-query/setup.ts"] \ No newline at end of file +preload = ["bun-test-env-dom"] \ No newline at end of file diff --git a/package.json b/package.json index 1e1f409..9ac504e 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,7 @@ "@testing-library/react": "^16.3.1", "@testing-library/react-hooks": "^8.0.1", "@types/bun": "latest", + "bun-test-env-dom": "^1.0.3", "husky": "^9", "react": "^19.2.3", "react-dom": "^19.2.3" diff --git a/packages/core/src/__tests__/types.test.ts b/packages/core/src/__tests__/types.test.ts new file mode 100644 index 0000000..e488fa7 --- /dev/null +++ b/packages/core/src/__tests__/types.test.ts @@ -0,0 +1,408 @@ +/** + * Type tests for core utility types + * Verify that utility type transformations work correctly + */ +import { describe, expectTypeOf, test } from 'bun:test' +import type { + Additional, + ApiOption, + BoildApiOption, + ExtractValue, + RequiredOptions, +} from '../additional' +import type { + DevupDeleteApiStruct, + DevupGetApiStruct, + DevupGetApiStructKey, + DevupObject, + DevupPatchApiStruct, + DevupPostApiStruct, + DevupPostApiStructKey, + DevupPutApiStruct, +} from '../api-struct' + +// ============================================================================= +// Test Fixtures +// ============================================================================= + +declare module '../api-struct' { + interface DevupApiServers { + 'test-api.json': never + } + + interface DevupGetApiStruct { + 'test-api.json': { + '/users': { + response: { id: number; name: string }[] + error: { message: string } + } + '/users/{id}': { + params: { id: string } + query?: { include?: string } + response: { id: number; name: string; email: string } + error: { message: string; code: number } + } + getUser: { + params: { id: string } + response: { id: number; name: string } + error: { message: string } + } + } + } + + interface DevupPostApiStruct { + 'test-api.json': { + '/users': { + body: { name: string; email: string } + response: { id: number; name: string } + error: { message: string } + } + } + } + + interface DevupPutApiStruct { + 'test-api.json': { + '/users/{id}': { + params: { id: string } + body: { name: string; email: string } + response: { id: number } + error: { message: string } + } + } + } + + interface DevupDeleteApiStruct { + 'test-api.json': { + '/users/{id}': { + params: { id: string } + response: { success: boolean } + error: { message: string } + } + } + } + + interface DevupPatchApiStruct { + 'test-api.json': { + '/users/{id}': { + params: { id: string } + body: { name?: string; email?: string } + response: { id: number } + error: { message: string } + } + } + } + + interface DevupRequestComponentStruct { + 'test-api.json': { + CreateUserRequest: { name: string; email: string } + UpdateUserRequest: { name?: string; email?: string } + } + } + + interface DevupResponseComponentStruct { + 'test-api.json': { + User: { id: number; name: string; email: string } + UserList: { id: number; name: string }[] + } + } + + interface DevupErrorComponentStruct { + 'test-api.json': { + ApiError: { code: string; message: string } + NotFoundError: { message: string; resource: string } + } + } +} + +// ============================================================================= +// ExtractValue - Verify value extraction from nested types +// ============================================================================= + +describe('ExtractValue', () => { + test('extracts response type from endpoint', () => { + type Endpoint = DevupGetApiStruct['test-api.json']['/users'] + type Response = ExtractValue + + // response type should be extracted correctly + expectTypeOf().toEqualTypeOf<{ id: number; name: string }[]>() + }) + + test('extracts error type from endpoint', () => { + type Endpoint = DevupGetApiStruct['test-api.json']['/users/{id}'] + type Error = ExtractValue + + expectTypeOf().toEqualTypeOf<{ message: string; code: number }>() + }) + + test('extracts params type from endpoint', () => { + type Endpoint = DevupGetApiStruct['test-api.json']['/users/{id}'] + type Params = ExtractValue + + expectTypeOf().toEqualTypeOf<{ id: string }>() + }) + + test('returns fallback for non-existent key', () => { + type Endpoint = DevupGetApiStruct['test-api.json']['/users'] + type Body = ExtractValue + + expectTypeOf().toBeNever() + }) +}) + +// ============================================================================= +// Additional - Verify type lookup by endpoint key +// ============================================================================= + +describe('Additional', () => { + test('looks up endpoint type by existing path', () => { + type Scope = DevupGetApiStruct['test-api.json'] + type Result = Additional<'/users', Scope> + + // should return the full type of /users endpoint + expectTypeOf().toHaveProperty('response') + expectTypeOf().toHaveProperty('error') + }) + + test('returns empty object for non-existent path', () => { + type Scope = DevupGetApiStruct['test-api.json'] + type Result = Additional<'/nonexistent', Scope> + + expectTypeOf().toEqualTypeOf() + }) + + test('looks up endpoint type by operationId', () => { + type Scope = DevupGetApiStruct['test-api.json'] + type Result = Additional<'getUser', Scope> + + expectTypeOf().toHaveProperty('params') + expectTypeOf().toHaveProperty('response') + }) +}) + +// ============================================================================= +// RequiredOptions - Determine if params/query/body are required +// ============================================================================= + +describe('RequiredOptions', () => { + test('options required when params exist', () => { + type Endpoint = { params: { id: string }; response: { data: string } } + type Result = RequiredOptions + + // not never = options required + expectTypeOf().not.toBeNever() + expectTypeOf().toEqualTypeOf() + }) + + test('options required when body exists', () => { + type Endpoint = { body: { name: string }; response: { id: number } } + type Result = RequiredOptions + + expectTypeOf().not.toBeNever() + }) + + test('options required when query exists', () => { + type Endpoint = { query: { page: number }; response: { data: string } } + type Result = RequiredOptions + + expectTypeOf().not.toBeNever() + }) + + test('options optional when no params/query/body', () => { + type Endpoint = { response: { data: string }; error: { message: string } } + type Result = RequiredOptions + + // never = options not required + expectTypeOf().toBeNever() + }) +}) + +// ============================================================================= +// BoildApiOption - Generate API options from endpoint +// ============================================================================= + +describe('BoildApiOption', () => { + test('includes params/body/query, excludes response/error', () => { + type Endpoint = { + params: { id: string } + body: { name: string } + response: { data: string } + error: { message: string } + } + type Result = BoildApiOption + + // params, body are included + expectTypeOf().toHaveProperty('params') + expectTypeOf().toHaveProperty('body') + + // DevupApiRequestInit properties are also included + expectTypeOf().toHaveProperty('headers') + }) +}) + +// ============================================================================= +// ApiOption - Generate required/optional option tuple +// ============================================================================= + +describe('ApiOption', () => { + test('required option tuple when params exist', () => { + type Endpoint = { params: { id: string }; response: string } + type Result = ApiOption + + // [options: BoildApiOption] format + expectTypeOf().toEqualTypeOf<[BoildApiOption]>() + }) + + test('required option tuple when body exists', () => { + type Endpoint = { body: { name: string }; response: string } + type Result = ApiOption + + expectTypeOf().toEqualTypeOf<[BoildApiOption]>() + }) + + test('optional option tuple when no params/body/query', () => { + type Endpoint = { response: string } + type Result = ApiOption + + // [options?: ...] format - length is 0 or 1 + type IsOptional = Result extends [options?: infer _] ? true : false + expectTypeOf().toEqualTypeOf() + }) +}) + +// ============================================================================= +// DevupApiStructKey - Extract endpoint keys by HTTP method +// ============================================================================= + +describe('DevupApiStructKey', () => { + test('extracts GET endpoint keys', () => { + type Keys = DevupGetApiStructKey<'test-api.json'> + + // should include all defined GET endpoints + type HasUsers = '/users' extends Keys ? true : false + type HasUserId = '/users/{id}' extends Keys ? true : false + type HasGetUser = 'getUser' extends Keys ? true : false + + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + expectTypeOf().toEqualTypeOf() + }) + + test('extracts POST endpoint keys', () => { + type Keys = DevupPostApiStructKey<'test-api.json'> + + type HasUsers = '/users' extends Keys ? true : false + expectTypeOf().toEqualTypeOf() + }) +}) + +// ============================================================================= +// DevupObject - Access component schemas +// ============================================================================= + +describe('DevupObject', () => { + test('accesses response component', () => { + type User = DevupObject<'response', 'test-api.json'>['User'] + + expectTypeOf().toEqualTypeOf<{ + id: number + name: string + email: string + }>() + }) + + test('accesses request component', () => { + type CreateUser = DevupObject< + 'request', + 'test-api.json' + >['CreateUserRequest'] + + expectTypeOf().toEqualTypeOf<{ name: string; email: string }>() + }) + + test('accesses error component', () => { + type ApiError = DevupObject<'error', 'test-api.json'>['ApiError'] + + expectTypeOf().toEqualTypeOf<{ code: string; message: string }>() + }) + + test('uses component types in function signatures', () => { + type User = DevupObject<'response', 'test-api.json'>['User'] + type CreateUserRequest = DevupObject< + 'request', + 'test-api.json' + >['CreateUserRequest'] + + // should be usable as types in actual functions + const createUser = (data: CreateUserRequest): User => ({ + id: 1, + name: data.name, + email: data.email, + }) + + // verify with Parameters and ReturnType utility types + expectTypeOf< + Parameters[0] + >().toEqualTypeOf() + expectTypeOf>().toEqualTypeOf() + }) +}) + +// ============================================================================= +// HTTP Method Endpoint Structure Verification +// ============================================================================= + +describe('HTTP Method Structs', () => { + test('GET endpoint - response/error required, params/query optional', () => { + type Endpoint = DevupGetApiStruct['test-api.json']['/users/{id}'] + + expectTypeOf().toEqualTypeOf<{ + id: number + name: string + email: string + }>() + expectTypeOf().toEqualTypeOf<{ + message: string + code: number + }>() + expectTypeOf().toEqualTypeOf<{ id: string }>() + }) + + test('POST endpoint - body required', () => { + type Endpoint = DevupPostApiStruct['test-api.json']['/users'] + + expectTypeOf().toEqualTypeOf<{ + name: string + email: string + }>() + expectTypeOf().toEqualTypeOf<{ + id: number + name: string + }>() + }) + + test('PUT endpoint - params and body required', () => { + type Endpoint = DevupPutApiStruct['test-api.json']['/users/{id}'] + + expectTypeOf().toEqualTypeOf<{ id: string }>() + expectTypeOf().toEqualTypeOf<{ + name: string + email: string + }>() + }) + + test('DELETE endpoint - params required', () => { + type Endpoint = DevupDeleteApiStruct['test-api.json']['/users/{id}'] + + expectTypeOf().toEqualTypeOf<{ id: string }>() + expectTypeOf().toEqualTypeOf<{ success: boolean }>() + }) + + test('PATCH endpoint - body for partial update', () => { + type Endpoint = DevupPatchApiStruct['test-api.json']['/users/{id}'] + + expectTypeOf().toEqualTypeOf<{ + name?: string + email?: string + }>() + }) +}) diff --git a/packages/fetch/src/__tests__/types.test.ts b/packages/fetch/src/__tests__/types.test.ts new file mode 100644 index 0000000..c9d7403 --- /dev/null +++ b/packages/fetch/src/__tests__/types.test.ts @@ -0,0 +1,365 @@ +/** + * Type tests for fetch API client + * Verify that API client type inference works correctly + */ +import { describe, expectTypeOf, test } from 'bun:test' +import type { + DevupDeleteApiStruct, + DevupGetApiStruct, + DevupObject, + DevupPostApiStruct, + DevupPutApiStruct, +} from '@devup-api/core' +import type { DevupApiResponse } from '../api' +import { createApi } from '../create-api' + +// ============================================================================= +// Test Fixtures +// ============================================================================= + +declare module '@devup-api/core' { + interface DevupApiServers { + 'openapi.json': never + 'admin-api.json': never + } + + interface DevupGetApiStruct { + 'openapi.json': { + '/users': { + response: { id: number; name: string }[] + error: { message: string } + } + '/users/{id}': { + params: { id: string } + query?: { include?: string } + response: { id: number; name: string; email: string } + error: { message: string; code: number } + } + } + 'admin-api.json': { + '/admin/users': { + response: { id: number; role: string }[] + error: { message: string } + } + } + } + + interface DevupPostApiStruct { + 'openapi.json': { + '/users': { + body: { name: string; email: string } + response: { id: number; name: string } + error: { message: string; errors?: { field: string; msg: string }[] } + } + } + } + + interface DevupPutApiStruct { + 'openapi.json': { + '/users/{id}': { + params: { id: string } + body: { name: string; email: string } + response: { id: number } + error: { message: string } + } + } + } + + interface DevupDeleteApiStruct { + 'openapi.json': { + '/users/{id}': { + params: { id: string } + response: { success: boolean } + error: { message: string } + } + } + } + + interface DevupResponseComponentStruct { + 'openapi.json': { + User: { id: number; name: string; email: string } + } + } + + interface DevupRequestComponentStruct { + 'openapi.json': { + CreateUserRequest: { name: string; email: string } + } + } + + interface DevupErrorComponentStruct { + 'openapi.json': { + ApiError: { message: string; code: number } + } + } +} + +// ============================================================================= +// DevupApiResponse - Response type verification +// ============================================================================= + +describe('DevupApiResponse', () => { + test('success response type - data exists, error undefined', () => { + type Response = DevupApiResponse<{ id: number }, { message: string }> + type SuccessCase = Extract + + expectTypeOf().toEqualTypeOf<{ id: number }>() + expectTypeOf().toEqualTypeOf() + }) + + test('error response type - error exists, data undefined', () => { + type Response = DevupApiResponse<{ id: number }, { message: string }> + type ErrorCase = Extract + + expectTypeOf().toEqualTypeOf<{ message: string }>() + expectTypeOf().toEqualTypeOf() + }) + + test('response type from actual endpoint', () => { + type Endpoint = DevupGetApiStruct['openapi.json']['/users/{id}'] + type Response = DevupApiResponse + + type SuccessData = Extract['data'] + type ErrorData = Extract['error'] + + expectTypeOf().toEqualTypeOf<{ + id: number + name: string + email: string + }>() + expectTypeOf().toEqualTypeOf<{ message: string; code: number }>() + }) +}) + +// ============================================================================= +// createApi - API instance creation verification +// ============================================================================= + +describe('createApi', () => { + test('creates with default server', () => { + const api = createApi('https://api.example.com') + + // default server is 'openapi.json' + expectTypeOf(api.get).toBeFunction() + expectTypeOf(api.post).toBeFunction() + expectTypeOf(api.put).toBeFunction() + expectTypeOf(api.delete).toBeFunction() + expectTypeOf(api.patch).toBeFunction() + }) + + test('creates with different server', () => { + const adminApi = createApi({ + baseUrl: 'https://admin.example.com', + serverName: 'admin-api.json', + }) + + // can use admin-api.json server endpoints + expectTypeOf(adminApi.get).toBeFunction() + }) + + test('instance method types', () => { + const api = createApi('https://api.example.com') + + expectTypeOf(api.getBaseUrl()).toEqualTypeOf() + expectTypeOf(api.use).toBeFunction() + expectTypeOf(api.setDefaultOptions).toBeFunction() + }) +}) + +// ============================================================================= +// GET method type verification +// ============================================================================= + +describe('DevupApi.get type inference', () => { + const api = createApi('https://api.example.com') + + test('endpoint without params - options optional', () => { + // /users has no params so options are optional + type GetUsers = typeof api.get< + '/users', + DevupGetApiStruct['openapi.json']['/users'] + > + type Params = Parameters + + // first parameter is path, second is options (optional) + expectTypeOf().toEqualTypeOf<'/users'>() + }) + + test('endpoint with params - options required', () => { + // /users/{id} has params so options are required + type Endpoint = DevupGetApiStruct['openapi.json']['/users/{id}'] + + // params must be { id: string } + expectTypeOf().toEqualTypeOf<{ id: string }>() + }) + + test('GET/get case aliases are identical', () => { + expectTypeOf(api.get).toEqualTypeOf(api.GET) + }) +}) + +// ============================================================================= +// POST method type verification +// ============================================================================= + +describe('DevupApi.post type inference', () => { + test('endpoint with required body', () => { + type Endpoint = DevupPostApiStruct['openapi.json']['/users'] + + // body is required + expectTypeOf().toEqualTypeOf<{ + name: string + email: string + }>() + + // response type should also be correct + expectTypeOf().toEqualTypeOf<{ + id: number + name: string + }>() + }) + + test('error type can include additional fields', () => { + type Endpoint = DevupPostApiStruct['openapi.json']['/users'] + type Error = Endpoint['error'] + + // error can optionally include errors array + expectTypeOf().toEqualTypeOf<{ + message: string + errors?: { field: string; msg: string }[] + }>() + }) +}) + +// ============================================================================= +// PUT/DELETE method type verification +// ============================================================================= + +describe('DevupApi.put/delete type inference', () => { + test('PUT - both params and body required', () => { + type Endpoint = DevupPutApiStruct['openapi.json']['/users/{id}'] + + expectTypeOf().toEqualTypeOf<{ id: string }>() + expectTypeOf().toEqualTypeOf<{ + name: string + email: string + }>() + }) + + test('DELETE - params required, no body', () => { + type Endpoint = DevupDeleteApiStruct['openapi.json']['/users/{id}'] + + expectTypeOf().toEqualTypeOf<{ id: string }>() + expectTypeOf().toEqualTypeOf<{ success: boolean }>() + + // body should not exist + type HasBody = 'body' extends keyof Endpoint ? true : false + expectTypeOf().toEqualTypeOf() + }) +}) + +// ============================================================================= +// DevupObject integration verification +// ============================================================================= + +describe('DevupObject type access', () => { + test('uses response component type', () => { + type User = DevupObject<'response', 'openapi.json'>['User'] + + expectTypeOf().toEqualTypeOf<{ + id: number + name: string + email: string + }>() + }) + + test('uses request component type', () => { + type CreateUserRequest = DevupObject< + 'request', + 'openapi.json' + >['CreateUserRequest'] + + expectTypeOf().toEqualTypeOf<{ + name: string + email: string + }>() + }) + + test('uses error component type', () => { + type ApiError = DevupObject<'error', 'openapi.json'>['ApiError'] + + expectTypeOf().toEqualTypeOf<{ message: string; code: number }>() + }) +}) + +// ============================================================================= +// Type Narrowing verification +// ============================================================================= + +describe('Response type narrowing', () => { + test('narrows to success type with data check', () => { + type Response = DevupApiResponse<{ id: number }, { message: string }> + + // if data exists, narrow to success type + const handleResponse = (res: Response) => { + if (res.data) { + // here res.data is { id: number } + // res.error is undefined + expectTypeOf(res.data).toEqualTypeOf<{ id: number }>() + expectTypeOf(res.error).toEqualTypeOf() + } + } + + expectTypeOf(handleResponse).toBeFunction() + }) + + test('narrows to error type with error check', () => { + type Response = DevupApiResponse<{ id: number }, { message: string }> + + const handleResponse = (res: Response) => { + if (res.error) { + // here res.error is { message: string } + // res.data is undefined + expectTypeOf(res.error).toEqualTypeOf<{ message: string }>() + expectTypeOf(res.data).toEqualTypeOf() + } + } + + expectTypeOf(handleResponse).toBeFunction() + }) +}) + +// ============================================================================= +// Multi-server support verification +// ============================================================================= + +describe('Multi-server type separation', () => { + test('different endpoint types per server', () => { + // openapi.json server + type MainUsers = DevupGetApiStruct['openapi.json']['/users'] + expectTypeOf().toEqualTypeOf< + { id: number; name: string }[] + >() + + // admin-api.json server + type AdminUsers = DevupGetApiStruct['admin-api.json']['/admin/users'] + expectTypeOf().toEqualTypeOf< + { id: number; role: string }[] + >() + }) + + test('API instance type separation per server', () => { + const mainApi = createApi({ + baseUrl: 'https://api.example.com', + serverName: 'openapi.json', + }) + const adminApi = createApi({ + baseUrl: 'https://admin.example.com', + serverName: 'admin-api.json', + }) + + // each accesses different server endpoints + expectTypeOf(mainApi.get).toBeFunction() + expectTypeOf(adminApi.get).toBeFunction() + }) +}) diff --git a/packages/generator/src/__tests__/types.test.ts b/packages/generator/src/__tests__/types.test.ts new file mode 100644 index 0000000..a5b8f4e --- /dev/null +++ b/packages/generator/src/__tests__/types.test.ts @@ -0,0 +1,632 @@ +/** + * Type tests for generator + * Verify that generated type structures are correct + */ +import { describe, expect, test } from 'bun:test' +import type { OpenAPIV3_1 } from 'openapi-types' +import { generateInterface } from '../generate-interface' +import { extractParameters, getTypeFromSchema } from '../generate-schema' + +// ============================================================================= +// Helper +// ============================================================================= + +const createDocument = ( + doc: Partial = {}, +): OpenAPIV3_1.Document => + ({ + openapi: '3.1.0', + info: { title: 'Test API', version: '1.0.0' }, + paths: {}, + ...doc, + }) as OpenAPIV3_1.Document + +// ============================================================================= +// getTypeFromSchema - OpenAPI schema to TypeScript type conversion +// ============================================================================= + +describe('getTypeFromSchema type conversion', () => { + const doc = createDocument() + + test('primitive type conversion', () => { + expect(getTypeFromSchema({ type: 'string' }, doc).type).toBe('string') + expect(getTypeFromSchema({ type: 'number' }, doc).type).toBe('number') + expect(getTypeFromSchema({ type: 'integer' }, doc).type).toBe('number') + expect(getTypeFromSchema({ type: 'boolean' }, doc).type).toBe('boolean') + }) + + test('enum to union type conversion', () => { + const schema: OpenAPIV3_1.SchemaObject = { + type: 'string', + enum: ['active', 'inactive', 'pending'], + } + const result = getTypeFromSchema(schema, doc) + + expect(result.type).toBe('"active" | "inactive" | "pending"') + }) + + test('array type conversion', () => { + const schema: OpenAPIV3_1.SchemaObject = { + type: 'array', + items: { type: 'string' }, + } + const result = getTypeFromSchema(schema, doc) + + expect(result.type).toEqual({ __isArray: true, items: 'string' }) + }) + + test('object type conversion - required field distinction', () => { + const schema: OpenAPIV3_1.SchemaObject = { + type: 'object', + properties: { + id: { type: 'integer' }, + name: { type: 'string' }, + email: { type: 'string' }, + }, + required: ['id', 'name'], + } + const result = getTypeFromSchema(schema, doc) + + // required fields have no ?, optional fields have ? + expect(result.type).toHaveProperty('id') + expect(result.type).toHaveProperty('name') + expect(result.type).toHaveProperty('email?') + }) + + test('$ref resolution', () => { + const docWithRef = createDocument({ + components: { + schemas: { + User: { + type: 'object', + properties: { + id: { type: 'integer' }, + }, + }, + }, + }, + }) + + const schema: OpenAPIV3_1.ReferenceObject = { + $ref: '#/components/schemas/User', + } + const result = getTypeFromSchema(schema, docWithRef) + + expect(result.type).toHaveProperty('id?') + }) + + test('allOf to intersection type', () => { + const docWithSchemas = createDocument({ + components: { + schemas: { + Base: { + type: 'object', + properties: { id: { type: 'integer' } }, + }, + }, + }, + }) + + const schema: OpenAPIV3_1.SchemaObject = { + allOf: [ + { $ref: '#/components/schemas/Base' }, + { type: 'object', properties: { name: { type: 'string' } } }, + ], + } + const result = getTypeFromSchema(schema, docWithSchemas) + + expect(String(result.type)).toContain('&') + }) + + test('oneOf/anyOf to union type', () => { + const schema: OpenAPIV3_1.SchemaObject = { + oneOf: [{ type: 'string' }, { type: 'number' }], + } + const result = getTypeFromSchema(schema, doc) + + expect(String(result.type)).toContain('|') + }) +}) + +// ============================================================================= +// extractParameters - Parameter extraction +// ============================================================================= + +describe('extractParameters parameter classification', () => { + const doc = createDocument() + + test('separates path/query/header parameters', () => { + const operation: OpenAPIV3_1.OperationObject = { + parameters: [ + { + name: 'userId', + in: 'path', + required: true, + schema: { type: 'string' }, + }, + { + name: 'page', + in: 'query', + required: false, + schema: { type: 'integer' }, + }, + { + name: 'Authorization', + in: 'header', + required: true, + schema: { type: 'string' }, + }, + ], + responses: {}, + } + + const result = extractParameters(undefined, operation, doc) + + expect(result.pathParams).toHaveProperty('userId') + expect(result.queryParams).toHaveProperty('page') + expect(result.headerParams).toHaveProperty('Authorization') + }) + + test('preserves required status', () => { + const operation: OpenAPIV3_1.OperationObject = { + parameters: [ + { name: 'id', in: 'path', required: true, schema: { type: 'string' } }, + { + name: 'filter', + in: 'query', + required: false, + schema: { type: 'string' }, + }, + ], + responses: {}, + } + + const result = extractParameters(undefined, operation, doc) + + expect(result.pathParams.id.required).toBe(true) + expect(result.queryParams.filter.required).toBe(false) + }) +}) + +// ============================================================================= +// generateInterface - Interface generation verification +// ============================================================================= + +describe('generateInterface structure verification', () => { + test('generates module augmentation', () => { + const result = generateInterface({ 'openapi.json': createDocument() }) + + expect(result).toContain('import "@devup-api/fetch"') + expect(result).toContain('declare module "@devup-api/fetch"') + expect(result).toContain('interface DevupApiServers') + }) + + test('GET endpoint to DevupGetApiStruct', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { type: 'array', items: { type: 'object' } }, + }, + }, + }, + }, + }, + }, + }, + }), + }) + + expect(result).toContain('interface DevupGetApiStruct') + expect(result).toContain('getUsers') + expect(result).toContain('/users') + }) + + test('POST endpoint to DevupPostApiStruct with body', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + post: { + operationId: 'createUser', + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + name: { type: 'string' }, + email: { type: 'string' }, + }, + }, + }, + }, + }, + responses: { '201': { description: 'Created' } }, + }, + }, + }, + }), + }) + + expect(result).toContain('interface DevupPostApiStruct') + expect(result).toContain('createUser') + expect(result).toContain('body') + }) + + test('path parameter to params generation', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users/{userId}': { + get: { + operationId: 'getUserById', + parameters: [ + { + name: 'userId', + in: 'path', + required: true, + schema: { type: 'string' }, + }, + ], + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }) + + expect(result).toContain('params') + expect(result).toContain('userId') + }) + + test('query parameter to query generation', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + parameters: [ + { + name: 'page', + in: 'query', + required: false, + schema: { type: 'integer' }, + }, + { + name: 'limit', + in: 'query', + required: true, + schema: { type: 'integer' }, + }, + ], + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }) + + expect(result).toContain('query') + expect(result).toContain('page') + expect(result).toContain('limit') + }) + + test('error response to error type generation', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + responses: { + '200': { description: 'Success' }, + '400': { + description: 'Bad Request', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { message: { type: 'string' } }, + }, + }, + }, + }, + }, + }, + }, + }, + }), + }) + + expect(result).toContain('error') + }) +}) + +// ============================================================================= +// DevupObject reference generation verification +// ============================================================================= + +describe('DevupObject reference generation', () => { + test('response $ref to DevupObject reference', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/User' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + User: { + type: 'object', + properties: { id: { type: 'integer' } }, + }, + }, + }, + }), + }) + + expect(result).toContain("DevupObject<'response', 'openapi.json'>['User']") + expect(result).toContain('interface DevupResponseComponentStruct') + }) + + test('request $ref to DevupObject reference', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + post: { + operationId: 'createUser', + requestBody: { + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/CreateUserRequest' }, + }, + }, + }, + responses: { '201': { description: 'Created' } }, + }, + }, + }, + components: { + schemas: { + CreateUserRequest: { + type: 'object', + properties: { name: { type: 'string' } }, + }, + }, + }, + }), + }) + + expect(result).toContain( + "DevupObject<'request', 'openapi.json'>['CreateUserRequest']", + ) + expect(result).toContain('interface DevupRequestComponentStruct') + }) + + test('error $ref to DevupObject reference', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + responses: { + '200': { description: 'Success' }, + '400': { + description: 'Error', + content: { + 'application/json': { + schema: { $ref: '#/components/schemas/ApiError' }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + ApiError: { + type: 'object', + properties: { message: { type: 'string' } }, + }, + }, + }, + }), + }) + + expect(result).toContain("DevupObject<'error', 'openapi.json'>['ApiError']") + expect(result).toContain('interface DevupErrorComponentStruct') + }) + + test('array response $ref to Array reference', () => { + const result = generateInterface({ + 'openapi.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + responses: { + '200': { + description: 'Success', + content: { + 'application/json': { + schema: { + type: 'array', + items: { $ref: '#/components/schemas/User' }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + User: { + type: 'object', + properties: { id: { type: 'integer' } }, + }, + }, + }, + }), + }) + + expect(result).toContain( + "Array['User']>", + ) + }) +}) + +// ============================================================================= +// Case conversion verification +// ============================================================================= + +describe('Case conversion', () => { + test('camelCase conversion', () => { + const result = generateInterface( + { + 'openapi.json': createDocument({ + paths: { + '/users/{user_id}': { + get: { + operationId: 'get_user_by_id', + parameters: [ + { + name: 'user_id', + in: 'path', + required: true, + schema: { type: 'string' }, + }, + ], + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }, + { convertCase: 'camel' }, + ) + + expect(result).toContain('getUserById') + expect(result).toContain('userId') + }) + + test('snake_case conversion', () => { + const result = generateInterface( + { + 'openapi.json': createDocument({ + paths: { + '/users/{userId}': { + get: { + operationId: 'getUserById', + parameters: [ + { + name: 'userId', + in: 'path', + required: true, + schema: { type: 'string' }, + }, + ], + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }, + { convertCase: 'snake' }, + ) + + expect(result).toContain('get_user_by_id') + expect(result).toContain('user_id') + }) + + test('PascalCase conversion', () => { + const result = generateInterface( + { + 'openapi.json': createDocument({ + paths: { + '/users/{user_id}': { + get: { + operationId: 'get_user', + parameters: [ + { + name: 'user_id', + in: 'path', + required: true, + schema: { type: 'string' }, + }, + ], + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }, + { convertCase: 'pascal' }, + ) + + expect(result).toContain('GetUser') + expect(result).toContain('UserId') + }) +}) + +// ============================================================================= +// Multi-server support verification +// ============================================================================= + +describe('Multi-server support', () => { + test('processes multiple server files', () => { + const result = generateInterface({ + 'main-api.json': createDocument({ + paths: { + '/users': { + get: { + operationId: 'getUsers', + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + 'admin-api.json': createDocument({ + paths: { + '/admin/users': { + get: { + operationId: 'getAdminUsers', + responses: { '200': { description: 'Success' } }, + }, + }, + }, + }), + }) + + expect(result).toContain('main-api.json') + expect(result).toContain('admin-api.json') + expect(result).toContain('getUsers') + expect(result).toContain('getAdminUsers') + }) +}) diff --git a/packages/react-query/setup.ts b/packages/react-query/setup.ts deleted file mode 100644 index 08f52f4..0000000 --- a/packages/react-query/setup.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { beforeAll } from 'bun:test' - -// Setup DOM environment for React testing -if (typeof globalThis.document === 'undefined') { - // @ts-expect-error - happy-dom types - const { Window } = await import('happy-dom') - const window = new Window() - const document = window.document - - // @ts-expect-error - setting global document - globalThis.window = window - // @ts-expect-error - setting global document - globalThis.document = document - // @ts-expect-error - setting global navigator - globalThis.navigator = window.navigator - // @ts-expect-error - setting global HTMLElement - globalThis.HTMLElement = window.HTMLElement -} - -beforeAll(() => { - // Ensure DOM is ready - if (globalThis.document) { - const root = globalThis.document.createElement('div') - root.id = 'root' - globalThis.document.body.appendChild(root) - } -}) From a43d5e5a1768d8084b58dec6cb09b38a4683d47f Mon Sep 17 00:00:00 2001 From: owjs3901 Date: Sun, 4 Jan 2026 18:16:58 +0900 Subject: [PATCH 2/2] Add typing test --- packages/fetch/src/__tests__/api.test.ts | 24 ++++----- packages/fetch/src/api.ts | 65 ++++++++++++----------- packages/generator/src/generate-schema.ts | 3 -- 3 files changed, 44 insertions(+), 48 deletions(-) diff --git a/packages/fetch/src/__tests__/api.test.ts b/packages/fetch/src/__tests__/api.test.ts index 14658c1..45808a5 100644 --- a/packages/fetch/src/__tests__/api.test.ts +++ b/packages/fetch/src/__tests__/api.test.ts @@ -423,10 +423,7 @@ test('onError middleware is called when onResponse is not defined and error exis const api = new DevupApi('https://api.example.com', undefined, 'openapi.json') let errorMiddlewareCalled = false - // onError is only called when onResponse is not defined and error exists - // The condition is: if (response && middleware.onResponse) - if onResponse is not defined, the block doesn't execute - // So onError is never called in the current implementation - // This test verifies the middleware structure exists + // onError is called when there's an error and no onResponse handler returned a result api.use({ onError: async ({ error }) => { errorMiddlewareCalled = true @@ -437,9 +434,8 @@ test('onError middleware is called when onResponse is not defined and error exis await api.get('/test' as never) - // Note: onError is not called because the condition requires response && middleware.onResponse - // If onResponse is not defined, the entire block is skipped - expect(errorMiddlewareCalled).toBe(false) + // onError should be called when there's an error response (404) + expect(errorMiddlewareCalled).toBe(true) }) test('onError middleware can return Error', async () => { @@ -455,7 +451,7 @@ test('onError middleware can return Error', async () => { const api = new DevupApi('https://api.example.com', undefined, 'openapi.json') const customError = new Error('Custom error from middleware') - // onError is registered but won't be called due to the condition check + // onError returns a custom Error to replace the original error api.use({ onError: async () => customError, }) @@ -466,9 +462,8 @@ test('onError middleware can return Error', async () => { response: Response } - // Since onError is not called, error comes from convertResponse - expect(result.error).toBeDefined() - expect(result.error).not.toBe(customError) + // onError is called and returns the custom error + expect(result.error).toBe(customError) }) test('onError middleware can return Response', async () => { @@ -487,7 +482,7 @@ test('onError middleware can return Response', async () => { headers: { 'Content-Type': 'application/json' }, }) - // onError is registered but won't be called due to the condition check + // onError returns a recovery Response to replace the error api.use({ onError: async () => recoveryResponse, }) @@ -498,9 +493,8 @@ test('onError middleware can return Response', async () => { response: Response } - // Since onError is not called, response comes from convertResponse - expect(result.response).toBeDefined() - expect(result.response).not.toBe(recoveryResponse) + // onError is called and returns the recovery response + expect(result.response).toBe(recoveryResponse) }) test('middleware can be passed in request options', async () => { diff --git a/packages/fetch/src/api.ts b/packages/fetch/src/api.ts index 0867c0c..5719940 100644 --- a/packages/fetch/src/api.ts +++ b/packages/fetch/src/api.ts @@ -292,36 +292,41 @@ export class DevupApi> { let error: unknown = ret.error for (const middleware of finalMiddleware) { - if (response && middleware.onResponse) { - const result = await (response && middleware.onResponse - ? middleware.onResponse({ - request, - schemaPath: url, - params: requestOptions.params, - query: requestOptions.query, - headers: requestOptions.headers, - body: requestOptions.body, - response: ret.response, - }) - : error && middleware.onError - ? middleware.onError({ - request, - schemaPath: url, - params: requestOptions.params, - query: requestOptions.query, - headers: requestOptions.headers, - body: requestOptions.body, - error: ret.error, - }) - : undefined) - if (result) { - if (result instanceof Response) { - response = result - break - } else if (result instanceof Error) { - error = result - break - } + const middlewareParams = { + request, + schemaPath: url, + params: requestOptions.params, + query: requestOptions.query, + headers: requestOptions.headers, + body: requestOptions.body, + } + + let result: Response | Error | undefined + + // Call onResponse if it exists + if (middleware.onResponse) { + result = await middleware.onResponse({ + ...middlewareParams, + response: ret.response, + }) + } + + // Call onError if there's an error and onResponse didn't return a result + if (!result && error && middleware.onError) { + result = await middleware.onError({ + ...middlewareParams, + error: ret.error, + }) + } + + if (result) { + if (result instanceof Response) { + response = result + break + } + if (result instanceof Error) { + error = result + break } } } diff --git a/packages/generator/src/generate-schema.ts b/packages/generator/src/generate-schema.ts index 379c74a..50d15cc 100644 --- a/packages/generator/src/generate-schema.ts +++ b/packages/generator/src/generate-schema.ts @@ -91,9 +91,6 @@ export function getTypeFromSchema( // Handle primitive types if (schemaObj.type === 'string') { - if (schemaObj.format === 'date' || schemaObj.format === 'date-time') { - return { type: 'string', default: schemaObj.default } - } return { type: 'string', default: schemaObj.default } }