diff --git a/package.json b/package.json index 18fda5b..be7009f 100644 --- a/package.json +++ b/package.json @@ -33,9 +33,11 @@ "prettier": "^3.0.2", "prettier-plugin-pkg": "^0.18.0", "tsup": "^7.2.0", + "tsx": "^3.14.0", "typescript": "^5.0.4", - "vitest": "^0.31.0" + "vitest": "^0.34.6" }, + "sideEffects": false, "lint-staged": { "*.{ts,yaml,json,md}": "prettier --write" }, diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 96d292a..1717c5e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -1,5 +1,9 @@ lockfileVersion: '6.0' +settings: + autoInstallPeers: true + excludeLinksFromLockfile: false + devDependencies: husky: specifier: ^8.0.3 @@ -16,12 +20,15 @@ devDependencies: tsup: specifier: ^7.2.0 version: 7.2.0(typescript@5.0.4) + tsx: + specifier: ^3.14.0 + version: 3.14.0 typescript: specifier: ^5.0.4 version: 5.0.4 vitest: - specifier: ^0.31.0 - version: 0.31.0 + specifier: ^0.34.6 + version: 0.34.6 packages: @@ -223,6 +230,13 @@ packages: dev: true optional: true + /@jest/schemas@29.6.3: + resolution: {integrity: sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dependencies: + '@sinclair/typebox': 0.27.8 + dev: true + /@jridgewell/gen-mapping@0.3.3: resolution: {integrity: sha512-HLhSWOLRi875zjjMG/r+Nv0oCW8umGb0BgEhyX3dDX3egwZtB8PqLnjz3yedt8R5StBrzcg4aBpnh8UA9D1BoQ==} engines: {node: '>=6.0.0'} @@ -274,66 +288,71 @@ packages: fastq: 1.15.0 dev: true - /@types/chai-subset@1.3.3: - resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} + /@sinclair/typebox@0.27.8: + resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + dev: true + + /@types/chai-subset@1.3.4: + resolution: {integrity: sha512-CCWNXrJYSUIojZ1149ksLl3AN9cmZ5djf+yUoVVV+NuYrtydItQVlL2ZDqyC6M6O9LWRnVf8yYDxbXHO2TfQZg==} dependencies: - '@types/chai': 4.3.5 + '@types/chai': 4.3.9 dev: true - /@types/chai@4.3.5: - resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} + /@types/chai@4.3.9: + resolution: {integrity: sha512-69TtiDzu0bcmKQv3yg1Zx409/Kd7r0b5F1PfpYJfSHzLGtB53547V4u+9iqKYsTu/O2ai6KTb0TInNpvuQ3qmg==} dev: true - /@types/node@18.16.3: - resolution: {integrity: sha512-OPs5WnnT1xkCBiuQrZA4+YAV4HEJejmHneyraIaxsbev5yCEr6KMwINNFP9wQeFIw8FWcoTqF3vQsa5CDaI+8Q==} + /@types/node@20.8.10: + resolution: {integrity: sha512-TlgT8JntpcbmKUFzjhsyhGfP2fsiz1Mv56im6enJ905xG1DAYesxJaeSbGqQmAw8OWPdhyJGhGSQGKRNJ45u9w==} + dependencies: + undici-types: 5.26.5 dev: true - /@vitest/expect@0.31.0: - resolution: {integrity: sha512-Jlm8ZTyp6vMY9iz9Ny9a0BHnCG4fqBa8neCF6Pk/c/6vkUk49Ls6UBlgGAU82QnzzoaUs9E/mUhq/eq9uMOv/g==} + /@vitest/expect@0.34.6: + resolution: {integrity: sha512-QUzKpUQRc1qC7qdGo7rMK3AkETI7w18gTCUrsNnyjjJKYiuUB9+TQK3QnR1unhCnWRC0AbKv2omLGQDF/mIjOw==} dependencies: - '@vitest/spy': 0.31.0 - '@vitest/utils': 0.31.0 - chai: 4.3.7 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + chai: 4.3.10 dev: true - /@vitest/runner@0.31.0: - resolution: {integrity: sha512-H1OE+Ly7JFeBwnpHTrKyCNm/oZgr+16N4qIlzzqSG/YRQDATBYmJb/KUn3GrZaiQQyL7GwpNHVZxSQd6juLCgw==} + /@vitest/runner@0.34.6: + resolution: {integrity: sha512-1CUQgtJSLF47NnhN+F9X2ycxUP0kLHQ/JWvNHbeBfwW8CzEGgeskzNnHDyv1ieKTltuR6sdIHV+nmR6kPxQqzQ==} dependencies: - '@vitest/utils': 0.31.0 - concordance: 5.0.4 + '@vitest/utils': 0.34.6 p-limit: 4.0.0 - pathe: 1.1.0 + pathe: 1.1.1 dev: true - /@vitest/snapshot@0.31.0: - resolution: {integrity: sha512-5dTXhbHnyUMTMOujZPB0wjFjQ6q5x9c8TvAsSPUNKjp1tVU7i9pbqcKPqntyu2oXtmVxKbuHCqrOd+Ft60r4tg==} + /@vitest/snapshot@0.34.6: + resolution: {integrity: sha512-B3OZqYn6k4VaN011D+ve+AA4whM4QkcwcrwaKwAbyyvS/NB1hCWjFIBQxAQQSQir9/RtyAAGuq+4RJmbn2dH4w==} dependencies: - magic-string: 0.30.0 - pathe: 1.1.0 - pretty-format: 27.5.1 + magic-string: 0.30.5 + pathe: 1.1.1 + pretty-format: 29.7.0 dev: true - /@vitest/spy@0.31.0: - resolution: {integrity: sha512-IzCEQ85RN26GqjQNkYahgVLLkULOxOm5H/t364LG0JYb3Apg0PsYCHLBYGA006+SVRMWhQvHlBBCyuByAMFmkg==} + /@vitest/spy@0.34.6: + resolution: {integrity: sha512-xaCvneSaeBw/cz8ySmF7ZwGvL0lBjfvqc1LpQ/vcdHEvpLn3Ff1vAvjw+CoGn0802l++5L/pxb7whwcWAw+DUQ==} dependencies: - tinyspy: 2.1.0 + tinyspy: 2.2.0 dev: true - /@vitest/utils@0.31.0: - resolution: {integrity: sha512-kahaRyLX7GS1urekRXN2752X4gIgOGVX4Wo8eDUGUkTWlGpXzf5ZS6N9RUUS+Re3XEE8nVGqNyxkSxF5HXlGhQ==} + /@vitest/utils@0.34.6: + resolution: {integrity: sha512-IG5aDD8S6zlvloDsnzHw0Ut5xczlF+kv2BOTo+iXfPr54Yhi5qbVOgGB1hZaVq4iJ4C/MZ2J0y15IlsV/ZcI0A==} dependencies: - concordance: 5.0.4 - loupe: 2.3.6 - pretty-format: 27.5.1 + diff-sequences: 29.6.3 + loupe: 2.3.7 + pretty-format: 29.7.0 dev: true - /acorn-walk@8.2.0: - resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} + /acorn-walk@8.3.0: + resolution: {integrity: sha512-FS7hV565M5l1R08MXqo8odwMTB02C2UqzB17RVgu9EyuYFBqJZ3/ZY97sQD5FewVu1UyDFc1yztUDrAwT0EypA==} engines: {node: '>=0.4.0'} dev: true - /acorn@8.8.2: - resolution: {integrity: sha512-xjIYgE8HBrkpd/sJqOGNspf8uHG+NOHGOw6a/Urj8taM2EXfdNAH2oFcPeIFfsv3+kz/mJrS5VuMqbNLjCa2vw==} + /acorn@8.11.2: + resolution: {integrity: sha512-nc0Axzp/0FILLEVsm4fNwLCwMttvhEI263QtVPQcbpfZZ3ts0hLsZGOpE6czNlid7CJ9MlyH8reXkpsf3YUY4w==} engines: {node: '>=0.4.0'} hasBin: true dev: true @@ -345,11 +364,6 @@ packages: type-fest: 1.4.0 dev: true - /ansi-regex@5.0.1: - resolution: {integrity: sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==} - engines: {node: '>=8'} - dev: true - /ansi-regex@6.0.1: resolution: {integrity: sha512-n5M855fKb2SsfMIiFFoVrABHJC8QtHwVx+mHWP3QcEqBHYienj5dHSgjbxtC0WEZXYt4wcD6zrQElDPhFuZgfA==} engines: {node: '>=12'} @@ -395,10 +409,6 @@ packages: engines: {node: '>=8'} dev: true - /blueimp-md5@2.19.0: - resolution: {integrity: sha512-DRQrD6gJyy8FbiE4s+bDoXS9hiW3Vbx5uCdwvcCf3zLHL+Iv7LtGHLpr+GZV8rHG8tK766FGYBwRbu8pELTt+w==} - dev: true - /brace-expansion@1.1.11: resolution: {integrity: sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==} dependencies: @@ -413,6 +423,10 @@ packages: fill-range: 7.0.1 dev: true + /buffer-from@1.1.2: + resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + dev: true + /bundle-require@4.0.1(esbuild@0.18.20): resolution: {integrity: sha512-9NQkRHlNdNpDBGmLpngF3EFDcwodhMUuLz9PaWYciVcQF9SE4LFjM2DB/xV1Li5JiuDMv7ZUWuC3rGbqR0MAXQ==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -428,15 +442,15 @@ packages: engines: {node: '>=8'} dev: true - /chai@4.3.7: - resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} + /chai@4.3.10: + resolution: {integrity: sha512-0UXG04VuVbruMUYbJ6JctvH0YnC/4q3/AkT18q4NaITo91CUm0liMS9VqzT9vZhVQ/1eqPanMWjBM+Juhfb/9g==} engines: {node: '>=4'} dependencies: assertion-error: 1.1.0 - check-error: 1.0.2 + check-error: 1.0.3 deep-eql: 4.1.3 - get-func-name: 2.0.0 - loupe: 2.3.6 + get-func-name: 2.0.2 + loupe: 2.3.7 pathval: 1.1.1 type-detect: 4.0.8 dev: true @@ -446,8 +460,10 @@ packages: engines: {node: ^12.17.0 || ^14.13 || >=16.0.0} dev: true - /check-error@1.0.2: - resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} + /check-error@1.0.3: + resolution: {integrity: sha512-iKEoDYaRmd1mxM90a2OEfWhjsjPpYPuQ+lMYsoxB126+t8fw7ySEO48nmDg5COTjxDI65/Y2OWpeEHk3ZOe8zg==} + dependencies: + get-func-name: 2.0.2 dev: true /chokidar@3.5.3: @@ -498,20 +514,6 @@ packages: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} dev: true - /concordance@5.0.4: - resolution: {integrity: sha512-OAcsnTEYu1ARJqWVGwf4zh4JDfHZEaSNlNccFmt8YjB2l/n19/PF2viLINHc57vO4FKIAFl2FWASIGZZWZ2Kxw==} - engines: {node: '>=10.18.0 <11 || >=12.14.0 <13 || >=14'} - dependencies: - date-time: 3.1.0 - esutils: 2.0.3 - fast-diff: 1.2.0 - js-string-escape: 1.0.1 - lodash: 4.17.21 - md5-hex: 3.0.1 - semver: 7.5.0 - well-known-symbols: 2.0.0 - dev: true - /cross-spawn@7.0.3: resolution: {integrity: sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==} engines: {node: '>= 8'} @@ -521,13 +523,6 @@ packages: which: 2.0.2 dev: true - /date-time@3.1.0: - resolution: {integrity: sha512-uqCUKXE5q1PNBXjPqvwhwJf9SwMoAHBgWJ6DcrnS5o+W2JOiIILl0JEdVD8SGujrNS02GGxgwAg2PN2zONgtjg==} - engines: {node: '>=6'} - dependencies: - time-zone: 1.0.0 - dev: true - /debug@4.3.4: resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} engines: {node: '>=6.0'} @@ -547,6 +542,11 @@ packages: type-detect: 4.0.8 dev: true + /diff-sequences@29.6.3: + resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + dev: true + /dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -592,11 +592,6 @@ packages: '@esbuild/win32-x64': 0.18.20 dev: true - /esutils@2.0.3: - resolution: {integrity: sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==} - engines: {node: '>=0.10.0'} - dev: true - /eventemitter3@5.0.1: resolution: {integrity: sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==} dev: true @@ -631,10 +626,6 @@ packages: strip-final-newline: 3.0.0 dev: true - /fast-diff@1.2.0: - resolution: {integrity: sha512-xJuoT5+L99XlZ8twedaRf6Ax2TgQVxvgZOYoPKqZufmJib0tL2tegPBOZb1pVNgIhlqDlA0eO0c3wBvQcmzx4w==} - dev: true - /fast-glob@3.3.1: resolution: {integrity: sha512-kNFPyjhh5cKjrUltxs+wFx+ZkbRaxxmZ+X0ZU31SOsxCEtP9VPgtq2teZw1DebupL5GmDaNQ6yKMMVcM41iqDg==} engines: {node: '>=8.6.0'} @@ -671,8 +662,8 @@ packages: dev: true optional: true - /get-func-name@2.0.0: - resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} + /get-func-name@2.0.2: + resolution: {integrity: sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==} dev: true /get-stream@6.0.1: @@ -680,6 +671,12 @@ packages: engines: {node: '>=10'} dev: true + /get-tsconfig@4.7.2: + resolution: {integrity: sha512-wuMsz4leaj5hbGgg4IvDU0bqJagpftG5l5cXIAvo8uZrqn0NJqwtfupTN00VnkQJPcIRrxYrm1Ue24btpCha2A==} + dependencies: + resolve-pkg-maps: 1.0.0 + dev: true + /glob-parent@5.1.2: resolution: {integrity: sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==} engines: {node: '>= 6'} @@ -790,11 +787,6 @@ packages: engines: {node: '>=10'} dev: true - /js-string-escape@1.0.1: - resolution: {integrity: sha512-Smw4xcfIQ5LVjAOuJCvN/zIodzA/BBSsluuoSykP+lUvScIi4U6RJLfwHet5cxFnCswUjISV8oAXaqaJDY3chg==} - engines: {node: '>= 0.8'} - dev: true - /jsonc-parser@3.2.0: resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} dev: true @@ -859,10 +851,6 @@ packages: resolution: {integrity: sha512-HDWXG8isMntAyRF5vZ7xKuEvOhT4AhlRt/3czTSjvGUxjYCBVRQY48ViDHyfYz9VIoBkW4TMGQNapx+l3RUwdA==} dev: true - /lodash@4.17.21: - resolution: {integrity: sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==} - dev: true - /log-update@5.0.1: resolution: {integrity: sha512-5UtUDQ/6edw4ofyljDNcOVJQ4c7OjDro4h3y8e1GQL5iYElYclVHJ3zeWchylvMaKnDbDilC8irOVyexnA/Slw==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -874,33 +862,19 @@ packages: wrap-ansi: 8.1.0 dev: true - /loupe@2.3.6: - resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} + /loupe@2.3.7: + resolution: {integrity: sha512-zSMINGVYkdpYSOBmLi0D1Uo7JU9nVdQKrHxC8eYlV+9YKK9WePqAlL7lSlorG/U2Fw1w0hTBmaa/jrQ3UbPHtA==} dependencies: - get-func-name: 2.0.0 + get-func-name: 2.0.2 dev: true - /lru-cache@6.0.0: - resolution: {integrity: sha512-Jo6dJ04CmSjuznwJSS3pUeWmd/H0ffTlkXXgwZi+eq1UCmqQwCh+eLsYOYCwY991i2Fah4h1BEMCx4qThGbsiA==} - engines: {node: '>=10'} - dependencies: - yallist: 4.0.0 - dev: true - - /magic-string@0.30.0: - resolution: {integrity: sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==} + /magic-string@0.30.5: + resolution: {integrity: sha512-7xlpfBaQaP/T6Vh8MO/EqXSW5En6INHEvEXQiuff7Gku0PWjU3uf6w/j9o7O+SpB5fOAkrI5HeoNgwjEO0pFsA==} engines: {node: '>=12'} dependencies: '@jridgewell/sourcemap-codec': 1.4.15 dev: true - /md5-hex@3.0.1: - resolution: {integrity: sha512-BUiRtTtV39LIJwinWBjqVsU9xhdnz7/i889V859IBFpuqGAj6LuOvHv5XLbgZ2R7ptJoJaEcxkv88/h25T7Ciw==} - engines: {node: '>=8'} - dependencies: - blueimp-md5: 2.19.0 - dev: true - /merge-stream@2.0.0: resolution: {integrity: sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==} dev: true @@ -934,13 +908,13 @@ packages: brace-expansion: 1.1.11 dev: true - /mlly@1.2.0: - resolution: {integrity: sha512-+c7A3CV0KGdKcylsI6khWyts/CYrGTrRVo4R/I7u/cUsy0Conxa6LUhiEzVKIw14lc2L5aiO4+SeVe4TeGRKww==} + /mlly@1.4.2: + resolution: {integrity: sha512-i/Ykufi2t1EZ6NaPLdfnZk2AX8cs0d+mTzVKuPfqPKPatxLApaBoxJQ9x1/uckXtrS/U5oisPMDkNs0yQTaBRg==} dependencies: - acorn: 8.8.2 - pathe: 1.1.0 + acorn: 8.11.2 + pathe: 1.1.1 pkg-types: 1.0.3 - ufo: 1.1.1 + ufo: 1.3.1 dev: true /ms@2.1.2: @@ -1032,8 +1006,8 @@ packages: engines: {node: '>=8'} dev: true - /pathe@1.1.0: - resolution: {integrity: sha512-ODbEPR0KKHqECXW1GoxdDb+AZvULmXjVPy4rt+pGo2+TnjJTIPJQSVS6N63n8T2Ip+syHhbn52OewKicV0373w==} + /pathe@1.1.1: + resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} dev: true /pathval@1.1.1: @@ -1064,8 +1038,8 @@ packages: resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} dependencies: jsonc-parser: 3.2.0 - mlly: 1.2.0 - pathe: 1.1.0 + mlly: 1.4.2 + pathe: 1.1.1 dev: true /postcss-load-config@4.0.1: @@ -1084,8 +1058,8 @@ packages: yaml: 2.3.1 dev: true - /postcss@8.4.28: - resolution: {integrity: sha512-Z7V5j0cq8oEKyejIKfpD8b4eBy9cwW2JWPk0+fB1HOAMsfHbnAXLLS+PfVWlzMSLQaWttKDt607I0XHmpE67Vw==} + /postcss@8.4.31: + resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} dependencies: nanoid: 3.3.6 @@ -1108,13 +1082,13 @@ packages: hasBin: true dev: true - /pretty-format@27.5.1: - resolution: {integrity: sha512-Qb1gy5OrP5+zDf2Bvnzdl3jsTf1qXVMazbvCoKhtKqVs4/YK4ozX4gKQJJVyNe+cajNPn0KoC0MC3FUmaHWEmQ==} - engines: {node: ^10.13.0 || ^12.13.0 || ^14.15.0 || >=15.0.0} + /pretty-format@29.7.0: + resolution: {integrity: sha512-Pdlw/oPxN+aXdmM9R00JVC9WVFoCLTKJvDVLgmJ+qAffBMxsV85l/Lu7sNx4zSzPyoL2euImuEwHhOXdEgNFZQ==} + engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} dependencies: - ansi-regex: 5.0.1 + '@jest/schemas': 29.6.3 ansi-styles: 5.2.0 - react-is: 17.0.2 + react-is: 18.2.0 dev: true /punycode@2.3.0: @@ -1126,8 +1100,8 @@ packages: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} dev: true - /react-is@17.0.2: - resolution: {integrity: sha512-w2GsyukL62IJnlaff/nRegPQR94C/XXamvMWmSHRJ4y7Ts/4ocGRmTHvOs8PSE6pB3dWOrD/nueuU5sduBsQ4w==} + /react-is@18.2.0: + resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} dev: true /readdirp@3.6.0: @@ -1142,6 +1116,10 @@ packages: engines: {node: '>=8'} dev: true + /resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + dev: true + /restore-cursor@4.0.0: resolution: {integrity: sha512-I9fPXU9geO9bHOt9pHHOhOkYerIMsmVaWB0rA2AI9ERh/+x/i7MV5HKBNrg+ljO5eoPVgCcnFuRjJ9uH6I/3eg==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1167,20 +1145,20 @@ packages: fsevents: 2.3.3 dev: true + /rollup@3.29.4: + resolution: {integrity: sha512-oWzmBZwvYrU0iJHtDmhsm662rC15FRXmcjCk1xD771dFDx5jJ02ufAQQTn0etB2emNk4J9EZg/yWKpsn9BWGRw==} + engines: {node: '>=14.18.0', npm: '>=8.0.0'} + hasBin: true + optionalDependencies: + fsevents: 2.3.3 + dev: true + /run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} dependencies: queue-microtask: 1.2.3 dev: true - /semver@7.5.0: - resolution: {integrity: sha512-+XC0AD/R7Q2mPSRuy2Id0+CGTZ98+8f+KvwirxOKIEyid+XSx6HbC63p+O4IndTHuX5Z+JxQ0TghCkO5Cg/2HA==} - engines: {node: '>=10'} - hasBin: true - dependencies: - lru-cache: 6.0.0 - dev: true - /shebang-command@2.0.0: resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} engines: {node: '>=8'} @@ -1219,6 +1197,18 @@ packages: engines: {node: '>=0.10.0'} dev: true + /source-map-support@0.5.21: + resolution: {integrity: sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==} + dependencies: + buffer-from: 1.1.2 + source-map: 0.6.1 + dev: true + + /source-map@0.6.1: + resolution: {integrity: sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==} + engines: {node: '>=0.10.0'} + dev: true + /source-map@0.8.0-beta.0: resolution: {integrity: sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA==} engines: {node: '>= 8'} @@ -1230,8 +1220,8 @@ packages: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} dev: true - /std-env@3.3.2: - resolution: {integrity: sha512-uUZI65yrV2Qva5gqE0+A7uVAvO40iPo6jGhs7s8keRfHCmtg+uB2X6EiLGCI9IgL1J17xGhvoOqSz79lzICPTA==} + /std-env@3.4.3: + resolution: {integrity: sha512-f9aPhy8fYBuMN+sNfakZV18U39PbalgjXG3lLB9WkaYTxijru61wb57V9wxxNthXM5Sd88ETBWi29qLAsHO52Q==} dev: true /string-argv@0.3.2: @@ -1265,10 +1255,10 @@ packages: engines: {node: '>=12'} dev: true - /strip-literal@1.0.1: - resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} + /strip-literal@1.3.0: + resolution: {integrity: sha512-PugKzOsyXpArk0yWmUwqOZecSO0GH0bPoctLcqNDH9J04pVW3lflYE0ujElBGTloevcxF5MofAOZ7C5l2b+wLg==} dependencies: - acorn: 8.8.2 + acorn: 8.11.2 dev: true /sucrase@3.34.0: @@ -1298,22 +1288,17 @@ packages: any-promise: 1.3.0 dev: true - /time-zone@1.0.0: - resolution: {integrity: sha512-TIsDdtKo6+XrPtiTm1ssmMngN1sAhyKnTO2kunQWqNPWIVvCm15Wmw4SWInwTVgJ5u/Tr04+8Ei9TNcw4x4ONA==} - engines: {node: '>=4'} - dev: true - - /tinybench@2.5.0: - resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} + /tinybench@2.5.1: + resolution: {integrity: sha512-65NKvSuAVDP/n4CqH+a9w2kTlLReS9vhsAP06MWx+/89nMinJyB2icyl58RIcqCmIggpojIGeuJGhjU1aGMBSg==} dev: true - /tinypool@0.5.0: - resolution: {integrity: sha512-paHQtnrlS1QZYKF/GnLoOM/DN9fqaGOFbCbxzAhwniySnzl9Ebk8w73/dd34DAhe/obUbPAOldTyYXQZxnPBPQ==} + /tinypool@0.7.0: + resolution: {integrity: sha512-zSYNUlYSMhJ6Zdou4cJwo/p7w5nmAH17GRfU/ui3ctvjXFErXXkruT4MWW6poDeXgCaIBlGLrfU6TbTXxyGMww==} engines: {node: '>=14.0.0'} dev: true - /tinyspy@2.1.0: - resolution: {integrity: sha512-7eORpyqImoOvkQJCSkL0d0mB4NHHIFAy4b1u8PHdDa7SjGS2njzl6/lyGoZLm+eyYEtlUmFGE0rFj66SWxZgQQ==} + /tinyspy@2.2.0: + resolution: {integrity: sha512-d2eda04AN/cPOR89F7Xv5bK/jrQEhmcLFe6HFldoeO9AJtps+fqEnh486vnT/8y4bw38pSyxDcTCAq+Ks2aJTg==} engines: {node: '>=14.0.0'} dev: true @@ -1375,6 +1360,17 @@ packages: - ts-node dev: true + /tsx@3.14.0: + resolution: {integrity: sha512-xHtFaKtHxM9LOklMmJdI3BEnQq/D5F73Of2E1GDrITi9sgoVkvIsrQUTY1G8FlmGtA+awCI4EBlTRRYxkL2sRg==} + hasBin: true + dependencies: + esbuild: 0.18.20 + get-tsconfig: 4.7.2 + source-map-support: 0.5.21 + optionalDependencies: + fsevents: 2.3.3 + dev: true + /type-detect@4.0.8: resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} engines: {node: '>=4'} @@ -1391,21 +1387,25 @@ packages: hasBin: true dev: true - /ufo@1.1.1: - resolution: {integrity: sha512-MvlCc4GHrmZdAllBc0iUDowff36Q9Ndw/UzqmEKyrfSzokTd9ZCy1i+IIk5hrYKkjoYVQyNbrw7/F8XJ2rEwTg==} + /ufo@1.3.1: + resolution: {integrity: sha512-uY/99gMLIOlJPwATcMVYfqDSxUR9//AUcgZMzwfSTJPDKzA1S8mX4VLqa+fiAtveraQUBCz4FFcwVZBGbwBXIw==} dev: true - /vite-node@0.31.0(@types/node@18.16.3): - resolution: {integrity: sha512-8x1x1LNuPvE2vIvkSB7c1mApX5oqlgsxzHQesYF7l5n1gKrEmrClIiZuOFbFDQcjLsmcWSwwmrWrcGWm9Fxc/g==} + /undici-types@5.26.5: + resolution: {integrity: sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==} + dev: true + + /vite-node@0.34.6(@types/node@20.8.10): + resolution: {integrity: sha512-nlBMJ9x6n7/Amaz6F3zJ97EBwR2FkzhBRxF5e+jE6LA3yi6Wtc2lyTij1OnDMIr34v5g/tVQtsVAzhT0jc5ygA==} engines: {node: '>=v14.18.0'} hasBin: true dependencies: cac: 6.7.14 debug: 4.3.4 - mlly: 1.2.0 - pathe: 1.1.0 + mlly: 1.4.2 + pathe: 1.1.1 picocolors: 1.0.0 - vite: 4.4.9(@types/node@18.16.3) + vite: 4.5.0(@types/node@20.8.10) transitivePeerDependencies: - '@types/node' - less @@ -1417,8 +1417,8 @@ packages: - terser dev: true - /vite@4.4.9(@types/node@18.16.3): - resolution: {integrity: sha512-2mbUn2LlUmNASWwSCNSJ/EG2HuSRTnVNaydp6vMCm5VIqJsjMfbIWtbH2kDuwUVW5mMUKKZvGPX/rqeqVvv1XA==} + /vite@4.5.0(@types/node@20.8.10): + resolution: {integrity: sha512-ulr8rNLA6rkyFAlVWw2q5YJ91v098AFQ2R0PRFwPzREXOUJQPtFUG0t+/ZikhaOCDqFoDhN6/v8Sq0o4araFAw==} engines: {node: ^14.18.0 || >=16.0.0} hasBin: true peerDependencies: @@ -1445,16 +1445,16 @@ packages: terser: optional: true dependencies: - '@types/node': 18.16.3 + '@types/node': 20.8.10 esbuild: 0.18.20 - postcss: 8.4.28 - rollup: 3.28.1 + postcss: 8.4.31 + rollup: 3.29.4 optionalDependencies: fsevents: 2.3.3 dev: true - /vitest@0.31.0: - resolution: {integrity: sha512-JwWJS9p3GU9GxkG7eBSmr4Q4x4bvVBSswaCFf1PBNHiPx00obfhHRJfgHcnI0ffn+NMlIh9QGvG75FlaIBdKGA==} + /vitest@0.34.6: + resolution: {integrity: sha512-+5CALsOvbNKnS+ZHMXtuUC7nL8/7F1F2DnHGjSsszX8zCjWSSviphCb/NuS9Nzf4Q03KyyDRBAXhF/8lffME4Q==} engines: {node: '>=v14.18.0'} hasBin: true peerDependencies: @@ -1484,30 +1484,29 @@ packages: webdriverio: optional: true dependencies: - '@types/chai': 4.3.5 - '@types/chai-subset': 1.3.3 - '@types/node': 18.16.3 - '@vitest/expect': 0.31.0 - '@vitest/runner': 0.31.0 - '@vitest/snapshot': 0.31.0 - '@vitest/spy': 0.31.0 - '@vitest/utils': 0.31.0 - acorn: 8.8.2 - acorn-walk: 8.2.0 + '@types/chai': 4.3.9 + '@types/chai-subset': 1.3.4 + '@types/node': 20.8.10 + '@vitest/expect': 0.34.6 + '@vitest/runner': 0.34.6 + '@vitest/snapshot': 0.34.6 + '@vitest/spy': 0.34.6 + '@vitest/utils': 0.34.6 + acorn: 8.11.2 + acorn-walk: 8.3.0 cac: 6.7.14 - chai: 4.3.7 - concordance: 5.0.4 + chai: 4.3.10 debug: 4.3.4 local-pkg: 0.4.3 - magic-string: 0.30.0 - pathe: 1.1.0 + magic-string: 0.30.5 + pathe: 1.1.1 picocolors: 1.0.0 - std-env: 3.3.2 - strip-literal: 1.0.1 - tinybench: 2.5.0 - tinypool: 0.5.0 - vite: 4.4.9(@types/node@18.16.3) - vite-node: 0.31.0(@types/node@18.16.3) + std-env: 3.4.3 + strip-literal: 1.3.0 + tinybench: 2.5.1 + tinypool: 0.7.0 + vite: 4.5.0(@types/node@20.8.10) + vite-node: 0.34.6(@types/node@20.8.10) why-is-node-running: 2.2.2 transitivePeerDependencies: - less @@ -1523,11 +1522,6 @@ packages: resolution: {integrity: sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg==} dev: true - /well-known-symbols@2.0.0: - resolution: {integrity: sha512-ZMjC3ho+KXo0BfJb7JgtQ5IBuvnShdlACNkKkdsqBmYw3bPAaJfPeYUo6tLUaT5tG/Gkh7xkpBhKRQ9e7pyg9Q==} - engines: {node: '>=6'} - dev: true - /whatwg-url@7.1.0: resolution: {integrity: sha512-WUu7Rg1DroM7oQvGWfOiAK21n74Gg+T4elXEQYkOhtyLeWiJFoOGLXPKI/9gzIie9CtwVLm8wtw6YJdKyxSjeg==} dependencies: @@ -1566,10 +1560,6 @@ packages: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} dev: true - /yallist@4.0.0: - resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} - dev: true - /yaml@2.3.1: resolution: {integrity: sha512-2eHWfjaoXgTBC2jNM1LRef62VQa0umtvRiDSk6HSzW7RvS5YtkabJrwYLLEKWBc8a5U2PTSCs+dJjUTJdlHsWQ==} engines: {node: '>= 14'} diff --git a/src/error/panic.test.ts b/src/error/panic.test.ts new file mode 100644 index 0000000..91dc42b --- /dev/null +++ b/src/error/panic.test.ts @@ -0,0 +1,38 @@ +import {expect, it} from "vitest" +import {Panic} from "./panic" + +it("returns an instance without args", () => { + const panic = new Panic() + + expect(panic).toBeInstanceOf(Error) + expect(panic).toBeInstanceOf(Panic) + + expect(panic.message).toEqual("") + expect(panic.stack).toContain("Panic: ") +}) + +it("returns an instance with message", () => { + const msg = "msg" + const panic = new Panic(msg) + + expect(panic).toBeInstanceOf(Error) + expect(panic).toBeInstanceOf(Panic) + + expect(panic.message).toEqual(msg) + expect(panic.stack).toContain(`Panic: ${msg}`) +}) + +it("returns an instance with error", () => { + let origin = new Error("msg") + let panic = new Panic(origin) + expect(panic).toBeInstanceOf(Error) + expect(panic).toBeInstanceOf(Panic) + + expect(panic.origin).toEqual(origin) + expect(panic.message).toEqual(origin.message) + expect(panic.stack).toContain(`Panic: ${origin.message}`) + + origin.name = "MyError" + panic = new Panic(origin) + expect(panic.stack).toContain(`Panic from MyError: ${origin.message}`) +}) diff --git a/src/error/panic.ts b/src/error/panic.ts new file mode 100644 index 0000000..af2d202 --- /dev/null +++ b/src/error/panic.ts @@ -0,0 +1,52 @@ +import {inspectSymbol} from "../util" +import {getName, replaceStack} from "./util" + +export class Panic extends Error { + readonly origin?: Error + private readonly originName: string + private readonly _stack?: string + + constructor(messageOrError?: string | Error) { + if (messageOrError instanceof Error) { + super(messageOrError.message) + this.origin = messageOrError + } else { + super(messageOrError) + } + this.originName = this.origin?.name ?? "Error" + this.name = getName("Panic", this.originName) + this._stack = this.stack // Save a copy of the stack trace before it gets overridden. + } + + override get stack() { + return replaceStack(this.name, this.originName, this._stack) + } + + [inspectSymbol]() { + return this.stack + } +} + +export class UnwrapPanic extends Panic { + constructor(msg: string) { + super(msg) + } +} + +export class InvalidErrorPanic extends Panic { + constructor(value: unknown) { + super(`Invalid error: "${value}"`) + } +} + +export class TodoPanic extends Panic { + override message = "Todo" +} + +export class UnreachablePanic extends Panic { + override message = "Unreachable" +} + +export class UnimplementedPanic extends Panic { + override message = "Unimplemented" +} diff --git a/src/error/result_error.test.ts b/src/error/result_error.test.ts new file mode 100644 index 0000000..43b090a --- /dev/null +++ b/src/error/result_error.test.ts @@ -0,0 +1,74 @@ +import {describe, expect, it} from "vitest" +import {InvalidErrorPanic, Panic, ResultError, toStdError, StdError} from ".." + +describe.concurrent("ResultError and StdError", () => { + it("returns instance with no args", () => { + const error = new StdError() + + expect(error).toBeInstanceOf(ResultError) + expect(error).toBeInstanceOf(StdError) + + expect(error.tag).toEqual("StdError") + + expect(error.message).toEqual("") + expect(error.stack).toContain("StdError: ") + }) + + it("returns instance with message", () => { + const msg = "msg" + const error = new StdError(msg) + + expect(error).toBeInstanceOf(ResultError) + expect(error).toBeInstanceOf(StdError) + + expect(error.tag).toEqual("StdError") + + expect(error.message).toEqual(msg) + expect(error.stack).toContain(`StdError: ${msg}`) + }) + + it("returns instance with error", () => { + let origin = new Error("msg") + let error = new StdError(origin) + + expect(error).toBeInstanceOf(ResultError) + expect(error).toBeInstanceOf(StdError) + + expect(error.tag).toEqual("StdError") + + expect(error.origin).toEqual(origin) + expect(error.message).toEqual(origin.message) + expect(error.stack).toContain(`StdError: ${origin.message}`) + + origin = new Error("msg") + origin.name = "MyError" + error = new StdError(origin) + expect(error.stack).toContain(`StdError from MyError: ${origin.message}`) + }) +}) + +describe.concurrent("toStdError", () => { + it("returns an StdError when given an Error", () => { + class TestError extends Error {} + const error = new TestError("Test error") + const stdError = toStdError(error) + expect(stdError).toBeInstanceOf(StdError) + expect(stdError.origin).toEqual(error) + }) + + it("throws a Panic when given a Panic", () => { + const msg = "Test panic" + const panic = new Panic(msg) + expect(() => toStdError(panic)).toThrow(panic) + }) + + it("throws a Panic when given an unknown value", () => { + expect(() => toStdError(0)).toThrow(InvalidErrorPanic) + expect(() => toStdError("")).toThrow(InvalidErrorPanic) + expect(() => toStdError(true)).toThrow(InvalidErrorPanic) + expect(() => toStdError(undefined)).toThrow(InvalidErrorPanic) + expect(() => toStdError(null)).toThrow(InvalidErrorPanic) + expect(() => toStdError({})).toThrow(InvalidErrorPanic) + expect(() => toStdError([])).toThrow(InvalidErrorPanic) + }) +}) diff --git a/src/error/result_error.ts b/src/error/result_error.ts new file mode 100644 index 0000000..bef9615 --- /dev/null +++ b/src/error/result_error.ts @@ -0,0 +1,60 @@ +import {inspectSymbol} from "../util" +import {InvalidErrorPanic, Panic} from "./panic" +import {getName, getOriginName, replaceStack} from "./util" + +export abstract class ResultError implements Error { + abstract readonly tag: string + + readonly message: string + readonly origin?: Readonly + private readonly originName: string + private readonly _stack?: string + + constructor(messageOrError: string | Error = "") { + if (messageOrError instanceof Error) { + this.message = messageOrError.message + this.origin = messageOrError + if (this.origin.stack) { + this._stack = this.origin.stack + } + } else { + this.message = messageOrError + } + if (!this._stack) { + this._stack = new Error(this.message).stack + } + this.originName = getOriginName(this.origin) + } + + get name() { + return getName(this.tag, this.originName) + } + + get stack() { + return replaceStack(this.name, this.originName, this._stack) + } + + toString() { + return `${this.name}: ${this.message}` + } + + [inspectSymbol]() { + return this.stack + } +} + +export class StdError extends ResultError { + readonly tag = "StdError" +} + +export type ErrorHandler = (error: StdError) => E + +export function toStdError(error: unknown): StdError { + if (error instanceof Panic) { + throw error + } + if (error instanceof Error) { + return new StdError(error) + } + throw new InvalidErrorPanic(error) +} diff --git a/src/error/util.ts b/src/error/util.ts new file mode 100644 index 0000000..920fe95 --- /dev/null +++ b/src/error/util.ts @@ -0,0 +1,21 @@ +/** + * Tries to replace the stack trace to include the subclass error name. + * + * May not work in every environment, since `stack` property is implementation-dependent and isn't standardized, + * + * meaning different JavaScript engines might produce different stack traces. + * + * See: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error/stack + */ +export function replaceStack(name: string, originName: string, stack?: string) { + const r = new RegExp(`^${originName}`) + return stack?.replace(r, name) +} + +export function getOriginName(origin?: Error) { + return origin?.name ?? "Error" +} + +export function getName(name: string, originName: string) { + return originName !== "Error" ? `${name} from ${originName}` : name +} diff --git a/src/guard.ts b/src/guard.ts deleted file mode 100644 index 3e29630..0000000 --- a/src/guard.ts +++ /dev/null @@ -1,13 +0,0 @@ -import {tryAsyncFn, tryFn} from "./try" - -export function guard any>(fn: T) { - return function (...args: Parameters) { - return tryFn>(() => fn(...args)) - } -} - -export function guardAsync Promise>(fn: T) { - return function (...args: Parameters) { - return tryAsyncFn>>(() => fn(...args)) - } -} diff --git a/test/fn.test.ts b/src/helpers/fn.test.ts similarity index 94% rename from test/fn.test.ts rename to src/helpers/fn.test.ts index 94c5616..d5c10e7 100644 --- a/test/fn.test.ts +++ b/src/helpers/fn.test.ts @@ -1,5 +1,5 @@ import {describe, expect, it} from "vitest" -import {asyncFn, fn, Ok, Err} from "../src" +import {asyncFn, fn, Ok, Err} from ".." describe.concurrent("fn", () => { it("returns Ok result when provided function does not throw", () => { diff --git a/src/fn.ts b/src/helpers/fn.ts similarity index 73% rename from src/fn.ts rename to src/helpers/fn.ts index 9d614ed..d3024c3 100644 --- a/src/fn.ts +++ b/src/helpers/fn.ts @@ -1,6 +1,6 @@ -import type {ResultValueType, ResultErrorType} from "./util" -import {type Result} from "./result" -import {PromiseResult} from "./promise_result" +import type {ResultValueType, ResultErrorType} from "../util" +import type {Result} from "../result/interface" +import {PromiseResult} from "../result/promise" export function fn Result>( f: T, diff --git a/src/helpers/guard.test.ts b/src/helpers/guard.test.ts new file mode 100644 index 0000000..bbcbb2a --- /dev/null +++ b/src/helpers/guard.test.ts @@ -0,0 +1,92 @@ +import {expect, it, describe} from "vitest" +import {ResultError, guard, guardAsync, guardAsyncWith, guardWith} from ".." + +class MyError extends ResultError { + readonly tag = "MyError" +} + +describe.concurrent("guard", () => { + it("transforms a function into a function that returns a Result object", () => { + const fn = (x: number, y: number) => x + y + const wrappedFn = guard(fn) + const result = wrappedFn(40, 2) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("transforms a throwing function into a function that returns an Err result", () => { + const error = new Error("Test error") + const fn = () => { + throw error + } + const wrappedFn = guard(fn) + const result = wrappedFn() + expect(result.ok).toEqual(false) + expect(result.unwrapErr().origin).toEqual(error) + }) +}) + +describe.concurrent("guardWith", () => { + it("transforms a function into a function that returns a Result object", () => { + const fn = (x: number, y: number) => x + y + const wrappedFn = guardWith(fn, () => new MyError()) + const result = wrappedFn(40, 2) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("transforms a throwing function into a function that returns an Err result", () => { + const error = new Error("Test error") + const fn = () => { + throw error + } + const myError = new MyError("My error") + const wrappedFn = guardWith(fn, () => myError) + const result = wrappedFn() + expect(result.ok).toEqual(false) + expect(result.unwrapErr()).toEqual(myError) + }) +}) + +describe.concurrent("guardAsync", () => { + it("transforms an async function into a function that returns a Promise of a Result object", async () => { + const fn = async (x: number, y: number) => Promise.resolve(x + y) + const wrappedFn = guardAsync(fn) + const result = await wrappedFn(40, 2) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("transforms a throwing async function into a function that returns a Promise of an Err result", async () => { + const error = new Error("Test error") + const fn = async (): Promise => { + throw error + } + const wrappedFn = guardAsync(fn) + const result = await wrappedFn() + expect(result.ok).toEqual(false) + expect(result.unwrapErr().origin).toEqual(error) + }) +}) + +describe.concurrent("guardAsyncWith", () => { + it("transforms an async function into a function that returns a Promise of a Result object", async () => { + const fn = async (x: number, y: number) => Promise.resolve(x + y) + const wrappedFn = guardAsyncWith(fn, () => new MyError()) + const result = await wrappedFn(40, 2) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("transforms a throwing async function into a function that returns a Promise of an Err result", async () => { + const error = new Error("Test error") + const fn = async (): Promise => { + throw error + } + const myError = new MyError("My error") + const wrappedFn = guardAsyncWith(fn, () => myError) + const result = await wrappedFn() + expect(result.ok).toEqual(false) + expect(result.unwrapErr()).toEqual(myError) + }) +}) diff --git a/src/helpers/guard.ts b/src/helpers/guard.ts new file mode 100644 index 0000000..ab3f2ac --- /dev/null +++ b/src/helpers/guard.ts @@ -0,0 +1,32 @@ +import type {ErrorHandler, ResultError} from "../error/result_error" +import {tryAsyncFn, tryAsyncFnWith, tryFn, tryFnWith} from "./try" + +type Fn = (...args: any[]) => any +type AsyncFn = (...args: any[]) => Promise + +export function guard(f: T) { + return function (...args: Parameters) { + return tryFn>(() => f(...args)) + } +} + +export function guardWith(f: T, handleError: ErrorHandler) { + return function (...args: Parameters) { + return tryFnWith, E>(() => f(...args), handleError) + } +} + +export function guardAsync(fn: T) { + return function (...args: Parameters) { + return tryAsyncFn>>(() => fn(...args)) + } +} + +export function guardAsyncWith( + fn: T, + handleError: ErrorHandler, +) { + return function (...args: Parameters) { + return tryAsyncFnWith>, E>(() => fn(...args), handleError) + } +} diff --git a/src/helpers/try.test.ts b/src/helpers/try.test.ts new file mode 100644 index 0000000..cf0bdb2 --- /dev/null +++ b/src/helpers/try.test.ts @@ -0,0 +1,127 @@ +import {describe, expect, it} from "vitest" +import { + ResultError, + tryAsyncFn, + tryAsyncFnWith, + tryFn, + tryFnWith, + tryPromise, + tryPromiseWith, +} from ".." + +class MyError extends ResultError { + readonly tag = "MyError" +} + +describe.concurrent("tryPromise", () => { + it("settles a Promise to an Ok result", async () => { + const promise = Promise.resolve(42) + const result = await tryPromise(promise) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("settles a rejected Promise to an Err result", async () => { + const error = new Error("Test error") + const promise = Promise.reject(error) + const result = await tryPromise(promise) + expect(result.ok).toEqual(false) + expect(result.unwrapErr().origin).toEqual(error) + }) +}) + +describe.concurrent("tryPromiseWith", () => { + it("settles a Promise to an Ok result", async () => { + const promise = Promise.resolve(42) + const result = await tryPromiseWith(promise, () => new MyError()) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("settles a rejected Promise to an Err result", async () => { + const error = new Error("Test error") + const promise = Promise.reject(error) + const myError = new MyError() + const result = await tryPromiseWith(promise, () => myError) + expect(result.ok).toEqual(false) + expect(result.unwrapErr()).toEqual(myError) + }) +}) + +describe.concurrent("tryFn", () => { + it("wraps a function call into a Result object", () => { + const fn = () => 42 + const result = tryFn(fn) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("wraps a throwing function call into an Err result", () => { + const error = new Error("Test error") + const fn = () => { + throw error + } + const result = tryFn(fn) + expect(result.ok).toEqual(false) + expect(result.unwrapErr().origin).toEqual(error) + }) +}) + +describe.concurrent("tryFnWith", () => { + it("wraps a function call into a Result object", () => { + const fn = () => 42 + const result = tryFnWith(fn, () => new MyError()) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("wraps a throwing function call into an Err result", () => { + const error = new Error("Test error") + const fn = () => { + throw error + } + const myError = new MyError() + const result = tryFnWith(fn, () => myError) + expect(result.ok).toEqual(false) + expect(result.unwrapErr()).toEqual(myError) + }) +}) + +describe.concurrent("tryAsyncFn", () => { + it("wraps an async function call into a Result object", async () => { + const fn = async () => Promise.resolve(42) + const result = await tryAsyncFn(fn) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("wraps a throwing async function call into an Err result", async () => { + const error = new Error("Test error") + const fn = async (): Promise => { + throw error + } + const result = await tryAsyncFn(fn) + expect(result.ok).toEqual(false) + expect(result.unwrapErr().origin).toEqual(error) + }) +}) + +describe.concurrent("tryAsyncFnWith", () => { + it("wraps an async function call into a Result object", async () => { + const fn = async () => Promise.resolve(42) + const result = await tryAsyncFnWith(fn, () => new MyError()) + expect(result.ok).toEqual(true) + expect(result.unwrap()).toEqual(42) + }) + + it("wraps a throwing async function call into an Err result", async () => { + const error = new Error("Test error") + const fn = async (): Promise => { + throw error + } + const myError = new MyError() + const result = await tryAsyncFnWith(fn, () => myError) + expect(result.ok).toEqual(false) + expect(result.unwrapErr()).toEqual(myError) + }) +}) diff --git a/src/helpers/try.ts b/src/helpers/try.ts new file mode 100644 index 0000000..b9eecee --- /dev/null +++ b/src/helpers/try.ts @@ -0,0 +1,59 @@ +import {type ErrorHandler, type ResultError, type StdError, toStdError} from "../error/result_error" +import {Err} from "../result/err" +import type {Result} from "../result/interface" +import {Ok} from "../result/ok" +import {PromiseResult} from "../result/promise" + +// Couldn't figure out how to overload these functions without a TypeScript error and making +// the error handler required if the error template param is defined. + +export function tryFn(f: () => T): Result { + try { + return Ok(f()) + } catch (error) { + return Err(toStdError(error)) + } +} + +export function tryFnWith( + f: () => T, + handleError: ErrorHandler, +): Result { + try { + return Ok(f()) + } catch (error) { + return Err(handleError(toStdError(error))) + } +} + +export function tryPromise(promise: Promise): PromiseResult { + return new PromiseResult( + promise.then( + (value) => Ok(value), + (error: unknown) => Err(toStdError(error)), + ), + ) +} + +export function tryPromiseWith( + promise: Promise, + handleError: ErrorHandler, +): PromiseResult { + return new PromiseResult( + promise.then( + (value) => Ok(value), + (error: unknown) => Err(handleError(toStdError(error))), + ), + ) +} + +export function tryAsyncFn(f: () => Promise): PromiseResult { + return tryPromise(f()) +} + +export function tryAsyncFnWith( + f: () => Promise, + handleError: ErrorHandler, +): PromiseResult { + return tryPromiseWith(f(), handleError) +} diff --git a/src/index.ts b/src/index.ts index 66671ad..f1fdcb0 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,18 @@ -export * from "./fn" -export * from "./guard" -export * from "./panic" -export * from "./promise_result" -export * from "./result" -export * from "./try" +export * from "./error/panic" +export * from "./error/result_error" + +export * from "./helpers/fn" +export * from "./helpers/guard" +export * from "./helpers/try" + +export * from "./option/interface" +export * from "./option/none" +export * from "./option/promise" +export * from "./option/some" + +export * from "./result/interface" +export * from "./result/err" +export * from "./result/ok" +export * from "./result/promise" + export * from "./util" diff --git a/src/option.ts b/src/option.ts deleted file mode 100644 index 25a4e48..0000000 --- a/src/option.ts +++ /dev/null @@ -1,231 +0,0 @@ -import {Panic, UnwrapPanic} from "./panic" - -export interface OptionMethods { - and(other: Option): Option - andThen(f: (value: T) => Option): Option - expect(panic: string | Panic): T - filter(f: (value: T) => boolean): Option - inspect(f: (value: T) => void): Option - isNone(): this is None - isSome(): this is Some - isSomeAnd(f: (value: T) => boolean): this is Some - map(f: (value: T) => U): Option - mapOr(defaultValue: A, f: (value: T) => B): A | B - mapOrElse(defaultValue: () => A, f: (value: T) => B): A | B - or(other: Option): Option - orElse(f: () => Option): Option - unwrap(): T - unwrapOr(defaultValue: U): T | U - unwrapOrElse(defaultValue: () => U): T | U - xor(other: Option): Option - - get(): T | null - match(some: (value: T) => A, none: () => B): A | B - - toString(): `Some(${string})` | "None" - toObject(): {some: true; value: T} | {some: false; value: null} - toJSON(): {meta: "Some"; data: T} | {meta: "None"} -} - -export class SomeImpl implements OptionMethods { - readonly some = true - readonly none = false - readonly value: T - - constructor(value: T) { - this.value = value - } - - and(other: Option) { - return other - } - - andThen(f: (value: T) => Option) { - return f(this.value) - } - - expect(_panic: string | Panic) { - return this.value - } - - filter(f: (value: T) => boolean) { - return f(this.value) ? this : None - } - - inspect(f: (value: T) => void) { - f(this.value) - return this - } - - isNone(): this is None { - return false - } - - isSome(): this is Some { - return true - } - - isSomeAnd(f: (value: T) => boolean): this is Some { - return f(this.value) - } - - map(f: (value: T) => U) { - return Some(f(this.value)) - } - - mapOr(_defaultValue: A, f: (value: T) => B) { - return f(this.value) - } - - mapOrElse(_defaultValue: () => A, f: (value: T) => B) { - return f(this.value) - } - - or(_other: Option) { - return this - } - - orElse(_f: () => Option) { - return this - } - - unwrap() { - return this.value - } - - unwrapOr(_defaultValue: U) { - return this.value - } - - unwrapOrElse(_defaultValue: () => U) { - return this.value - } - - xor(other: Option) { - return other.isSome() ? None : this - } - - get() { - return this.value - } - - match(some: (value: T) => A, _none: () => B) { - return some(this.value) - } - - toString() { - return `Some(${this.value})` as const - } - - toObject() { - return {some: true, value: this.value} as const - } - - toJSON() { - return {meta: "Some", data: this.value} as const - } -} - -export interface Some extends SomeImpl {} -export function Some(value: T): Some { - return new SomeImpl(value) -} - -export class NoneImpl implements OptionMethods { - readonly some = false - readonly none = true - readonly value = null - - and(_other: Option) { - return None - } - - andThen(_f: (value: never) => Option) { - return None - } - - expect(panic: string | Panic): never { - throw typeof panic === "string" ? new Panic(panic) : panic - } - - filter(_f: (value: never) => boolean) { - return None - } - - inspect(_f: (value: never) => void) { - return this - } - - isNone(): this is None { - return true - } - - isSome(): this is Some { - return false - } - - isSomeAnd(_f: (value: never) => boolean): this is Some { - return false - } - - map(_f: (value: never) => U) { - return None - } - - mapOr(defaultValue: A, _f: (value: never) => B) { - return defaultValue - } - - mapOrElse(defaultValue: () => A, _f: (value: never) => B) { - return defaultValue() - } - - or(other: Option) { - return other - } - - orElse(f: () => Option) { - return f() - } - - unwrap(): never { - throw new UnwrapPanic("Cannot unwrap on a None") - } - - unwrapOr(defaultValue: U) { - return defaultValue - } - - unwrapOrElse(defaultValue: () => U) { - return defaultValue() - } - - xor(other: Option) { - return other - } - - get() { - return null - } - - match(_some: (value: never) => A, none: () => B) { - return none() - } - - toString() { - return "None" as const - } - - toObject() { - return {some: false, value: null} as const - } - - toJSON() { - return {meta: "None"} as const - } -} - -export interface None extends NoneImpl {} -export const None: None = Object.freeze(new NoneImpl()) - -export type Option = Some | None diff --git a/src/option/interface.ts b/src/option/interface.ts new file mode 100644 index 0000000..f25ddf5 --- /dev/null +++ b/src/option/interface.ts @@ -0,0 +1,34 @@ +import type {Panic} from "../error/panic" +import type {inspectSymbol} from "../util" +import type {None} from "./none" +import type {Some} from "./some" + +export interface OptionMethods { + and(other: Option): Option + andThen(f: (value: T) => Option): Option + expect(panic: string | Panic): T + filter(f: (value: T) => boolean): Option + inspect(f: (value: T) => void): Option + isNone(): this is None + isSome(): this is Some + isSomeAnd(f: (value: T) => boolean): this is Some + map(f: (value: T) => U): Option + mapOr(defaultValue: A, f: (value: T) => B): A | B + mapOrElse(defaultValue: () => A, f: (value: T) => B): A | B + or(other: Option): Option + orElse(f: () => Option): Option + unwrap(): T + unwrapOr(defaultValue: U): T | U + unwrapOrElse(defaultValue: () => U): T | U + xor(other: Option): Option + + into(): T | null + match(some: (value: T) => A, none: () => B): A | B + + toString(): `Some(${string})` | "None" + [inspectSymbol](): ReturnType["toString"]> + toObject(): {some: true; value: T} | {some: false; value: null} + toJSON(): {meta: "Some"; data: T} | {meta: "None"} +} + +export type Option = Some | None diff --git a/src/option/none.ts b/src/option/none.ts new file mode 100644 index 0000000..6a4df12 --- /dev/null +++ b/src/option/none.ts @@ -0,0 +1,104 @@ +import {Panic, UnwrapPanic} from "../error/panic" +import {inspectSymbol} from "../util" +import type {OptionMethods, Option} from "./interface" +import type {Some} from "./some" + +export class NoneImpl implements OptionMethods { + readonly some = false + readonly none = true + + and(_other: Option) { + return None + } + + andThen(_f: (value: never) => Option) { + return None + } + + expect(panic: string | Panic): never { + throw typeof panic === "string" ? new Panic(panic) : panic + } + + filter(_f: (value: never) => boolean) { + return None + } + + inspect(_f: (value: never) => void) { + return this + } + + isNone(): this is None { + return true + } + + isSome(): this is Some { + return false + } + + isSomeAnd(_f: (value: never) => boolean): this is Some { + return false + } + + map(_f: (value: never) => U) { + return None + } + + mapOr(defaultValue: A, _f: (value: never) => B) { + return defaultValue + } + + mapOrElse(defaultValue: () => A, _f: (value: never) => B) { + return defaultValue() + } + + or(other: Option) { + return other + } + + orElse(f: () => Option) { + return f() + } + + unwrap(): never { + throw new UnwrapPanic("Cannot unwrap on a None") + } + + unwrapOr(defaultValue: U) { + return defaultValue + } + + unwrapOrElse(defaultValue: () => U) { + return defaultValue() + } + + xor(other: Option) { + return other + } + + into() { + return null + } + + match(_some: (value: never) => A, none: () => B) { + return none() + } + + toString() { + return "None" as const + } + + [inspectSymbol]() { + return this.toString() + } + + toObject() { + return {some: false, value: null} as const + } + + toJSON() { + return {meta: "None"} as const + } +} + +export interface None extends NoneImpl {} +export const None: None = new NoneImpl() diff --git a/test/option.test.ts b/src/option/option.test.ts similarity index 97% rename from test/option.test.ts rename to src/option/option.test.ts index bacc835..ac4ef9a 100644 --- a/test/option.test.ts +++ b/src/option/option.test.ts @@ -1,6 +1,5 @@ import {describe, expect, it, vi} from "vitest" -import {None, Some} from "../src/option" -import {Panic, UnwrapPanic} from "../src" +import {Panic, UnwrapPanic, None, Some} from ".." it("returns a Some option", () => { const option = Some(42) @@ -13,7 +12,6 @@ it("returns a None option", () => { const option = None expect(option.some).toEqual(false) expect(option.none).toEqual(true) - expect(option.value).toEqual(null) }) describe.concurrent("and", () => { @@ -287,15 +285,15 @@ describe.concurrent("xor", () => { }) }) -describe.concurrent("get", () => { +describe.concurrent("into", () => { it("returns the value when called on a Some option", () => { const option = Some(42) - expect(option.get()).toEqual(42) + expect(option.into()).toEqual(42) }) it("throws when called on a None option", () => { const option = None - expect(option.get()).toEqual(null) + expect(option.into()).toEqual(null) }) }) diff --git a/test/promise_option.test.ts b/src/option/promise.test.ts similarity index 96% rename from test/promise_option.test.ts rename to src/option/promise.test.ts index 6aa7a03..8574c1a 100644 --- a/test/promise_option.test.ts +++ b/src/option/promise.test.ts @@ -1,7 +1,5 @@ import {describe, it, expect, vi} from "vitest" -import {Panic, UnwrapPanic} from "../src" -import {PromiseOption} from "../src/promise_option" -import {None, Some} from "../src/option" +import {Panic, UnwrapPanic, PromiseOption, Some, None} from ".." function promiseSome(value: T) { return new PromiseOption(Promise.resolve(Some(value))) @@ -279,15 +277,15 @@ describe.concurrent("xor", () => { }) }) -describe.concurrent("get", () => { +describe.concurrent("into", () => { it("returns the value when called on a Some option", async () => { const option = promiseSome(42) - await expect(option.get()).resolves.toEqual(42) + await expect(option.into()).resolves.toEqual(42) }) it("returns null when called on a None option", async () => { const option = promiseNone() - await expect(option.get()).resolves.toEqual(null) + await expect(option.into()).resolves.toEqual(null) }) }) diff --git a/src/promise_option.ts b/src/option/promise.ts similarity index 95% rename from src/promise_option.ts rename to src/option/promise.ts index df52ce3..ee0a329 100644 --- a/src/promise_option.ts +++ b/src/option/promise.ts @@ -1,5 +1,5 @@ -import type {Option} from "./option" -import {Panic} from "./panic" +import type {Panic} from "../error/panic" +import type {Option} from "./interface" export class PromiseOption implements PromiseLike> { constructor(readonly promise: Promise> | PromiseLike>) {} @@ -102,8 +102,8 @@ export class PromiseOption implements PromiseLike> { ) } - async get() { - return (await this).get() + async into() { + return (await this).into() } async match(some: (value: T) => A, none: () => B) { diff --git a/src/option/some.ts b/src/option/some.ts new file mode 100644 index 0000000..35da0c3 --- /dev/null +++ b/src/option/some.ts @@ -0,0 +1,114 @@ +import type {Panic} from "../error/panic" +import {inspectSymbol} from "../util" +import type {OptionMethods, Option} from "./interface" +import {None} from "./none" + +export class SomeImpl implements OptionMethods { + readonly some = true + readonly none = false + readonly value: T + + constructor(value: T) { + this.value = value + } + + and(other: Option) { + return other + } + + andThen(f: (value: T) => Option) { + return f(this.value) + } + + expect(_panic: string | Panic) { + return this.value + } + + filter(f: (value: T) => boolean) { + return f(this.value) ? this : None + } + + inspect(f: (value: T) => void) { + f(this.value) + return this + } + + isNone(): this is None { + return false + } + + isSome(): this is Some { + return true + } + + isSomeAnd(f: (value: T) => boolean): this is Some { + return f(this.value) + } + + map(f: (value: T) => U) { + return Some(f(this.value)) + } + + mapOr(_defaultValue: A, f: (value: T) => B) { + return f(this.value) + } + + mapOrElse(_defaultValue: () => A, f: (value: T) => B) { + return f(this.value) + } + + or(_other: Option) { + return this + } + + orElse(_f: () => Option) { + return this + } + + unwrap() { + return this.value + } + + unwrapOr(_defaultValue: U) { + return this.value + } + + unwrapOrElse(_defaultValue: () => U) { + return this.value + } + + xor(other: Option) { + return other.isSome() ? None : this + } + + into() { + return this.value + } + + match(some: (value: T) => A, _none: () => B) { + return some(this.value) + } + + toString() { + return `Some(${this.value})` as const + } + + [inspectSymbol]() { + return this.toString() + } + + toObject() { + return {some: true, value: this.value} as const + } + + toJSON() { + return {meta: "Some", data: this.value} as const + } + + static from(value: T): Some { + return new SomeImpl(value) + } +} + +export interface Some extends SomeImpl {} +export const Some = SomeImpl.from diff --git a/src/panic.ts b/src/panic.ts deleted file mode 100644 index 1e62319..0000000 --- a/src/panic.ts +++ /dev/null @@ -1,49 +0,0 @@ -/** Extends Error, used for unrecoverable errors. */ -export class Panic extends Error { - private static defaultName = "Panic" - - constructor(messageOrError: string | Error) { - if (messageOrError instanceof Error) { - const error = messageOrError - super(error.message) - this.name = `${Panic.defaultName}: ${error.name}` - if (error.stack) { - this.stack = error.stack - } - } else { - const message = messageOrError - super(message) - this.name = Panic.defaultName - } - } -} - -export class UnwrapPanic extends Panic { - constructor(messageOrError: string | Error) { - super(messageOrError) - } -} - -export class InvalidErrorPanic extends Panic { - constructor(public error: unknown) { - super("Invalid Error value") - } -} - -export class TodoPanic extends Panic { - constructor(message = "Todo") { - super(message) - } -} - -export class UnreachablePanic extends Panic { - constructor(message = "Unreachable") { - super(message) - } -} - -export class UnimplementedPanic extends Panic { - constructor(message = "Unimplemented") { - super(message) - } -} diff --git a/src/result.ts b/src/result.ts deleted file mode 100644 index 12c1432..0000000 --- a/src/result.ts +++ /dev/null @@ -1,277 +0,0 @@ -import {Panic, UnwrapPanic} from "./panic" - -export interface ResultMethods { - and(other: Result): Result - andThen(f: (value: T) => Result): Result - expect(panic: string | Panic): T - expectErr(panic: string | Panic): E - inspect(f: (value: T) => void): Result - inspectErr(f: (error: E) => void): Result - isErr(): this is Err - isErrAnd(f: (error: E) => boolean): this is Err - isOk(): this is Ok - isOkAnd(f: (value: T) => boolean): this is Ok - map(f: (value: T) => U): Result - mapErr(f: (error: E) => F): Result - mapOr(defaultValue: A, f: (value: T) => B): A | B - mapOrElse(defaultValue: (error: E) => A, f: (value: T) => B): A | B - or(other: Result): Result - orElse(f: (error: E) => Result): Result - unwrap(): T - unwrapErr(): E - unwrapOr(defaultValue: U): T | U - unwrapOrElse(defaultValue: (error: E) => U): T | U - - get(): T | E - match(ok: (value: T) => A, err: (error: E) => B): A | B - - toString(): `Ok(${string})` | `Err(${string})` - toObject(): {ok: true; value: T} | {ok: false; error: E} - toJSON(): {meta: "Ok"; data: T} | {meta: "Err"; data: E} -} - -export class OkImpl implements ResultMethods { - readonly ok = true - readonly value: T - readonly err = false - readonly error?: never - - constructor(value: T) { - this.value = value - } - - and(other: Result) { - return other - } - - andThen(f: (value: T) => Result) { - return f(this.value) - } - - expect(_panic: string | Panic) { - return this.value - } - - expectErr(panic: string | Panic): never { - if (panic instanceof Panic) { - throw panic - } - throw new Panic(panic) - } - - inspect(f: (value: T) => void) { - f(this.value) - return this - } - - inspectErr(_f: (error: never) => void) { - return this - } - - isErr(): this is Err { - return false - } - - isErrAnd(_f: (error: never) => boolean): this is Err { - return false - } - - isOk(): this is Ok { - return true - } - - isOkAnd(f: (value: T) => boolean): this is Ok { - return f(this.value) - } - - map(f: (value: T) => U) { - return Ok(f(this.value)) - } - - mapErr(_f: (error: never) => F) { - return this - } - - mapOr(_defaultValue: A, f: (value: T) => B) { - return f(this.value) - } - - mapOrElse(_defaultValue: (error: never) => A, f: (value: T) => B) { - return f(this.value) - } - - or(_other: Result) { - return this - } - - orElse(_f: (error: never) => Result) { - return this - } - - unwrap() { - return this.value - } - - unwrapErr(): never { - throw new UnwrapPanic("Cannot unwrapErr on an Ok") - } - - unwrapOr(_defaultValue: U) { - return this.value - } - - unwrapOrElse(_defaultValue: (error: never) => U) { - return this.value - } - - get() { - return this.value - } - - match(ok: (value: T) => A, _err: (error: never) => B) { - return ok(this.value) - } - - toString() { - return `Ok(${this.value})` as const - } - - toObject() { - return {ok: true, value: this.value} as const - } - - toJSON() { - return {meta: "Ok", data: this.value} as const - } -} - -export interface Ok extends OkImpl {} -export function Ok(): Ok -export function Ok(value: T): Ok -export function Ok(value?: T): Ok { - return new OkImpl(value ? value : null) as Ok -} - -export class ErrImpl implements ResultMethods { - readonly ok = false - readonly value?: never - readonly err = true - readonly error: E - - constructor(error: E) { - this.error = error - } - - and(_other: Result) { - return this - } - - andThen(_f: (value: never) => Result) { - return this - } - - expect(panic: string | Panic): never { - if (panic instanceof Panic) { - throw panic - } - throw new Panic(panic) - } - - expectErr(_panic: string | Panic) { - return this.error - } - - inspect(_f: (value: never) => void) { - return this - } - - inspectErr(f: (error: E) => void) { - f(this.error) - return this - } - - isErr(): this is Err { - return true - } - - isErrAnd(f: (error: E) => boolean): this is Err { - return f(this.error) - } - - isOk(): this is Ok { - return false - } - - isOkAnd(_f: (value: never) => boolean): this is Ok { - return false - } - - map(_f: (value: never) => U) { - return this - } - - mapErr(f: (error: E) => F) { - return Err(f(this.error)) - } - - mapOr(defaultValue: A, _f: (value: never) => B) { - return defaultValue - } - - mapOrElse(defaultValue: (error: E) => A, _f: (value: never) => B) { - return defaultValue(this.error) - } - - or(other: Result) { - return other - } - - orElse(f: (error: E) => Result) { - return f(this.error) - } - - unwrap(): never { - throw new UnwrapPanic(`Cannot unwrap on an Err: ${this.error}`) - } - - unwrapErr() { - return this.error - } - - unwrapOr(defaultValue: U) { - return defaultValue - } - - unwrapOrElse(defaultValue: (error: E) => U) { - return defaultValue(this.error) - } - - get() { - return this.error - } - - match(_ok: (value: never) => A, err: (error: E) => B) { - return err(this.error) - } - - toString() { - return `Err(${this.error})` as const - } - - toObject() { - return {ok: false, error: this.error} as const - } - - toJSON() { - return {meta: "Err", data: this.error} as const - } -} - -export interface Err extends ErrImpl {} -export function Err(): Err -export function Err(error: E): Err -export function Err(error?: E): Err { - return new ErrImpl(error ? error : null) as Err -} - -export type Result = Ok | Err diff --git a/src/result/err.ts b/src/result/err.ts new file mode 100644 index 0000000..0423f28 --- /dev/null +++ b/src/result/err.ts @@ -0,0 +1,132 @@ +import {Panic, UnwrapPanic} from "../error/panic" +import {inspectSymbol} from "../util" +import type {Result, ResultMethods} from "./interface" +import type {Ok} from "./ok" + +export class ErrImpl implements ResultMethods { + readonly ok = false + readonly value?: never + readonly err = true + readonly error: E + + constructor(error: E) { + this.error = error + } + + and(_other: Result) { + return this + } + + andThen(_f: (value: never) => Result) { + return this + } + + expect(panic: string | Panic): never { + if (panic instanceof Panic) { + throw panic + } + throw new Panic(panic) + } + + expectErr(_panic: string | Panic) { + return this.error + } + + inspect(_f: (value: never) => void) { + return this + } + + inspectErr(f: (error: E) => void) { + f(this.error) + return this + } + + isErr(): this is Err { + return true + } + + isErrAnd(f: (error: E) => boolean): this is Err { + return f(this.error) + } + + isOk(): this is Ok { + return false + } + + isOkAnd(_f: (value: never) => boolean): this is Ok { + return false + } + + map(_f: (value: never) => U) { + return this + } + + mapErr(f: (error: E) => F) { + return Err(f(this.error)) + } + + mapOr(defaultValue: A, _f: (value: never) => B) { + return defaultValue + } + + mapOrElse(defaultValue: (error: E) => A, _f: (value: never) => B) { + return defaultValue(this.error) + } + + or(other: Result) { + return other + } + + orElse(f: (error: E) => Result) { + return f(this.error) + } + + unwrap(): never { + throw new UnwrapPanic(`Cannot unwrap on an Err: ${this.error}`) + } + + unwrapErr() { + return this.error + } + + unwrapOr(defaultValue: U) { + return defaultValue + } + + unwrapOrElse(defaultValue: (error: E) => U) { + return defaultValue(this.error) + } + + into() { + return this.error + } + + match(_ok: (value: never) => A, err: (error: E) => B) { + return err(this.error) + } + + toString() { + return `Err(${this.error})` as const + } + + [inspectSymbol]() { + return this.toString() + } + + toObject() { + return {ok: false, error: this.error} as const + } + + toJSON() { + return {meta: "Err", data: this.error} as const + } + + static from(): Err + static from(error: E): Err + static from(error?: E): Err { + return new ErrImpl(error ? error : null) as Err + } +} + +export interface Err extends ErrImpl {} +export const Err = ErrImpl.from diff --git a/src/result/interface.ts b/src/result/interface.ts new file mode 100644 index 0000000..6916801 --- /dev/null +++ b/src/result/interface.ts @@ -0,0 +1,37 @@ +import type {Panic} from "../error/panic" +import type {inspectSymbol} from "../util" +import type {Err} from "./err" +import type {Ok} from "./ok" + +export interface ResultMethods { + and(other: Result): Result + andThen(f: (value: T) => Result): Result + expect(panic: string | Panic): T + expectErr(panic: string | Panic): E + inspect(f: (value: T) => void): Result + inspectErr(f: (error: E) => void): Result + isErr(): this is Err + isErrAnd(f: (error: E) => boolean): this is Err + isOk(): this is Ok + isOkAnd(f: (value: T) => boolean): this is Ok + map(f: (value: T) => U): Result + mapErr(f: (error: E) => F): Result + mapOr(defaultValue: A, f: (value: T) => B): A | B + mapOrElse(defaultValue: (error: E) => A, f: (value: T) => B): A | B + or(other: Result): Result + orElse(f: (error: E) => Result): Result + unwrap(): T + unwrapErr(): E + unwrapOr(defaultValue: U): T | U + unwrapOrElse(defaultValue: (error: E) => U): T | U + + into(): T | E + match(ok: (value: T) => A, err: (error: E) => B): A | B + + toString(): `Ok(${string})` | `Err(${string})` + [inspectSymbol](): ReturnType["toString"]> + toObject(): {ok: true; value: T} | {ok: false; error: E} + toJSON(): {meta: "Ok"; data: T} | {meta: "Err"; data: E} +} + +export type Result = Ok | Err diff --git a/src/result/ok.ts b/src/result/ok.ts new file mode 100644 index 0000000..74bb70d --- /dev/null +++ b/src/result/ok.ts @@ -0,0 +1,132 @@ +import {Panic, UnwrapPanic} from "../error/panic" +import {inspectSymbol} from "../util" +import type {Err} from "./err" +import type {Result, ResultMethods} from "./interface" + +export class OkImpl implements ResultMethods { + readonly ok = true + readonly value: T + readonly err = false + readonly error?: never + + constructor(value: T) { + this.value = value + } + + and(other: Result) { + return other + } + + andThen(f: (value: T) => Result) { + return f(this.value) + } + + expect(_panic: string | Panic) { + return this.value + } + + expectErr(panic: string | Panic): never { + if (panic instanceof Panic) { + throw panic + } + throw new Panic(panic) + } + + inspect(f: (value: T) => void) { + f(this.value) + return this + } + + inspectErr(_f: (error: never) => void) { + return this + } + + isErr(): this is Err { + return false + } + + isErrAnd(_f: (error: never) => boolean): this is Err { + return false + } + + isOk(): this is Ok { + return true + } + + isOkAnd(f: (value: T) => boolean): this is Ok { + return f(this.value) + } + + map(f: (value: T) => U) { + return Ok(f(this.value)) + } + + mapErr(_f: (error: never) => F) { + return this + } + + mapOr(_defaultValue: A, f: (value: T) => B) { + return f(this.value) + } + + mapOrElse(_defaultValue: (error: never) => A, f: (value: T) => B) { + return f(this.value) + } + + or(_other: Result) { + return this + } + + orElse(_f: (error: never) => Result) { + return this + } + + unwrap() { + return this.value + } + + unwrapErr(): never { + throw new UnwrapPanic("Cannot unwrapErr on an Ok") + } + + unwrapOr(_defaultValue: U) { + return this.value + } + + unwrapOrElse(_defaultValue: (error: never) => U) { + return this.value + } + + into() { + return this.value + } + + match(ok: (value: T) => A, _err: (error: never) => B) { + return ok(this.value) + } + + toString() { + return `Ok(${this.value})` as const + } + + [inspectSymbol]() { + return this.toString() + } + + toObject() { + return {ok: true, value: this.value} as const + } + + toJSON() { + return {meta: "Ok", data: this.value} as const + } + + static from(): Ok + static from(value: T): Ok + static from(value?: T): Ok { + return new OkImpl(value ? value : null) as Ok + } +} + +export interface Ok extends OkImpl {} +export const Ok = OkImpl.from diff --git a/test/promise_result.test.ts b/src/result/promise.test.ts similarity index 98% rename from test/promise_result.test.ts rename to src/result/promise.test.ts index 688d0a0..af43fa3 100644 --- a/test/promise_result.test.ts +++ b/src/result/promise.test.ts @@ -1,5 +1,5 @@ import {describe, it, expect} from "vitest" -import {Err, Panic, PromiseResult, Ok, UnwrapPanic} from "../src" +import {Err, Panic, PromiseResult, Ok, UnwrapPanic} from ".." function promiseOk(value: T) { return new PromiseResult(Promise.resolve(Ok(value))) @@ -363,15 +363,15 @@ describe.concurrent("unwrapOrElse", () => { }) }) -describe.concurrent("get", () => { +describe.concurrent("into", () => { it("returns the value for an Ok result", async () => { const result = promiseOk(42) - await expect(result.get()).resolves.toEqual(42) + await expect(result.into()).resolves.toEqual(42) }) it("returns the err for an Err result", async () => { const result = promiseErr("error") - await expect(result.get()).resolves.toEqual("error") + await expect(result.into()).resolves.toEqual("error") }) }) diff --git a/src/promise_result.ts b/src/result/promise.ts similarity index 95% rename from src/promise_result.ts rename to src/result/promise.ts index 86a1189..f9ebeff 100644 --- a/src/promise_result.ts +++ b/src/result/promise.ts @@ -1,5 +1,5 @@ -import type {Result} from "./result" -import {type Panic} from "./panic" +import type {Result} from "./interface" +import type {Panic} from "../error/panic" export class PromiseResult implements PromiseLike> { constructor(readonly promise: Promise> | PromiseLike>) {} @@ -116,8 +116,8 @@ export class PromiseResult implements PromiseLike> { return (await this).unwrapOrElse(defaultValue) } - async get() { - return (await this).get() + async into() { + return (await this).into() } async match(ok: (value: T) => A, err: (error: E) => B) { diff --git a/test/result.test.ts b/src/result/result.test.ts similarity index 98% rename from test/result.test.ts rename to src/result/result.test.ts index 3981b0f..f4babee 100644 --- a/test/result.test.ts +++ b/src/result/result.test.ts @@ -1,5 +1,5 @@ import {describe, it, expect} from "vitest" -import {Panic, UnwrapPanic, Ok, Err, Result} from "../src" +import {Panic, UnwrapPanic, Ok, Err, Result} from ".." describe.concurrent("ok", () => { it("returns an Ok result", () => { @@ -352,15 +352,15 @@ describe.concurrent("unwrapOrElse", () => { }) }) -describe.concurrent("get", () => { +describe.concurrent("into", () => { it("returns the value for an Ok result", () => { const result = Ok(42) as Result - expect(result.get()).toEqual(42) + expect(result.into()).toEqual(42) }) it("returns the err for an Err result", () => { const result = Err(42) as Result - expect(result.get()).toEqual(42) + expect(result.into()).toEqual(42) }) }) diff --git a/src/try.ts b/src/try.ts deleted file mode 100644 index 521f2bc..0000000 --- a/src/try.ts +++ /dev/null @@ -1,34 +0,0 @@ -import {PromiseResult} from "./promise_result" -import {Ok, type Result, Err} from "./result" -import {InvalidErrorPanic, Panic} from "./panic" - -export function handleError(error: unknown) { - if (error instanceof Panic) { - throw error - } - if (error instanceof Error) { - return error - } - throw new InvalidErrorPanic(error) -} - -export function tryFn(fn: () => T): Result { - try { - return Ok(fn()) - } catch (error) { - return Err(handleError(error)) - } -} - -export function tryPromise(promise: Promise): PromiseResult { - return new PromiseResult( - promise.then( - (value) => Ok(value), - (error) => Err(handleError(error)), - ), - ) -} - -export function tryAsyncFn(fn: () => Promise): PromiseResult { - return tryPromise(fn()) -} diff --git a/src/util.ts b/src/util.ts index e6fabb1..511dd73 100644 --- a/src/util.ts +++ b/src/util.ts @@ -1,7 +1,9 @@ -import {PromiseOption} from "./promise_option" -import {Option} from "./option" -import type {PromiseResult} from "./promise_result" -import type {Result} from "./result" +import type {Option} from "./option/interface" +import type {PromiseOption} from "./option/promise" +import type {Result} from "./result/interface" +import type {PromiseResult} from "./result/promise" + +export const inspectSymbol = Symbol.for("nodejs.util.inspect.custom") export type ResultValueErrorType = T extends | Result diff --git a/test/guard.test.ts b/test/guard.test.ts deleted file mode 100644 index 0410c5a..0000000 --- a/test/guard.test.ts +++ /dev/null @@ -1,44 +0,0 @@ -import {expect, it, describe} from "vitest" -import {guard, guardAsync} from "../src" - -describe.concurrent("guard", () => { - it("transforms a function into a function that returns a Result object", () => { - const fn = (x: number, y: number) => x + y - const wrappedFn = guard(fn) - const result = wrappedFn(40, 2) - expect(result.ok).toEqual(true) - expect(result.unwrap()).toEqual(42) - }) - - it("transforms a throwing function into a function that returns an Err result", () => { - const error = new Error("Test error") - const fn = () => { - throw error - } - const wrappedFn = guard(fn) - const result = wrappedFn() - expect(result.ok).toEqual(false) - expect(result.unwrapErr()).toEqual(error) - }) -}) - -describe.concurrent("guardAsync", () => { - it("transforms an async function into a function that returns a Promise of a Result object", async () => { - const fn = async (x: number, y: number) => Promise.resolve(x + y) - const wrappedFn = guardAsync(fn) - const result = await wrappedFn(40, 2) - expect(result.ok).toEqual(true) - expect(result.unwrap()).toEqual(42) - }) - - it("transforms a throwing async function into a function that returns a Promise of an Err result", async () => { - const error = new Error("Test error") - const fn = async (): Promise => { - throw error - } - const wrappedFn = guardAsync(fn) - const result = await wrappedFn() - expect(result.ok).toEqual(false) - expect(result.unwrapErr()).toEqual(error) - }) -}) diff --git a/test/try.test.ts b/test/try.test.ts deleted file mode 100644 index aa23526..0000000 --- a/test/try.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -import {describe, expect, it} from "vitest" -import {InvalidErrorPanic, Panic, handleError, tryAsyncFn, tryFn, tryPromise} from "../src" - -describe.concurrent("handleError", () => { - it("returns an Error when given an Error", () => { - class TestError extends Error {} - const error = new TestError("Test error") - const err = handleError(error) - expect(err).to.be.instanceof(TestError) - }) - - it("throws a Panic when given a Panic", () => { - const msg = "Test panic" - const panic = new Panic(msg) - expect(() => handleError(panic)).to.throw(Panic, msg) - }) - - it("throws a Panic when given an unknown value", () => { - expect(() => handleError(0)).to.throw(InvalidErrorPanic) - expect(() => handleError("")).to.throw(InvalidErrorPanic) - expect(() => handleError(true)).to.throw(InvalidErrorPanic) - expect(() => handleError(undefined)).to.throw(InvalidErrorPanic) - expect(() => handleError(null)).to.throw(InvalidErrorPanic) - expect(() => handleError({})).to.throw(InvalidErrorPanic) - expect(() => handleError([])).to.throw(InvalidErrorPanic) - }) -}) - -describe.concurrent("tryPromise", () => { - it("settles a Promise to an Ok result", async () => { - const promise = Promise.resolve(42) - const result = await tryPromise(promise) - expect(result.ok).toEqual(true) - expect(result.unwrap()).toEqual(42) - }) - - it("settles a rejected Promise to an Err result", async () => { - const error = new Error("Test error") - const promise = Promise.reject(error) - const result = await tryPromise(promise) - expect(result.ok).toEqual(false) - expect(result.unwrapErr()).toEqual(error) - }) -}) - -describe.concurrent("tryFn", () => { - it("wraps a function call into a Result object", () => { - const fn = () => 42 - const result = tryFn(fn) - expect(result.ok).toEqual(true) - expect(result.unwrap()).toEqual(42) - }) - - it("wraps a throwing function call into an Err result", () => { - const error = new Error("Test error") - const fn = () => { - throw error - } - const result = tryFn(fn) - expect(result.ok).toEqual(false) - expect(result.unwrapErr()).toEqual(error) - }) -}) - -describe.concurrent("tryAsyncFn", () => { - it("wraps an async function call into a Result object", async () => { - const fn = async () => Promise.resolve(42) - const result = await tryAsyncFn(fn) - expect(result.ok).toEqual(true) - expect(result.unwrap()).toEqual(42) - }) - - it("wraps a throwing async function call into an Err result", async () => { - const error = new Error("Test error") - const fn = async (): Promise => { - throw error - } - const result = await tryAsyncFn(fn) - expect(result.ok).toEqual(false) - expect(result.unwrapErr()).toEqual(error) - }) -}) diff --git a/vitest.config.ts b/vitest.config.ts index 4b550ba..f09ed3f 100644 --- a/vitest.config.ts +++ b/vitest.config.ts @@ -3,5 +3,6 @@ import type {UserConfig} from "vitest/config" export default { test: { include: ["**/*.test.ts"], + cache: false, }, } satisfies UserConfig