diff --git a/package-lock.json b/package-lock.json index 807bb8a3..c4bba23e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,7 +15,7 @@ "devDependencies": { "@semantic-release-extras/github.amrom.workers.devment-specific": "1.0.7", "@semantic-release/npm": "12.0.1", - "@types/node": "22.0.0", + "@types/node": "22.1.0", "multi-semantic-release": "3.0.2", "patch-package": "8.0.0", "typescript": "4.6.4" @@ -618,6 +618,40 @@ "node": ">= 8" } }, + "node_modules/@npmcli/agent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-2.2.2.tgz", + "integrity": "sha512-OrcNPXdpSl9UX7qPVRWbmWMCSXrcDa2M9DvrbOTj7ao1S4PlqVFYv9/yLKMkrJKZ/V5A/kDBC690or307i26Og==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.0", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.1", + "lru-cache": "^10.0.1", + "socks-proxy-agent": "^8.0.3" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/@npmcli/agent/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/@npmcli/fs": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-3.1.1.tgz", + "integrity": "sha512-q9CRWjpHCMIh5sVyefoD1cA7PkvILqCZsnSOEUUivORLjxCO/Irmue2DprETiNgEqktDBZaM1Bi+jrarx1XdCg==", + "dev": true, + "dependencies": { + "semver": "^7.3.5" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/@octokit/auth-token": { "version": "3.0.4", "resolved": "https://registry.npmjs.org/@octokit/auth-token/-/auth-token-3.0.4.tgz", @@ -1904,11 +1938,11 @@ "dev": true }, "node_modules/@types/node": { - "version": "22.0.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.0.0.tgz", - "integrity": "sha512-VT7KSYudcPOzP5Q0wfbowyNLaVR8QWUdw+088uFWwfvpY6uCWaXpqV6ieLAu9WBcnTa7H4Z5RLK8I5t2FuOcqw==", + "version": "22.1.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.1.0.tgz", + "integrity": "sha512-AOmuRF0R2/5j1knA3c6G3HOk523Ga+l+ZXltX8SF1+5oqcXijjfTd8fY3XRZqSihEu9XhtQnKYLmkFaoxgsJHw==", "dependencies": { - "undici-types": "~6.11.1" + "undici-types": "~6.13.0" } }, "node_modules/@types/normalize-package-data": { @@ -2001,6 +2035,15 @@ "integrity": "sha512-GpSwvyXOcOOlV70vbnzjj4fW5xW/FdUF6nQEt1ENy7m4ZCczi1+/buVUPAqmGfqznsORNFzUMjctTIp8a9tuCQ==", "dev": true }, + "node_modules/abbrev": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-2.0.0.tgz", + "integrity": "sha512-6/mh1E2u2YgEsCHdY0Yx5oW+61gZU+1vXaoiHHrpKeuRNNgFvS+/jrwHiQhB5apAf5oB7UB7E19ol2R2LKH8hQ==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/accepts": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", @@ -2014,9 +2057,9 @@ } }, "node_modules/agent-base": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.0.tgz", - "integrity": "sha512-o/zjMZRhJxny7OyEF+Op8X+efiELC7k7yOjMzgfzVqOzXqkBkWI79YoTdOtsuWd5BWhAGAuOY/Xa6xpiaWXiNg==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.1.tgz", + "integrity": "sha512-H0TSyFNDMomMNJQBn8wFV5YC/2eJ+VXECwOadZJT554xP6cODZHPX3H9QMQECxvrgiSOP1pHjy1sMWQVYJOUOA==", "dev": true, "dependencies": { "debug": "^4.3.4" @@ -2297,6 +2340,94 @@ } } }, + "node_modules/cacache": { + "version": "18.0.4", + "resolved": "https://registry.npmjs.org/cacache/-/cacache-18.0.4.tgz", + "integrity": "sha512-B+L5iIa9mgcjLbliir2th36yEwPftrzteHYujzsx3dFP/31GCHcIeS8f5MGd80odLOjaOvSpU3EEAmRQptkxLQ==", + "dev": true, + "dependencies": { + "@npmcli/fs": "^3.1.0", + "fs-minipass": "^3.0.0", + "glob": "^10.2.2", + "lru-cache": "^10.0.1", + "minipass": "^7.0.3", + "minipass-collect": "^2.0.1", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "p-map": "^4.0.0", + "ssri": "^10.0.0", + "tar": "^6.1.11", + "unique-filename": "^3.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/cacache/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/cacache/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/cacache/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/cacache/node_modules/p-map": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", + "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", + "dev": true, + "dependencies": { + "aggregate-error": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/call-bind": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.2.tgz", @@ -2382,6 +2513,15 @@ "node": ">=10" } }, + "node_modules/chownr": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", + "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", + "dev": true, + "engines": { + "node": ">=10" + } + }, "node_modules/ci-info": { "version": "3.8.0", "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.8.0.tgz", @@ -3167,6 +3307,29 @@ "node": ">= 0.8" } }, + "node_modules/encoding": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.13.tgz", + "integrity": "sha512-ETBauow1T35Y/WZMkio9jiM0Z5xjHHmJ4XmjZOq1l/dXz3lr2sRn87nJy20RupqSh1F2m3HHPSp8ShIPQJrJ3A==", + "dev": true, + "optional": true, + "dependencies": { + "iconv-lite": "^0.6.2" + } + }, + "node_modules/encoding/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dev": true, + "optional": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/end-of-stream": { "version": "1.4.4", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", @@ -3195,11 +3358,16 @@ "resolved": "https://registry.npmjs.org/env-paths/-/env-paths-2.2.1.tgz", "integrity": "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A==", "dev": true, - "peer": true, "engines": { "node": ">=6" } }, + "node_modules/err-code": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/err-code/-/err-code-2.0.3.tgz", + "integrity": "sha512-2bmlRpNKBxT/CRmPOlyISQpNj+qSeYvcym/uT0Jx2bMOlKLtSy1ZmLuVxSEKKyor/N5yhvp/ZiG1oE3DEYMSFA==", + "dev": true + }, "node_modules/error-ex": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", @@ -3306,6 +3474,12 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/exponential-backoff": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/exponential-backoff/-/exponential-backoff-3.1.1.tgz", + "integrity": "sha512-dX7e/LHVJ6W3DE1MHWi9S1EYzDESENfLrYohG2G++ovZrYOkm4Knwa0mc1cn84xJOR4KEU0WSchhLbd0UklbHw==", + "dev": true + }, "node_modules/express": { "version": "4.19.2", "resolved": "https://registry.npmjs.org/express/-/express-4.19.2.tgz", @@ -3624,6 +3798,18 @@ "node": ">=14.14" } }, + "node_modules/fs-minipass": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-3.0.3.tgz", + "integrity": "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/fs.realpath": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", @@ -3876,6 +4062,12 @@ "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", "dev": true }, + "node_modules/http-cache-semantics": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.1.1.tgz", + "integrity": "sha512-er295DKPVsV82j5kw1Gjt+ADA/XYHsajl82cGNQG2eyoPkvgUhX+nDIyelzhIWbbsXP39EHcI6l5tYs2FYqYXQ==", + "dev": true + }, "node_modules/http-errors": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", @@ -4009,6 +4201,15 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "engines": { + "node": ">=0.8.19" + } + }, "node_modules/indent-string": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", @@ -4051,6 +4252,16 @@ "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", "dev": true }, + "node_modules/install-artifact-from-github": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/install-artifact-from-github/-/install-artifact-from-github-1.3.5.tgz", + "integrity": "sha512-gZHC7f/cJgXz7MXlHFBxPVMsvIbev1OQN1uKQYKVJDydGNm9oYf9JstbU4Atnh/eSvk41WtEovoRm+8IF686xg==", + "dev": true, + "bin": { + "install-from-cache": "bin/install-from-cache.js", + "save-to-github-cache": "bin/save-to-github-cache.js" + } + }, "node_modules/into-stream": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/into-stream/-/into-stream-7.0.0.tgz", @@ -4087,6 +4298,25 @@ "newtype-ts": "^0.3.2" } }, + "node_modules/ip-address": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-9.0.5.tgz", + "integrity": "sha512-zHtQzGojZXTwZTHQqra+ETKd4Sn3vgi7uBmlPoXVWZqYvuKmtI0l/VZTjqGmJY9x88GGOaZ9+G9ES8hC4T4X8g==", + "dev": true, + "dependencies": { + "jsbn": "1.1.0", + "sprintf-js": "^1.1.3" + }, + "engines": { + "node": ">= 12" + } + }, + "node_modules/ip-address/node_modules/sprintf-js": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.1.3.tgz", + "integrity": "sha512-Oo+0REFV59/rz3gfJNKQiBlwfHaSESl1pcGyABQsnnIfWOFt6JNj5gCog2U6MLZ//IGYD+nA8nI+mTShREReaA==", + "dev": true + }, "node_modules/ipaddr.js": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", @@ -4157,6 +4387,12 @@ "node": ">=0.10.0" } }, + "node_modules/is-lambda": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/is-lambda/-/is-lambda-1.0.1.tgz", + "integrity": "sha512-z7CMFGNrENq5iFB9Bqo64Xk6Y9sg+epq1myIcdHaGnbMTYOxvzsEtdYqQUylB7LxfkvgrrjP32T6Ywciio9UIQ==", + "dev": true + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -4369,6 +4605,12 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsbn": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-1.1.0.tgz", + "integrity": "sha512-4bYVV3aAMtDTTu4+xsDYa6sy9GyJ69/amsu9sYF2zqjiEoZA5xJi3BrfX3uY+/IekIu7MwdObdbDWpoZdBv3/A==", + "dev": true + }, "node_modules/json-parse-better-errors": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/json-parse-better-errors/-/json-parse-better-errors-1.0.2.tgz", @@ -4596,6 +4838,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/make-fetch-happen": { + "version": "13.0.1", + "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-13.0.1.tgz", + "integrity": "sha512-cKTUFc/rbKUd/9meOvgrpJ2WrNzymt6jfRDdwg5UCnVzv9dTpEj9JS5m3wtziXVCjluIXyL8pcaukYqezIzZQA==", + "dev": true, + "dependencies": { + "@npmcli/agent": "^2.0.0", + "cacache": "^18.0.0", + "http-cache-semantics": "^4.1.1", + "is-lambda": "^1.0.1", + "minipass": "^7.0.2", + "minipass-fetch": "^3.0.0", + "minipass-flush": "^1.0.5", + "minipass-pipeline": "^1.2.4", + "negotiator": "^0.6.3", + "proc-log": "^4.2.0", + "promise-retry": "^2.0.1", + "ssri": "^10.0.0" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, "node_modules/map-obj": { "version": "4.3.0", "resolved": "https://registry.npmjs.org/map-obj/-/map-obj-4.3.0.tgz", @@ -4852,16 +5117,154 @@ "kind-of": "^6.0.3" }, "engines": { - "node": ">= 6" + "node": ">= 6" + } + }, + "node_modules/minipass": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", + "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-collect": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", + "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/minipass-fetch": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-3.0.5.tgz", + "integrity": "sha512-2N8elDQAtSnFV0Dk7gt15KHsS0Fyz6CbYZ360h0WTYV1Ty46li3rAXVOQj1THMNLdmrD9Vt5pBPtWtVkpwGBqg==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3", + "minipass-sized": "^1.0.3", + "minizlib": "^2.1.2" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + }, + "optionalDependencies": { + "encoding": "^0.1.13" + } + }, + "node_modules/minipass-flush": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/minipass-flush/-/minipass-flush-1.0.5.tgz", + "integrity": "sha512-JmQSYYpPUqX5Jyn1mXaRwOda1uQ8HP5KAT/oDSLCzt1BYRhQU0/hDtsB1ufZfEEzMZ9aAVmsBw8+FWsIXlClWw==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minipass-flush/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/minipass-pipeline/-/minipass-pipeline-1.2.4.tgz", + "integrity": "sha512-xuIq7cIOt09RPRJ19gdi4b+RiNvDFYe5JH+ggNvBqGqpQXcru3PcRmOZuHBKWK1Txf9+cQ+HMVN4d6z46LZP7A==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-pipeline/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/minipass-sized/-/minipass-sized-1.0.3.tgz", + "integrity": "sha512-MbkQQ2CTiBMlA2Dm/5cY+9SWFEN8pzzOXi6rlM5Xxq0Yqbda5ZQy9sU75a673FE9ZK0Zsbr6Y5iP6u9nktfg2g==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minipass-sized/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/minizlib": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", + "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/minizlib/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" } }, - "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "node_modules/mkdirp": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", + "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", "dev": true, + "bin": { + "mkdirp": "bin/cmd.js" + }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": ">=10" } }, "node_modules/modify-values": { @@ -8243,6 +8646,12 @@ "thenify-all": "^1.0.0" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "dev": true + }, "node_modules/negotiator": { "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", @@ -8324,6 +8733,113 @@ "webidl-conversions": "^3.0.0" } }, + "node_modules/node-gyp": { + "version": "10.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-10.2.0.tgz", + "integrity": "sha512-sp3FonBAaFe4aYTcFdZUn2NYkbP7xroPGYvQmP4Nl5PxamznItBnNCgjrVTKrEfQynInMsJvZrdmqUnysCJ8rw==", + "dev": true, + "dependencies": { + "env-paths": "^2.2.0", + "exponential-backoff": "^3.1.1", + "glob": "^10.3.10", + "graceful-fs": "^4.2.6", + "make-fetch-happen": "^13.0.0", + "nopt": "^7.0.0", + "proc-log": "^4.1.0", + "semver": "^7.3.5", + "tar": "^6.2.1", + "which": "^4.0.0" + }, + "bin": { + "node-gyp": "bin/node-gyp.js" + }, + "engines": { + "node": "^16.14.0 || >=18.0.0" + } + }, + "node_modules/node-gyp/node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/node-gyp/node_modules/glob": { + "version": "10.4.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", + "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "dev": true, + "dependencies": { + "foreground-child": "^3.1.0", + "jackspeak": "^3.1.2", + "minimatch": "^9.0.4", + "minipass": "^7.1.2", + "package-json-from-dist": "^1.0.0", + "path-scurry": "^1.11.1" + }, + "bin": { + "glob": "dist/esm/bin.mjs" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/isexe": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", + "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", + "dev": true, + "engines": { + "node": ">=16" + } + }, + "node_modules/node-gyp/node_modules/minimatch": { + "version": "9.0.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", + "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/node-gyp/node_modules/which": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/which/-/which-4.0.0.tgz", + "integrity": "sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==", + "dev": true, + "dependencies": { + "isexe": "^3.1.1" + }, + "bin": { + "node-which": "bin/which.js" + }, + "engines": { + "node": "^16.13.0 || >=18.0.0" + } + }, + "node_modules/nopt": { + "version": "7.2.1", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-7.2.1.tgz", + "integrity": "sha512-taM24ViiimT/XntxbPyJQzCG+p4EKOpgD3mxFwW38mGjVUrfERQOeY4EDHjdnptttfHuHQXFx+lTP08Q+mLa/w==", + "dev": true, + "dependencies": { + "abbrev": "^2.0.0" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/normalize-package-data": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-3.0.3.tgz", @@ -11392,6 +11908,12 @@ "node": ">=6" } }, + "node_modules/package-json-from-dist": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", + "integrity": "sha512-dATvCeZN/8wQsGywez1mzHtTlP22H8OEfPrVMLNr4/eGa+ijtLn/6M5f0dY8UKNrC2O9UCU6SSoG3qRKnt7STw==", + "dev": true + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -11728,6 +12250,15 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/proc-log": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-4.2.0.tgz", + "integrity": "sha512-g8+OnU/L2v+wyiVK+D5fA34J7EH8jZ8DDlvwhRCMxmMj7UCBvxiO1mGeN+36JXIKF4zevU4kRBd8lVgG9vLelA==", + "dev": true, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -11743,6 +12274,19 @@ "node": ">=8.0.0" } }, + "node_modules/promise-retry": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/promise-retry/-/promise-retry-2.0.1.tgz", + "integrity": "sha512-y+WKFlBR8BGXnsNlIHFGPZmyDf3DFMoLhaflAnyZgV6rG6xu+JwesTo2Q9R6XwYmtmwAFCkAk3e35jEdoeh/3g==", + "dev": true, + "dependencies": { + "err-code": "^2.0.2", + "retry": "^0.12.0" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/proto-list": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", @@ -11869,6 +12413,18 @@ "rc": "cli.js" } }, + "node_modules/re2": { + "version": "1.21.3", + "resolved": "https://registry.npmjs.org/re2/-/re2-1.21.3.tgz", + "integrity": "sha512-GI+KoGkHT4kxTaX+9p0FgNB1XUnCndO9slG5qqeEoZ7kbf6Dk6ohQVpmwKVeSp7LPLn+g6Q3BaCopz4oHuBDuQ==", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "install-artifact-from-github": "^1.3.5", + "nan": "^2.20.0", + "node-gyp": "^10.1.0" + } + }, "node_modules/read-package-up": { "version": "11.0.0", "resolved": "https://registry.npmjs.org/read-package-up/-/read-package-up-11.0.0.tgz", @@ -12222,6 +12778,15 @@ "node": ">=4" } }, + "node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "engines": { + "node": ">= 4" + } + }, "node_modules/reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -13159,6 +13724,44 @@ "node": ">=8" } }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.3.tgz", + "integrity": "sha512-l5x7VUUWbjVFbafGLxPWkYsHIhEvmF85tbIeFZWc8ZPtoMyybuEhL7Jye/ooC4/d48FgOjSJXgsF/AJPYCW8Zw==", + "dev": true, + "dependencies": { + "ip-address": "^9.0.5", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.4.tgz", + "integrity": "sha512-GNAq/eg8Udq2x0eNiFkr9gRg5bA7PXEWagQdeRX4cPSG+X/8V38v637gim9bjFptMk1QWsCTr0ttrJEiXbNnRw==", + "dev": true, + "dependencies": { + "agent-base": "^7.1.1", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, "node_modules/source-map": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", @@ -13243,6 +13846,18 @@ "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", "dev": true }, + "node_modules/ssri": { + "version": "10.0.6", + "resolved": "https://registry.npmjs.org/ssri/-/ssri-10.0.6.tgz", + "integrity": "sha512-MGrFH9Z4NP9Iyhqn16sDtBpRRNJ0Y2hNa6D65h736fVSaPCHr4DM4sWUNvVaSuC+0OBGhwsrydQwmgfg5LncqQ==", + "dev": true, + "dependencies": { + "minipass": "^7.0.3" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -13435,9 +14050,9 @@ } }, "node_modules/superagent": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", - "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-10.0.0.tgz", + "integrity": "sha512-Lm/+H7sEL6Ci/U0lx7Gn67A9x7+kA0OHBT9J6F6kK5r8DiuJiemG6qMqz0pjsmw4NOOxPuIEKLE8GE4vLtiomg==", "dev": true, "dependencies": { "component-emitter": "^1.3.0", @@ -13448,7 +14063,8 @@ "formidable": "^3.5.1", "methods": "^1.1.2", "mime": "2.6.0", - "qs": "^6.11.0" + "qs": "^6.11.0", + "re2": "^1.21.3" }, "engines": { "node": ">=14.18.0" @@ -13479,6 +14095,38 @@ "node": ">=14.18.0" } }, + "node_modules/supertest/node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/supertest/node_modules/superagent": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/superagent/-/superagent-9.0.2.tgz", + "integrity": "sha512-xuW7dzkUpcJq7QnhOsnNUgtYp3xRwpt2F7abdRYIpCsAt0hhUqia0EdxyXZQQpNmGtsCzYHryaKSV3q3GJnq7w==", + "dev": true, + "dependencies": { + "component-emitter": "^1.3.0", + "cookiejar": "^2.1.4", + "debug": "^4.3.4", + "fast-safe-stringify": "^2.1.1", + "form-data": "^4.0.0", + "formidable": "^3.5.1", + "methods": "^1.1.2", + "mime": "2.6.0", + "qs": "^6.11.0" + }, + "engines": { + "node": ">=14.18.0" + } + }, "node_modules/supports-color": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", @@ -13514,6 +14162,56 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/tar": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", + "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", + "dev": true, + "dependencies": { + "chownr": "^2.0.0", + "fs-minipass": "^2.0.0", + "minipass": "^5.0.0", + "minizlib": "^2.1.1", + "mkdirp": "^1.0.3", + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tar/node_modules/fs-minipass": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", + "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", + "dev": true, + "dependencies": { + "minipass": "^3.0.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/tar/node_modules/fs-minipass/node_modules/minipass": { + "version": "3.3.6", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", + "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", + "dev": true, + "dependencies": { + "yallist": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tar/node_modules/minipass": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", + "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, "node_modules/temp-dir": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/temp-dir/-/temp-dir-3.0.0.tgz", @@ -13823,9 +14521,9 @@ } }, "node_modules/undici-types": { - "version": "6.11.1", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.11.1.tgz", - "integrity": "sha512-mIDEX2ek50x0OlRgxryxsenE5XaQD4on5U2inY7RApK3SOJpofyw7uW2AyfMKkhAxXIceo2DeWGVGwyvng1GNQ==" + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.13.0.tgz", + "integrity": "sha512-xtFJHudx8S2DSoujjMd1WeWvn7KKWFRESZTMeL1RptAYERu29D6jphMjjY+vn96jvN3kVPDNxU/E13VTaXj6jg==" }, "node_modules/unicode-emoji-modifier-base": { "version": "1.0.0", @@ -13850,6 +14548,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/unique-filename": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", + "integrity": "sha512-afXhuC55wkAmZ0P18QsVE6kp8JaxrEokN2HGIoIVv2ijHQd419H0+6EigAFcIzXeMIkcIkNBpB3L/DXB3cTS/g==", + "dev": true, + "dependencies": { + "unique-slug": "^4.0.0" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, + "node_modules/unique-slug": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-4.0.0.tgz", + "integrity": "sha512-WrcA6AyEfqDX5bWige/4NQfPZMtASNVxdmWR76WESYQVAACSgWcR6e9i0mofqqBxYFtL4oAxPIptY73/0YE1DQ==", + "dev": true, + "dependencies": { + "imurmurhash": "^0.1.4" + }, + "engines": { + "node": "^14.17.0 || ^16.13.0 || >=18.0.0" + } + }, "node_modules/unique-string": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", @@ -14334,14 +15056,14 @@ "devDependencies": { "@swc-node/register": "1.10.9", "@types/express": "4.17.21", - "@types/node": "22.0.0", + "@types/node": "22.1.0", "@types/superagent": "8.1.8", "@types/supertest": "6.0.2", "@types/whatwg-url": "11.0.5", "c8": "10.1.2", "express": "4.19.2", "io-ts-types": "0.5.19", - "superagent": "9.0.2", + "superagent": "10.0.0", "supertest": "7.0.0", "typescript": "4.7.4" }, diff --git a/package.json b/package.json index 2c244adf..b0612a84 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,7 @@ "devDependencies": { "@semantic-release-extras/github.amrom.workers.devment-specific": "1.0.7", "@semantic-release/npm": "12.0.1", - "@types/node": "22.0.0", + "@types/node": "22.1.0", "multi-semantic-release": "3.0.2", "patch-package": "8.0.0", "typescript": "4.6.4" diff --git a/packages/io-ts-http/src/combinators.ts b/packages/io-ts-http/src/combinators.ts index 180fb927..1990e206 100644 --- a/packages/io-ts-http/src/combinators.ts +++ b/packages/io-ts-http/src/combinators.ts @@ -101,12 +101,12 @@ export const flattened = ( const innerProps = props[key]; flatProps = { ...flatProps, ...innerProps }; } - const flatCodec = t.exact(optionalized(flatProps)); + const flatCodec = t.exact(optionalized(flatProps), name); const nestedProps = R.map((innerProps: t.Props) => t.exact(optionalized(innerProps)))( props, ); - const nestedCodec = t.strict(nestedProps); + const nestedCodec = t.strict(nestedProps, name); return new t.Type( name, diff --git a/packages/io-ts-http/src/httpRequest.ts b/packages/io-ts-http/src/httpRequest.ts index ac3db2f2..b48bc2ec 100644 --- a/packages/io-ts-http/src/httpRequest.ts +++ b/packages/io-ts-http/src/httpRequest.ts @@ -52,8 +52,8 @@ type EmitPropsErrors

