diff --git a/.changeset/chilled-seahorses-serve.md b/.changeset/chilled-seahorses-serve.md new file mode 100644 index 0000000..54f52c2 --- /dev/null +++ b/.changeset/chilled-seahorses-serve.md @@ -0,0 +1,30 @@ +--- +"ajv-ts": minor +--- + +Make [strict numbers](#strict-numbers) + +### Strict numbers + +We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. + +Examples: + +```ts +s.number().format('float').int() // error in type! +s.int().const(3.4) // error in type! +s.number().int().format('float') // error in format! +s.number().int().format('double') // error in format! + +// ranges are also check for possibility + +s.number().min(5).max(3) // error in range! +s.number().min(3).max(5).const(10) // error in constant - out of range! +``` + +## 🏡 Chore/Infra + +- add [type-fest](https://www.npmjs.com/package/type-fest) library for correct type checking +- add [tsx](https://www.npmjs.com/package/tsx) package +- add minified files for cjs and esm modules in `dist` folder +- remove `bun-types` dependency diff --git a/.changeset/fair-dolls-lick.md b/.changeset/fair-dolls-lick.md index e49a8b9..7cac892 100644 --- a/.changeset/fair-dolls-lick.md +++ b/.changeset/fair-dolls-lick.md @@ -2,4 +2,4 @@ "ajv-ts": patch --- -fix # 61 +fix #61 diff --git a/.github/workflows/pr.yaml b/.github/workflows/pr.yaml index 13c446d..564e531 100644 --- a/.github/workflows/pr.yaml +++ b/.github/workflows/pr.yaml @@ -14,7 +14,7 @@ jobs: strategy: matrix: node_version: [18, 20, 22, latest] - pnpm_version: [9.4.0] + pnpm_version: [9.9.0] steps: - name: Clone repository uses: actions/checkout@v3 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index f2780db..92a928f 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -22,7 +22,7 @@ jobs: strategy: matrix: node_version: [20] - pnpm_version: [9.4.0] + pnpm_version: [9.9.0] steps: - name: Clone repository uses: actions/checkout@v3 diff --git a/.npmignore b/.npmignore index f0268f9..fb8ac4f 100644 --- a/.npmignore +++ b/.npmignore @@ -8,7 +8,7 @@ tests # Files bun.lockb yarn.lock -tsup.config.ts +tsup.config.*ts tsconfig.json .eslintrc.json CONTRIBUTING.md diff --git a/README.md b/README.md index 34c0016..3432085 100644 --- a/README.md +++ b/README.md @@ -14,6 +14,15 @@ - [String](#string) - [Typescript features](#typescript-features) - [Numbers](#numbers) + - [Types](#types) + - [Number](#number) + - [Int](#int) + - [Formats](#formats) + - [int32](#int32) + - [int64](#int64) + - [float](#float) + - [double](#double) + - [Typescript features](#typescript-features-1) - [BigInts](#bigints) - [NaNs](#nans) - [Dates](#dates) @@ -40,7 +49,7 @@ - [`.element`](#element) - [`.nonempty`](#nonempty) - [`.min`/`.max`/`.length`/`.minLength`/`.maxLength`](#minmaxlengthminlengthmaxlength) - - [Typescript features](#typescript-features-1) + - [Typescript features](#typescript-features-2) - [`.unique`](#unique) - [`.contains`/`.minContains`](#containsmincontains) - [Tuples](#tuples) @@ -276,6 +285,72 @@ s.number().nonpositive(); // <= 0 s.number().multipleOf(5); // Evenly divisible by 5. Alias .step(5) ``` +### Types + +#### Number + +Number - any number type + +```ts +s.number() +// same as +s.number().number() +``` + +#### Int + +Only integers values. + +Note: we check in runtime non-integer format (`float`, `double`) and give an error. + +```ts +s.number().int() +// or +s.number().integer() +// or +s.int() +``` + +### Formats + +Defines in [ajv-formats](https://ajv.js.org/packages/ajv-formats.html#formats) package + +#### int32 + +Signed 32 bits integer according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +#### int64 + +Signed 64 bits according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +#### float + +float: float according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +#### double + +double: double according to the [openApi 3.0.0 specification](https://spec.openapis.org/oas/v3.0.0#data-types) + +### Typescript features + +> from >= 0.8 + +We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. + +Examples: + +```ts +s.number().format('float').int() // error in type! +s.int().const(3.4) // error in type! +s.number().int().format('float') // error in format! +s.number().int().format('double') // error in format! + +// ranges are also check for possibility + +s.number().min(5).max(3) // error in range! +s.number().min(3).max(5).const(10) // error in constant! +``` + ## BigInts Not supported diff --git a/SECURITY.md b/SECURITY.md index b50f14e..2649ffd 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -9,7 +9,6 @@ currently being supported with security updates. | ------- | ------------------ | | < 0.x | :white_check_mark: | - ## Reporting a Vulnerability Use this section to tell people how to report a vulnerability. diff --git a/UPCOMING.md b/UPCOMING.md index cad621d..cecade5 100644 --- a/UPCOMING.md +++ b/UPCOMING.md @@ -4,8 +4,33 @@ ## ✅ New Features +- [strict number](#strict-numbers) + +### Strict numbers + +We make validation for number `type`, `format`, `minValue` and `maxValue` fields. That means we handle it in our side so you get an error for invalid values. + +Examples: + +```ts +s.number().format('float').int() // error in type! +s.int().const(3.4) // error in type! +s.number().int().format('float') // error in format! +s.number().int().format('double') // error in format! + +// ranges are also check for possibility + +s.number().min(5).max(3) // error in range! +s.number().min(3).max(5).const(10) // error in constant - out of range! +``` + ## 🐛 Bug Fixes - [#61](https://github.com/vitalics/ajv-ts/issues/61) ## 🏡 Chore/Infra + +- add [type-fest](https://www.npmjs.com/package/type-fest) library for correct type checking +- add [tsx](https://www.npmjs.com/package/tsx) package +- add minified files for cjs and esm modules in `dist` folder +- remove `bun-types` dependency diff --git a/package.json b/package.json index f7015b3..1e7e898 100644 --- a/package.json +++ b/package.json @@ -5,9 +5,9 @@ "main": "dist/index.cjs", "module": "dist/index.mjs", "types": "dist/index.d.ts", - "packageManager": "pnpm@9.4.0", + "packageManager": "pnpm@9.9.0", "scripts": { - "build": "tsup", + "build": "tsx ./tsup.config.mts", "test": "vitest run", "test:watch": "vitest", "ci:version": "changeset version", @@ -47,7 +47,6 @@ "@typescript-eslint/eslint-plugin": "6.4.0", "@vitest/ui": "1.6.0", "benchmark": "2.1.4", - "bun-types": "1.1.18", "eslint": "8.0.1", "eslint-plugin-import": "2.25.2", "eslint-plugin-n": "15.0.0", @@ -62,6 +61,7 @@ "dependencies": { "ajv": "8.16.0", "ajv-errors": "3.0.0", - "ajv-formats": "3.0.1" + "ajv-formats": "3.0.1", + "type-fest": "4.26.0" } -} +} \ No newline at end of file diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0cce9e6..24b9ba9 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -17,6 +17,9 @@ importers: ajv-formats: specifier: 3.0.1 version: 3.0.1(ajv@8.16.0) + type-fest: + specifier: 4.26.0 + version: 4.26.0 devDependencies: '@biomejs/biome': specifier: 1.8.3 @@ -36,9 +39,6 @@ importers: benchmark: specifier: 2.1.4 version: 2.1.4 - bun-types: - specifier: 1.1.18 - version: 1.1.18 eslint: specifier: 8.0.1 version: 8.0.1 @@ -50,7 +50,7 @@ importers: version: 15.0.0(eslint@8.0.1) tsup: specifier: 8.1.0 - version: 8.1.0(postcss@8.4.38)(typescript@5.5.3) + version: 8.1.0(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3))(typescript@5.5.3) tsx: specifier: 4.17.0 version: 4.17.0 @@ -184,6 +184,10 @@ packages: '@changesets/write@0.2.3': resolution: {integrity: sha512-Dbamr7AIMvslKnNYsLFafaVORx4H0pvCA2MHqgtNCySMe1blImEyAEOzDmcgKAkgz4+uwoLz7demIrX+JBr/Xw==} + '@cspotcode/source-map-support@0.8.1': + resolution: {integrity: sha512-IchNf6dN4tHoMFIn/7OE8LWZ19Y6q/67Bmf6vnGREv8RSbBVb9LPJxEcnwrcwX6ixSvaiGoomAUvu4YSxXrVgw==} + engines: {node: '>=12'} + '@esbuild/aix-ppc64@0.21.5': resolution: {integrity: sha512-1SDgH6ZSPTlggy1yI6+Dbkiz8xzpHJEVAlF/AM1tHPLsf5STom9rwtjE4hKAF20FfXXNTFqEYXyJNWh1GiZedQ==} engines: {node: '>=12'} @@ -515,6 +519,9 @@ packages: '@jridgewell/trace-mapping@0.3.25': resolution: {integrity: sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==} + '@jridgewell/trace-mapping@0.3.9': + resolution: {integrity: sha512-3Belt6tdc8bPgAtbcmdtNJlirVoTmEb5e2gC94PnkwEW9jI6CAHUeoG85tjWP5WquqfavoMtMwiG4P926ZKKuQ==} + '@manypkg/find-root@1.1.0': resolution: {integrity: sha512-mki5uBvhHzO8kYYix/WRy2WX8S3B5wdVSc9D6KcU5lQNglP2yt58/VfLuAK49glRXChosY8ap2oJ1qgma3GUVA==} @@ -623,6 +630,18 @@ packages: '@sinclair/typebox@0.27.8': resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} + '@tsconfig/node10@1.0.11': + resolution: {integrity: sha512-DcRjDCujK/kCk/cUe8Xz8ZSpm8mS3mNNpta+jGCA6USEDfktlNvm1+IuZ9eTcDbNk41BHwpHHeW+N1lKCz4zOw==} + + '@tsconfig/node12@1.0.11': + resolution: {integrity: sha512-cqefuRsh12pWyGsIoBKJA9luFu3mRxCA+ORZvA4ktLSzIuCUtWVxGIuXigEwO5/ywWFMZ2QEGKWvkZG1zDMTag==} + + '@tsconfig/node14@1.0.3': + resolution: {integrity: sha512-ysT8mhdixWK6Hw3i1V2AeRqZ5WfXg1G43mqoYlM2nc6388Fq5jcXyr5mRsqViLx/GJYdoL0bfXD8nmF+Zn/Iow==} + + '@tsconfig/node16@1.0.4': + resolution: {integrity: sha512-vxhUy4J8lyeyinH7Azl1pdd43GJhZH/tP2weN8TntQblOY+A0XbT8DJk1/oCPuOOyg/Ja757rG0CgHcWC8OfMA==} + '@types/benchmark@2.1.5': resolution: {integrity: sha512-cKio2eFB3v7qmKcvIHLUMw/dIx/8bhWPuzpzRT4unCPRTD8VdA9Zb0afxpcxOqR4PixRS7yT42FqGS8BYL8g1w==} @@ -644,9 +663,6 @@ packages: '@types/node@12.20.55': resolution: {integrity: sha512-J8xLz7q2OFulZ2cyGTLE1TbbZcjpno7FaN6zdJNrgAdrJ+DZzh/uFR6YrTb4C+nXakvud8Q4+rbhoIWlYQbUFQ==} - '@types/node@20.12.14': - resolution: {integrity: sha512-scnD59RpYD91xngrQQLGkE+6UrHUPzeKZWhhjBSa3HSkwjbQc38+q3RoIVEwxQGRw3M+j5hpNAM+lgV3cVormg==} - '@types/node@20.14.2': resolution: {integrity: sha512-xyu6WAMVwv6AKFLB+e/7ySZVr/0zLCzOa7rSpq6jNwpqOrUbcACDWC+53d4n2QHOnDou0fbIsg8wZu/sxrnI4Q==} @@ -656,9 +672,6 @@ packages: '@types/semver@7.5.8': resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} - '@types/ws@8.5.10': - resolution: {integrity: sha512-vmQSUcfalpIq0R9q7uTo2lXs6eGIpt9wtnLdMv9LVpIjCA/+ufZRozlVoVelIYixx1ugCBKDhn89vnsEGOCx9A==} - '@typescript-eslint/eslint-plugin@6.4.0': resolution: {integrity: sha512-62o2Hmc7Gs3p8SLfbXcipjWAa6qk2wZGChXG2JbBtYpwSRmti/9KHLqfbLs9uDigOexG+3PaQ9G2g3201FWLKg==} engines: {node: ^16.0.0 || >=18.0.0} @@ -826,6 +839,9 @@ packages: resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==} engines: {node: '>= 8'} + arg@4.1.3: + resolution: {integrity: sha512-58S9QDqG0Xx27YwPSt9fJxivjYl432YCwfDMfZ+71RAqUrZef7LrKQZ3LHLOwCS4FLNBplP533Zx895SeOCHvA==} + argparse@1.0.10: resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} @@ -890,9 +906,6 @@ packages: breakword@1.0.6: resolution: {integrity: sha512-yjxDAYyK/pBvws9H4xKYpLDpYKEH6CzrBPAuXq3x18I+c/2MkVtT3qAr7Oloi6Dss9qNhPVueAAVU1CSeNDIXw==} - bun-types@1.1.18: - resolution: {integrity: sha512-m5GnQrIpQdRyfWRoa5pvwpVAMDiQR1GTgMMZNvBWzJ+k2/fC55NRFZCEsXFE38HLFpM57o/diAjP3rgacdA4Eg==} - bundle-require@4.2.1: resolution: {integrity: sha512-7Q/6vkyYAwOmQNRw75x+4yRtZCZJXUDmHHlFdkiV0wgv/reNjtJwpu1jPJ0w2kbEpIM0uoKI3S4/f39dU7AjSA==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -979,6 +992,9 @@ packages: confbox@0.1.7: resolution: {integrity: sha512-uJcB/FKZtBMCJpK8MQji6bJHgu1tixKPxRLeGkNzBoOZzpnZUJm0jm2/sBDWcuBx1dYgxV4JU+g5hmNxCyAmdA==} + create-require@1.1.1: + resolution: {integrity: sha512-dcKFX3jn0MpIaXjisoRvexIJVEKzaq7z2rZKxf+MSr9TkdmHmsU4m2lcLojrj/FHl8mk5VxMmYA+ftRkP/3oKQ==} + cross-spawn@5.1.0: resolution: {integrity: sha512-pTgQJ5KC0d2hcY8eyL1IzlBPYjTkyH72XRZPnLyKus2mBfNjQs3klqbJU2VILqZryAZUt9JOb3h/mWMy23/f5A==} @@ -1070,6 +1086,10 @@ packages: resolution: {integrity: sha512-EjePK1srD3P08o2j4f0ExnylqRs5B9tJjcp9t1krH2qRi8CCdsYfwe9JgSLurFBWwq4uOlipzfk5fHNvwFKr8Q==} engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} + diff@4.0.2: + resolution: {integrity: sha512-58lmxKSA4BNyLz+HHMUzlOEpg09FV+ev6ZMe3vJihgdxzgcwZ8VoEEPmALCZG9LmqfVoNMMKpttIYTVG6uDY7A==} + engines: {node: '>=0.3.1'} + dir-glob@3.0.1: resolution: {integrity: sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==} engines: {node: '>=8'} @@ -1723,6 +1743,9 @@ packages: magic-string@0.30.10: resolution: {integrity: sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==} + make-error@1.3.6: + resolution: {integrity: sha512-s8UhlNe7vPKomQhC1qFelMokr/Sc3AgNbso3n74mVPA5LTZwkB9NlXf4XPamLxJE8h0gh73rM94xvwRT2CVInw==} + map-obj@1.0.1: resolution: {integrity: sha512-7N/q3lyZ+LVCp7PzuxrJr4KMbBE2hW7BT7YNia330OFxIf4d3r5zVpicP2650l7CPN6RM9zOJRl3NGpqSiw3Eg==} engines: {node: '>=0.10.0'} @@ -2334,6 +2357,20 @@ packages: ts-interface-checker@0.1.13: resolution: {integrity: sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==} + ts-node@10.9.2: + resolution: {integrity: sha512-f0FFpIdcHgn8zcPSbf1dRevwt047YMnaiJM3u2w2RewrB+fob/zePZcrOyQoLMMO7aBIddLcQIEK5dYjkLnGrQ==} + hasBin: true + peerDependencies: + '@swc/core': '>=1.2.50' + '@swc/wasm': '>=1.2.50' + '@types/node': '*' + typescript: '>=2.7' + peerDependenciesMeta: + '@swc/core': + optional: true + '@swc/wasm': + optional: true + tsconfig-paths@3.15.0: resolution: {integrity: sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==} @@ -2390,6 +2427,10 @@ packages: resolution: {integrity: sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==} engines: {node: '>=8'} + type-fest@4.26.0: + resolution: {integrity: sha512-OduNjVJsFbifKb57UqZ2EMP1i4u64Xwow3NYXUtBbD4vIwJdQd4+xl8YDou1dlm4DVrtwT/7Ky8z8WyCULVfxw==} + engines: {node: '>=16'} + typed-array-buffer@1.0.2: resolution: {integrity: sha512-gEymJYKZtKXzzBzM4jqa9w6Q1Jjm7x2d+sh19AdsD4wqnMPDYyvwpsIc2Q/835kHuo3BEQ7CjelGhfTsoBb2MQ==} engines: {node: '>= 0.4'} @@ -2427,6 +2468,9 @@ packages: uri-js@4.4.1: resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + v8-compile-cache-lib@3.0.1: + resolution: {integrity: sha512-wa7YjyUGfNZngI/vtK0UHAN+lgDCxBPCylVXGp0zu59Fz5aiGtNXaq3DhIov063MorB+VfufLh3JlF2KdTK3xg==} + v8-compile-cache@2.4.0: resolution: {integrity: sha512-ocyWc3bAHBB/guyqJQVI5o4BZkPhznPYUG2ea80Gond/BgNWpap8TOmLSeeQG7bnh2KMISxskdADG59j7zruhw==} @@ -2578,6 +2622,10 @@ packages: resolution: {integrity: sha512-7dSzzRQ++CKnNI/krKnYRV7JKKPUXMEh61soaHKg9mrWEhzFWhFnxPxGl+69cD1Ou63C13NUPCnmIcrvqCuM6w==} engines: {node: '>=12'} + yn@3.1.1: + resolution: {integrity: sha512-Ux4ygGWsu2c7isFWe8Yu1YluJmqVhxqK2cLXNQA5AcC3QfbGNpM7fu0Y8b/z16pXLnFxZYvWhd3fhBY9DLmC6Q==} + engines: {node: '>=6'} + yocto-queue@0.1.0: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} @@ -2790,6 +2838,11 @@ snapshots: human-id: 1.0.2 prettier: 2.8.8 + '@cspotcode/source-map-support@0.8.1': + dependencies: + '@jridgewell/trace-mapping': 0.3.9 + optional: true + '@esbuild/aix-ppc64@0.21.5': optional: true @@ -2992,6 +3045,12 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.4.15 + '@jridgewell/trace-mapping@0.3.9': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.4.15 + optional: true + '@manypkg/find-root@1.1.0': dependencies: '@babel/runtime': 7.24.7 @@ -3075,6 +3134,18 @@ snapshots: '@sinclair/typebox@0.27.8': {} + '@tsconfig/node10@1.0.11': + optional: true + + '@tsconfig/node12@1.0.11': + optional: true + + '@tsconfig/node14@1.0.3': + optional: true + + '@tsconfig/node16@1.0.4': + optional: true + '@types/benchmark@2.1.5': {} '@types/estree@1.0.5': {} @@ -3091,22 +3162,15 @@ snapshots: '@types/node@12.20.55': {} - '@types/node@20.12.14': - dependencies: - undici-types: 5.26.5 - '@types/node@20.14.2': dependencies: undici-types: 5.26.5 + optional: true '@types/normalize-package-data@2.4.4': {} '@types/semver@7.5.8': {} - '@types/ws@8.5.10': - dependencies: - '@types/node': 20.14.2 - '@typescript-eslint/eslint-plugin@6.4.0(@typescript-eslint/parser@6.21.0(eslint@8.0.1)(typescript@5.5.3))(eslint@8.0.1)(typescript@5.5.3)': dependencies: '@eslint-community/regexpp': 4.10.1 @@ -3316,6 +3380,9 @@ snapshots: normalize-path: 3.0.0 picomatch: 2.3.1 + arg@4.1.3: + optional: true + argparse@1.0.10: dependencies: sprintf-js: 1.0.3 @@ -3394,11 +3461,6 @@ snapshots: dependencies: wcwidth: 1.0.1 - bun-types@1.1.18: - dependencies: - '@types/node': 20.12.14 - '@types/ws': 8.5.10 - bundle-require@4.2.1(esbuild@0.21.5): dependencies: esbuild: 0.21.5 @@ -3497,6 +3559,9 @@ snapshots: confbox@0.1.7: {} + create-require@1.1.1: + optional: true + cross-spawn@5.1.0: dependencies: lru-cache: 4.1.5 @@ -3585,6 +3650,9 @@ snapshots: diff-sequences@29.6.3: {} + diff@4.0.2: + optional: true + dir-glob@3.0.1: dependencies: path-type: 4.0.0 @@ -4352,6 +4420,9 @@ snapshots: dependencies: '@jridgewell/sourcemap-codec': 1.4.15 + make-error@1.3.6: + optional: true + map-obj@1.0.1: {} map-obj@4.3.0: {} @@ -4577,12 +4648,13 @@ snapshots: possible-typed-array-names@1.0.0: {} - postcss-load-config@4.0.2(postcss@8.4.38): + postcss-load-config@4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3)): dependencies: lilconfig: 3.1.2 yaml: 2.4.5 optionalDependencies: postcss: 8.4.38 + ts-node: 10.9.2(@types/node@20.14.2)(typescript@5.5.3) postcss@8.4.38: dependencies: @@ -4942,6 +5014,25 @@ snapshots: ts-interface-checker@0.1.13: {} + ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3): + dependencies: + '@cspotcode/source-map-support': 0.8.1 + '@tsconfig/node10': 1.0.11 + '@tsconfig/node12': 1.0.11 + '@tsconfig/node14': 1.0.3 + '@tsconfig/node16': 1.0.4 + '@types/node': 20.14.2 + acorn: 8.12.0 + acorn-walk: 8.3.3 + arg: 4.1.3 + create-require: 1.1.1 + diff: 4.0.2 + make-error: 1.3.6 + typescript: 5.5.3 + v8-compile-cache-lib: 3.0.1 + yn: 3.1.1 + optional: true + tsconfig-paths@3.15.0: dependencies: '@types/json5': 0.0.29 @@ -4949,7 +5040,7 @@ snapshots: minimist: 1.2.8 strip-bom: 3.0.0 - tsup@8.1.0(postcss@8.4.38)(typescript@5.5.3): + tsup@8.1.0(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3))(typescript@5.5.3): dependencies: bundle-require: 4.2.1(esbuild@0.21.5) cac: 6.7.14 @@ -4959,7 +5050,7 @@ snapshots: execa: 5.1.1 globby: 11.1.0 joycon: 3.1.1 - postcss-load-config: 4.0.2(postcss@8.4.38) + postcss-load-config: 4.0.2(postcss@8.4.38)(ts-node@10.9.2(@types/node@20.14.2)(typescript@5.5.3)) resolve-from: 5.0.0 rollup: 4.18.0 source-map: 0.8.0-beta.0 @@ -5003,6 +5094,8 @@ snapshots: type-fest@0.8.1: {} + type-fest@4.26.0: {} + typed-array-buffer@1.0.2: dependencies: call-bind: 1.0.7 @@ -5046,7 +5139,8 @@ snapshots: has-symbols: 1.0.3 which-boxed-primitive: 1.0.2 - undici-types@5.26.5: {} + undici-types@5.26.5: + optional: true universalify@0.1.2: {} @@ -5054,6 +5148,9 @@ snapshots: dependencies: punycode: 2.3.1 + v8-compile-cache-lib@3.0.1: + optional: true + v8-compile-cache@2.4.0: {} validate-npm-package-license@3.0.4: @@ -5230,6 +5327,9 @@ snapshots: y18n: 5.0.8 yargs-parser: 21.1.1 + yn@3.1.1: + optional: true + yocto-queue@0.1.0: {} yocto-queue@1.0.0: {} diff --git a/src/builder.ts b/src/builder.ts index c1bb480..c6b06f7 100644 --- a/src/builder.ts +++ b/src/builder.ts @@ -2,6 +2,17 @@ import Ajv from "ajv"; import ajvErrors from "ajv-errors"; import addFormats from "ajv-formats"; +import type { + And, + GreaterThan, + GreaterThanOrEqual, + IsFloat, + IsInteger, + LessThan, + LessThanOrEqual, + Or, +} from 'type-fest' + import type { AnySchema, AnySchemaOrAnnotation, @@ -15,18 +26,18 @@ import type { ObjectSchema, StringSchema, } from "./schema/types"; -import { Create, MakeReadonly, Optional, Push } from "./types/array"; +import type { Create, MakeReadonly, Optional } from "./types/array"; import type { + // Debug, Fn, Object as ObjectTypes, UnionToIntersection, UnionToTuple, } from "./types/index"; -import type { GreaterThan, GreaterThanOrEqual, IsPositiveInteger, LessThan } from "./types/number"; import type { OmitByValue, OmitMany, PickMany, Prettify } from "./types/object"; import type { Email, UUID } from "./types/string"; import type { TRangeGenericError, TTypeGenericError } from './types/errors'; - +import type { IsPositiveInteger, NumericStringifyType } from './types/number' /** * Default Ajv instance. * @@ -663,27 +674,40 @@ export class SchemaBuilder< } } +type NumberSchemaOpts = SchemaBuilderOpts & { + const?: number, + type?: 'integer' | 'number', + format?: NumberSchema["format"], + minValue?: number, + maxValue?: number, +} class NumberSchemaBuilder< const N extends number = number, - Opts extends SchemaBuilderOpts = { + Opts extends NumberSchemaOpts = { _preProcesses: [], _postProcesses: [], + const: undefined, + type: 'number', + format: undefined, + minValue: undefined, + maxValue: undefined, } -> extends SchemaBuilder { +> extends SchemaBuilder { constructor() { super({ type: "number" }); } /** - * change schema type from `any integer number` to `any number`. - * + * Define schema as `any number`. * Set schema `{type: 'number'}` * - * This is default behavior + * @note This is default behavior */ - number() { + number(): NumberSchemaBuilder & { + type: 'number', + }>> { this.schema.type = "number"; - return this; + return this as never; } /** @@ -693,32 +717,77 @@ class NumberSchemaBuilder< * a.schema // {type: "number", const: 5} * s.infer // 5 */ - const(value: N): NumberSchemaBuilder { + const< + const N extends number, + TypeValid = Opts['type'] extends 'integer' ? IsInteger : + Or, IsInteger>, + FormatValid = Opts['format'] extends 'int32' ? IsInteger : + Opts['format'] extends 'int64' ? IsInteger + : Or, IsInteger>, + ValueValid = Opts['maxValue'] extends number ? + Opts['minValue'] extends number ? + And, GreaterThanOrEqual>: LessThanOrEqual : true + >(value: TypeValid extends true ? + FormatValid extends true ? + ValueValid extends true ? + N + : TTypeGenericError<`Constant cannot be more than "MaxValue" and less than "MinValue"`, [`MinValue:`, Opts['minValue'], 'MaxValue:', Opts['maxValue']]> + : TTypeGenericError<`Format invalid. Expected ${Opts['format']}. Got "${N}" (${NumericStringifyType})`> + : TTypeGenericError<`Type invalid. Expected ${Opts['type']}. Got "${N}" (${NumericStringifyType})`> + ): NumberSchemaBuilder & { + const: N, + }>> { this.schema.const = value; return this as never; } - /** Set schema `{type: 'integer'}` */ - integer() { + /** + * Define schema as `any integer number`. + * Set schema `{type: 'integer'}` + */ + integer(): NumberSchemaBuilder & { + type: 'integer', + }>> { this.schema.type = "integer"; - return this; + return this as never; } + /** + * Set schema `{type: 'integer'}`. Same as `integer` method + * @see {@link integer integer} method + */ + int = this.integer + + /** * Appends format for your number schema. */ - format(type: NumberSchema["format"]) { - this.schema.format = type; - return this; + format< + const Format extends 'int32' | 'double' | 'int64' | 'float', + FormatValid = Opts['type'] extends 'integer' ? + Format extends 'int32' ? true : + Format extends 'int64' ? true + // type=int. Format float or doouble + : false + // Rest + : true + >(format: FormatValid extends true ? + Format : + TTypeGenericError<`Wrong format for given type. Expected "int32" or "int64". Given: "${Format}"`> + ): NumberSchemaBuilder & { + format: Format, + }>> { + this.schema.format = format as Format; + return this as never; } /** Getter. Retuns `minimum` or `exclusiveMinimum` depends on your schema definition */ - get minValue() { + get minValue(): Opts['minValue'] extends number ? Opts['minValue'] : number { return this.schema.minimum ?? (this.schema.exclusiveMinimum as number); } /** Getter. Retuns `maximum` or `exclusiveMaximum` depends on your schema definition */ - get maxValue() { + get maxValue(): Opts['maxValue'] extends number ? Opts['maxValue'] : number { return this.schema.maximum ?? (this.schema.exclusiveMaximum as number); } @@ -731,13 +800,34 @@ class NumberSchemaBuilder< * s.number().min(2, true) // > 2 * s.number().min(2) // >= 2 */ - minimum(value: number, exclusive = false) { + minimum< + const Min extends number = number, + Exclusive extends boolean = false, + MinLengthValid = Opts['maxValue'] extends number ? GreaterThan : true, + TypeValid = Opts['type'] extends 'integer' ? IsInteger : + Or, IsInteger>, + FormatValid = Opts['format'] extends undefined ? true : Opts['format'] extends 'int32' ? IsInteger : + Opts['format'] extends 'int64' ? IsInteger + : Or, IsInteger>, + >( + value: MinLengthValid extends true ? + TypeValid extends true ? + FormatValid extends true ? + Min + : TTypeGenericError<`Format invalid. Expected ${Opts['format']}. Got "${Min}" (${NumericStringifyType})`> + : TTypeGenericError<`Type invalid. Expected ${Opts['type']}. Got "${Min}" (${NumericStringifyType})`> + : TTypeGenericError<`"MaxValue" is less than "MinValue"`, ['MaxValue:', Opts['maxValue'], "MinValue:", Min]>, + exclusive = false as Exclusive + ): NumberSchemaBuilder & { + minValue: Min, + }> + > { if (exclusive) { - this.schema.exclusiveMinimum = value; + this.schema.exclusiveMinimum = value as Min; } else { - this.schema.minimum = value; + this.schema.minimum = value as Min; } - return this; + return this as never; } step = this.multipleOf; @@ -747,7 +837,7 @@ class NumberSchemaBuilder< * It may be set to any positive number. Same as `step`. * * **NOTE**: Since JSON schema odes not allow to use `multipleOf` with negative value - we use `Math.abs` to transform negative values into positive - * @see {@link NumberSchemaBuilder.step step} + * @see {@link step step} method * @example * const a = s.number().multipleOf(10) * @@ -767,52 +857,87 @@ class NumberSchemaBuilder< /** * marks you number maximum value */ - maximum(value: number, exclusive = false) { + maximum< + const Max extends number = number, + Exclusive extends boolean = false, + FormatValid = Opts['format'] extends undefined ? true : IsInteger extends true ? Opts['format'] extends 'int32' ? true : Opts['format'] extends 'int64' ? true : false : true, + TypeValid = Opts['type'] extends 'integer' ? IsInteger : + Or, IsInteger>, + MinLengthValid = Opts['minValue'] extends number ? LessThan : true, + >( + value: MinLengthValid extends true ? + TypeValid extends true ? + FormatValid extends true ? + Max + : TTypeGenericError<`Format invalid. Expected ${Opts['format']}. Got "${Max}" (${NumericStringifyType})`> + : TTypeGenericError<`Type invalid. Expected ${Opts['type']}. Got "${Max}" (${NumericStringifyType})`> + : TTypeGenericError<`"MinValue" greater than "MaxValue"`, ['MinValue:', Opts['minValue'], 'MaxValue:', Max]>, + exclusive = false as Exclusive + ): NumberSchemaBuilder< + N, Prettify & { + maxValue: Max, + } + >> { if (exclusive) { - this.schema.exclusiveMaximum = value; + this.schema.exclusiveMaximum = value as Max; } else { - this.schema.maximum = value; + this.schema.maximum = value as Max; } - return this; + return this as never; } /** * Greater than * - * @see {@link NumberSchemaBuilder.maximum} - * @see {@link NumberSchemaBuilder.gte} + * Range: `(value; Infinity)` + * @see {@link maximum} method + * @see {@link gte} method */ - gt(value: number) { - return this.minimum(value, true); + gt(value: V): NumberSchemaBuilder & { + minValue: V, + }>> { + return this.minimum(value as never, true); } /** * Greater than or equal * * Range: `[value; Infinity)` - * @see {@link NumberSchemaBuilder.maximum maximum} - * @see {@link NumberSchemaBuilder.gt gt} + * @see {@link maximum maximum} + * @see {@link gt gt} */ - gte(value: number) { - return this.minimum(value); + gte(value: V): NumberSchemaBuilder & { + minValue: V, + }>> { + return this.minimum(value as never); } + /** * Less than * * Range: `(value; Infinity)` - * @see {@link NumberSchemaBuilder.minimum minimum} - * @see {@link NumberSchemaBuilder.lte lte} + * @see {@link minimum minimum} method + * @see {@link lte lte} method */ - lt(value: number) { - return this.max(value, true); + lt(value: V): NumberSchemaBuilder< + N, Prettify & { + maxValue: V, + } + >> { + return this.max(value as never, true); } /** * Less than or Equal * * Range: `[value; Infinity)` - * @see {@link NumberSchemaBuilder.minimum} - * @see {@link NumberSchemaBuilder.lt} + * @see {@link minimum} method + * @see {@link lt} method */ - lte(value: number) { - return this.max(value); + lte(value: V): NumberSchemaBuilder & { + maxValue: V, + }>> { + return this.max(value as never); } /** Any positive number (greater than `0`) * Range: `(0; Infinity)` @@ -843,7 +968,7 @@ class NumberSchemaBuilder< } /** Marks incoming number between `MAX_SAFE_INTEGER` and `MIN_SAFE_INTEGER` */ safe() { - return this.lte(Number.MAX_SAFE_INTEGER).gte(Number.MIN_SAFE_INTEGER); + return this.lte(Number.MAX_SAFE_INTEGER as 9007199254740991).gte(Number.MIN_SAFE_INTEGER as -9007199254740991); } } /** @@ -869,13 +994,14 @@ function number() { * * **NOTE:** By default Ajv fails `{"type": "integer"}` validation for `Infinity` and `NaN`. */ -function integer() { - return new NumberSchemaBuilder().integer(); +function integer() { + return new NumberSchemaBuilder().integer(); } export type StringBuilderOpts = { minLength?: number maxLength?: number, + pattern?: string | RegExp } class StringSchemaBuilder< const S extends string = string, @@ -925,10 +1051,14 @@ class StringSchemaBuilder< * const str1 = prefixS.parse("qwe") // Error * const str2 = prefixS.parse("S_Some") // OK */ - pattern( - pattern: string, - ): StringSchemaBuilder { - this.schema.pattern = pattern; + pattern( + pattern: In, + ): StringSchemaBuilder> { + if (typeof pattern === 'string') { + this.schema.pattern = pattern; + } else if (pattern instanceof RegExp) { + this.schema.pattern = pattern.source + } return this as never; } @@ -944,18 +1074,18 @@ class StringSchemaBuilder< /** * Define minimum string length. * - * Same as `min` - * @see {@link StringSchemaBuilder.min min} + * Same as `min` method + * @see {@link min min} method */ minLength< const L extends number, Valid = IsPositiveInteger, - MinLengthValid extends boolean = GreaterThan + MinLengthValid = GreaterThan, >( value: Valid extends true - ? MinLengthValid extends true + ? Opts['maxLength'] extends undefined ? L - : TRangeGenericError<`MinLength are greater than MaxLength. MinLength: ${L}. MaxLength: ${Opts['maxLength']}`> + : MinLengthValid extends true ? L : TRangeGenericError<`MinLength are greater than MaxLength. MinLength: ${L}. MaxLength: ${Opts['maxLength']}`> : TTypeGenericError< `Only Positive and non floating numbers are supported. Received: '${L}'` > @@ -975,14 +1105,15 @@ class StringSchemaBuilder< * Define maximum string length. * * Same as `max` - * @see {@link StringSchemaBuilder.max max} + * @see {@link max max} method */ maxLength< const L extends number, Valid = IsPositiveInteger, - MinLengthValid = LessThan>( + MinLengthValid = GreaterThan>( value: Valid extends true - ? MinLengthValid extends true ? L + ? Opts['minLength'] extends undefined ? L + : MinLengthValid extends true ? L : TRangeGenericError<`MinLength are greater than MaxLength. MinLength: ${Opts['minLength']}. MaxLength: ${L}`> : TTypeGenericError<`Expected positive integer. Received: '${L}'`>, ): StringSchemaBuilder { diff --git a/src/types/array.d.ts b/src/types/array.d.ts index 3701703..be54e50 100644 --- a/src/types/array.d.ts +++ b/src/types/array.d.ts @@ -1,4 +1,4 @@ -import type { GreaterThan, GreaterThanOrEqual, LessThan, IsFloat, IsNegative, IsPositiveInteger, LessThanOrEqual } from './number'; +import type { GreaterThan, GreaterThanOrEqual, LessThan, IsFloat, IsPositiveInteger, LessThanOrEqual } from './number'; export type Create< L extends number, diff --git a/src/types/misc.d.ts b/src/types/misc.d.ts index e0b17b4..7bfa87c 100644 --- a/src/types/misc.d.ts +++ b/src/types/misc.d.ts @@ -55,3 +55,5 @@ export type Fn< export type Return> = F extends Fn ? Res : never export type Param = F extends Fn ? Args : never + +export type Debug = [Name, T] diff --git a/src/types/number.d.ts b/src/types/number.d.ts index ba1a24f..de4a627 100644 --- a/src/types/number.d.ts +++ b/src/types/number.d.ts @@ -1,5 +1,7 @@ +import { NumberSchema } from '../schema/types'; import { Create } from './array' import { Reverse } from './string'; +import { IsInteger, IsNegative } from 'type-fest' /** `T > U` */ export type GreaterThan = Create extends [...Create, ...infer _] ? false : true; @@ -23,22 +25,12 @@ export type IsFloat = N extends number : N extends `${number}.${number extends 0 ? '' : number}` ? true : false - -export type IsNegative = N extends number - ? IsNegative<`${N}`> - : N extends `-${number}` - ? true - : false - -export type IsPositiveInteger = IsFloat extends true - ? false - : IsNegative extends true - ? false - : true - +export type IsPositiveInteger = IsInteger extends true ? IsNegative extends false ? true : false : false export type Negative = `${N}` extends `-${infer V extends number}` ? N : V export type IsNumberSubset = GreaterThanOrEqual extends false ? LessThanOrEqual extends false ? true : [false, 'less than '] : [false, 'greater than'] + +export type NumericStringifyType = IsFloat extends true ? "Float" : IsInteger extends true ? "Int" : "Unknown" \ No newline at end of file diff --git a/tests/array.test.ts b/tests/array.test.ts index ce77336..935743f 100644 --- a/tests/array.test.ts +++ b/tests/array.test.ts @@ -10,13 +10,12 @@ const intNum = s.array(s.string()).nonEmpty(); const nonEmptyMax = s.array(s.string()).nonEmpty().maxLength(2); const nonEmpty = s.array(s.string()).nonEmpty(); - test('types', () => { type t0 = s.infer assertType([]) type t1 = s.infer; - type A= typeof nonEmptyMax + type A = typeof nonEmptyMax assertType(['string', 'sd']); diff --git a/tests/number.test.ts b/tests/number.test.ts index faa8cc2..1ac393d 100644 --- a/tests/number.test.ts +++ b/tests/number.test.ts @@ -122,3 +122,29 @@ test('integer should supports only integers', () => { expect(schema.validate(400)).toBe(false) expect(schema.validate(12.4)).toBe(false) }) + +test('incompatible format should fail type', () => { + // @ts-expect-error should fails + const schema1 = s.int().format('double') + // @ts-expect-error should fails + const schema2 = s.int().format('float') + // @ts-expect-error should fails + const schema3 = s.int().const(3.4) + + // @ts-expect-error should fails + const schema4 = s.int().max(3.4) + // @ts-expect-error should fails + const schema5 = s.int().min(3.4) + // @ts-expect-error should fails + const schema6 = s.int().const(3.4) +}) + +test('ranges should fails for out of range', () => { + // @ts-expect-error should fails + s.int().min(1).max(3).const(-1) + + // @ts-expect-error should fails + s.int().min(5).max(3) + // @ts-expect-error should fails + s.int().max(2).min(3) +}) diff --git a/tests/string.test.ts b/tests/string.test.ts index 416b5ca..2dd866b 100644 --- a/tests/string.test.ts +++ b/tests/string.test.ts @@ -22,3 +22,10 @@ test('should pass validation', () => { optionalStr.parse(null) expect(() => optionalStr.parse(undefined)).toThrow() }) + +test.todo('pattern should be applied for RegExp instance', () => { + const etalon = s.string().pattern('^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$') + const withRegExp = s.string().pattern(new RegExp('^(\\([0-9]{3}\\))?[0-9]{3}-[0-9]{4}$')) + + etalon.parse() +}) diff --git a/tsconfig.json b/tsconfig.json index 015837b..4d11d12 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -16,9 +16,6 @@ "allowSyntheticDefaultImports": true, "forceConsistentCasingInFileNames": true, "allowJs": true, - "types": [ - "bun-types" // add Bun global - ] }, "include": [ "src", diff --git a/tsup.config.mts b/tsup.config.mts new file mode 100644 index 0000000..dcd43a7 --- /dev/null +++ b/tsup.config.mts @@ -0,0 +1,37 @@ +import { build, Options } from 'tsup' + +const common: Options = { + entry: ['src/index.ts'], + format: ['cjs', 'esm'], + external: [], + splitting: true, + cjsInterop: true, + dts: true, + target: ['node18'], + shims: true, + tsconfig: './tsconfig.json', + +} +// minify +await build({ + ...common, + clean: true, + minify: true, + minifySyntax: true, + minifyWhitespace: true, + minifyIdentifiers: true, + outExtension({ format }) { + return { + js: format === 'cjs' ? '.min.cjs' : format === 'esm' ? `.min.mjs` : '.min.js', + } + }, +}) + +await build({ + ...common, + outExtension({ format }) { + return { + js: format === 'cjs' ? '.cjs' : format === 'esm' ? `.mjs` : '.js', + } + }, +}) diff --git a/tsup.config.ts b/tsup.config.ts deleted file mode 100644 index aa3d29d..0000000 --- a/tsup.config.ts +++ /dev/null @@ -1,19 +0,0 @@ -import { defineConfig } from 'tsup' - -export default defineConfig(() => ({ - entry: ['src/index.ts'], - format: ['cjs', 'esm'], - external: [], - splitting: false, - clean: true, - cjsInterop: true, - dts: true, - target: ['node18'], - shims: true, - tsconfig: './tsconfig.json', - outExtension({ format }) { - return { - js: format === 'cjs' ? '.cjs' : format === 'esm' ? `.mjs` : '.js', - } - }, -})); \ No newline at end of file