= { export function httpRequest< Props extends HttpRequestCombinatorProps & EmitPropsErrors, ->(props: Props) { - return flattened('httpRequest', { +>(props: Props, name?: string) { + return flattened(name ?? 'httpRequest', { query: {}, params: {}, ...(props as Omit), diff --git a/packages/io-ts-http/test/httpRequest.test.ts b/packages/io-ts-http/test/httpRequest.test.ts index 1567716a..bf170e94 100644 --- a/packages/io-ts-http/test/httpRequest.test.ts +++ b/packages/io-ts-http/test/httpRequest.test.ts @@ -1,10 +1,11 @@ import { describe, it } from 'node:test'; import { strict as assert } from 'node:assert'; - +import * as PathReporter from 'io-ts/lib/PathReporter'; import * as NEA from 'fp-ts/NonEmptyArray'; import * as t from 'io-ts'; import { nonEmptyArray, JsonFromString, NumberFromString } from 'io-ts-types'; -import { assertRight } from './utils'; + +import { assertLeft, assertRight } from './utils'; import { optional } from '../src/combinators'; import * as h from '../src/httpRequest'; @@ -138,4 +139,38 @@ describe('httpRequest', () => { // tslint:disable-next-line: no-unused-expression void _codec; }); + + it('Displays error with codec name on decode', () => { + const request = h.httpRequest( + { + params: {}, + query: { + foo: t.string, + }, + body: { + bar: t.number, + }, + }, + 'TestRequestWithCodecName', + ); + + const test = { + params: {}, + query: { + foo: 'hello', + }, + body: { + bar: 'world', + }, + }; + + const errors = assertLeft(request.decode(test)); + const validationErrors = PathReporter.failure(errors); + const validationMessage = validationErrors.join('\n'); + + assert( + validationMessage.includes('TestRequestWithCodecName'), + 'Expected error to include codec name', + ); + }); }); diff --git a/packages/io-ts-http/test/utils.ts b/packages/io-ts-http/test/utils.ts index d3d78ee7..52bb5aea 100644 --- a/packages/io-ts-http/test/utils.ts +++ b/packages/io-ts-http/test/utils.ts @@ -7,6 +7,11 @@ export const assertRight = E.getOrElseW(() => { throw new Error('Failed to decode object'); }); +export const assertLeft = (e: E.Either) => { + assert(E.isLeft(e), 'Expected a failure, got a success'); + return e.left; +}; + export const assertEncodes = (codec: t.Mixed, test: unknown, expected = test) => { const encoded = codec.encode(test); assert.deepEqual(encoded, expected); diff --git a/packages/openapi-generator/package.json b/packages/openapi-generator/package.json index 7cf4a290..612d8917 100644 --- a/packages/openapi-generator/package.json +++ b/packages/openapi-generator/package.json @@ -17,7 +17,8 @@ "clean": "rm -rf -- dist", "format": "prettier --check .", "format:fix": "prettier --write .", - "test": "c8 --all --src src node --require @swc-node/register --test test/*.test.ts", + "node:test": "node --require @swc-node/register --test test/*.test.ts test/**/*.test.ts", + "test": "c8 --all --src src npm run node:test", "test:target": "c8 --all --src src node --require @swc-node/register" }, "dependencies": { diff --git a/packages/openapi-generator/test/openapi.test.ts b/packages/openapi-generator/test/openapi.test.ts deleted file mode 100644 index e35b1d03..00000000 --- a/packages/openapi-generator/test/openapi.test.ts +++ /dev/null @@ -1,4702 +0,0 @@ -import * as E from 'fp-ts/lib/Either'; -import assert from 'node:assert/strict'; -import test from 'node:test'; - -import { - convertRoutesToOpenAPI, - parsePlainInitializer, - parseSource, - parseRoute, - Project, - type Route, - type Schema, -} from '../src'; -import { SourceFile } from '../src/sourceFile'; - -async function testCase( - description: string, - src: string, - expected: any, - expectedErrors: string[] = [], -) { - test(description, async () => { - const sourceFile = await parseSource('./index.ts', src); - if (sourceFile === undefined) { - throw new Error('Failed to parse source file'); - } - const files: Record = { './index.ts': sourceFile }; - const project = new Project(files); - const routes: Route[] = []; - const schemas: Record = {}; - const errors: string[] = []; - for (const symbol of sourceFile.symbols.declarations) { - if (symbol.init !== undefined) { - const routeSchemaE = parsePlainInitializer(project, sourceFile, symbol.init); - if (E.isLeft(routeSchemaE)) { - errors.push(routeSchemaE.left); - continue; - } - if (symbol.comment !== undefined) { - routeSchemaE.right.comment = symbol.comment; - } - const result = parseRoute(project, routeSchemaE.right); - if (E.isLeft(result)) { - schemas[symbol.name] = routeSchemaE.right; - } else { - routes.push(result.right); - } - } - } - - const actual = convertRoutesToOpenAPI( - { title: 'Test', version: '1.0.0' }, - [], - routes, - schemas, - ); - - - assert.deepEqual(errors, expectedErrors); - assert.deepEqual(actual, expected); - }); -} - -const SIMPLE = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * ## How to call the route - * - * \`\`\` - * curl -X GET http://localhost:3000/foo?foo=bar - * \`\`\` - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** foo param */ - foo: t.string, - }, - }), - response: { - /** foo response */ - 200: t.string - }, -}); - -/** - * An internal route - * - * @private - * @operationId api.v1.private - * @tag Internal Routes - */ -export const internalRoute = h.httpRoute({ - path: '/private/foo', - method: 'GET', - request: h.httpRequest({ - query: { - foo: t.string, - }, - }), - response: { - 200: t.string - }, -}); - -/** - * An unstable route - * - * @unstable - * @operationId api.v1.unstable - * @tag Unstable Routes - */ -export const unstableRoute = h.httpRoute({ - path: '/unstable/foo', - method: 'GET', - request: h.httpRequest({ - query: { - foo: t.string, - }, - }), - response: { - 200: t.string - }, -}); -`; - -testCase('simple route', SIMPLE, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - description: - '## How to call the route\n' + - '\n' + - '```\n' + - 'curl -X GET http://localhost:3000/foo?foo=bar\n' + - '```', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [ - { - in: 'query', - name: 'foo', - description: 'foo param', - required: true, - schema: { - type: 'string', - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - '/private/foo': { - get: { - summary: 'An internal route', - operationId: 'api.v1.private', - tags: ['Internal Routes'], - 'x-internal': true, - parameters: [ - { - in: 'query', - name: 'foo', - required: true, - schema: { - type: 'string', - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - '/unstable/foo': { - get: { - summary: 'An unstable route', - operationId: 'api.v1.unstable', - tags: ['Unstable Routes'], - 'x-unstable': true, - parameters: [ - { - in: 'query', - name: 'foo', - required: true, - schema: { - type: 'string', - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const REQUEST_BODY = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - body: { - foo: t.string, - }, - }), - response: { - /** foo response */ - 200: t.string - }, -}); -`; - -testCase('request body route', REQUEST_BODY, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - required: ['foo'], - }, - }, - }, - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const UNION = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: t.union([ - h.httpRequest({ - query: { - foo: t.string - } - }), - h.httpRequest({ - query: { - bar: t.string - } - }), - ]), - response: { - /** foo response */ - 200: t.string - }, -}); -`; - -testCase('request union route', UNION, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [ - { - in: 'query', - name: 'union', - required: true, - style: 'form', - explode: true, - schema: { - oneOf: [ - { - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - required: ['foo'], - }, - { - type: 'object', - properties: { - bar: { - type: 'string', - }, - }, - required: ['bar'], - }, - ], - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const NULLABLE_PROPERTY = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - body: { - foo: t.union([t.string, t.null]), - }, - }), - response: { - /** foo response */ - 200: t.string - }, -}); -`; - -testCase('nullable property route', NULLABLE_PROPERTY, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'string', - nullable: true, - }, - }, - required: ['foo'], - }, - }, - }, - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const HEADER_COMMENT = ` -/* - * This is a comment - */ - -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * ## How to call the route - * - * \`\`\` - * curl -X GET http://localhost:3000/foo?foo=bar - * \`\`\` - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: t.string - }, -}); -`; - -testCase('source file with a header comment', HEADER_COMMENT, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - description: - '## How to call the route\n' + - '\n' + - '```\n' + - 'curl -X GET http://localhost:3000/foo?foo=bar\n' + - '```', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const EMPTY_REQUIRED = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - body: { - foo: t.string, - }, - }), - response: { - /** foo response */ - 200: t.partial({ - /** string called foo */ - foo: t.string - }) - }, -}); -`; - -// Test that `required` is not emitted as an empty array -testCase('object with no required properties', EMPTY_REQUIRED, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - required: ['foo'], - }, - }, - }, - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - description: 'foo response', - properties: { - foo: { - description: 'string called foo', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const SCHEMA_REF = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: t.type({ - body: Foo, - }), - response: { - /** foo response */ - 200: t.string - }, -}); - -const Foo = t.type({ foo: t.string }); -`; - -testCase('request body ref', SCHEMA_REF, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Foo', - }, - }, - }, - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: { - Foo: { - title: 'Foo', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - required: ['foo'], - }, - }, - }, -}); - -const SCHEMA_REF_WITH_COMMENT_AT_DECLARATION = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - params: { - body: t.string, - /** - * Size of the body - * @example 10 - */ - size: t.number, - } - }), - response: { - 200: Foo - }, -}); - -/** - * a Foo of type 'string' - * @example "foo" - */ -const Foo = t.string; -`; - -testCase('request body ref with comments', SCHEMA_REF_WITH_COMMENT_AT_DECLARATION, { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - "/foo": { - get: { - parameters: [ - { - name: "body", - in: "path", - required: true, - schema: { - type: "string" - } - }, - { - name: "size", - description: "Size of the body", - in: "path", - required: true, - schema: { - type: "number", - example: 10 - } - } - ], - responses: { - "200": { - description: "OK", - content: { - "application/json": { - schema: { - $ref: "#/components/schemas/Foo" - } - } - } - } - } - } - } - }, - components: { - schemas: { - Foo: { - title: "Foo", - type: "string", - description: "a Foo of type 'string'", - example: "foo" - } - } - } -}); - -const SCHEMA_DOUBLE_REF = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: t.type({ - body: Bar, - }), - response: { - /** foo response */ - 200: t.string - }, -}); - -const Foo = t.type({ foo: t.string }); - -const Bar = Foo; -`; - -testCase('request body double ref', SCHEMA_DOUBLE_REF, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/Bar', - }, - }, - }, - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: { - Foo: { - title: 'Foo', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - required: ['foo'], - }, - Bar: { - allOf: [{ title: 'Bar' }, { $ref: '#/components/schemas/Foo' }], - }, - }, - }, -}); - -const SCHEMA_NULLABLE_REF = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: t.type({ - body: t.union([Foo, t.null]), - }), - response: { - /** foo response */ - 200: t.string - }, -}); - -const Foo = t.type({ foo: t.string }); -`; - -testCase('request body nullable ref', SCHEMA_NULLABLE_REF, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - nullable: true, - allOf: [ - { - $ref: '#/components/schemas/Foo', - }, - ], - }, - }, - }, - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: { - Foo: { - title: 'Foo', - type: 'object', - properties: { - foo: { - type: 'string', - }, - }, - required: ['foo'], - }, - }, - }, -}); - -const TITLE_TAG = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const oneOfRoute = h.httpRoute({ - path: '/foo', - method: 'GET', - request: t.union([ - h.httpRequest({ - /** @title this is a title for a oneOf option */ - query: { - /** @title this is a title for a oneOf option's property */ - foo: t.string - } - }), - h.httpRequest({ - query: { - bar: t.string - } - }), - ]), - response: { - /** foo response */ - 200: t.string - }, -}); - -export const route = h.httpRoute({ - path: '/bar', - method: 'GET', - request: h.httpRequest({ - query: { - /** - * bar param - * @title this is a bar parameter - * */ - bar: t.string, - }, - }), - response: { - /** bar response */ - 200: t.string - }, -}); -`; - -testCase('schema parameter with title tag', TITLE_TAG, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [ - { - in: 'query', - name: 'union', - required: true, - style: 'form', - explode: true, - schema: { - oneOf: [ - { - type: 'object', - title: 'this is a title for a oneOf option', - properties: { - foo: { - type: 'string', - title: "this is a title for a oneOf option's property", - }, - }, - required: ['foo'], - }, - { - type: 'object', - properties: { - bar: { - type: 'string', - }, - }, - required: ['bar'], - }, - ], - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - '/bar': { - get: { - parameters: [ - { - in: 'query', - name: 'bar', - description: 'bar param', - required: true, - schema: { - title: 'this is a bar parameter', - type: 'string', - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'bar response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const OPTIONAL_PARAM = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - foo: h.optional(t.string), - }, - }), - response: { - /** foo response */ - 200: t.string - }, -}); -`; - -testCase('optional parameter', OPTIONAL_PARAM, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - parameters: [ - { - in: 'query', - name: 'foo', - schema: { - type: 'string', - }, - }, - ], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'foo response', - type: 'string', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const ROUTE_WITH_RESPONSE_EXAMPLE_STRING = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * @operationId api.v1.test - * @tag Test Routes - * @example bar - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: t.string - }, -}); -`; - -testCase('route with example string', ROUTE_WITH_RESPONSE_EXAMPLE_STRING, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'string', - }, - example: 'bar', - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * @operationId api.v1.test - * @tag Test Routes - * @example { "test": "bar" } - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with example object', ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - required: ['test'], - }, - example: { - test: 'bar', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT_MULTILINE = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * @operationId api.v1.test - * @tag Test Routes - * @example { - * "test": "bar" - * } - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase( - 'route with example object multi-line', - ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT_MULTILINE, - { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - required: ['test'], - }, - example: { - test: 'bar', - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, - }, -); - -const ROUTE_WITH_UNKNOWN_TAG = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * @operationId api.v1.test - * @tag Test Routes - * @optout true - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with unknown tag', ROUTE_WITH_UNKNOWN_TAG, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - operationId: 'api.v1.test', - tags: ['Test Routes'], - 'x-unknown-tags': { - optout: 'true', - }, - parameters: [], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - required: ['test'], - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const ROUTE_WITH_MULTIPLE_UNKNOWN_TAGS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route - * - * @operationId api.v1.test - * @tag Test Routes - * @optout true - * @critical false - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with multiple unknown tags', ROUTE_WITH_MULTIPLE_UNKNOWN_TAGS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route', - operationId: 'api.v1.test', - tags: ['Test Routes'], - 'x-unknown-tags': { - optout: 'true', - critical: 'false', - }, - parameters: [], - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - required: ['test'], - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - - -const ROUTE_WITH_TYPE_DESCRIPTIONS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** bar param */ - bar: t.string, - }, - body: { - /** foo description */ - foo: t.string, - /** bar description */ - bar: t.number, - child: { - /** child description */ - child: t.string, - } - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with type descriptions', ROUTE_WITH_TYPE_DESCRIPTIONS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [ - { - description: 'bar param', - in: 'query', - name: 'bar', - required: true, - schema: { - type: 'string' - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - properties: { - bar: { - description: 'bar description', - type: 'number' - }, - child: { - properties: { - child: { - description: 'child description', - type: 'string' - } - }, - required: [ - 'child' - ], - type: 'object' - }, - foo: { - description: 'foo description', - type: 'string' - } - }, - required: [ - 'foo', - 'bar', - 'child' - ], - type: 'object' - } - } - } - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - required: ['test'], - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - - -const ROUTE_WITH_TYPE_DESCRIPTIONS_OPTIONAL = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** bar param */ - bar: t.string, - }, - body: { - /** foo description */ - foo: h.optional(t.string), - /** bar description */ - bar: h.optional(t.number), - child: { - /** child description */ - child: h.optional(t.string), - } - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - - -testCase('route with type descriptions with optional fields', ROUTE_WITH_TYPE_DESCRIPTIONS_OPTIONAL, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0', - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions', - operationId: 'api.v1.test', - tags: ['Test Routes'], - parameters: [ - { - description: 'bar param', - in: 'query', - name: 'bar', - required: true, - schema: { - type: 'string' - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - properties: { - bar: { - description: 'bar description', - type: 'number' - }, - child: { - properties: { - child: { - description: 'child description', - type: 'string' - } - }, - type: 'object' - }, - foo: { - description: 'foo description', - type: 'string' - } - }, - required: [ - 'child' - ], - type: 'object' - } - } - } - }, - responses: { - 200: { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string', - }, - }, - required: ['test'], - }, - }, - }, - }, - }, - }, - }, - }, - components: { - schemas: {}, - }, -}); - -const ROUTE_WITH_MIXED_TYPES_AND_DESCRIPTIONS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** bar param */ - bar: t.string, - }, - body: { - /** description to describe an optional string */ - foo: h.optional(t.string), - /** description to describe an optional union of number and string */ - bar: h.optional(t.union([t.number, t.string])), - /** description to describe an object */ - child: { - /** dsecription to describe an intersection of a type and a partial */ - child: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.number })]), - }, - /** description to describe a t.type */ - error: t.type({ error: t.string }), - /** description to describe an optional t.object */ - obj: h.optional(t.object({})), - /** description to describe a t.exact */ - exact: t.exact(t.type({ foo: t.string })), - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with mixed types and descriptions', ROUTE_WITH_MIXED_TYPES_AND_DESCRIPTIONS, - { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - '/foo': { - get: { - summary: "A simple route with type descriptions", - operationId: "api.v1.test", - tags: [ - "Test Routes" - ], - parameters: [ - { - name: "bar", - description: "bar param", - in: "query", - required: true, - schema: { - type: "string" - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: "object", - properties: { - foo: { - type: "string", - description: "description to describe an optional string" - }, - bar: { - oneOf: [ - { - type: "number" - }, - { - type: "string" - } - ], - description: "description to describe an optional union of number and string" - }, - child: { - type: "object", - description: "description to describe an object", - properties: { - child: { - type: "object", - description: "dsecription to describe an intersection of a type and a partial", - properties: { - foo: { - type: "string" - }, - bar: { - type: "number" - } - }, - required: [ - "foo" - ] - } - }, - required: [ - "child" - ] - }, - error: { - type: "object", - description: "description to describe a t.type", - properties: { - error: { - type: "string" - } - }, - required: [ - "error" - ] - }, - obj: { - type: "object", - description: "description to describe an optional t.object", - properties: {} - }, - exact: { - type: "object", - description: "description to describe a t.exact", - properties: { - foo: { - type: "string" - } - }, - required: [ - "foo" - ] - } - }, - required: [ - "child", - "error", - "exact" - ] - } - } - } - }, - responses: { - 200: { - description: "OK", - content: { - 'application/json': { - schema: { - type: "object", - properties: { - test: { - type: "string" - } - }, - required: [ - "test" - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } - }); - -const ROUTE_WITH_ARRAY_TYPES_AND_DESCRIPTIONS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** bar param */ - bar: t.string, - }, - body: { - /** foo description */ - foo: t.array(t.string), - /** bar description */ - bar: t.array(t.number), - child: { - /** child description */ - child: t.array(t.union([t.string, t.number])), - } - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with array types and descriptions', ROUTE_WITH_ARRAY_TYPES_AND_DESCRIPTIONS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - name: 'bar', - description: 'bar param', - in: 'query', - required: true, - schema: { - type: 'string' - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'array', - items: { - type: 'string', - description: 'foo description' - }, - }, - bar: { - type: 'array', - items: { - type: 'number', - description: 'bar description' - }, - }, - child: { - type: 'object', - properties: { - child: { - type: 'array', - items: { - oneOf: [ - { - type: 'string' - }, - { - type: 'number' - } - ], - description: 'child description' - }, - } - }, - required: [ - 'child' - ] - } - }, - required: [ - 'foo', - 'bar', - 'child' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_RECORD_TYPES_AND_DESCRIPTIONS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** bar param */ - bar: t.record(t.string, t.string), - }, - body: { - /** foo description */ - foo: t.record(t.string, t.number), - child: { - /** child description */ - child: t.record(t.string, t.array(t.union([t.string, t.number]))), - } - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with record types and descriptions', ROUTE_WITH_RECORD_TYPES_AND_DESCRIPTIONS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - name: 'bar', - description: 'bar param', - in: 'query', - required: true, - schema: { - type: 'object', - additionalProperties: { - type: 'string' - } - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'object', - additionalProperties: { - type: 'number' - }, - description: 'foo description' - }, - child: { - type: 'object', - properties: { - child: { - type: 'object', - additionalProperties: { - type: 'array', - items: { - oneOf: [ - { - type: 'string' - }, - { - type: 'number' - } - ] - } - }, - description: 'child description' - } - }, - required: [ - 'child' - ] - } - }, - required: [ - 'foo', - 'child' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_DESCRIPTIONS_PATTERNS_EXAMPLES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** - * This is a bar param. - * @example { "foo": "bar" } - */ - bar: t.record(t.string, t.string), - }, - body: { - /** - * foo description - * @pattern ^[1-9][0-9]{4}$ - * @example 12345 - */ - foo: t.number, - child: { - /** - * child description - */ - child: t.array(t.union([t.string, t.number])), - } - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTIONS_PATTERNS_EXAMPLES, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - name: 'bar', - description: 'This is a bar param.', - in: 'query', - required: true, - schema: { - type: 'object', - example: { - foo: 'bar' - }, - additionalProperties: { - type: 'string' - } - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'number', - description: 'foo description', - example: 12345, - pattern: '^[1-9][0-9]{4}$' - }, - child: { - type: 'object', - properties: { - child: { - type: 'array', - items: { - description: 'child description', - oneOf: [ - { - type: 'string' - }, - { - type: 'number' - } - ] - }, - } - }, - required: [ - 'child' - ] - } - }, - required: [ - 'foo', - 'child' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_DESCRIPTIONS_FOR_REFERENCES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -const Foo = t.type({ foo: t.string }); -const Bar = t.type({ bar: t.number }); - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - bar: t.array(t.string), - }, - body: { - /** - * This is a foo description. - * @example BitGo Inc - */ - foo: Foo, - bar: Bar, - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with descriptions for references', ROUTE_WITH_DESCRIPTIONS_FOR_REFERENCES, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - name: 'bar', - in: 'query', - required: true, - schema: { - type: 'array', - items: { - type: 'string' - } - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - // needs to be wrapped in an allOf to preserve the description - foo: { - allOf: [ - { - $ref: '#/components/schemas/Foo' - } - ], - description: 'This is a foo description.', - example: 'BitGo Inc' - }, - // should not need to be wrapped in an allOf - bar: { - $ref: '#/components/schemas/Bar' - } - }, - required: [ - 'foo', - 'bar' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: { - Foo: { - title: 'Foo', - type: 'object', - properties: { - foo: { - type: 'string' - } - }, - required: [ - 'foo' - ] - }, - Bar: { - title: 'Bar', - type: 'object', - properties: { - bar: { - type: 'number' - } - }, - required: [ - 'bar' - ] - } - } - } -}); - -const ROUTE_WITH_MIN_AND_MAX_VALUES_FOR_STRINGS_AND_DEFAULT = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - bar: t.array(t.string), - }, - body: { - /** - * This is a foo description. - * @minLength 5 - * @maxLength 10 - * @example SomeInc - * @default BitgoInc - */ - foo: t.string() - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with min and max values for strings and default value', ROUTE_WITH_MIN_AND_MAX_VALUES_FOR_STRINGS_AND_DEFAULT, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - name: 'bar', - in: 'query', - required: true, - schema: { - type: 'array', - items: { - type: 'string' - } - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'string', - description: 'This is a foo description.', - example: 'SomeInc', - default: 'BitgoInc', - minLength: 5, - maxLength: 10 - } - }, - required: [ - 'foo' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_DEPRECATED_TAG = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - body: { - /** - * This is a foo description. - * @deprecated - */ - foo: t.string() - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with deprecated tag', ROUTE_WITH_DEPRECATED_TAG, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - parameters: [], - tags: [ - 'Test Routes' - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'string', - description: 'This is a foo description.', - deprecated: true - } - }, - required: [ - 'foo' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_MIN_MAX_AND_OTHER_TAGS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - body: { - /** - * This is a foo description. - * @minimum 5 - * @maximum 10 - * @minItems 1 - * @maxItems 5 - * @minProperties 1 - * @maxProperties 500 - * @exclusiveMinimum true - * @exclusiveMaximum true - * @multipleOf 7 - * @uniqueItems true - * @readOnly true - * @writeOnly true - */ - foo: t.number() - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with min and max tags', ROUTE_WITH_MIN_MAX_AND_OTHER_TAGS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - parameters: [], - tags: [ - 'Test Routes' - ], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - foo: { - type: 'number', - description: 'This is a foo description.', - minimum: 5, - maximum: 10, - minItems: 1, - maxItems: 5, - minProperties: 1, - multipleOf: 7, - maxProperties: 500, - exclusiveMinimum: true, - exclusiveMaximum: true, - uniqueItems: true, - readOnly: true, - writeOnly: true - } - }, - required: [ - 'foo' - ] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_ARRAY_QUERY_PARAM = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** - * This is a foo description. - * @example abc - * @pattern ^[a-z]+$ - */ - foo: h.optional(t.array(t.string)) - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with optional array query parameter and documentation', ROUTE_WITH_ARRAY_QUERY_PARAM, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - description: 'This is a foo description.', - in: 'query', - name: 'foo', - schema: { - items: { - description: 'This is a foo description.', - example: 'abc', - type: 'string', - pattern: '^[a-z]+$' - }, - type: 'array' - } - } - ], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - - -const ROUTE_WITH_ARRAY_UNION_NULL_UNDEFINED_QUERY_PARAM = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - /** - * This is a foo description. - * @example abc - * @pattern ^[a-z]+$ - */ - ipRestrict: t.union([t.array(t.string), t.null, t.undefined]), - }, - }), - response: { - 200: { - test: t.string - } - }, -}); -`; - -testCase('route with array union of null and undefined', ROUTE_WITH_ARRAY_UNION_NULL_UNDEFINED_QUERY_PARAM, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [ - { - description: 'This is a foo description.', - in: 'query', - name: 'ipRestrict', - schema: { - items: { - description: 'This is a foo description.', - example: 'abc', - type: 'string', - pattern: '^[a-z]+$' - }, - type: 'array' - } - } - ], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_SCHEMA_WITH_COMMENT = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * A simple route with type descriptions for references - * - * @operationId api.v1.test - * @tag Test Routes - */ -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: SimpleRouteResponse, - 400: ApiError, - 401: InvalidError - }, - }); - -/** - * Human readable description of the Simple Route Response - * @title Human Readable Simple Route Response - */ -const SimpleRouteResponse = t.type({ - test: t.string, -}); - -/** - * Human readable description of the InvalidError schema - * @title Human Readable Invalid Error Schema - */ -const InvalidError = t.intersection([ - ApiError, - t.type({ error: t.literal('invalid') })]); - -/** - * Human readable description of the ApiError schema - * @title Human Readable Api Error Schema - */ -const ApiError = t.type({ - error: t.string, -}); - `; - -testCase('route with api error schema', ROUTE_WITH_SCHEMA_WITH_COMMENT, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - summary: 'A simple route with type descriptions for references', - operationId: 'api.v1.test', - tags: [ - 'Test Routes' - ], - parameters: [], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - '$ref': '#/components/schemas/SimpleRouteResponse' - } - } - } - }, - '400': { - content: { - 'application/json': { - schema: { - '$ref': '#/components/schemas/ApiError' - } - } - }, - description: 'Bad Request' - }, - '401': { - description: 'Unauthorized', - content: { - 'application/json': { - schema: { - $ref: '#/components/schemas/InvalidError' - } - } - } - } - } - } - }, - }, - components: { - schemas: { - ApiError: { - properties: { - error: { - type: 'string' - } - }, - required: [ - 'error' - ], - title: 'Human Readable Api Error Schema', - description: 'Human readable description of the ApiError schema', - type: 'object' - }, - SimpleRouteResponse: { - description: 'Human readable description of the Simple Route Response', - properties: { - test: { - type: 'string' - } - }, - required: [ - 'test' - ], - title: 'Human Readable Simple Route Response', - type: 'object', - }, - InvalidError: { - title: 'Human Readable Invalid Error Schema', - description: 'Human readable description of the InvalidError schema', - allOf: [ - { - type: 'object', - properties: { - error: { - type: 'string', - enum: [ - 'invalid' - ] - } - }, - required: [ - 'error' - ] - }, - { - $ref: '#/components/schemas/ApiError' - } - ], - }, - } - } -}); - -const ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; -import { DateFromNumber } from 'io-ts-types'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - ipRestrict: t.boolean - }, - }), - response: { - 200: { - test: DateFromNumber - } - }, -}); -`; - -testCase('route with schema with default metadata', ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - parameters: [ - { - in: 'query', - name: 'ipRestrict', - required: true, - schema: { - type: 'boolean', - } - } - ], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'number', - format: 'number', - title: 'Unix Time (milliseconds)', - description: 'Number of milliseconds since the Unix epoch', - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_OVERIDDEN_METADATA = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; -import { DateFromNumber } from 'io-ts-types'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - ipRestrict: t.boolean - }, - }), - response: { - 200: { - /** - * Testing overridden metadata - * @format string - */ - test: DateFromNumber - } - }, -}); -`; - -testCase('route with schema with default metadata', ROUTE_WITH_OVERIDDEN_METADATA, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - parameters: [ - { - in: 'query', - name: 'ipRestrict', - required: true, - schema: { - type: 'boolean', - } - } - ], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - test: { - type: 'number', - format: 'string', - title: 'Unix Time (milliseconds)', - description: 'Testing overridden metadata', - } - }, - required: [ - 'test' - ] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - - -const SCHEMA_WITH_MANY_RESPONSE_TYPES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -const ApiError = t.type({ - /** error message */ - error: t.string, -}); - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - /** string response type */ - 200: t.string, - 400: ApiError - }, -}) -`; - -testCase('route with many response codes uses default status code descriptions', SCHEMA_WITH_MANY_RESPONSE_TYPES, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - parameters: [], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - description: 'string response type', - type: 'string' - } - } - } - }, - '400': { - description: 'Bad Request', - content: { - 'application/json': { - schema: { - '$ref': '#/components/schemas/ApiError' - } - } - } - } - } - } - } - }, - components: { - schemas: { - ApiError: { - properties: { - error: { - type: 'string', - description: 'error message', - } - }, - required: [ - 'error' - ], - type: 'object', - title: 'ApiError' - }, - } - } -}); - -const SCHEMA_WITH_REDUNDANT_UNIONS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - foo: t.union([t.string, t.string]), - bar: t.union([t.number, t.number, t.number]), - bucket: t.union([t.string, t.number, t.boolean, t.string, t.number, t.boolean]), - }, - body: { - typeUnion: t.union([ - t.type({ foo: t.string, bar: t.number }), - t.type({ bar: t.number, foo: t.string}), - ]), - nestedTypeUnion: t.union([ - t.type({ nested: t.type({ foo: t.string, bar: t.number }) }), - t.type({ nested: t.type({ foo: t.string, bar: t.number }) }) - ]) - } - }), - response: { - 200: t.union([t.string, t.string, t.union([t.number, t.number])]), - 400: t.union([t.boolean, t.boolean, t.boolean]) - }, -}) -`; - -testCase('route with reduntant response schemas', SCHEMA_WITH_REDUNDANT_UNIONS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - parameters: [ - { - in: 'query', - name: 'foo', - required: true, - schema: { - type: 'string' - } - }, - { - in: 'query', - name: 'bar', - required: true, - schema: { - type: 'number' - } - }, - { - in: 'query', - name: 'bucket', - required: true, - schema: { - oneOf: [ - { type: 'string' }, - { type: 'number' }, - { type: 'boolean' } - ] - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - properties: { - nestedTypeUnion: { - properties: { - nested: { - properties: { - bar: { - type: 'number' - }, - foo: { - type: 'string' - } - }, - required: [ - 'bar', - 'foo' - ], - type: 'object' - } - }, - required: [ - 'nested' - ], - type: 'object' - }, - typeUnion: { - properties: { - bar: { - type: 'number' - }, - foo: { - type: 'string' - } - }, - required: [ - 'bar', - 'foo' - ], - type: 'object' - } - }, - required: [ - 'typeUnion', - 'nestedTypeUnion' - ], - type: 'object' - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - oneOf: [{ - type: 'string' - }, { - type: 'number' - }] - } - } - } - }, - '400': { - description: 'Bad Request', - content: { - 'application/json': { - schema: { - type: 'boolean' - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const SCHEMA_WITH_TITLES_IN_REQUEST_BODIES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * @title Some Readable BodyFoo Title - */ -const BodyFoo = t.type({ - /** a foo description */ - foo: t.string, -}); - -/** - * @title Some Readable ParamsFoo Title - */ -const ParamsFoo = { someId: t.string }; - -export const route = h.httpRoute({ - path: '/foo', - method: 'POST', - request: h.httpRequest({ - params: {}, - body: h.httpRequest({ params: ParamsFoo, body: BodyFoo, }) - }), - response: { - 200: t.literal('OK'), - }, -}); -`; - -testCase("route with titles in request bodies", SCHEMA_WITH_TITLES_IN_REQUEST_BODIES, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - post: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - params: { - type: 'object', - title: "Some Readable ParamsFoo Title", - properties: { - someId: { type: 'string' } - }, - required: ['someId'] - }, - body: { - type: 'object', - title: 'Some Readable BodyFoo Title', - properties: { - foo: { - type: 'string', - description: 'a foo description' - } - }, - required: ['foo'] - } - }, - required: ['params', 'body'] - } - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'string', - enum: ['OK'] - } - } - } - } - } - } - } - }, - components: { - schemas: { - ParamsFoo: { - title: 'Some Readable ParamsFoo Title', - type: 'object', - properties: { someId: { type: 'string' } }, - required: ['someId'] - }, - BodyFoo: { - title: 'Some Readable BodyFoo Title', - type: 'object', - properties: { - foo: { - type: 'string', - description: 'a foo description' - } - }, - required: ['foo'] - } - } - } -}); - - -const ROUTE_WITH_ARRAY_EXAMPLE = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * @example btc - */ -const innerItems = t.string; - -export const route = h.httpRoute({ - path: '/foo', - method: 'POST', - request: h.httpRequest({ - params: {}, - body: t.type({ - /** - * @example "btc" - */ - array1: t.array(t.string), - /** - * @example ["btc", "eth"] - */ - array2: t.array(innerItems), - /** - * @minItems 1 - * @maxItems 5 - */ - array3: t.array(t.number), - objectWithArray: t.type({ - /** - * @example ["btc", "eth"] - */ - nestedArray: t.array(innerItems) - }) - }) - }), - response: { - 200: t.literal('OK'), - }, -});`; - -testCase("route with array examples", ROUTE_WITH_ARRAY_EXAMPLE, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - post: { - parameters: [], - requestBody: { - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - array1: { - type: 'array', - items: { - type: 'string', - example: '"btc"' - }, - }, - array2: { - type: 'array', - example: ['btc', 'eth'], - items: { - type: 'string', - example: 'btc' - }, - }, - array3: { - items: { - type: 'number' - }, - maxItems: 5, - minItems: 1, - type: 'array' - }, - objectWithArray: { - properties: { - nestedArray: { - example: [ - 'btc', - 'eth' - ], - items: { - example: 'btc', - type: 'string' - }, - type: 'array' - } - }, - required: [ - 'nestedArray' - ], - type: 'object' - }, - }, - required: ['array1', 'array2', 'array3', 'objectWithArray'], - }, - } - } - }, - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'string', - enum: ['OK'] - } - } - } - } - } - } - } - }, - components: { - schemas: { - innerItems: { - title: "innerItems", - type: "string", - example: 'btc' - } - } - } -}); - -const ROUTE_WITH_CONSOLIDATABLE_UNION_SCHEMAS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; -import { BooleanFromString, BooleanFromNumber, NumberFromString } from 'io-ts-types'; - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - // are not consolidatable - firstUnion: t.union([t.string, t.number]), - secondUnion: t.union([BooleanFromString, NumberFromString]), - thirdUnion: t.union([t.string, BooleanFromString]), - firstNonUnion: BooleanFromString, - secondNonUnion: NumberFromString, - thirdNonUnion: t.string, - }, - }), - response: { - 200: { - // are consolidatable - fourthUnion: t.union([t.boolean, BooleanFromNumber]), - fifthUnion: h.optional(t.union([t.boolean, t.boolean, BooleanFromNumber, BooleanFromString])), - sixthUnion: t.union([t.number, NumberFromString]), - } - }, -}); -`; - -testCase("route with consolidatable union schemas", ROUTE_WITH_CONSOLIDATABLE_UNION_SCHEMAS, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - parameters: [ - { - name: 'firstUnion', - in: 'query', - required: true, - schema: { - oneOf: [ - { type: 'string' }, - { type: 'number' } - ] - } - }, - { - name: 'secondUnion', - in: 'query', - required: true, - schema: { - oneOf: [ - { type: 'string', format: 'number' }, - { type: 'string', enum: ['true', 'false'] } - ] - } - }, - { - name: 'thirdUnion', - in: 'query', - required: true, - schema: { - oneOf: [ - { type: 'string' }, - { type: 'string', enum: ['true', 'false'] } - ] - } - }, - { - name: 'firstNonUnion', - in: 'query', - required: true, - schema: { type: 'string', enum: ['true', 'false'] } - }, - { - name: 'secondNonUnion', - in: 'query', - required: true, - schema: { type: 'string', format: 'number' } - }, - { - name: 'thirdNonUnion', - in: 'query', - required: true, - schema: { type: 'string' } - } - ], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - fourthUnion: { type: 'boolean' }, - fifthUnion: { type: 'boolean' }, - sixthUnion: { type: 'number' } - }, - required: ['fourthUnion', 'sixthUnion'] - } - } - } - } - } - } - } - }, - components: { - schemas: {} - } -}); - -const ROUTE_WITH_NESTED_ARRAY_EXAMPLES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * @example ["a", "b"] - */ -const firstLevel = t.array(t.string); - -/** - * @example [["a", "b"], ["c", "d"]] - */ -const secondLevel = t.array(firstLevel); - -/** - * @example [[["a"], ["b"]], [["c"], ["d"]]] - */ -const thirdLevel = t.array(secondLevel); - -export const route = h.httpRoute({ - path: '/foo', - method: 'POST', - request: h.httpRequest({ - params: {}, - body: t.type({ - nested: thirdLevel - }) - }), - response: { - 200: t.literal('OK'), - }, -}); -`; - -testCase("route with nested array examples", ROUTE_WITH_NESTED_ARRAY_EXAMPLES, { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - "/foo": { - post: { - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - nested: { - "$ref": "#/components/schemas/thirdLevel" - } - }, - required: [ - "nested" - ] - } - } - } - }, - responses: { - 200: { - description: "OK", - content: { - "application/json": { - schema: { - type: "string", - enum: [ - "OK" - ] - } - } - } - } - } - } - } - }, - components: { - schemas: { - firstLevel: { - title: "firstLevel", - type: "array", - example: [ "a", "b" ], - items: { - type: "string" - } - }, - secondLevel: { - title: "secondLevel", - type: "array", - example: [ [ "a", "b" ], [ "c", "d" ] ], - items: { - type: "array", - example: [ "a", "b" ], - items: { - type: "string" - } - } - }, - thirdLevel: { - title: "thirdLevel", - type: "array", - example: [[["a"],["b"]],[["c"],["d"]]], - items: { - type: "array", - example: [["a","b"],["c","d"]], - items: { - type: "array", - example: ["a","b"], - items: { - type: "string" - } - } - } - } - } - } -}); - -const ROUTE_WITH_OVERRIDING_COMMENTS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * @example "abc" - */ -const TargetSchema = t.string; - -const ParentSchema = t.type({ - /** This description should show with the example */ - target: h.optional(TargetSchema) -}) - -export const route = h.httpRoute({ - path: '/foo', - method: 'POST', - request: h.httpRequest({ - params: {}, - body: ParentSchema - }), - response: { - 200: t.literal('OK'), - }, -}); -`; - -testCase("route with overriding comments", ROUTE_WITH_OVERRIDING_COMMENTS, { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - "/foo": { - post: { - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - target: { - type: "string", - description: "This description should show with the example", - example: "abc" - } - } - } - } - } - }, - responses: { - 200: { - description: "OK", - content: { - "application/json": { - schema: { - type: "string", - enum: [ - "OK" - ] - } - } - } - } - } - } - } - }, - components: { - schemas: { - TargetSchema: { - title: "TargetSchema", - type: "string", - example: "abc" - }, - ParentSchema: { - title: "ParentSchema", - type: "object", - properties: { - target: { - type: "string", - description: "This description should show with the example", - example: "abc" - } - } - } - } - } -}); - -const ROUTE_WITH_NESTED_OVERRIDEN_COMMENTS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * @example "abc" - */ -const TargetSchema = t.string; - -const ParentSchema = t.type({ - /** This description should show with the example */ - target: h.optional(TargetSchema) -}) - -const GrandParentSchema = t.type({ - /** This description should override the previous description */ - parent: ParentSchema -}) - -export const route = h.httpRoute({ - path: '/foo', - method: 'POST', - request: h.httpRequest({ - params: {}, - body: GrandParentSchema - }), - response: { - 200: t.literal('OK'), - }, -}); -`; - - -testCase("route with nested overriding comments", ROUTE_WITH_NESTED_OVERRIDEN_COMMENTS, { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - "/foo": { - post: { - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - type: "object", - properties: { - parent: { - allOf: [ - { - '$ref': '#/components/schemas/ParentSchema' - } - ], - description: 'This description should override the previous description', - }, - }, - required: ['parent'] - } - } - } - }, - responses: { - 200: { - description: "OK", - content: { - "application/json": { - schema: { - type: "string", - enum: [ - "OK" - ] - } - } - } - } - } - } - } - }, - components: { - schemas: { - TargetSchema: { - title: "TargetSchema", - type: "string", - example: "abc" - }, - ParentSchema: { - title: "ParentSchema", - type: "object", - properties: { - target: { - type: "string", - description: "This description should show with the example", - example: "abc" - } - } - }, - GrandParentSchema: { - title: "GrandParentSchema", - type: "object", - properties: { - parent: { - allOf: [ - { - '$ref': '#/components/schemas/ParentSchema' - } - ], - description: 'This description should override the previous description' - } - }, - required: ['parent'] - } - } - } -}); - -const ROUTE_WITH_OVERRIDEN_COMMENTS_IN_UNION = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -/** - * @example "abc" - */ -const TargetSchema = t.string; - -/** - * @example "def" - */ -const TargetSchema2 = t.string; - -const ParentSchema = t.type({ - /** This description should show with the example */ - target: h.optional(t.union([TargetSchema, TargetSchema2])) -}) - -const SecondaryParentSchema = t.type({ - /** - * This description should show with the overriden example - * @example "overridden example" - */ - target: h.optional(t.union([TargetSchema, TargetSchema2])) -}) - -/** - * This is grandparent schema description - * @title Grand Parent Schema - */ -const GrandParentSchema = t.type({ - parent: ParentSchema, - secondaryParent: SecondaryParentSchema -}); - -export const route = h.httpRoute({ - path: '/foo', - method: 'POST', - request: h.httpRequest({ - params: {}, - body: GrandParentSchema - }), - response: { - 200: t.literal('OK'), - }, -}); -`; - -testCase("route with overriden comments in union", ROUTE_WITH_OVERRIDEN_COMMENTS_IN_UNION, { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - "/foo": { - post: { - parameters: [], - requestBody: { - content: { - "application/json": { - schema: { - title: "Grand Parent Schema", - description: 'This is grandparent schema description', - type: "object", - properties: { - parent: { - "$ref": "#/components/schemas/ParentSchema" - }, - secondaryParent: { - "$ref": "#/components/schemas/SecondaryParentSchema" - } - }, - required: [ - "parent", - "secondaryParent" - ] - } - } - } - }, - responses: { - 200: { - description: "OK", - content: { - "application/json": { - schema: { - type: "string", - enum: [ - "OK" - ] - } - } - } - } - } - } - } - }, - components: { - schemas: { - TargetSchema: { - title: "TargetSchema", - type: "string", - example: "abc" - }, - TargetSchema2: { - title: "TargetSchema2", - type: "string", - example: "def" - }, - ParentSchema: { - title: "ParentSchema", - type: "object", - properties: { - target: { - oneOf: [ - { - "$ref": "#/components/schemas/TargetSchema" - }, - { - "$ref": "#/components/schemas/TargetSchema2" - } - ], - description: "This description should show with the example" - } - } - }, - SecondaryParentSchema: { - title: "SecondaryParentSchema", - type: "object", - properties: { - target: { - oneOf: [ - { - "$ref": "#/components/schemas/TargetSchema" - }, - { - "$ref": "#/components/schemas/TargetSchema2" - } - ], - description: "This description should show with the overriden example", - example: "\"overridden example\"" - } - } - }, - GrandParentSchema: { - title: "Grand Parent Schema", - description: 'This is grandparent schema description', - type: "object", - properties: { - parent: { - "$ref": "#/components/schemas/ParentSchema" - }, - secondaryParent: { - "$ref": "#/components/schemas/SecondaryParentSchema" - } - }, - required: [ - "parent", - "secondaryParent" - ] - } - } - } -}); - -const ROUTE_WITH_PRIVATE_PROPERTIES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -const SampleType = t.type({ - foo: t.string, - /** @private */ - bar: t.string, // This should show up with x-internal, - /** @private */ - privateObject: t.type({ - privateFieldInObject: t.boolean - }) -}); - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - params: { - /** @private */ - path: t.string - }, - query: { - /** @private */ - query: t.string - }, - body: SampleType - }), - response: { - 200: SampleType - }, -}); -`; - -testCase("route with private properties in request query, params, body, and response", ROUTE_WITH_PRIVATE_PROPERTIES, { - openapi: "3.0.3", - info: { - title: "Test", - version: "1.0.0" - }, - paths: { - '/foo': { - get: { - parameters: [ - { - 'x-internal': true, - description: '', - in: 'query', - name: 'query', - required: true, - schema: { - type: 'string' - } - }, - { - 'x-internal': true, - description: '', - in: 'path', - name: 'path', - required: true, - schema: { - type: 'string' - } - } - ], - requestBody: { - content: { - 'application/json': { - schema: { - properties: { - bar: { - 'x-internal': true, - type: 'string' - }, - foo: { - type: 'string' - }, - privateObject: { - 'x-internal': true, - properties: { - privateFieldInObject: { - type: 'boolean' - } - }, - required: [ - 'privateFieldInObject' - ], - type: 'object' - } - }, - required: [ - 'foo', - 'bar', - 'privateObject' - ], - type: 'object' - } - } - }, - }, - responses: { - '200': { - content: { - 'application/json': { - schema: { - '$ref': '#/components/schemas/SampleType' - } - } - }, - description: 'OK' - } - } - } - }, - }, - components: { - schemas: { - SampleType: { - properties: { - bar: { - 'x-internal': true, - type: 'string' - }, - foo: { - type: 'string' - }, - privateObject: { - 'x-internal': true, - properties: { - privateFieldInObject: { - type: 'boolean' - } - }, - required: [ - 'privateFieldInObject' - ], - type: 'object' - } - }, - required: [ - 'foo', - 'bar', - 'privateObject' - ], - title: 'SampleType', - type: 'object' - } - } - }, -}); - -const ROUTE_WITH_RECORD_TYPES = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; -const ValidKeys = t.keyof({ name: "name", age: "age", address: "address" }); -const PersonObject = t.type({ bigName: t.string, bigAge: t.number }); -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({ - query: { - name: t.string, - }, - }), - response: { - 200: { - person: t.record(ValidKeys, t.string), - anotherPerson: t.record(ValidKeys, PersonObject), - bigPerson: t.record(t.string, t.string), - anotherBigPerson: t.record(t.string, PersonObject), - } - }, -}); -`; - -testCase("route with record types", ROUTE_WITH_RECORD_TYPES, { - openapi: '3.0.3', - info: { - title: 'Test', - version: '1.0.0' - }, - paths: { - '/foo': { - get: { - parameters: [ - { - name: 'name', - in: 'query', - required: true, - schema: { - type: 'string' - } - } - ], - responses: { - '200': { - description: 'OK', - content: { - 'application/json': { - schema: { - type: 'object', - properties: { - // becomes t.type() - person: { - type: 'object', - properties: { - name: { type: 'string' }, - age: { type: 'string' }, - address: { type: 'string' } - }, - required: [ 'name', 'age', 'address' ] - }, - // becomes t.type() - anotherPerson: { - type: 'object', - properties: { - name: { - type: 'object', - properties: { - bigName: { type: 'string' }, - bigAge: { type: 'number' } - }, - required: [ 'bigName', 'bigAge' ] - }, - age: { - type: 'object', - properties: { - bigName: { type: 'string' }, - bigAge: { type: 'number' } - }, - required: [ 'bigName', 'bigAge' ] - }, - address: { - type: 'object', - properties: { - bigName: { type: 'string' }, - bigAge: { type: 'number' } - }, - required: [ 'bigName', 'bigAge' ] - } - }, - required: [ 'name', 'age', 'address' ] - }, - bigPerson: { - // stays as t.record() - type: 'object', - additionalProperties: { type: 'string' } - }, - anotherBigPerson: { - // stays as t.record() - type: 'object', - additionalProperties: { - type: 'object', - properties: { - bigName: { type: 'string' }, - bigAge: { type: 'number' } - }, - required: [ 'bigName', 'bigAge' ] - } - } - }, - required: [ 'person', 'anotherPerson', 'bigPerson', 'anotherBigPerson' ] - } - } - } - } - } - } - } - }, - components: { - schemas: { - ValidKeys: { - title: 'ValidKeys', - type: 'string', - enum: [ 'name', 'age', 'address' ] - }, - PersonObject: { - title: 'PersonObject', - type: 'object', - properties: { bigName: { type: 'string' }, bigAge: { type: 'number' } }, - required: [ 'bigName', 'bigAge' ] - } - } - } -}); - -const ROUTE_WITH_UNKNOWN_UNIONS = ` -import * as t from 'io-ts'; -import * as h from '@api-ts/io-ts-http'; - -const UnknownUnion = t.union([t.string, t.number, t.boolean, t.unknown]); -const SingleUnknownUnion = t.union([t.unknown, t.string]); - -const NestedUnknownUnion = t.union([t.union([t.string, t.unknown]), t.union([t.boolean, t.unknown])]); - -export const route = h.httpRoute({ - path: '/foo', - method: 'GET', - request: h.httpRequest({}), - response: { - 200: { - single: SingleUnknownUnion, - unknown: UnknownUnion, - nested: NestedUnknownUnion, - } - }, -}); -`; - -testCase("route with unknown unions", ROUTE_WITH_UNKNOWN_UNIONS, { - info: { - title: 'Test', - version: '1.0.0' - }, - openapi: '3.0.3', - paths: { - '/foo': { - get: { - parameters: [], - responses: { - '200': { - content: { - 'application/json': { - schema: { - properties: { - nested: { - '$ref': '#/components/schemas/NestedUnknownUnion' - }, - single: { - '$ref': '#/components/schemas/SingleUnknownUnion' - }, - unknown: { - '$ref': '#/components/schemas/UnknownUnion' - } - }, - required: [ - 'single', - 'unknown', - 'nested' - ], - type: 'object' - } - } - }, - description: 'OK' - } - } - } - } - }, - components: { - schemas: { - NestedUnknownUnion: { - oneOf: [ - { - type: 'string' - }, - { - type: 'boolean' - } - ], - title: 'NestedUnknownUnion' - }, - SingleUnknownUnion: { - title: 'SingleUnknownUnion', - type: 'string' - }, - UnknownUnion: { - oneOf: [ - { - type: 'string' - }, - { - type: 'number' - }, - { - type: 'boolean' - } - ], - title: 'UnknownUnion' - } - } - }, -}); \ No newline at end of file diff --git a/packages/openapi-generator/test/openapi/base.test.ts b/packages/openapi-generator/test/openapi/base.test.ts new file mode 100644 index 00000000..b101c9e5 --- /dev/null +++ b/packages/openapi-generator/test/openapi/base.test.ts @@ -0,0 +1,717 @@ +import { testCase } from "./testHarness"; + + +const SIMPLE = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * ## How to call the route + * + * \`\`\` + * curl -X GET http://localhost:3000/foo?foo=bar + * \`\`\` + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** foo param */ + foo: t.string, + }, + }), + response: { + /** foo response */ + 200: t.string + }, +}); + +/** + * An internal route + * + * @private + * @operationId api.v1.private + * @tag Internal Routes + */ +export const internalRoute = h.httpRoute({ + path: '/private/foo', + method: 'GET', + request: h.httpRequest({ + query: { + foo: t.string, + }, + }), + response: { + 200: t.string + }, +}); + +/** + * An unstable route + * + * @unstable + * @operationId api.v1.unstable + * @tag Unstable Routes + */ +export const unstableRoute = h.httpRoute({ + path: '/unstable/foo', + method: 'GET', + request: h.httpRequest({ + query: { + foo: t.string, + }, + }), + response: { + 200: t.string + }, +}); +`; + +testCase('simple route', SIMPLE, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + description: + '## How to call the route\n' + + '\n' + + '```\n' + + 'curl -X GET http://localhost:3000/foo?foo=bar\n' + + '```', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [ + { + in: 'query', + name: 'foo', + description: 'foo param', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + '/private/foo': { + get: { + summary: 'An internal route', + operationId: 'api.v1.private', + tags: ['Internal Routes'], + 'x-internal': true, + parameters: [ + { + in: 'query', + name: 'foo', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + '/unstable/foo': { + get: { + summary: 'An unstable route', + operationId: 'api.v1.unstable', + tags: ['Unstable Routes'], + 'x-unstable': true, + parameters: [ + { + in: 'query', + name: 'foo', + required: true, + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const REQUEST_BODY = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + body: { + foo: t.string, + }, + }), + response: { + /** foo response */ + 200: t.string + }, +}); +`; + +testCase('request body route', REQUEST_BODY, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const UNION = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: t.union([ + h.httpRequest({ + query: { + foo: t.string + } + }), + h.httpRequest({ + query: { + bar: t.string + } + }), + ]), + response: { + /** foo response */ + 200: t.string + }, +}); +`; + +testCase('request union route', UNION, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'union', + required: true, + style: 'form', + explode: true, + schema: { + oneOf: [ + { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + { + type: 'object', + properties: { + bar: { + type: 'string', + }, + }, + required: ['bar'], + }, + ], + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const NULLABLE_PROPERTY = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + body: { + foo: t.union([t.string, t.null]), + }, + }), + response: { + /** foo response */ + 200: t.string + }, +}); +`; + +testCase('nullable property route', NULLABLE_PROPERTY, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'string', + nullable: true, + }, + }, + required: ['foo'], + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const EMPTY_REQUIRED = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + body: { + foo: t.string, + }, + }), + response: { + /** foo response */ + 200: t.partial({ + /** string called foo */ + foo: t.string + }) + }, +}); +`; + +// Test that `required` is not emitted as an empty array +testCase('object with no required properties', EMPTY_REQUIRED, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + description: 'foo response', + properties: { + foo: { + description: 'string called foo', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const OPTIONAL_PARAM = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + foo: h.optional(t.string), + }, + }), + response: { + /** foo response */ + 200: t.string + }, +}); +`; + +testCase('optional parameter', OPTIONAL_PARAM, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'foo', + schema: { + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + + + + +const ROUTE_WITH_ARRAY_QUERY_PARAM = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** + * This is a foo description. + * @example abc + * @pattern ^[a-z]+$ + */ + foo: h.optional(t.array(t.string)) + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with optional array query parameter and documentation', ROUTE_WITH_ARRAY_QUERY_PARAM, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + description: 'This is a foo description.', + in: 'query', + name: 'foo', + schema: { + items: { + description: 'This is a foo description.', + example: 'abc', + type: 'string', + pattern: '^[a-z]+$' + }, + type: 'array' + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + + +const ROUTE_WITH_ARRAY_UNION_NULL_UNDEFINED_QUERY_PARAM = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** + * This is a foo description. + * @example abc + * @pattern ^[a-z]+$ + */ + ipRestrict: t.union([t.array(t.string), t.null, t.undefined]), + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with array union of null and undefined', ROUTE_WITH_ARRAY_UNION_NULL_UNDEFINED_QUERY_PARAM, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + description: 'This is a foo description.', + in: 'query', + name: 'ipRestrict', + schema: { + items: { + description: 'This is a foo description.', + example: 'abc', + type: 'string', + pattern: '^[a-z]+$' + }, + type: 'array' + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); \ No newline at end of file diff --git a/packages/openapi-generator/test/openapi/comments.test.ts b/packages/openapi-generator/test/openapi/comments.test.ts new file mode 100644 index 00000000..fc8930e6 --- /dev/null +++ b/packages/openapi-generator/test/openapi/comments.test.ts @@ -0,0 +1,1491 @@ +import { testCase } from "./testHarness"; + +const ROUTE_WITH_TYPE_DESCRIPTIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** bar param */ + bar: t.string, + }, + body: { + /** foo description */ + foo: t.string, + /** bar description */ + bar: t.number, + child: { + /** child description */ + child: t.string, + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with type descriptions', ROUTE_WITH_TYPE_DESCRIPTIONS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [ + { + description: 'bar param', + in: 'query', + name: 'bar', + required: true, + schema: { + type: 'string' + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + bar: { + description: 'bar description', + type: 'number' + }, + child: { + properties: { + child: { + description: 'child description', + type: 'string' + } + }, + required: [ + 'child' + ], + type: 'object' + }, + foo: { + description: 'foo description', + type: 'string' + } + }, + required: [ + 'foo', + 'bar', + 'child' + ], + type: 'object' + } + } + } + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + + +const ROUTE_WITH_TYPE_DESCRIPTIONS_OPTIONAL = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** bar param */ + bar: t.string, + }, + body: { + /** foo description */ + foo: h.optional(t.string), + /** bar description */ + bar: h.optional(t.number), + child: { + /** child description */ + child: h.optional(t.string), + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + + +testCase('route with type descriptions with optional fields', ROUTE_WITH_TYPE_DESCRIPTIONS_OPTIONAL, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [ + { + description: 'bar param', + in: 'query', + name: 'bar', + required: true, + schema: { + type: 'string' + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + bar: { + description: 'bar description', + type: 'number' + }, + child: { + properties: { + child: { + description: 'child description', + type: 'string' + } + }, + type: 'object' + }, + foo: { + description: 'foo description', + type: 'string' + } + }, + required: [ + 'child' + ], + type: 'object' + } + } + } + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const ROUTE_WITH_MIXED_TYPES_AND_DESCRIPTIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** bar param */ + bar: t.string, + }, + body: { + /** description to describe an optional string */ + foo: h.optional(t.string), + /** description to describe an optional union of number and string */ + bar: h.optional(t.union([t.number, t.string])), + /** description to describe an object */ + child: { + /** dsecription to describe an intersection of a type and a partial */ + child: t.intersection([t.type({ foo: t.string }), t.partial({ bar: t.number })]), + }, + /** description to describe a t.type */ + error: t.type({ error: t.string }), + /** description to describe an optional t.object */ + obj: h.optional(t.object({})), + /** description to describe a t.exact */ + exact: t.exact(t.type({ foo: t.string })), + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with mixed types and descriptions', ROUTE_WITH_MIXED_TYPES_AND_DESCRIPTIONS, + { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + '/foo': { + get: { + summary: "A simple route with type descriptions", + operationId: "api.v1.test", + tags: [ + "Test Routes" + ], + parameters: [ + { + name: "bar", + description: "bar param", + in: "query", + required: true, + schema: { + type: "string" + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: "object", + properties: { + foo: { + type: "string", + description: "description to describe an optional string" + }, + bar: { + oneOf: [ + { + type: "number" + }, + { + type: "string" + } + ], + description: "description to describe an optional union of number and string" + }, + child: { + type: "object", + description: "description to describe an object", + properties: { + child: { + type: "object", + description: "dsecription to describe an intersection of a type and a partial", + properties: { + foo: { + type: "string" + }, + bar: { + type: "number" + } + }, + required: [ + "foo" + ] + } + }, + required: [ + "child" + ] + }, + error: { + type: "object", + description: "description to describe a t.type", + properties: { + error: { + type: "string" + } + }, + required: [ + "error" + ] + }, + obj: { + type: "object", + description: "description to describe an optional t.object", + properties: {} + }, + exact: { + type: "object", + description: "description to describe a t.exact", + properties: { + foo: { + type: "string" + } + }, + required: [ + "foo" + ] + } + }, + required: [ + "child", + "error", + "exact" + ] + } + } + } + }, + responses: { + 200: { + description: "OK", + content: { + 'application/json': { + schema: { + type: "object", + properties: { + test: { + type: "string" + } + }, + required: [ + "test" + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } + }); + +const ROUTE_WITH_ARRAY_TYPES_AND_DESCRIPTIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** bar param */ + bar: t.string, + }, + body: { + /** foo description */ + foo: t.array(t.string), + /** bar description */ + bar: t.array(t.number), + child: { + /** child description */ + child: t.array(t.union([t.string, t.number])), + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with array types and descriptions', ROUTE_WITH_ARRAY_TYPES_AND_DESCRIPTIONS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + description: 'bar param', + in: 'query', + required: true, + schema: { + type: 'string' + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'array', + items: { + type: 'string', + description: 'foo description' + }, + }, + bar: { + type: 'array', + items: { + type: 'number', + description: 'bar description' + }, + }, + child: { + type: 'object', + properties: { + child: { + type: 'array', + items: { + oneOf: [ + { + type: 'string' + }, + { + type: 'number' + } + ], + description: 'child description' + }, + } + }, + required: [ + 'child' + ] + } + }, + required: [ + 'foo', + 'bar', + 'child' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + +const ROUTE_WITH_RECORD_TYPES_AND_DESCRIPTIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** bar param */ + bar: t.record(t.string, t.string), + }, + body: { + /** foo description */ + foo: t.record(t.string, t.number), + child: { + /** child description */ + child: t.record(t.string, t.array(t.union([t.string, t.number]))), + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with record types and descriptions', ROUTE_WITH_RECORD_TYPES_AND_DESCRIPTIONS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + description: 'bar param', + in: 'query', + required: true, + schema: { + type: 'object', + additionalProperties: { + type: 'string' + } + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'object', + additionalProperties: { + type: 'number' + }, + description: 'foo description' + }, + child: { + type: 'object', + properties: { + child: { + type: 'object', + additionalProperties: { + type: 'array', + items: { + oneOf: [ + { + type: 'string' + }, + { + type: 'number' + } + ] + } + }, + description: 'child description' + } + }, + required: [ + 'child' + ] + } + }, + required: [ + 'foo', + 'child' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + +const ROUTE_WITH_DESCRIPTIONS_PATTERNS_EXAMPLES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + /** + * This is a bar param. + * @example { "foo": "bar" } + */ + bar: t.record(t.string, t.string), + }, + body: { + /** + * foo description + * @pattern ^[1-9][0-9]{4}$ + * @example 12345 + */ + foo: t.number, + child: { + /** + * child description + */ + child: t.array(t.union([t.string, t.number])), + } + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with descriptions, patterns, and examples', ROUTE_WITH_DESCRIPTIONS_PATTERNS_EXAMPLES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + description: 'This is a bar param.', + in: 'query', + required: true, + schema: { + type: 'object', + example: { + foo: 'bar' + }, + additionalProperties: { + type: 'string' + } + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'number', + description: 'foo description', + example: 12345, + pattern: '^[1-9][0-9]{4}$' + }, + child: { + type: 'object', + properties: { + child: { + type: 'array', + items: { + description: 'child description', + oneOf: [ + { + type: 'string' + }, + { + type: 'number' + } + ] + }, + } + }, + required: [ + 'child' + ] + } + }, + required: [ + 'foo', + 'child' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + +const ROUTE_WITH_DESCRIPTIONS_FOR_REFERENCES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +const Foo = t.type({ foo: t.string }); +const Bar = t.type({ bar: t.number }); + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + bar: t.array(t.string), + }, + body: { + /** + * This is a foo description. + * @example BitGo Inc + */ + foo: Foo, + bar: Bar, + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with descriptions for references', ROUTE_WITH_DESCRIPTIONS_FOR_REFERENCES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + in: 'query', + required: true, + schema: { + type: 'array', + items: { + type: 'string' + } + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + // needs to be wrapped in an allOf to preserve the description + foo: { + allOf: [ + { + $ref: '#/components/schemas/Foo' + } + ], + description: 'This is a foo description.', + example: 'BitGo Inc' + }, + // should not need to be wrapped in an allOf + bar: { + $ref: '#/components/schemas/Bar' + } + }, + required: [ + 'foo', + 'bar' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + Foo: { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string' + } + }, + required: [ + 'foo' + ] + }, + Bar: { + title: 'Bar', + type: 'object', + properties: { + bar: { + type: 'number' + } + }, + required: [ + 'bar' + ] + } + } + } +}); + +const ROUTE_WITH_MIN_AND_MAX_VALUES_FOR_STRINGS_AND_DEFAULT = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + bar: t.array(t.string), + }, + body: { + /** + * This is a foo description. + * @minLength 5 + * @maxLength 10 + * @example SomeInc + * @default BitgoInc + */ + foo: t.string() + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with min and max values for strings and default value', ROUTE_WITH_MIN_AND_MAX_VALUES_FOR_STRINGS_AND_DEFAULT, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [ + { + name: 'bar', + in: 'query', + required: true, + schema: { + type: 'array', + items: { + type: 'string' + } + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'string', + description: 'This is a foo description.', + example: 'SomeInc', + default: 'BitgoInc', + minLength: 5, + maxLength: 10 + } + }, + required: [ + 'foo' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + + + + +const ROUTE_WITH_OVERRIDING_COMMENTS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * @example "abc" + */ +const TargetSchema = t.string; + +const ParentSchema = t.type({ + /** This description should show with the example */ + target: h.optional(TargetSchema) +}) + +export const route = h.httpRoute({ + path: '/foo', + method: 'POST', + request: h.httpRequest({ + params: {}, + body: ParentSchema + }), + response: { + 200: t.literal('OK'), + }, +}); +`; + +testCase("route with overriding comments", ROUTE_WITH_OVERRIDING_COMMENTS, { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + "/foo": { + post: { + parameters: [], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + target: { + type: "string", + description: "This description should show with the example", + example: "abc" + } + } + } + } + } + }, + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + type: "string", + enum: [ + "OK" + ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + TargetSchema: { + title: "TargetSchema", + type: "string", + example: "abc" + }, + ParentSchema: { + title: "ParentSchema", + type: "object", + properties: { + target: { + type: "string", + description: "This description should show with the example", + example: "abc" + } + } + } + } + } +}); + +const ROUTE_WITH_NESTED_OVERRIDEN_COMMENTS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * @example "abc" + */ +const TargetSchema = t.string; + +const ParentSchema = t.type({ + /** This description should show with the example */ + target: h.optional(TargetSchema) +}) + +const GrandParentSchema = t.type({ + /** This description should override the previous description */ + parent: ParentSchema +}) + +export const route = h.httpRoute({ + path: '/foo', + method: 'POST', + request: h.httpRequest({ + params: {}, + body: GrandParentSchema + }), + response: { + 200: t.literal('OK'), + }, +}); +`; + + +testCase("route with nested overriding comments", ROUTE_WITH_NESTED_OVERRIDEN_COMMENTS, { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + "/foo": { + post: { + parameters: [], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + parent: { + allOf: [ + { + '$ref': '#/components/schemas/ParentSchema' + } + ], + description: 'This description should override the previous description', + }, + }, + required: ['parent'] + } + } + } + }, + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + type: "string", + enum: [ + "OK" + ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + TargetSchema: { + title: "TargetSchema", + type: "string", + example: "abc" + }, + ParentSchema: { + title: "ParentSchema", + type: "object", + properties: { + target: { + type: "string", + description: "This description should show with the example", + example: "abc" + } + } + }, + GrandParentSchema: { + title: "GrandParentSchema", + type: "object", + properties: { + parent: { + allOf: [ + { + '$ref': '#/components/schemas/ParentSchema' + } + ], + description: 'This description should override the previous description' + } + }, + required: ['parent'] + } + } + } +}); + +const ROUTE_WITH_OVERRIDEN_COMMENTS_IN_UNION = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * @example "abc" + */ +const TargetSchema = t.string; + +/** + * @example "def" + */ +const TargetSchema2 = t.string; + +const ParentSchema = t.type({ + /** This description should show with the example */ + target: h.optional(t.union([TargetSchema, TargetSchema2])) +}) + +const SecondaryParentSchema = t.type({ + /** + * This description should show with the overriden example + * @example "overridden example" + */ + target: h.optional(t.union([TargetSchema, TargetSchema2])) +}) + +/** + * This is grandparent schema description + * @title Grand Parent Schema + */ +const GrandParentSchema = t.type({ + parent: ParentSchema, + secondaryParent: SecondaryParentSchema +}); + +export const route = h.httpRoute({ + path: '/foo', + method: 'POST', + request: h.httpRequest({ + params: {}, + body: GrandParentSchema + }), + response: { + 200: t.literal('OK'), + }, +}); +`; + +testCase("route with overriden comments in union", ROUTE_WITH_OVERRIDEN_COMMENTS_IN_UNION, { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + "/foo": { + post: { + parameters: [], + requestBody: { + content: { + "application/json": { + schema: { + title: "Grand Parent Schema", + description: 'This is grandparent schema description', + type: "object", + properties: { + parent: { + "$ref": "#/components/schemas/ParentSchema" + }, + secondaryParent: { + "$ref": "#/components/schemas/SecondaryParentSchema" + } + }, + required: [ + "parent", + "secondaryParent" + ] + } + } + } + }, + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + type: "string", + enum: [ + "OK" + ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + TargetSchema: { + title: "TargetSchema", + type: "string", + example: "abc" + }, + TargetSchema2: { + title: "TargetSchema2", + type: "string", + example: "def" + }, + ParentSchema: { + title: "ParentSchema", + type: "object", + properties: { + target: { + oneOf: [ + { + "$ref": "#/components/schemas/TargetSchema" + }, + { + "$ref": "#/components/schemas/TargetSchema2" + } + ], + description: "This description should show with the example" + } + } + }, + SecondaryParentSchema: { + title: "SecondaryParentSchema", + type: "object", + properties: { + target: { + oneOf: [ + { + "$ref": "#/components/schemas/TargetSchema" + }, + { + "$ref": "#/components/schemas/TargetSchema2" + } + ], + description: "This description should show with the overriden example", + example: "\"overridden example\"" + } + } + }, + GrandParentSchema: { + title: "Grand Parent Schema", + description: 'This is grandparent schema description', + type: "object", + properties: { + parent: { + "$ref": "#/components/schemas/ParentSchema" + }, + secondaryParent: { + "$ref": "#/components/schemas/SecondaryParentSchema" + } + }, + required: [ + "parent", + "secondaryParent" + ] + } + } + } +}); \ No newline at end of file diff --git a/packages/openapi-generator/test/openapi/jsdoc.test.ts b/packages/openapi-generator/test/openapi/jsdoc.test.ts new file mode 100644 index 00000000..fcc0bf6f --- /dev/null +++ b/packages/openapi-generator/test/openapi/jsdoc.test.ts @@ -0,0 +1,1192 @@ +import { testCase } from "./testHarness"; + + +const TITLE_TAG = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const oneOfRoute = h.httpRoute({ + path: '/foo', + method: 'GET', + request: t.union([ + h.httpRequest({ + /** @title this is a title for a oneOf option */ + query: { + /** @title this is a title for a oneOf option's property */ + foo: t.string + } + }), + h.httpRequest({ + query: { + bar: t.string + } + }), + ]), + response: { + /** foo response */ + 200: t.string + }, +}); + +export const route = h.httpRoute({ + path: '/bar', + method: 'GET', + request: h.httpRequest({ + query: { + /** + * bar param + * @title this is a bar parameter + * */ + bar: t.string, + }, + }), + response: { + /** bar response */ + 200: t.string + }, +}); +`; + +testCase('schema parameter with title tag', TITLE_TAG, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'union', + required: true, + style: 'form', + explode: true, + schema: { + oneOf: [ + { + type: 'object', + title: 'this is a title for a oneOf option', + properties: { + foo: { + type: 'string', + title: "this is a title for a oneOf option's property", + }, + }, + required: ['foo'], + }, + { + type: 'object', + properties: { + bar: { + type: 'string', + }, + }, + required: ['bar'], + }, + ], + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + '/bar': { + get: { + parameters: [ + { + in: 'query', + name: 'bar', + description: 'bar param', + required: true, + schema: { + title: 'this is a bar parameter', + type: 'string', + }, + }, + ], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'bar response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + + +const ROUTE_WITH_RESPONSE_EXAMPLE_STRING = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * @operationId api.v1.test + * @tag Test Routes + * @example bar + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: t.string + }, +}); +`; + +testCase('route with example string', ROUTE_WITH_RESPONSE_EXAMPLE_STRING, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string', + }, + example: 'bar', + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * @operationId api.v1.test + * @tag Test Routes + * @example { "test": "bar" } + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with example object', ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + example: { + test: 'bar', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT_MULTILINE = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * @operationId api.v1.test + * @tag Test Routes + * @example { + * "test": "bar" + * } + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase( + 'route with example object multi-line', + ROUTE_WITH_RESPONSE_EXAMPLE_OBJECT_MULTILINE, + { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + example: { + test: 'bar', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, + }, +); + +const ROUTE_WITH_UNKNOWN_TAG = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * @operationId api.v1.test + * @tag Test Routes + * @optout true + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with unknown tag', ROUTE_WITH_UNKNOWN_TAG, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + operationId: 'api.v1.test', + tags: ['Test Routes'], + 'x-unknown-tags': { + optout: 'true', + }, + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + +const ROUTE_WITH_MULTIPLE_UNKNOWN_TAGS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * @operationId api.v1.test + * @tag Test Routes + * @optout true + * @critical false + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with multiple unknown tags', ROUTE_WITH_MULTIPLE_UNKNOWN_TAGS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + operationId: 'api.v1.test', + tags: ['Test Routes'], + 'x-unknown-tags': { + optout: 'true', + critical: 'false', + }, + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string', + }, + }, + required: ['test'], + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + + + + +const ROUTE_WITH_DEPRECATED_TAG = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + body: { + /** + * This is a foo description. + * @deprecated + */ + foo: t.string() + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with deprecated tag', ROUTE_WITH_DEPRECATED_TAG, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + parameters: [], + tags: [ + 'Test Routes' + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'string', + description: 'This is a foo description.', + deprecated: true + } + }, + required: [ + 'foo' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + + + +const ROUTE_WITH_MIN_MAX_AND_OTHER_TAGS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + body: { + /** + * This is a foo description. + * @minimum 5 + * @maximum 10 + * @minItems 1 + * @maxItems 5 + * @minProperties 1 + * @maxProperties 500 + * @exclusiveMinimum true + * @exclusiveMaximum true + * @multipleOf 7 + * @uniqueItems true + * @readOnly true + * @writeOnly true + */ + foo: t.number() + }, + }), + response: { + 200: { + test: t.string + } + }, +}); +`; + +testCase('route with min and max tags', ROUTE_WITH_MIN_MAX_AND_OTHER_TAGS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + parameters: [], + tags: [ + 'Test Routes' + ], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + foo: { + type: 'number', + description: 'This is a foo description.', + minimum: 5, + maximum: 10, + minItems: 1, + maxItems: 5, + minProperties: 1, + multipleOf: 7, + maxProperties: 500, + exclusiveMinimum: true, + exclusiveMaximum: true, + uniqueItems: true, + readOnly: true, + writeOnly: true + } + }, + required: [ + 'foo' + ] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + + +const SCHEMA_WITH_TITLES_IN_REQUEST_BODIES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * @title Some Readable BodyFoo Title + */ +const BodyFoo = t.type({ + /** a foo description */ + foo: t.string, +}); + +/** + * @title Some Readable ParamsFoo Title + */ +const ParamsFoo = { someId: t.string }; + +export const route = h.httpRoute({ + path: '/foo', + method: 'POST', + request: h.httpRequest({ + params: {}, + body: h.httpRequest({ params: ParamsFoo, body: BodyFoo, }) + }), + response: { + 200: t.literal('OK'), + }, +}); +`; + +testCase("route with titles in request bodies", SCHEMA_WITH_TITLES_IN_REQUEST_BODIES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + post: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + params: { + type: 'object', + title: "Some Readable ParamsFoo Title", + properties: { + someId: { type: 'string' } + }, + required: ['someId'] + }, + body: { + type: 'object', + title: 'Some Readable BodyFoo Title', + properties: { + foo: { + type: 'string', + description: 'a foo description' + } + }, + required: ['foo'] + } + }, + required: ['params', 'body'] + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string', + enum: ['OK'] + } + } + } + } + } + } + } + }, + components: { + schemas: { + ParamsFoo: { + title: 'Some Readable ParamsFoo Title', + type: 'object', + properties: { someId: { type: 'string' } }, + required: ['someId'] + }, + BodyFoo: { + title: 'Some Readable BodyFoo Title', + type: 'object', + properties: { + foo: { + type: 'string', + description: 'a foo description' + } + }, + required: ['foo'] + } + } + } +}); + + +const ROUTE_WITH_ARRAY_EXAMPLE = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * @example btc + */ +const innerItems = t.string; + +export const route = h.httpRoute({ + path: '/foo', + method: 'POST', + request: h.httpRequest({ + params: {}, + body: t.type({ + /** + * @example "btc" + */ + array1: t.array(t.string), + /** + * @example ["btc", "eth"] + */ + array2: t.array(innerItems), + /** + * @minItems 1 + * @maxItems 5 + */ + array3: t.array(t.number), + objectWithArray: t.type({ + /** + * @example ["btc", "eth"] + */ + nestedArray: t.array(innerItems) + }) + }) + }), + response: { + 200: t.literal('OK'), + }, +});`; + +testCase("route with array examples", ROUTE_WITH_ARRAY_EXAMPLE, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + post: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + array1: { + type: 'array', + items: { + type: 'string', + example: '"btc"' + }, + }, + array2: { + type: 'array', + example: ['btc', 'eth'], + items: { + type: 'string', + example: 'btc' + }, + }, + array3: { + items: { + type: 'number' + }, + maxItems: 5, + minItems: 1, + type: 'array' + }, + objectWithArray: { + properties: { + nestedArray: { + example: [ + 'btc', + 'eth' + ], + items: { + example: 'btc', + type: 'string' + }, + type: 'array' + } + }, + required: [ + 'nestedArray' + ], + type: 'object' + }, + }, + required: ['array1', 'array2', 'array3', 'objectWithArray'], + }, + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string', + enum: ['OK'] + } + } + } + } + } + } + } + }, + components: { + schemas: { + innerItems: { + title: "innerItems", + type: "string", + example: 'btc' + } + } + } +}); + + +const ROUTE_WITH_NESTED_ARRAY_EXAMPLES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * @example ["a", "b"] + */ +const firstLevel = t.array(t.string); + +/** + * @example [["a", "b"], ["c", "d"]] + */ +const secondLevel = t.array(firstLevel); + +/** + * @example [[["a"], ["b"]], [["c"], ["d"]]] + */ +const thirdLevel = t.array(secondLevel); + +export const route = h.httpRoute({ + path: '/foo', + method: 'POST', + request: h.httpRequest({ + params: {}, + body: t.type({ + nested: thirdLevel + }) + }), + response: { + 200: t.literal('OK'), + }, +}); +`; + +testCase("route with nested array examples", ROUTE_WITH_NESTED_ARRAY_EXAMPLES, { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + "/foo": { + post: { + parameters: [], + requestBody: { + content: { + "application/json": { + schema: { + type: "object", + properties: { + nested: { + "$ref": "#/components/schemas/thirdLevel" + } + }, + required: [ + "nested" + ] + } + } + } + }, + responses: { + 200: { + description: "OK", + content: { + "application/json": { + schema: { + type: "string", + enum: [ + "OK" + ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + firstLevel: { + title: "firstLevel", + type: "array", + example: [ "a", "b" ], + items: { + type: "string" + } + }, + secondLevel: { + title: "secondLevel", + type: "array", + example: [ [ "a", "b" ], [ "c", "d" ] ], + items: { + type: "array", + example: [ "a", "b" ], + items: { + type: "string" + } + } + }, + thirdLevel: { + title: "thirdLevel", + type: "array", + example: [[["a"],["b"]],[["c"],["d"]]], + items: { + type: "array", + example: [["a","b"],["c","d"]], + items: { + type: "array", + example: ["a","b"], + items: { + type: "string" + } + } + } + } + } + } +}); + +const ROUTE_WITH_PRIVATE_PROPERTIES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +const SampleType = t.type({ + foo: t.string, + /** @private */ + bar: t.string, // This should show up with x-internal, + /** @private */ + privateObject: t.type({ + privateFieldInObject: t.boolean + }) +}); + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + params: { + /** @private */ + path: t.string + }, + query: { + /** @private */ + query: t.string + }, + body: SampleType + }), + response: { + 200: SampleType + }, +}); +`; + +testCase("route with private properties in request query, params, body, and response", ROUTE_WITH_PRIVATE_PROPERTIES, { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + '/foo': { + get: { + parameters: [ + { + 'x-internal': true, + description: '', + in: 'query', + name: 'query', + required: true, + schema: { + type: 'string' + } + }, + { + 'x-internal': true, + description: '', + in: 'path', + name: 'path', + required: true, + schema: { + type: 'string' + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + bar: { + 'x-internal': true, + type: 'string' + }, + foo: { + type: 'string' + }, + privateObject: { + 'x-internal': true, + properties: { + privateFieldInObject: { + type: 'boolean' + } + }, + required: [ + 'privateFieldInObject' + ], + type: 'object' + } + }, + required: [ + 'foo', + 'bar', + 'privateObject' + ], + type: 'object' + } + } + }, + }, + responses: { + '200': { + content: { + 'application/json': { + schema: { + '$ref': '#/components/schemas/SampleType' + } + } + }, + description: 'OK' + } + } + } + }, + }, + components: { + schemas: { + SampleType: { + properties: { + bar: { + 'x-internal': true, + type: 'string' + }, + foo: { + type: 'string' + }, + privateObject: { + 'x-internal': true, + properties: { + privateFieldInObject: { + type: 'boolean' + } + }, + required: [ + 'privateFieldInObject' + ], + type: 'object' + } + }, + required: [ + 'foo', + 'bar', + 'privateObject' + ], + title: 'SampleType', + type: 'object' + } + } + }, +}); diff --git a/packages/openapi-generator/test/openapi/knownImports.test.ts b/packages/openapi-generator/test/openapi/knownImports.test.ts new file mode 100644 index 00000000..f0ec846b --- /dev/null +++ b/packages/openapi-generator/test/openapi/knownImports.test.ts @@ -0,0 +1,147 @@ +import { testCase } from "./testHarness"; + +const ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +import { DateFromNumber } from 'io-ts-types'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + ipRestrict: t.boolean + }, + }), + response: { + 200: { + test: DateFromNumber + } + }, +}); +`; + +testCase('route with schema with default metadata', ROUTE_WITH_SCHEMA_WITH_DEFAULT_METADATA, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'ipRestrict', + required: true, + schema: { + type: 'boolean', + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'number', + format: 'number', + title: 'Unix Time (milliseconds)', + description: 'Number of milliseconds since the Unix epoch', + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + +const ROUTE_WITH_OVERIDDEN_METADATA = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +import { DateFromNumber } from 'io-ts-types'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + ipRestrict: t.boolean + }, + }), + response: { + 200: { + /** + * Testing overridden metadata + * @format string + */ + test: DateFromNumber + } + }, +}); +`; + +testCase('route with schema with default metadata', ROUTE_WITH_OVERIDDEN_METADATA, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'ipRestrict', + required: true, + schema: { + type: 'boolean', + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + test: { + type: 'number', + format: 'string', + title: 'Unix Time (milliseconds)', + description: 'Testing overridden metadata', + } + }, + required: [ + 'test' + ] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); diff --git a/packages/openapi-generator/test/openapi/misc.test.ts b/packages/openapi-generator/test/openapi/misc.test.ts new file mode 100644 index 00000000..a132da7e --- /dev/null +++ b/packages/openapi-generator/test/openapi/misc.test.ts @@ -0,0 +1,295 @@ +import { testCase } from "./testHarness"; + +const HEADER_COMMENT = ` +/* + * This is a comment + */ + +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +/** + * A simple route + * + * ## How to call the route + * + * \`\`\` + * curl -X GET http://localhost:3000/foo?foo=bar + * \`\`\` + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: t.string + }, +}); +`; + +testCase('source file with a header comment', HEADER_COMMENT, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + summary: 'A simple route', + description: + '## How to call the route\n' + + '\n' + + '```\n' + + 'curl -X GET http://localhost:3000/foo?foo=bar\n' + + '```', + operationId: 'api.v1.test', + tags: ['Test Routes'], + parameters: [], + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: {}, + }, +}); + + + + + + + +const SCHEMA_WITH_MANY_RESPONSE_TYPES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +const ApiError = t.type({ + /** error message */ + error: t.string, +}); + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + /** string response type */ + 200: t.string, + 400: ApiError + }, +}) +`; + +testCase('route with many response codes uses default status code descriptions', SCHEMA_WITH_MANY_RESPONSE_TYPES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'string response type', + type: 'string' + } + } + } + }, + '400': { + description: 'Bad Request', + content: { + 'application/json': { + schema: { + '$ref': '#/components/schemas/ApiError' + } + } + } + } + } + } + } + }, + components: { + schemas: { + ApiError: { + properties: { + error: { + type: 'string', + description: 'error message', + } + }, + required: [ + 'error' + ], + type: 'object', + title: 'ApiError' + }, + } + } +}); + + + + + + + +const ROUTE_WITH_RECORD_TYPES = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +const ValidKeys = t.keyof({ name: "name", age: "age", address: "address" }); +const PersonObject = t.type({ bigName: t.string, bigAge: t.number }); +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + name: t.string, + }, + }), + response: { + 200: { + person: t.record(ValidKeys, t.string), + anotherPerson: t.record(ValidKeys, PersonObject), + bigPerson: t.record(t.string, t.string), + anotherBigPerson: t.record(t.string, PersonObject), + } + }, +}); +`; + +testCase("route with record types", ROUTE_WITH_RECORD_TYPES, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + name: 'name', + in: 'query', + required: true, + schema: { + type: 'string' + } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + // becomes t.type() + person: { + type: 'object', + properties: { + name: { type: 'string' }, + age: { type: 'string' }, + address: { type: 'string' } + }, + required: [ 'name', 'age', 'address' ] + }, + // becomes t.type() + anotherPerson: { + type: 'object', + properties: { + name: { + type: 'object', + properties: { + bigName: { type: 'string' }, + bigAge: { type: 'number' } + }, + required: [ 'bigName', 'bigAge' ] + }, + age: { + type: 'object', + properties: { + bigName: { type: 'string' }, + bigAge: { type: 'number' } + }, + required: [ 'bigName', 'bigAge' ] + }, + address: { + type: 'object', + properties: { + bigName: { type: 'string' }, + bigAge: { type: 'number' } + }, + required: [ 'bigName', 'bigAge' ] + } + }, + required: [ 'name', 'age', 'address' ] + }, + bigPerson: { + // stays as t.record() + type: 'object', + additionalProperties: { type: 'string' } + }, + anotherBigPerson: { + // stays as t.record() + type: 'object', + additionalProperties: { + type: 'object', + properties: { + bigName: { type: 'string' }, + bigAge: { type: 'number' } + }, + required: [ 'bigName', 'bigAge' ] + } + } + }, + required: [ 'person', 'anotherPerson', 'bigPerson', 'anotherBigPerson' ] + } + } + } + } + } + } + } + }, + components: { + schemas: { + ValidKeys: { + title: 'ValidKeys', + type: 'string', + enum: [ 'name', 'age', 'address' ] + }, + PersonObject: { + title: 'PersonObject', + type: 'object', + properties: { bigName: { type: 'string' }, bigAge: { type: 'number' } }, + required: [ 'bigName', 'bigAge' ] + } + } + } +}); + diff --git a/packages/openapi-generator/test/openapi/ref.test.ts b/packages/openapi-generator/test/openapi/ref.test.ts new file mode 100644 index 00000000..585c1d98 --- /dev/null +++ b/packages/openapi-generator/test/openapi/ref.test.ts @@ -0,0 +1,454 @@ +import { testCase } from "./testHarness"; + +const SCHEMA_REF = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: t.type({ + body: Foo, + }), + response: { + /** foo response */ + 200: t.string + }, +}); + +const Foo = t.type({ foo: t.string }); +`; + +testCase('request body ref', SCHEMA_REF, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Foo', + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Foo: { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + }, + }, +}); + +const SCHEMA_REF_WITH_COMMENT_AT_DECLARATION = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + params: { + body: t.string, + /** + * Size of the body + * @example 10 + */ + size: t.number, + } + }), + response: { + 200: Foo + }, +}); + +/** + * a Foo of type 'string' + * @example "foo" + */ +const Foo = t.string; +`; + +testCase('request body ref with comments', SCHEMA_REF_WITH_COMMENT_AT_DECLARATION, { + openapi: "3.0.3", + info: { + title: "Test", + version: "1.0.0" + }, + paths: { + "/foo": { + get: { + parameters: [ + { + name: "body", + in: "path", + required: true, + schema: { + type: "string" + } + }, + { + name: "size", + description: "Size of the body", + in: "path", + required: true, + schema: { + type: "number", + example: 10 + } + } + ], + responses: { + "200": { + description: "OK", + content: { + "application/json": { + schema: { + $ref: "#/components/schemas/Foo" + } + } + } + } + } + } + } + }, + components: { + schemas: { + Foo: { + title: "Foo", + type: "string", + description: "a Foo of type 'string'", + example: "foo" + } + } + } +}); + +const SCHEMA_DOUBLE_REF = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: t.type({ + body: Bar, + }), + response: { + /** foo response */ + 200: t.string + }, +}); + +const Foo = t.type({ foo: t.string }); + +const Bar = Foo; +`; + +testCase('request body double ref', SCHEMA_DOUBLE_REF, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Bar', + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Foo: { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + Bar: { + allOf: [{ title: 'Bar' }, { $ref: '#/components/schemas/Foo' }], + }, + }, + }, +}); + +const SCHEMA_NULLABLE_REF = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: t.type({ + body: t.union([Foo, t.null]), + }), + response: { + /** foo response */ + 200: t.string + }, +}); + +const Foo = t.type({ foo: t.string }); +`; + +testCase('request body nullable ref', SCHEMA_NULLABLE_REF, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0', + }, + paths: { + '/foo': { + get: { + parameters: [], + requestBody: { + content: { + 'application/json': { + schema: { + nullable: true, + allOf: [ + { + $ref: '#/components/schemas/Foo', + }, + ], + }, + }, + }, + }, + responses: { + 200: { + description: 'OK', + content: { + 'application/json': { + schema: { + description: 'foo response', + type: 'string', + }, + }, + }, + }, + }, + }, + }, + }, + components: { + schemas: { + Foo: { + title: 'Foo', + type: 'object', + properties: { + foo: { + type: 'string', + }, + }, + required: ['foo'], + }, + }, + }, +}); + + +const ROUTE_WITH_SCHEMA_WITH_COMMENT = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +/** + * A simple route with type descriptions for references + * + * @operationId api.v1.test + * @tag Test Routes + */ +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: SimpleRouteResponse, + 400: ApiError, + 401: InvalidError + }, + }); +/** + * Human readable description of the Simple Route Response + * @title Human Readable Simple Route Response + */ +const SimpleRouteResponse = t.type({ + test: t.string, +}); +/** + * Human readable description of the InvalidError schema + * @title Human Readable Invalid Error Schema + */ +const InvalidError = t.intersection([ + ApiError, + t.type({ error: t.literal('invalid') })]); +/** + * Human readable description of the ApiError schema + * @title Human Readable Api Error Schema + */ +const ApiError = t.type({ + error: t.string, +}); + `; + +testCase('route with api error schema', ROUTE_WITH_SCHEMA_WITH_COMMENT, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + summary: 'A simple route with type descriptions for references', + operationId: 'api.v1.test', + tags: [ + 'Test Routes' + ], + parameters: [], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + '$ref': '#/components/schemas/SimpleRouteResponse' + } + } + } + }, + '400': { + content: { + 'application/json': { + schema: { + '$ref': '#/components/schemas/ApiError' + } + } + }, + description: 'Bad Request' + }, + '401': { + description: 'Unauthorized', + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/InvalidError' + } + } + } + } + } + } + }, + }, + components: { + schemas: { + ApiError: { + properties: { + error: { + type: 'string' + } + }, + required: [ + 'error' + ], + title: 'Human Readable Api Error Schema', + description: 'Human readable description of the ApiError schema', + type: 'object' + }, + SimpleRouteResponse: { + description: 'Human readable description of the Simple Route Response', + properties: { + test: { + type: 'string' + } + }, + required: [ + 'test' + ], + title: 'Human Readable Simple Route Response', + type: 'object', + }, + InvalidError: { + title: 'Human Readable Invalid Error Schema', + description: 'Human readable description of the InvalidError schema', + allOf: [ + { + type: 'object', + properties: { + error: { + type: 'string', + enum: [ + 'invalid' + ] + } + }, + required: [ + 'error' + ] + }, + { + $ref: '#/components/schemas/ApiError' + } + ], + }, + } + } +}); \ No newline at end of file diff --git a/packages/openapi-generator/test/openapi/testHarness.ts b/packages/openapi-generator/test/openapi/testHarness.ts new file mode 100644 index 00000000..61fd7bd2 --- /dev/null +++ b/packages/openapi-generator/test/openapi/testHarness.ts @@ -0,0 +1,60 @@ +import * as E from 'fp-ts/lib/Either'; +import assert from 'node:assert/strict'; +import test from 'node:test'; +import { + convertRoutesToOpenAPI, + parsePlainInitializer, + parseSource, + parseRoute, + Project, + type Route, + type Schema, + } from '../../src'; +import { SourceFile } from '../../src/sourceFile'; + +export async function testCase( + description: string, + src: string, + expected: any, + expectedErrors: string[] = [], + ) { + test(description, async () => { + const sourceFile = await parseSource('./index.ts', src); + if (sourceFile === undefined) { + throw new Error('Failed to parse source file'); + } + const files: Record = { './index.ts': sourceFile }; + const project = new Project(files); + const routes: Route[] = []; + const schemas: Record = {}; + const errors: string[] = []; + for (const symbol of sourceFile.symbols.declarations) { + if (symbol.init !== undefined) { + const routeSchemaE = parsePlainInitializer(project, sourceFile, symbol.init); + if (E.isLeft(routeSchemaE)) { + errors.push(routeSchemaE.left); + continue; + } + if (symbol.comment !== undefined) { + routeSchemaE.right.comment = symbol.comment; + } + const result = parseRoute(project, routeSchemaE.right); + if (E.isLeft(result)) { + schemas[symbol.name] = routeSchemaE.right; + } else { + routes.push(result.right); + } + } + } + + const actual = convertRoutesToOpenAPI( + { title: 'Test', version: '1.0.0' }, + [], + routes, + schemas, + ); + + assert.deepEqual(errors, expectedErrors); + assert.deepEqual(actual, expected); + }); + } \ No newline at end of file diff --git a/packages/openapi-generator/test/openapi/union.test.ts b/packages/openapi-generator/test/openapi/union.test.ts new file mode 100644 index 00000000..e8b62212 --- /dev/null +++ b/packages/openapi-generator/test/openapi/union.test.ts @@ -0,0 +1,376 @@ +import { testCase } from "./testHarness"; + +const SCHEMA_WITH_REDUNDANT_UNIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + foo: t.union([t.string, t.string]), + bar: t.union([t.number, t.number, t.number]), + bucket: t.union([t.string, t.number, t.boolean, t.string, t.number, t.boolean]), + }, + body: { + typeUnion: t.union([ + t.type({ foo: t.string, bar: t.number }), + t.type({ bar: t.number, foo: t.string}), + ]), + nestedTypeUnion: t.union([ + t.type({ nested: t.type({ foo: t.string, bar: t.number }) }), + t.type({ nested: t.type({ foo: t.string, bar: t.number }) }) + ]) + } + }), + response: { + 200: t.union([t.string, t.string, t.union([t.number, t.number])]), + 400: t.union([t.boolean, t.boolean, t.boolean]) + }, +}) +`; + +testCase('route with reduntant response schemas', SCHEMA_WITH_REDUNDANT_UNIONS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + in: 'query', + name: 'foo', + required: true, + schema: { + type: 'string' + } + }, + { + in: 'query', + name: 'bar', + required: true, + schema: { + type: 'number' + } + }, + { + in: 'query', + name: 'bucket', + required: true, + schema: { + oneOf: [ + { type: 'string' }, + { type: 'number' }, + { type: 'boolean' } + ] + } + } + ], + requestBody: { + content: { + 'application/json': { + schema: { + properties: { + nestedTypeUnion: { + properties: { + nested: { + properties: { + bar: { + type: 'number' + }, + foo: { + type: 'string' + } + }, + required: [ + 'bar', + 'foo' + ], + type: 'object' + } + }, + required: [ + 'nested' + ], + type: 'object' + }, + typeUnion: { + properties: { + bar: { + type: 'number' + }, + foo: { + type: 'string' + } + }, + required: [ + 'bar', + 'foo' + ], + type: 'object' + } + }, + required: [ + 'typeUnion', + 'nestedTypeUnion' + ], + type: 'object' + } + } + } + }, + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + oneOf: [{ + type: 'string' + }, { + type: 'number' + }] + } + } + } + }, + '400': { + description: 'Bad Request', + content: { + 'application/json': { + schema: { + type: 'boolean' + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + + +const ROUTE_WITH_CONSOLIDATABLE_UNION_SCHEMAS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; +import { BooleanFromString, BooleanFromNumber, NumberFromString } from 'io-ts-types'; + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({ + query: { + // are not consolidatable + firstUnion: t.union([t.string, t.number]), + secondUnion: t.union([BooleanFromString, NumberFromString]), + thirdUnion: t.union([t.string, BooleanFromString]), + firstNonUnion: BooleanFromString, + secondNonUnion: NumberFromString, + thirdNonUnion: t.string, + }, + }), + response: { + 200: { + // are consolidatable + fourthUnion: t.union([t.boolean, BooleanFromNumber]), + fifthUnion: h.optional(t.union([t.boolean, t.boolean, BooleanFromNumber, BooleanFromString])), + sixthUnion: t.union([t.number, NumberFromString]), + } + }, +}); +`; + +testCase("route with consolidatable union schemas", ROUTE_WITH_CONSOLIDATABLE_UNION_SCHEMAS, { + openapi: '3.0.3', + info: { + title: 'Test', + version: '1.0.0' + }, + paths: { + '/foo': { + get: { + parameters: [ + { + name: 'firstUnion', + in: 'query', + required: true, + schema: { + oneOf: [ + { type: 'string' }, + { type: 'number' } + ] + } + }, + { + name: 'secondUnion', + in: 'query', + required: true, + schema: { + oneOf: [ + { type: 'string', format: 'number' }, + { type: 'string', enum: ['true', 'false'] } + ] + } + }, + { + name: 'thirdUnion', + in: 'query', + required: true, + schema: { + oneOf: [ + { type: 'string' }, + { type: 'string', enum: ['true', 'false'] } + ] + } + }, + { + name: 'firstNonUnion', + in: 'query', + required: true, + schema: { type: 'string', enum: ['true', 'false'] } + }, + { + name: 'secondNonUnion', + in: 'query', + required: true, + schema: { type: 'string', format: 'number' } + }, + { + name: 'thirdNonUnion', + in: 'query', + required: true, + schema: { type: 'string' } + } + ], + responses: { + '200': { + description: 'OK', + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + fourthUnion: { type: 'boolean' }, + fifthUnion: { type: 'boolean' }, + sixthUnion: { type: 'number' } + }, + required: ['fourthUnion', 'sixthUnion'] + } + } + } + } + } + } + } + }, + components: { + schemas: {} + } +}); + +const ROUTE_WITH_UNKNOWN_UNIONS = ` +import * as t from 'io-ts'; +import * as h from '@api-ts/io-ts-http'; + +const UnknownUnion = t.union([t.string, t.number, t.boolean, t.unknown]); +const SingleUnknownUnion = t.union([t.unknown, t.string]); + +const NestedUnknownUnion = t.union([t.union([t.string, t.unknown]), t.union([t.boolean, t.unknown])]); + +export const route = h.httpRoute({ + path: '/foo', + method: 'GET', + request: h.httpRequest({}), + response: { + 200: { + single: SingleUnknownUnion, + unknown: UnknownUnion, + nested: NestedUnknownUnion, + } + }, +}); +`; + +testCase("route with unknown unions", ROUTE_WITH_UNKNOWN_UNIONS, { + info: { + title: 'Test', + version: '1.0.0' + }, + openapi: '3.0.3', + paths: { + '/foo': { + get: { + parameters: [], + responses: { + '200': { + content: { + 'application/json': { + schema: { + properties: { + nested: { + '$ref': '#/components/schemas/NestedUnknownUnion' + }, + single: { + '$ref': '#/components/schemas/SingleUnknownUnion' + }, + unknown: { + '$ref': '#/components/schemas/UnknownUnion' + } + }, + required: [ + 'single', + 'unknown', + 'nested' + ], + type: 'object' + } + } + }, + description: 'OK' + } + } + } + } + }, + components: { + schemas: { + NestedUnknownUnion: { + oneOf: [ + { + type: 'string' + }, + { + type: 'boolean' + } + ], + title: 'NestedUnknownUnion' + }, + SingleUnknownUnion: { + title: 'SingleUnknownUnion', + type: 'string' + }, + UnknownUnion: { + oneOf: [ + { + type: 'string' + }, + { + type: 'number' + }, + { + type: 'boolean' + } + ], + title: 'UnknownUnion' + } + } + }, +}); \ No newline at end of file diff --git a/packages/superagent-wrapper/package.json b/packages/superagent-wrapper/package.json index a3fcdec8..e15ccf98 100644 --- a/packages/superagent-wrapper/package.json +++ b/packages/superagent-wrapper/package.json @@ -25,14 +25,14 @@ "devDependencies": { "@swc-node/register": "1.10.9", "@types/express": "4.17.21", - "@types/node": "22.0.0", + "@types/node": "22.1.0", "@types/superagent": "8.1.8", "@types/supertest": "6.0.2", "@types/whatwg-url": "11.0.5", "c8": "10.1.2", "express": "4.19.2", "io-ts-types": "0.5.19", - "superagent": "9.0.2", + "superagent": "10.0.0", "supertest": "7.0.0", "typescript": "4.7.4" },