├── .eslintrc ├── .github └── workflows │ └── node.js.yml ├── .gitignore ├── README.md ├── biome.json ├── index.js ├── jsonschema.js ├── package.json ├── pnpm-lock.yaml └── test ├── jsonschema.nullable.test.js ├── jsonschema.test.js ├── rfc8927.nullable.test.js └── rfc8927.test.js /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "standard" 3 | } 4 | -------------------------------------------------------------------------------- /.github/workflows/node.js.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Node.js CI 5 | 6 | on: 7 | push: 8 | branches: [ main ] 9 | pull_request: 10 | branches: [ main ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: ubuntu-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [16.x, 17.x, 18.x, 19.x, 20.x] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: npm install 29 | - run: npm test 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .nyc_output 2 | node_modules 3 | 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # jsontypedef 2 | 3 | Syntactic sugar for creating **JSON Type Definition** ([RFC 8927](https://jsontypedef.com/)). 4 | 5 | All types from [Learn JSON Typedef in 5 Minutes](https://jsontypedef.com/docs/jtd-in-5-minutes/) are covered, preserving original naming with a few exceptions: 6 | 7 | - `enum` is created via `values()`, _because `enum` is a reserved keyword_ 8 | - `elements` is created via `array()`, _for brevity_ 9 | - `properties` is called `object()`, _for brevity_ 10 | - `discriminator` is called `match()`, _for brevity_ 11 | 12 | ## Install 13 | 14 | ```bash 15 | npm install jsontypedef 16 | ``` 17 | 18 | ## Example 19 | 20 | Write: 21 | 22 | ```js 23 | import { string, number, object } from 'jsontypedef' 24 | 25 | console.log(object({ 26 | propertyA: string(), 27 | propertyB: object({ 28 | innerPropertyC: number(), 29 | }) 30 | })) 31 | ``` 32 | 33 | Get: 34 | 35 | ```js 36 | { 37 | properties: { 38 | propertyA: { type: 'string' }, 39 | propertyB: { 40 | properties: { 41 | innerPropertyC: { type: 'float64' } 42 | } 43 | } 44 | } 45 | } 46 | ``` 47 | 48 | Saves you a good deal of typing maintaining big type definitions. 49 | 50 | See all examples [in the test suite](https://github.com/galvez/jsontypedef/blob/main/test.js). 51 | 52 | ## API: Basic Types 53 | 54 | - `empty(metadata)` 55 | - `boolean(metadata)` 56 | - `string(metadata)` 57 | - `timestamp(metadata)` 58 | - `float64(metadata)` (JavaScript numbers) 59 | - `number(metadata)` # alias to float64() 60 | - `integer(metadata)` # alias to float64() 61 | 62 | ### API: Specialized Numeric Types 63 | 64 | For compatibility with other languages and full JTD support: 65 | 66 | - `float32(metadata)` 67 | - `int8(metadata)` 68 | - `uint8(metadata)` 69 | - `int16(metadata)` 70 | - `uint16(metadata)` 71 | - `int32(metadata)` 72 | - `uint32(metadata)` 73 | 74 | ## API: Advanced Types 75 | 76 | - `values(items, metadata)` is an alias to create the `enum` JTD form. 77 | - `enum` is a reserved word in JavaScript. 78 | - `array(type, metadata)` creates the `elements` JTD form 79 | - `object(props, optional, additional)` creates the `properties` JTD form 80 | - `match(field, mapping, metadata)` creates the `discriminator` JTD form 81 | 82 | ## API: Nullable Types 83 | 84 | A `nullable` helper is provided for easily creating nullable rules. 85 | 86 | ```js 87 | const { nullable } = require('jsontypedef') 88 | ``` 89 | 90 | - `nullable.empty(metadata)` 91 | - `nullable.boolean(metadata)` 92 | - `nullable.string(metadata)` 93 | - `nullable.timestamp(metadata)` 94 | - `nullable.float64(metadata)` (JavaScript numbers) 95 | - `nullable.number(metadata)` # alias to float64() 96 | - `nullable.integer(metadata)` # alias to float64() 97 | - `nullable.float32(metadata)` 98 | - `nullable.int8(metadata)` 99 | - `nullable.uint8(metadata)` 100 | - `nullable.int16(metadata)` 101 | - `nullable.uint16(metadata)` 102 | - `nullable.int32(metadata)` 103 | - `nullable.uint32(metadata)` 104 | - `nullable.values(items, metadata)` is an alias to create the `enum` JTD form. 105 | - `enum` is a reserved word in JavaScript. 106 | - `nullable.array(type, metadata)` creates the `elements` JTD form 107 | - `nullable.object(props, optional, additional)` creates the `properties` JTD form 108 | 109 | 110 | ## JSON Schema compatibility 111 | 112 | Functionality-wise, [JSON Type Definition](https://jsontypedef.com/) is a subset of [JSON Schema](https://json-schema.org/), with one exception: JSON Schema doesn't support tagged unions (the `discriminator` JTD form). There are no data constraints, so you can't say a string is supposed to have a length of 5, you can only say a string is a string. 113 | 114 | Fastify uses `ajv` for validation and serialization, but only supports JSON Schema out of the box, although its `addSchema()` method could be reworked to recognize JTD in the future. Although `ajv` itself [already supports JSON Type Definition](https://github.com/ajv-validator/ajv/blob/master/docs/json-type-definition.md), it might take a while for the support to come to Fastify. 115 | 116 | That is not a problem because we can easily translate JTD schemas to JSON Schema with the `jsonschema` submodule provided by this library. which offers all methods from the main API but will output JSON Schema compatible schemas. So you can use it with Fastify: 117 | 118 | ```js 119 | import Fastify from 'fastify' 120 | import { object, string } from 'jsontypedef/jsonschema' 121 | 122 | const schema = { 123 | headers: object({ 124 | 'x-foo': string() 125 | }), 126 | } 127 | 128 | fastify.post('/the/url', { schema }, (req, reply) => { 129 | reply.send('ok') 130 | }) 131 | ``` 132 | 133 | The `jsonschema` submodule also includes `number()` and `integer()` for convenience. 134 | 135 | See the [**full test suite**](https://github.com/galvez/jsontypedef/tree/main/test) for more usage examples. 136 | 137 | -------------------------------------------------------------------------------- /biome.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "ignore": ["dist"] 4 | }, 5 | "javascript": { 6 | "formatter": { 7 | "indentStyle": "space", 8 | "semicolons": "asNeeded", 9 | "quoteStyle": "single" 10 | } 11 | }, 12 | "json": { 13 | "formatter": { 14 | "indentStyle": "space" 15 | } 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | export const empty = (metadata) => ({ 2 | ...(metadata && { metadata }), 3 | }) 4 | 5 | export const boolean = (metadata) => ({ 6 | type: 'boolean', 7 | ...(metadata && { metadata }), 8 | }) 9 | 10 | export const string = (metadata) => ({ 11 | type: 'string', 12 | ...(metadata && { metadata }), 13 | }) 14 | 15 | export const timestamp = (metadata) => ({ 16 | type: 'timestamp', 17 | ...(metadata && { metadata }), 18 | }) 19 | 20 | export const number = (metadata) => ({ 21 | type: 'float64', 22 | ...(metadata && { metadata }), 23 | }) 24 | 25 | export const integer = (metadata) => ({ 26 | type: 'float64', 27 | ...(metadata && { metadata }), 28 | }) 29 | 30 | export const float32 = (metadata) => ({ 31 | type: 'float32', 32 | ...(metadata && { metadata }), 33 | }) 34 | 35 | export const float64 = (metadata) => ({ 36 | type: 'float64', 37 | ...(metadata && { metadata }), 38 | }) 39 | 40 | export const int8 = (metadata) => ({ 41 | type: 'int8', 42 | ...(metadata && { metadata }), 43 | }) 44 | 45 | export const uint8 = (metadata) => ({ 46 | type: 'uint8', 47 | ...(metadata && { metadata }), 48 | }) 49 | 50 | export const int16 = (metadata) => ({ 51 | type: 'int16', 52 | ...(metadata && { metadata }), 53 | }) 54 | 55 | export const uint16 = (metadata) => ({ 56 | type: 'uint16', 57 | ...(metadata && { metadata }), 58 | }) 59 | 60 | export const int32 = (metadata) => ({ 61 | type: 'int32', 62 | ...(metadata && { metadata }), 63 | }) 64 | 65 | export const uint32 = (metadata) => ({ 66 | type: 'uint32', 67 | ...(metadata && { metadata }), 68 | }) 69 | 70 | export const values = (items, metadata) => ({ 71 | enum: items, 72 | ...(metadata && { metadata }), 73 | }) 74 | 75 | export const array = (type, metadata) => ({ 76 | elements: type, 77 | ...(metadata && { metadata }), 78 | }) 79 | 80 | export const object = (props, optional, metadata) => ({ 81 | additionalProperties: true, 82 | ...(props && { properties: props }), 83 | ...(optional && { optionalProperties: optional }), 84 | ...(metadata && { metadata }), 85 | }) 86 | 87 | export const sealed = (props, optional, metadata) => ({ 88 | additionalProperties: false, 89 | ...(props && { properties: props }), 90 | ...(optional && { optionalProperties: optional }), 91 | ...(metadata && { metadata }), 92 | }) 93 | 94 | export const match = (field, mapping, metadata) => ({ 95 | discriminator: field, 96 | mapping, 97 | ...(metadata && { metadata }), 98 | }) 99 | 100 | export const nullable = { 101 | boolean: (metadata) => ({ 102 | nullable: true, 103 | type: 'boolean', 104 | ...(metadata && { metadata }), 105 | }), 106 | string: (metadata) => ({ 107 | nullable: true, 108 | type: 'string', 109 | ...(metadata && { metadata }), 110 | }), 111 | timestamp: (metadata) => ({ 112 | nullable: true, 113 | type: 'timestamp', 114 | ...(metadata && { metadata }), 115 | }), 116 | number: (metadata) => ({ 117 | nullable: true, 118 | type: 'float64', 119 | ...(metadata && { metadata }), 120 | }), 121 | integer: (metadata) => ({ 122 | nullable: true, 123 | type: 'float64', 124 | ...(metadata && { metadata }), 125 | }), 126 | float32: (metadata) => ({ 127 | nullable: true, 128 | type: 'float32', 129 | ...(metadata && { metadata }), 130 | }), 131 | float64: (metadata) => ({ 132 | nullable: true, 133 | type: 'float64', 134 | ...(metadata && { metadata }), 135 | }), 136 | int8: (metadata) => ({ 137 | nullable: true, 138 | type: 'int8', 139 | ...(metadata && { metadata }), 140 | }), 141 | uint8: (metadata) => ({ 142 | nullable: true, 143 | type: 'uint8', 144 | ...(metadata && { metadata }), 145 | }), 146 | int16: (metadata) => ({ 147 | nullable: true, 148 | type: 'int16', 149 | ...(metadata && { metadata }), 150 | }), 151 | uint16: (metadata) => ({ 152 | nullable: true, 153 | type: 'uint16', 154 | ...(metadata && { metadata }), 155 | }), 156 | int32: (metadata) => ({ 157 | nullable: true, 158 | type: 'int32', 159 | ...(metadata && { metadata }), 160 | }), 161 | uint32: (metadata) => ({ 162 | nullable: true, 163 | type: 'uint32', 164 | ...(metadata && { metadata }), 165 | }), 166 | values: (items, metadata) => ({ 167 | nullable: true, 168 | enum: items, 169 | ...(metadata && { metadata }), 170 | }), 171 | array: (type, metadata) => ({ 172 | nullable: true, 173 | elements: type, 174 | ...(metadata && { metadata }), 175 | }), 176 | object: (props, optional, metadata) => ({ 177 | nullable: true, 178 | additionalProperties: true, 179 | ...(props && { properties: props }), 180 | ...(optional && { optionalProperties: optional }), 181 | ...(metadata && { metadata }), 182 | }), 183 | sealed: (props, optional, metadata) => ({ 184 | nullable: true, 185 | additionalProperties: false, 186 | ...(props && { properties: props }), 187 | ...(optional && { optionalProperties: optional }), 188 | ...(metadata && { metadata }), 189 | }), 190 | } 191 | 192 | export default { 193 | empty, 194 | boolean, 195 | string, 196 | timestamp, 197 | number, 198 | integer, 199 | float32, 200 | float64, 201 | int8, 202 | uint8, 203 | int16, 204 | uint16, 205 | int32, 206 | uint32, 207 | values, 208 | array, 209 | object, 210 | sealed, 211 | match, 212 | nullable, 213 | } 214 | -------------------------------------------------------------------------------- /jsonschema.js: -------------------------------------------------------------------------------- 1 | export const empty = (metadata) => ({ 2 | ...(metadata && { metadata }), 3 | }) 4 | 5 | export const boolean = (metadata) => ({ 6 | type: 'boolean', 7 | ...(metadata && { metadata }), 8 | }) 9 | 10 | export const string = (metadata) => ({ 11 | type: 'string', 12 | ...(metadata && { metadata }), 13 | }) 14 | 15 | export const timestamp = (metadata) => ({ 16 | type: 'string', 17 | format: 'date-time', 18 | ...(metadata && { metadata }), 19 | }) 20 | 21 | export const number = (metadata) => ({ 22 | type: 'number', 23 | ...(metadata && { metadata }), 24 | }) 25 | 26 | // Added for convenience 27 | export const integer = (metadata) => ({ 28 | type: 'integer', 29 | ...(metadata && { metadata }), 30 | }) 31 | 32 | export const float32 = (metadata) => ({ 33 | type: 'number', 34 | ...(metadata && { metadata }), 35 | }) 36 | 37 | export const float64 = (metadata) => ({ 38 | type: 'number', 39 | ...(metadata && { metadata }), 40 | }) 41 | 42 | export const int8 = (metadata) => ({ 43 | type: 'integer', 44 | ...(metadata && { metadata }), 45 | }) 46 | 47 | export const uint8 = (metadata) => ({ 48 | type: 'integer', 49 | ...(metadata && { metadata }), 50 | }) 51 | 52 | export const int16 = (metadata) => ({ 53 | type: 'integer', 54 | ...(metadata && { metadata }), 55 | }) 56 | 57 | export const uint16 = (metadata) => ({ 58 | type: 'integer', 59 | ...(metadata && { metadata }), 60 | }) 61 | 62 | export const int32 = (metadata) => ({ 63 | type: 'integer', 64 | ...(metadata && { metadata }), 65 | }) 66 | 67 | export const uint32 = (metadata) => ({ 68 | type: 'integer', 69 | ...(metadata && { metadata }), 70 | }) 71 | 72 | export const values = (items, metadata) => ({ 73 | type: 'string', 74 | enum: items, 75 | ...(metadata && { metadata }), 76 | }) 77 | export const array = (type, metadata) => ({ 78 | type: 'array', 79 | items: type, 80 | ...(metadata && { metadata }), 81 | }) 82 | 83 | export const object = (props, optional, metadata) => { 84 | const allProperties = Object.assign({}, props || {}, optional || {}) 85 | const requiredKeys = Object.keys(Object.assign(props || {})) 86 | return { 87 | type: 'object', 88 | additionalProperties: true, 89 | ...(Object.keys(allProperties).length && { properties: allProperties }), 90 | ...(requiredKeys.length && { required: requiredKeys }), 91 | ...(metadata && { metadata }), 92 | } 93 | } 94 | 95 | export const sealed = (props, optional, metadata) => { 96 | const allProperties = Object.assign({}, props || {}, optional || {}) 97 | const requiredKeys = Object.keys(Object.assign(props || {})) 98 | return { 99 | type: 'object', 100 | additionalProperties: false, 101 | ...(Object.keys(allProperties).length && { properties: allProperties }), 102 | ...(requiredKeys.length && { required: requiredKeys }), 103 | ...(metadata && { metadata }), 104 | } 105 | } 106 | 107 | export const nullable = { 108 | boolean: (metadata) => ({ 109 | type: ['boolean', 'null'], 110 | ...(metadata && { metadata }), 111 | }), 112 | string: (metadata) => ({ 113 | type: ['string', 'null'], 114 | ...(metadata && { metadata }), 115 | }), 116 | timestamp: (metadata) => ({ 117 | type: ['string', 'null'], 118 | format: 'date-time', 119 | ...(metadata && { metadata }), 120 | }), 121 | number: (metadata) => ({ 122 | type: ['number', 'null'], 123 | ...(metadata && { metadata }), 124 | }), 125 | float32: (metadata) => ({ 126 | type: ['number', 'null'], 127 | ...(metadata && { metadata }), 128 | }), 129 | float64: (metadata) => ({ 130 | type: ['number', 'null'], 131 | ...(metadata && { metadata }), 132 | }), 133 | // Added for convenience 134 | integer: (metadata) => ({ 135 | type: ['integer', 'null'], 136 | ...(metadata && { metadata }), 137 | }), 138 | int8: (metadata) => ({ 139 | type: ['integer', 'null'], 140 | ...(metadata && { metadata }), 141 | }), 142 | uint8: (metadata) => ({ 143 | type: ['integer', 'null'], 144 | ...(metadata && { metadata }), 145 | }), 146 | int16: (metadata) => ({ 147 | type: ['integer', 'null'], 148 | ...(metadata && { metadata }), 149 | }), 150 | uint16: (metadata) => ({ 151 | type: ['integer', 'null'], 152 | ...(metadata && { metadata }), 153 | }), 154 | int32: (metadata) => ({ 155 | type: ['integer', 'null'], 156 | ...(metadata && { metadata }), 157 | }), 158 | uint32: (metadata) => ({ 159 | type: ['integer', 'null'], 160 | ...(metadata && { metadata }), 161 | }), 162 | values: (items, metadata) => ({ 163 | type: ['string', 'null'], 164 | enum: items, 165 | ...(metadata && { metadata }), 166 | }), 167 | array: (type, metadata) => ({ 168 | type: ['array', 'null'], 169 | items: type, 170 | ...(metadata && { metadata }), 171 | }), 172 | object: (props, optional, metadata) => { 173 | const allProperties = Object.assign({}, props || {}, optional || {}) 174 | const requiredKeys = Object.keys(Object.assign(props || {})) 175 | return { 176 | type: ['object', 'null'], 177 | additionalProperties: true, 178 | ...(Object.keys(allProperties).length && { properties: allProperties }), 179 | ...(requiredKeys.length && { required: requiredKeys }), 180 | ...(metadata && { metadata }), 181 | } 182 | }, 183 | sealed: (props, optional, metadata) => { 184 | const allProperties = Object.assign({}, props || {}, optional || {}) 185 | const requiredKeys = Object.keys(Object.assign(props || {})) 186 | return { 187 | type: ['object', 'null'], 188 | additionalProperties: false, 189 | ...(Object.keys(allProperties).length && { properties: allProperties }), 190 | ...(requiredKeys.length && { required: requiredKeys }), 191 | ...(metadata && { metadata }), 192 | } 193 | }, 194 | } 195 | 196 | export default { 197 | empty, 198 | boolean, 199 | string, 200 | timestamp, 201 | number, 202 | integer, 203 | float32, 204 | float64, 205 | int8, 206 | uint8, 207 | int16, 208 | uint16, 209 | int32, 210 | uint32, 211 | values, 212 | array, 213 | object, 214 | sealed, 215 | nullable, 216 | } 217 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "exports": { 3 | ".": "./index.js", 4 | "./jsonschema": "./jsonschema.js" 5 | }, 6 | "scripts": { 7 | "lint": "biome check --apply .", 8 | "test": "vitest run --bail 1" 9 | }, 10 | "bugs": { 11 | "url": "https://github.com/galvez/jsontypedef/issues" 12 | }, 13 | "bundleDependencies": false, 14 | "deprecated": false, 15 | "description": "Syntactic sugar for creating JSON Type Definition (JTD/RFC8927) schemas, and generating compatible JSON Schema from JTD schemas", 16 | "files": ["index.js", "jsonschema.js"], 17 | "homepage": "https://github.com/galvez/jsontypedef", 18 | "license": "MIT", 19 | "main": "index.js", 20 | "type": "module", 21 | "name": "jsontypedef", 22 | "repository": { 23 | "type": "git", 24 | "url": "git+https://github.com/galvez/jsontypedef.git" 25 | }, 26 | "version": "2.0.1", 27 | "devDependencies": { 28 | "vitest": "^0.33.0", 29 | "@biomejs/biome": "^1.6.1" 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /pnpm-lock.yaml: -------------------------------------------------------------------------------- 1 | lockfileVersion: '6.0' 2 | 3 | settings: 4 | autoInstallPeers: true 5 | excludeLinksFromLockfile: false 6 | 7 | devDependencies: 8 | '@biomejs/biome': 9 | specifier: ^1.6.1 10 | version: 1.6.1 11 | vitest: 12 | specifier: ^0.33.0 13 | version: 0.33.0 14 | 15 | packages: 16 | 17 | /@biomejs/biome@1.6.1: 18 | resolution: {integrity: sha512-SILQvA2S0XeaOuu1bivv6fQmMo7zMfr2xqDEN+Sz78pGbAKZnGmg0emsXjQWoBY/RVm9kPCgX+aGEpZZTYaM7w==} 19 | engines: {node: '>=14.*'} 20 | hasBin: true 21 | requiresBuild: true 22 | optionalDependencies: 23 | '@biomejs/cli-darwin-arm64': 1.6.1 24 | '@biomejs/cli-darwin-x64': 1.6.1 25 | '@biomejs/cli-linux-arm64': 1.6.1 26 | '@biomejs/cli-linux-arm64-musl': 1.6.1 27 | '@biomejs/cli-linux-x64': 1.6.1 28 | '@biomejs/cli-linux-x64-musl': 1.6.1 29 | '@biomejs/cli-win32-arm64': 1.6.1 30 | '@biomejs/cli-win32-x64': 1.6.1 31 | dev: true 32 | 33 | /@biomejs/cli-darwin-arm64@1.6.1: 34 | resolution: {integrity: sha512-KlvY00iB9T/vFi4m/GXxEyYkYnYy6aw06uapzUIIdiMMj7I/pmZu7CsZlzWdekVD0j+SsQbxdZMsb0wPhnRSsg==} 35 | engines: {node: '>=14.*'} 36 | cpu: [arm64] 37 | os: [darwin] 38 | requiresBuild: true 39 | dev: true 40 | optional: true 41 | 42 | /@biomejs/cli-darwin-x64@1.6.1: 43 | resolution: {integrity: sha512-jP4E8TXaQX5e3nvRJSzB+qicZrdIDCrjR0sSb1DaDTx4JPZH5WXq/BlTqAyWi3IijM+IYMjWqAAK4kOHsSCzxw==} 44 | engines: {node: '>=14.*'} 45 | cpu: [x64] 46 | os: [darwin] 47 | requiresBuild: true 48 | dev: true 49 | optional: true 50 | 51 | /@biomejs/cli-linux-arm64-musl@1.6.1: 52 | resolution: {integrity: sha512-YdkDgFecdHJg7PJxAMaZIixVWGB6St4yH08BHagO0fEhNNiY8cAKEVo2mcXlsnEiTMpeSEAY9VxLUrVT3IVxpw==} 53 | engines: {node: '>=14.*'} 54 | cpu: [arm64] 55 | os: [linux] 56 | requiresBuild: true 57 | dev: true 58 | optional: true 59 | 60 | /@biomejs/cli-linux-arm64@1.6.1: 61 | resolution: {integrity: sha512-nxD1UyX3bWSl/RSKlib/JsOmt+652/9yieogdSC/UTLgVCZYOF7u8L/LK7kAa0Y4nA8zSPavAQTgko7mHC2ObA==} 62 | engines: {node: '>=14.*'} 63 | cpu: [arm64] 64 | os: [linux] 65 | requiresBuild: true 66 | dev: true 67 | optional: true 68 | 69 | /@biomejs/cli-linux-x64-musl@1.6.1: 70 | resolution: {integrity: sha512-aSISIDmxq04NNy7tm4x9rBk2vH0ub2VDIE4outEmdC2LBtEJoINiphlZagx/FvjbsqUfygent9QUSn0oREnAXg==} 71 | engines: {node: '>=14.*'} 72 | cpu: [x64] 73 | os: [linux] 74 | requiresBuild: true 75 | dev: true 76 | optional: true 77 | 78 | /@biomejs/cli-linux-x64@1.6.1: 79 | resolution: {integrity: sha512-BYAzenlMF3QdngjNFw9QVBXKGNzeecqwF3pwDgUGEvU7OJpn1/lyVkJVxYPtVGRNdjQ9e6l/s8NjKuBpW/ZR4Q==} 80 | engines: {node: '>=14.*'} 81 | cpu: [x64] 82 | os: [linux] 83 | requiresBuild: true 84 | dev: true 85 | optional: true 86 | 87 | /@biomejs/cli-win32-arm64@1.6.1: 88 | resolution: {integrity: sha512-/eCHQKZ1kEawUpkSuXq4urtxMsD1P1678OPG3zNKt3ru16AqqspLdO3jzBe3k74xCPYnQ36e9Yqc97Mo0qgPtg==} 89 | engines: {node: '>=14.*'} 90 | cpu: [arm64] 91 | os: [win32] 92 | requiresBuild: true 93 | dev: true 94 | optional: true 95 | 96 | /@biomejs/cli-win32-x64@1.6.1: 97 | resolution: {integrity: sha512-5TUZbzBwnDLFxLVGEPsorNi6eC2Gt+z4Oei9Qvq0M/4c4/mjZ96ABgwao/tMxf4ZBr/qyy2YdvF+gX9Rc+xC0A==} 98 | engines: {node: '>=14.*'} 99 | cpu: [x64] 100 | os: [win32] 101 | requiresBuild: true 102 | dev: true 103 | optional: true 104 | 105 | /@esbuild/android-arm64@0.18.13: 106 | resolution: {integrity: sha512-j7NhycJUoUAG5kAzGf4fPWfd17N6SM3o1X6MlXVqfHvs2buFraCJzos9vbeWjLxOyBKHyPOnuCuipbhvbYtTAg==} 107 | engines: {node: '>=12'} 108 | cpu: [arm64] 109 | os: [android] 110 | requiresBuild: true 111 | dev: true 112 | optional: true 113 | 114 | /@esbuild/android-arm@0.18.13: 115 | resolution: {integrity: sha512-KwqFhxRFMKZINHzCqf8eKxE0XqWlAVPRxwy6rc7CbVFxzUWB2sA/s3hbMZeemPdhN3fKBkqOaFhTbS8xJXYIWQ==} 116 | engines: {node: '>=12'} 117 | cpu: [arm] 118 | os: [android] 119 | requiresBuild: true 120 | dev: true 121 | optional: true 122 | 123 | /@esbuild/android-x64@0.18.13: 124 | resolution: {integrity: sha512-M2eZkRxR6WnWfVELHmv6MUoHbOqnzoTVSIxgtsyhm/NsgmL+uTmag/VVzdXvmahak1I6sOb1K/2movco5ikDJg==} 125 | engines: {node: '>=12'} 126 | cpu: [x64] 127 | os: [android] 128 | requiresBuild: true 129 | dev: true 130 | optional: true 131 | 132 | /@esbuild/darwin-arm64@0.18.13: 133 | resolution: {integrity: sha512-f5goG30YgR1GU+fxtaBRdSW3SBG9pZW834Mmhxa6terzcboz7P2R0k4lDxlkP7NYRIIdBbWp+VgwQbmMH4yV7w==} 134 | engines: {node: '>=12'} 135 | cpu: [arm64] 136 | os: [darwin] 137 | requiresBuild: true 138 | dev: true 139 | optional: true 140 | 141 | /@esbuild/darwin-x64@0.18.13: 142 | resolution: {integrity: sha512-RIrxoKH5Eo+yE5BtaAIMZaiKutPhZjw+j0OCh8WdvKEKJQteacq0myZvBDLU+hOzQOZWJeDnuQ2xgSScKf1Ovw==} 143 | engines: {node: '>=12'} 144 | cpu: [x64] 145 | os: [darwin] 146 | requiresBuild: true 147 | dev: true 148 | optional: true 149 | 150 | /@esbuild/freebsd-arm64@0.18.13: 151 | resolution: {integrity: sha512-AfRPhHWmj9jGyLgW/2FkYERKmYR+IjYxf2rtSLmhOrPGFh0KCETFzSjx/JX/HJnvIqHt/DRQD/KAaVsUKoI3Xg==} 152 | engines: {node: '>=12'} 153 | cpu: [arm64] 154 | os: [freebsd] 155 | requiresBuild: true 156 | dev: true 157 | optional: true 158 | 159 | /@esbuild/freebsd-x64@0.18.13: 160 | resolution: {integrity: sha512-pGzWWZJBInhIgdEwzn8VHUBang8UvFKsvjDkeJ2oyY5gZtAM6BaxK0QLCuZY+qoj/nx/lIaItH425rm/hloETA==} 161 | engines: {node: '>=12'} 162 | cpu: [x64] 163 | os: [freebsd] 164 | requiresBuild: true 165 | dev: true 166 | optional: true 167 | 168 | /@esbuild/linux-arm64@0.18.13: 169 | resolution: {integrity: sha512-hCzZbVJEHV7QM77fHPv2qgBcWxgglGFGCxk6KfQx6PsVIdi1u09X7IvgE9QKqm38OpkzaAkPnnPqwRsltvLkIQ==} 170 | engines: {node: '>=12'} 171 | cpu: [arm64] 172 | os: [linux] 173 | requiresBuild: true 174 | dev: true 175 | optional: true 176 | 177 | /@esbuild/linux-arm@0.18.13: 178 | resolution: {integrity: sha512-4iMxLRMCxGyk7lEvkkvrxw4aJeC93YIIrfbBlUJ062kilUUnAiMb81eEkVvCVoh3ON283ans7+OQkuy1uHW+Hw==} 179 | engines: {node: '>=12'} 180 | cpu: [arm] 181 | os: [linux] 182 | requiresBuild: true 183 | dev: true 184 | optional: true 185 | 186 | /@esbuild/linux-ia32@0.18.13: 187 | resolution: {integrity: sha512-I3OKGbynl3AAIO6onXNrup/ttToE6Rv2XYfFgLK/wnr2J+1g+7k4asLrE+n7VMhaqX+BUnyWkCu27rl+62Adug==} 188 | engines: {node: '>=12'} 189 | cpu: [ia32] 190 | os: [linux] 191 | requiresBuild: true 192 | dev: true 193 | optional: true 194 | 195 | /@esbuild/linux-loong64@0.18.13: 196 | resolution: {integrity: sha512-8pcKDApAsKc6WW51ZEVidSGwGbebYw2qKnO1VyD8xd6JN0RN6EUXfhXmDk9Vc4/U3Y4AoFTexQewQDJGsBXBpg==} 197 | engines: {node: '>=12'} 198 | cpu: [loong64] 199 | os: [linux] 200 | requiresBuild: true 201 | dev: true 202 | optional: true 203 | 204 | /@esbuild/linux-mips64el@0.18.13: 205 | resolution: {integrity: sha512-6GU+J1PLiVqWx8yoCK4Z0GnfKyCGIH5L2KQipxOtbNPBs+qNDcMJr9euxnyJ6FkRPyMwaSkjejzPSISD9hb+gg==} 206 | engines: {node: '>=12'} 207 | cpu: [mips64el] 208 | os: [linux] 209 | requiresBuild: true 210 | dev: true 211 | optional: true 212 | 213 | /@esbuild/linux-ppc64@0.18.13: 214 | resolution: {integrity: sha512-pfn/OGZ8tyR8YCV7MlLl5hAit2cmS+j/ZZg9DdH0uxdCoJpV7+5DbuXrR+es4ayRVKIcfS9TTMCs60vqQDmh+w==} 215 | engines: {node: '>=12'} 216 | cpu: [ppc64] 217 | os: [linux] 218 | requiresBuild: true 219 | dev: true 220 | optional: true 221 | 222 | /@esbuild/linux-riscv64@0.18.13: 223 | resolution: {integrity: sha512-aIbhU3LPg0lOSCfVeGHbmGYIqOtW6+yzO+Nfv57YblEK01oj0mFMtvDJlOaeAZ6z0FZ9D13oahi5aIl9JFphGg==} 224 | engines: {node: '>=12'} 225 | cpu: [riscv64] 226 | os: [linux] 227 | requiresBuild: true 228 | dev: true 229 | optional: true 230 | 231 | /@esbuild/linux-s390x@0.18.13: 232 | resolution: {integrity: sha512-Pct1QwF2sp+5LVi4Iu5Y+6JsGaV2Z2vm4O9Dd7XZ5tKYxEHjFtb140fiMcl5HM1iuv6xXO8O1Vrb1iJxHlv8UA==} 233 | engines: {node: '>=12'} 234 | cpu: [s390x] 235 | os: [linux] 236 | requiresBuild: true 237 | dev: true 238 | optional: true 239 | 240 | /@esbuild/linux-x64@0.18.13: 241 | resolution: {integrity: sha512-zTrIP0KzYP7O0+3ZnmzvUKgGtUvf4+piY8PIO3V8/GfmVd3ZyHJGz7Ht0np3P1wz+I8qJ4rjwJKqqEAbIEPngA==} 242 | engines: {node: '>=12'} 243 | cpu: [x64] 244 | os: [linux] 245 | requiresBuild: true 246 | dev: true 247 | optional: true 248 | 249 | /@esbuild/netbsd-x64@0.18.13: 250 | resolution: {integrity: sha512-I6zs10TZeaHDYoGxENuksxE1sxqZpCp+agYeW039yqFwh3MgVvdmXL5NMveImOC6AtpLvE4xG5ujVic4NWFIDQ==} 251 | engines: {node: '>=12'} 252 | cpu: [x64] 253 | os: [netbsd] 254 | requiresBuild: true 255 | dev: true 256 | optional: true 257 | 258 | /@esbuild/openbsd-x64@0.18.13: 259 | resolution: {integrity: sha512-W5C5nczhrt1y1xPG5bV+0M12p2vetOGlvs43LH8SopQ3z2AseIROu09VgRqydx5qFN7y9qCbpgHLx0kb0TcW7g==} 260 | engines: {node: '>=12'} 261 | cpu: [x64] 262 | os: [openbsd] 263 | requiresBuild: true 264 | dev: true 265 | optional: true 266 | 267 | /@esbuild/sunos-x64@0.18.13: 268 | resolution: {integrity: sha512-X/xzuw4Hzpo/yq3YsfBbIsipNgmsm8mE/QeWbdGdTTeZ77fjxI2K0KP3AlhZ6gU3zKTw1bKoZTuKLnqcJ537qw==} 269 | engines: {node: '>=12'} 270 | cpu: [x64] 271 | os: [sunos] 272 | requiresBuild: true 273 | dev: true 274 | optional: true 275 | 276 | /@esbuild/win32-arm64@0.18.13: 277 | resolution: {integrity: sha512-4CGYdRQT/ILd+yLLE5i4VApMPfGE0RPc/wFQhlluDQCK09+b4JDbxzzjpgQqTPrdnP7r5KUtGVGZYclYiPuHrw==} 278 | engines: {node: '>=12'} 279 | cpu: [arm64] 280 | os: [win32] 281 | requiresBuild: true 282 | dev: true 283 | optional: true 284 | 285 | /@esbuild/win32-ia32@0.18.13: 286 | resolution: {integrity: sha512-D+wKZaRhQI+MUGMH+DbEr4owC2D7XnF+uyGiZk38QbgzLcofFqIOwFs7ELmIeU45CQgfHNy9Q+LKW3cE8g37Kg==} 287 | engines: {node: '>=12'} 288 | cpu: [ia32] 289 | os: [win32] 290 | requiresBuild: true 291 | dev: true 292 | optional: true 293 | 294 | /@esbuild/win32-x64@0.18.13: 295 | resolution: {integrity: sha512-iVl6lehAfJS+VmpF3exKpNQ8b0eucf5VWfzR8S7xFve64NBNz2jPUgx1X93/kfnkfgP737O+i1k54SVQS7uVZA==} 296 | engines: {node: '>=12'} 297 | cpu: [x64] 298 | os: [win32] 299 | requiresBuild: true 300 | dev: true 301 | optional: true 302 | 303 | /@jest/schemas@29.6.0: 304 | resolution: {integrity: sha512-rxLjXyJBTL4LQeJW3aKo0M/+GkCOXsO+8i9Iu7eDb6KwtP65ayoDsitrdPBtujxQ88k4wI2FNYfa6TOGwSn6cQ==} 305 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 306 | dependencies: 307 | '@sinclair/typebox': 0.27.8 308 | dev: true 309 | 310 | /@jridgewell/sourcemap-codec@1.4.15: 311 | resolution: {integrity: sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==} 312 | dev: true 313 | 314 | /@sinclair/typebox@0.27.8: 315 | resolution: {integrity: sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==} 316 | dev: true 317 | 318 | /@types/chai-subset@1.3.3: 319 | resolution: {integrity: sha512-frBecisrNGz+F4T6bcc+NLeolfiojh5FxW2klu669+8BARtyQv2C/GkNW6FUodVe4BroGMP/wER/YDGc7rEllw==} 320 | dependencies: 321 | '@types/chai': 4.3.5 322 | dev: true 323 | 324 | /@types/chai@4.3.5: 325 | resolution: {integrity: sha512-mEo1sAde+UCE6b2hxn332f1g1E8WfYRu6p5SvTKr2ZKC1f7gFJXk4h5PyGP9Dt6gCaG8y8XhwnXWC6Iy2cmBng==} 326 | dev: true 327 | 328 | /@types/node@20.4.2: 329 | resolution: {integrity: sha512-Dd0BYtWgnWJKwO1jkmTrzofjK2QXXcai0dmtzvIBhcA+RsG5h8R3xlyta0kGOZRNfL9GuRtb1knmPEhQrePCEw==} 330 | dev: true 331 | 332 | /@vitest/expect@0.33.0: 333 | resolution: {integrity: sha512-sVNf+Gla3mhTCxNJx+wJLDPp/WcstOe0Ksqz4Vec51MmgMth/ia0MGFEkIZmVGeTL5HtjYR4Wl/ZxBxBXZJTzQ==} 334 | dependencies: 335 | '@vitest/spy': 0.33.0 336 | '@vitest/utils': 0.33.0 337 | chai: 4.3.7 338 | dev: true 339 | 340 | /@vitest/runner@0.33.0: 341 | resolution: {integrity: sha512-UPfACnmCB6HKRHTlcgCoBh6ppl6fDn+J/xR8dTufWiKt/74Y9bHci5CKB8tESSV82zKYtkBJo9whU3mNvfaisg==} 342 | dependencies: 343 | '@vitest/utils': 0.33.0 344 | p-limit: 4.0.0 345 | pathe: 1.1.1 346 | dev: true 347 | 348 | /@vitest/snapshot@0.33.0: 349 | resolution: {integrity: sha512-tJjrl//qAHbyHajpFvr8Wsk8DIOODEebTu7pgBrP07iOepR5jYkLFiqLq2Ltxv+r0uptUb4izv1J8XBOwKkVYA==} 350 | dependencies: 351 | magic-string: 0.30.1 352 | pathe: 1.1.1 353 | pretty-format: 29.6.1 354 | dev: true 355 | 356 | /@vitest/spy@0.33.0: 357 | resolution: {integrity: sha512-Kv+yZ4hnH1WdiAkPUQTpRxW8kGtH8VRTnus7ZTGovFYM1ZezJpvGtb9nPIjPnptHbsyIAxYZsEpVPYgtpjGnrg==} 358 | dependencies: 359 | tinyspy: 2.1.1 360 | dev: true 361 | 362 | /@vitest/utils@0.33.0: 363 | resolution: {integrity: sha512-pF1w22ic965sv+EN6uoePkAOTkAPWM03Ri/jXNyMIKBb/XHLDPfhLvf/Fa9g0YECevAIz56oVYXhodLvLQ/awA==} 364 | dependencies: 365 | diff-sequences: 29.4.3 366 | loupe: 2.3.6 367 | pretty-format: 29.6.1 368 | dev: true 369 | 370 | /acorn-walk@8.2.0: 371 | resolution: {integrity: sha512-k+iyHEuPgSw6SbuDpGQM+06HQUa04DZ3o+F6CSzXMvvI5KMvnaEqXe+YVe555R9nn6GPt404fos4wcgpw12SDA==} 372 | engines: {node: '>=0.4.0'} 373 | dev: true 374 | 375 | /acorn@8.10.0: 376 | resolution: {integrity: sha512-F0SAmZ8iUtS//m8DmCTA0jlh6TDKkHQyK6xc6V4KDTyZKA9dnvX9/3sRTVQrWm79glUAZbnmmNcdYwUIHWVybw==} 377 | engines: {node: '>=0.4.0'} 378 | hasBin: true 379 | dev: true 380 | 381 | /ansi-styles@5.2.0: 382 | resolution: {integrity: sha512-Cxwpt2SfTzTtXcfOlzGEee8O+c+MmUgGrNiBcXnuWxuFJHe6a5Hz7qwhwe5OgaSYI0IJvkLqWX1ASG+cJOkEiA==} 383 | engines: {node: '>=10'} 384 | dev: true 385 | 386 | /assertion-error@1.1.0: 387 | resolution: {integrity: sha512-jgsaNduz+ndvGyFt3uSuWqvy4lCnIJiovtouQN5JZHOKCS2QuhEdbcQHFhVksz2N2U9hXJo8odG7ETyWlEeuDw==} 388 | dev: true 389 | 390 | /cac@6.7.14: 391 | resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} 392 | engines: {node: '>=8'} 393 | dev: true 394 | 395 | /chai@4.3.7: 396 | resolution: {integrity: sha512-HLnAzZ2iupm25PlN0xFreAlBA5zaBSv3og0DdeGA4Ar6h6rJ3A0rolRUKJhSF2V10GZKDgWF/VmAEsNWjCRB+A==} 397 | engines: {node: '>=4'} 398 | dependencies: 399 | assertion-error: 1.1.0 400 | check-error: 1.0.2 401 | deep-eql: 4.1.3 402 | get-func-name: 2.0.0 403 | loupe: 2.3.6 404 | pathval: 1.1.1 405 | type-detect: 4.0.8 406 | dev: true 407 | 408 | /check-error@1.0.2: 409 | resolution: {integrity: sha512-BrgHpW9NURQgzoNyjfq0Wu6VFO6D7IZEmJNdtgNqpzGG8RuNFHt2jQxWlAs4HMe119chBnv+34syEZtc6IhLtA==} 410 | dev: true 411 | 412 | /debug@4.3.4: 413 | resolution: {integrity: sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==} 414 | engines: {node: '>=6.0'} 415 | peerDependencies: 416 | supports-color: '*' 417 | peerDependenciesMeta: 418 | supports-color: 419 | optional: true 420 | dependencies: 421 | ms: 2.1.2 422 | dev: true 423 | 424 | /deep-eql@4.1.3: 425 | resolution: {integrity: sha512-WaEtAOpRA1MQ0eohqZjpGD8zdI0Ovsm8mmFhaDN8dvDZzyoUMcYDnf5Y6iu7HTXxf8JDS23qWa4a+hKCDyOPzw==} 426 | engines: {node: '>=6'} 427 | dependencies: 428 | type-detect: 4.0.8 429 | dev: true 430 | 431 | /diff-sequences@29.4.3: 432 | resolution: {integrity: sha512-ofrBgwpPhCD85kMKtE9RYFFq6OC1A89oW2vvgWZNCwxrUpRUILopY7lsYyMDSjc8g6U6aiO0Qubg6r4Wgt5ZnA==} 433 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 434 | dev: true 435 | 436 | /esbuild@0.18.13: 437 | resolution: {integrity: sha512-vhg/WR/Oiu4oUIkVhmfcc23G6/zWuEQKFS+yiosSHe4aN6+DQRXIfeloYGibIfVhkr4wyfuVsGNLr+sQU1rWWw==} 438 | engines: {node: '>=12'} 439 | hasBin: true 440 | requiresBuild: true 441 | optionalDependencies: 442 | '@esbuild/android-arm': 0.18.13 443 | '@esbuild/android-arm64': 0.18.13 444 | '@esbuild/android-x64': 0.18.13 445 | '@esbuild/darwin-arm64': 0.18.13 446 | '@esbuild/darwin-x64': 0.18.13 447 | '@esbuild/freebsd-arm64': 0.18.13 448 | '@esbuild/freebsd-x64': 0.18.13 449 | '@esbuild/linux-arm': 0.18.13 450 | '@esbuild/linux-arm64': 0.18.13 451 | '@esbuild/linux-ia32': 0.18.13 452 | '@esbuild/linux-loong64': 0.18.13 453 | '@esbuild/linux-mips64el': 0.18.13 454 | '@esbuild/linux-ppc64': 0.18.13 455 | '@esbuild/linux-riscv64': 0.18.13 456 | '@esbuild/linux-s390x': 0.18.13 457 | '@esbuild/linux-x64': 0.18.13 458 | '@esbuild/netbsd-x64': 0.18.13 459 | '@esbuild/openbsd-x64': 0.18.13 460 | '@esbuild/sunos-x64': 0.18.13 461 | '@esbuild/win32-arm64': 0.18.13 462 | '@esbuild/win32-ia32': 0.18.13 463 | '@esbuild/win32-x64': 0.18.13 464 | dev: true 465 | 466 | /fsevents@2.3.2: 467 | resolution: {integrity: sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==} 468 | engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} 469 | os: [darwin] 470 | requiresBuild: true 471 | dev: true 472 | optional: true 473 | 474 | /get-func-name@2.0.0: 475 | resolution: {integrity: sha512-Hm0ixYtaSZ/V7C8FJrtZIuBBI+iSgL+1Aq82zSu8VQNB4S3Gk8e7Qs3VwBDJAhmRZcFqkl3tQu36g/Foh5I5ig==} 476 | dev: true 477 | 478 | /jsonc-parser@3.2.0: 479 | resolution: {integrity: sha512-gfFQZrcTc8CnKXp6Y4/CBT3fTc0OVuDofpre4aEeEpSBPV5X5v4+Vmx+8snU7RLPrNHPKSgLxGo9YuQzz20o+w==} 480 | dev: true 481 | 482 | /local-pkg@0.4.3: 483 | resolution: {integrity: sha512-SFppqq5p42fe2qcZQqqEOiVRXl+WCP1MdT6k7BDEW1j++sp5fIY+/fdRQitvKgB5BrBcmrs5m/L0v2FrU5MY1g==} 484 | engines: {node: '>=14'} 485 | dev: true 486 | 487 | /loupe@2.3.6: 488 | resolution: {integrity: sha512-RaPMZKiMy8/JruncMU5Bt6na1eftNoo++R4Y+N2FrxkDVTrGvcyzFTsaGif4QTeKESheMGegbhw6iUAq+5A8zA==} 489 | dependencies: 490 | get-func-name: 2.0.0 491 | dev: true 492 | 493 | /magic-string@0.30.1: 494 | resolution: {integrity: sha512-mbVKXPmS0z0G4XqFDCTllmDQ6coZzn94aMlb0o/A4HEHJCKcanlDZwYJgwnkmgD3jyWhUgj9VsPrfd972yPffA==} 495 | engines: {node: '>=12'} 496 | dependencies: 497 | '@jridgewell/sourcemap-codec': 1.4.15 498 | dev: true 499 | 500 | /mlly@1.4.0: 501 | resolution: {integrity: sha512-ua8PAThnTwpprIaU47EPeZ/bPUVp2QYBbWMphUQpVdBI3Lgqzm5KZQ45Agm3YJedHXaIHl6pBGabaLSUPPSptg==} 502 | dependencies: 503 | acorn: 8.10.0 504 | pathe: 1.1.1 505 | pkg-types: 1.0.3 506 | ufo: 1.1.2 507 | dev: true 508 | 509 | /ms@2.1.2: 510 | resolution: {integrity: sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==} 511 | dev: true 512 | 513 | /nanoid@3.3.6: 514 | resolution: {integrity: sha512-BGcqMMJuToF7i1rt+2PWSNVnWIkGCU78jBG3RxO/bZlnZPK2Cmi2QaffxGO/2RvWi9sL+FAiRiXMgsyxQ1DIDA==} 515 | engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} 516 | hasBin: true 517 | dev: true 518 | 519 | /p-limit@4.0.0: 520 | resolution: {integrity: sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==} 521 | engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} 522 | dependencies: 523 | yocto-queue: 1.0.0 524 | dev: true 525 | 526 | /pathe@1.1.1: 527 | resolution: {integrity: sha512-d+RQGp0MAYTIaDBIMmOfMwz3E+LOZnxx1HZd5R18mmCZY0QBlK0LDZfPc8FW8Ed2DlvsuE6PRjroDY+wg4+j/Q==} 528 | dev: true 529 | 530 | /pathval@1.1.1: 531 | resolution: {integrity: sha512-Dp6zGqpTdETdR63lehJYPeIOqpiNBNtc7BpWSLrOje7UaIsE5aY92r/AunQA7rsXvet3lrJ3JnZX29UPTKXyKQ==} 532 | dev: true 533 | 534 | /picocolors@1.0.0: 535 | resolution: {integrity: sha512-1fygroTLlHu66zi26VoTDv8yRgm0Fccecssto+MhsZ0D/DGW2sm8E8AjW7NU5VVTRt5GxbeZ5qBuJr+HyLYkjQ==} 536 | dev: true 537 | 538 | /pkg-types@1.0.3: 539 | resolution: {integrity: sha512-nN7pYi0AQqJnoLPC9eHFQ8AcyaixBUOwvqc5TDnIKCMEE6I0y8P7OKA7fPexsXGCGxQDl/cmrLAp26LhcwxZ4A==} 540 | dependencies: 541 | jsonc-parser: 3.2.0 542 | mlly: 1.4.0 543 | pathe: 1.1.1 544 | dev: true 545 | 546 | /postcss@8.4.26: 547 | resolution: {integrity: sha512-jrXHFF8iTloAenySjM/ob3gSj7pCu0Ji49hnjqzsgSRa50hkWCKD0HQ+gMNJkW38jBI68MpAAg7ZWwHwX8NMMw==} 548 | engines: {node: ^10 || ^12 || >=14} 549 | dependencies: 550 | nanoid: 3.3.6 551 | picocolors: 1.0.0 552 | source-map-js: 1.0.2 553 | dev: true 554 | 555 | /pretty-format@29.6.1: 556 | resolution: {integrity: sha512-7jRj+yXO0W7e4/tSJKoR7HRIHLPPjtNaUGG2xxKQnGvPNRkgWcQ0AZX6P4KBRJN4FcTBWb3sa7DVUJmocYuoog==} 557 | engines: {node: ^14.15.0 || ^16.10.0 || >=18.0.0} 558 | dependencies: 559 | '@jest/schemas': 29.6.0 560 | ansi-styles: 5.2.0 561 | react-is: 18.2.0 562 | dev: true 563 | 564 | /react-is@18.2.0: 565 | resolution: {integrity: sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==} 566 | dev: true 567 | 568 | /rollup@3.26.2: 569 | resolution: {integrity: sha512-6umBIGVz93er97pMgQO08LuH3m6PUb3jlDUUGFsNJB6VgTCUaDFpupf5JfU30529m/UKOgmiX+uY6Sx8cOYpLA==} 570 | engines: {node: '>=14.18.0', npm: '>=8.0.0'} 571 | hasBin: true 572 | optionalDependencies: 573 | fsevents: 2.3.2 574 | dev: true 575 | 576 | /siginfo@2.0.0: 577 | resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} 578 | dev: true 579 | 580 | /source-map-js@1.0.2: 581 | resolution: {integrity: sha512-R0XvVJ9WusLiqTCEiGCmICCMplcCkIwwR11mOSD9CR5u+IXYdiseeEuXCVAjS54zqwkLcPNnmU4OeJ6tUrWhDw==} 582 | engines: {node: '>=0.10.0'} 583 | dev: true 584 | 585 | /stackback@0.0.2: 586 | resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} 587 | dev: true 588 | 589 | /std-env@3.3.3: 590 | resolution: {integrity: sha512-Rz6yejtVyWnVjC1RFvNmYL10kgjC49EOghxWn0RFqlCHGFpQx+Xe7yW3I4ceK1SGrWIGMjD5Kbue8W/udkbMJg==} 591 | dev: true 592 | 593 | /strip-literal@1.0.1: 594 | resolution: {integrity: sha512-QZTsipNpa2Ppr6v1AmJHESqJ3Uz247MUS0OjrnnZjFAvEoWqxuyFuXn2xLgMtRnijJShAa1HL0gtJyUs7u7n3Q==} 595 | dependencies: 596 | acorn: 8.10.0 597 | dev: true 598 | 599 | /tinybench@2.5.0: 600 | resolution: {integrity: sha512-kRwSG8Zx4tjF9ZiyH4bhaebu+EDz1BOx9hOigYHlUW4xxI/wKIUQUqo018UlU4ar6ATPBsaMrdbKZ+tmPdohFA==} 601 | dev: true 602 | 603 | /tinypool@0.6.0: 604 | resolution: {integrity: sha512-FdswUUo5SxRizcBc6b1GSuLpLjisa8N8qMyYoP3rl+bym+QauhtJP5bvZY1ytt8krKGmMLYIRl36HBZfeAoqhQ==} 605 | engines: {node: '>=14.0.0'} 606 | dev: true 607 | 608 | /tinyspy@2.1.1: 609 | resolution: {integrity: sha512-XPJL2uSzcOyBMky6OFrusqWlzfFrXtE0hPuMgW8A2HmaqrPo4ZQHRN/V0QXN3FSjKxpsbRrFc5LI7KOwBsT1/w==} 610 | engines: {node: '>=14.0.0'} 611 | dev: true 612 | 613 | /type-detect@4.0.8: 614 | resolution: {integrity: sha512-0fr/mIH1dlO+x7TlcMy+bIDqKPsw/70tVyeHW787goQjhmqaZe10uwLujubK9q9Lg6Fiho1KUKDYz0Z7k7g5/g==} 615 | engines: {node: '>=4'} 616 | dev: true 617 | 618 | /ufo@1.1.2: 619 | resolution: {integrity: sha512-TrY6DsjTQQgyS3E3dBaOXf0TpPD8u9FVrVYmKVegJuFw51n/YB9XPt+U6ydzFG5ZIN7+DIjPbNmXoBj9esYhgQ==} 620 | dev: true 621 | 622 | /vite-node@0.33.0(@types/node@20.4.2): 623 | resolution: {integrity: sha512-19FpHYbwWWxDr73ruNahC+vtEdza52kA90Qb3La98yZ0xULqV8A5JLNPUff0f5zID4984tW7l3DH2przTJUZSw==} 624 | engines: {node: '>=v14.18.0'} 625 | hasBin: true 626 | dependencies: 627 | cac: 6.7.14 628 | debug: 4.3.4 629 | mlly: 1.4.0 630 | pathe: 1.1.1 631 | picocolors: 1.0.0 632 | vite: 4.4.4(@types/node@20.4.2) 633 | transitivePeerDependencies: 634 | - '@types/node' 635 | - less 636 | - lightningcss 637 | - sass 638 | - stylus 639 | - sugarss 640 | - supports-color 641 | - terser 642 | dev: true 643 | 644 | /vite@4.4.4(@types/node@20.4.2): 645 | resolution: {integrity: sha512-4mvsTxjkveWrKDJI70QmelfVqTm+ihFAb6+xf4sjEU2TmUCTlVX87tmg/QooPEMQb/lM9qGHT99ebqPziEd3wg==} 646 | engines: {node: ^14.18.0 || >=16.0.0} 647 | hasBin: true 648 | peerDependencies: 649 | '@types/node': '>= 14' 650 | less: '*' 651 | lightningcss: ^1.21.0 652 | sass: '*' 653 | stylus: '*' 654 | sugarss: '*' 655 | terser: ^5.4.0 656 | peerDependenciesMeta: 657 | '@types/node': 658 | optional: true 659 | less: 660 | optional: true 661 | lightningcss: 662 | optional: true 663 | sass: 664 | optional: true 665 | stylus: 666 | optional: true 667 | sugarss: 668 | optional: true 669 | terser: 670 | optional: true 671 | dependencies: 672 | '@types/node': 20.4.2 673 | esbuild: 0.18.13 674 | postcss: 8.4.26 675 | rollup: 3.26.2 676 | optionalDependencies: 677 | fsevents: 2.3.2 678 | dev: true 679 | 680 | /vitest@0.33.0: 681 | resolution: {integrity: sha512-1CxaugJ50xskkQ0e969R/hW47za4YXDUfWJDxip1hwbnhUjYolpfUn2AMOulqG/Dtd9WYAtkHmM/m3yKVrEejQ==} 682 | engines: {node: '>=v14.18.0'} 683 | hasBin: true 684 | peerDependencies: 685 | '@edge-runtime/vm': '*' 686 | '@vitest/browser': '*' 687 | '@vitest/ui': '*' 688 | happy-dom: '*' 689 | jsdom: '*' 690 | playwright: '*' 691 | safaridriver: '*' 692 | webdriverio: '*' 693 | peerDependenciesMeta: 694 | '@edge-runtime/vm': 695 | optional: true 696 | '@vitest/browser': 697 | optional: true 698 | '@vitest/ui': 699 | optional: true 700 | happy-dom: 701 | optional: true 702 | jsdom: 703 | optional: true 704 | playwright: 705 | optional: true 706 | safaridriver: 707 | optional: true 708 | webdriverio: 709 | optional: true 710 | dependencies: 711 | '@types/chai': 4.3.5 712 | '@types/chai-subset': 1.3.3 713 | '@types/node': 20.4.2 714 | '@vitest/expect': 0.33.0 715 | '@vitest/runner': 0.33.0 716 | '@vitest/snapshot': 0.33.0 717 | '@vitest/spy': 0.33.0 718 | '@vitest/utils': 0.33.0 719 | acorn: 8.10.0 720 | acorn-walk: 8.2.0 721 | cac: 6.7.14 722 | chai: 4.3.7 723 | debug: 4.3.4 724 | local-pkg: 0.4.3 725 | magic-string: 0.30.1 726 | pathe: 1.1.1 727 | picocolors: 1.0.0 728 | std-env: 3.3.3 729 | strip-literal: 1.0.1 730 | tinybench: 2.5.0 731 | tinypool: 0.6.0 732 | vite: 4.4.4(@types/node@20.4.2) 733 | vite-node: 0.33.0(@types/node@20.4.2) 734 | why-is-node-running: 2.2.2 735 | transitivePeerDependencies: 736 | - less 737 | - lightningcss 738 | - sass 739 | - stylus 740 | - sugarss 741 | - supports-color 742 | - terser 743 | dev: true 744 | 745 | /why-is-node-running@2.2.2: 746 | resolution: {integrity: sha512-6tSwToZxTOcotxHeA+qGCq1mVzKR3CwcJGmVcY+QE8SHy6TnpFnh8PAvPNHYr7EcuVeG0QSMxtYCuO1ta/G/oA==} 747 | engines: {node: '>=8'} 748 | hasBin: true 749 | dependencies: 750 | siginfo: 2.0.0 751 | stackback: 0.0.2 752 | dev: true 753 | 754 | /yocto-queue@1.0.0: 755 | resolution: {integrity: sha512-9bnSc/HEW2uRy67wc+T8UwauLuPJVn28jb+GtJY16iiKWyvmYJRXVT4UamsAEGQfPohgr2q4Tq0sQbQlxTfi1g==} 756 | engines: {node: '>=12.20'} 757 | dev: true 758 | -------------------------------------------------------------------------------- /test/jsonschema.nullable.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import { nullable } from '../jsonschema.js' 3 | 4 | // Test JSON Schema output (JTD-compatible subset) 5 | 6 | describe('JSON Schema support', () => { 7 | it('should create nullable boolean types', () => { 8 | expect(nullable.boolean()).toEqual({ type: ['boolean', 'null'] }) 9 | expect(nullable.boolean({ title: 'flag' })).toEqual({ 10 | type: ['boolean', 'null'], 11 | metadata: { 12 | title: 'flag', 13 | }, 14 | }) 15 | }) 16 | 17 | it('should create nullable string types', () => { 18 | expect(nullable.string()).toEqual({ type: ['string', 'null'] }) 19 | expect(nullable.string({ info: 'Information' })).toEqual({ 20 | type: ['string', 'null'], 21 | metadata: { 22 | info: 'Information', 23 | }, 24 | }) 25 | }) 26 | 27 | it('should create nullable timestamp types', () => { 28 | expect(nullable.timestamp()).toEqual({ 29 | type: ['string', 'null'], 30 | format: 'date-time', 31 | }) 32 | }) 33 | 34 | it('should create nullable number types', () => { 35 | // Aliases 36 | expect(nullable.number()).toEqual({ type: ['number', 'null'] }) 37 | expect(nullable.integer()).toEqual({ type: ['integer', 'null'] }) 38 | // JTD types 39 | expect(nullable.float32()).toEqual({ type: ['number', 'null'] }) 40 | expect(nullable.float64()).toEqual({ type: ['number', 'null'] }) 41 | expect(nullable.int8()).toEqual({ type: ['integer', 'null'] }) 42 | expect(nullable.uint8()).toEqual({ type: ['integer', 'null'] }) 43 | expect(nullable.int16()).toEqual({ type: ['integer', 'null'] }) 44 | expect(nullable.uint16()).toEqual({ type: ['integer', 'null'] }) 45 | expect(nullable.int32()).toEqual({ type: ['integer', 'null'] }) 46 | expect(nullable.uint32()).toEqual({ type: ['integer', 'null'] }) 47 | }) 48 | 49 | it('should create nullable enum types', () => { 50 | expect(nullable.values(['A', 'B', 'C'])).toEqual({ 51 | type: ['string', 'null'], 52 | enum: ['A', 'B', 'C'], 53 | }) 54 | }) 55 | 56 | it('should create nullable elements form (arrays)', () => { 57 | expect(nullable.array(nullable.string())).toEqual({ 58 | type: ['array', 'null'], 59 | items: { type: ['string', 'null'] }, 60 | }) 61 | }) 62 | 63 | it('should create nullable properties form (objects)', () => { 64 | expect( 65 | nullable.object({ 66 | propertyA: nullable.string(), 67 | propertyB: nullable.object({ 68 | innerPropertyC: nullable.float64(), 69 | }), 70 | }), 71 | ).toEqual({ 72 | additionalProperties: true, 73 | type: ['object', 'null'], 74 | properties: { 75 | propertyA: { type: ['string', 'null'] }, 76 | propertyB: { 77 | type: ['object', 'null'], 78 | additionalProperties: true, 79 | properties: { 80 | innerPropertyC: { type: ['number', 'null'] }, 81 | }, 82 | required: ['innerPropertyC'], 83 | }, 84 | }, 85 | required: ['propertyA', 'propertyB'], 86 | }) 87 | }) 88 | 89 | it('should create nullable optional properties', () => { 90 | expect( 91 | nullable.object( 92 | { 93 | propertyA: nullable.string(), 94 | propertyB: nullable.object({ 95 | innerPropertyC: nullable.float64(), 96 | }), 97 | }, 98 | { 99 | propertyC: { type: ['string', 'null'] }, 100 | }, 101 | { 102 | metadataProperty: 'metatada', 103 | }, 104 | ), 105 | ).toEqual({ 106 | type: ['object', 'null'], 107 | additionalProperties: true, 108 | properties: { 109 | propertyA: { type: ['string', 'null'] }, 110 | propertyB: { 111 | type: ['object', 'null'], 112 | additionalProperties: true, 113 | properties: { 114 | innerPropertyC: { type: ['number', 'null'] }, 115 | }, 116 | required: ['innerPropertyC'], 117 | }, 118 | propertyC: { type: ['string', 'null'] }, 119 | }, 120 | required: ['propertyA', 'propertyB'], 121 | metadata: { 122 | metadataProperty: 'metatada', 123 | }, 124 | }) 125 | }) 126 | 127 | it('should not create required properties when there are none', () => { 128 | expect( 129 | nullable.object(null, { 130 | propertyA: nullable.string(), 131 | propertyB: nullable.object({ 132 | innerPropertyC: nullable.float64(), 133 | }), 134 | }), 135 | ).toEqual({ 136 | type: ['object', 'null'], 137 | additionalProperties: true, 138 | properties: { 139 | propertyA: { type: ['string', 'null'] }, 140 | propertyB: { 141 | type: ['object', 'null'], 142 | additionalProperties: true, 143 | properties: { 144 | innerPropertyC: { type: ['number', 'null'] }, 145 | }, 146 | required: ['innerPropertyC'], 147 | }, 148 | }, 149 | }) 150 | }) 151 | 152 | it('should create nullable objects with no allowed additional properties', () => { 153 | expect( 154 | nullable.sealed(null, { 155 | propertyA: nullable.string(), 156 | propertyB: nullable.object({ 157 | innerPropertyC: nullable.float64(), 158 | }), 159 | }), 160 | ).toEqual({ 161 | type: ['object', 'null'], 162 | additionalProperties: false, 163 | properties: { 164 | propertyA: { type: ['string', 'null'] }, 165 | propertyB: { 166 | type: ['object', 'null'], 167 | additionalProperties: true, 168 | properties: { 169 | innerPropertyC: { type: ['number', 'null'] }, 170 | }, 171 | required: ['innerPropertyC'], 172 | }, 173 | }, 174 | }) 175 | }) 176 | }) 177 | -------------------------------------------------------------------------------- /test/jsonschema.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | import schema from '../jsonschema.js' 3 | 4 | // Test JSON Schema output (JTD-compatible subset) 5 | 6 | describe('JSON Schema support', () => { 7 | it('should create empty form', () => { 8 | expect(schema.empty()).toEqual({}) 9 | }) 10 | 11 | it('should create boolean types', () => { 12 | expect(schema.boolean()).toEqual({ type: 'boolean' }) 13 | expect(schema.boolean({ title: 'flag' })).toEqual({ 14 | type: 'boolean', 15 | metadata: { 16 | title: 'flag', 17 | }, 18 | }) 19 | }) 20 | 21 | it('should create string types', () => { 22 | expect(schema.string()).toEqual({ type: 'string' }) 23 | expect(schema.string({ info: 'Information' })).toEqual({ 24 | type: 'string', 25 | metadata: { 26 | info: 'Information', 27 | }, 28 | }) 29 | }) 30 | 31 | it('should create timestamp types', () => { 32 | expect(schema.timestamp()).toEqual({ type: 'string', format: 'date-time' }) 33 | }) 34 | 35 | it('should create number types', () => { 36 | // Aliases 37 | expect(schema.number()).toEqual({ type: 'number' }) 38 | expect(schema.integer()).toEqual({ type: 'integer' }) 39 | // JTD types 40 | expect(schema.float32()).toEqual({ type: 'number' }) 41 | expect(schema.float64()).toEqual({ type: 'number' }) 42 | expect(schema.int8()).toEqual({ type: 'integer' }) 43 | expect(schema.uint8()).toEqual({ type: 'integer' }) 44 | expect(schema.int16()).toEqual({ type: 'integer' }) 45 | expect(schema.uint16()).toEqual({ type: 'integer' }) 46 | expect(schema.int32()).toEqual({ type: 'integer' }) 47 | expect(schema.uint32()).toEqual({ type: 'integer' }) 48 | }) 49 | 50 | it('should create enum types', () => { 51 | expect(schema.values(['A', 'B', 'C'])).toEqual({ 52 | type: 'string', 53 | enum: ['A', 'B', 'C'], 54 | }) 55 | }) 56 | 57 | it('should create elements form (arrays)', () => { 58 | expect(schema.array(schema.string())).toEqual({ 59 | type: 'array', 60 | items: { type: 'string' }, 61 | }) 62 | }) 63 | 64 | it('should create properties form (objects)', () => { 65 | expect( 66 | schema.object({ 67 | propertyA: schema.string(), 68 | propertyB: schema.object({ 69 | innerPropertyC: schema.float64(), 70 | }), 71 | }), 72 | ).toEqual({ 73 | additionalProperties: true, 74 | type: 'object', 75 | properties: { 76 | propertyA: { type: 'string' }, 77 | propertyB: { 78 | type: 'object', 79 | additionalProperties: true, 80 | properties: { 81 | innerPropertyC: { type: 'number' }, 82 | }, 83 | required: ['innerPropertyC'], 84 | }, 85 | }, 86 | required: ['propertyA', 'propertyB'], 87 | }) 88 | }) 89 | 90 | it('should create optional properties', () => { 91 | expect( 92 | schema.object( 93 | { 94 | propertyA: schema.string(), 95 | propertyB: schema.object({ 96 | innerPropertyC: schema.float64(), 97 | }), 98 | }, 99 | { 100 | propertyC: { type: 'string' }, 101 | }, 102 | { 103 | metadataProperty: 'metatada', 104 | }, 105 | ), 106 | ).toEqual({ 107 | type: 'object', 108 | additionalProperties: true, 109 | properties: { 110 | propertyA: { type: 'string' }, 111 | propertyB: { 112 | type: 'object', 113 | additionalProperties: true, 114 | properties: { 115 | innerPropertyC: { type: 'number' }, 116 | }, 117 | required: ['innerPropertyC'], 118 | }, 119 | propertyC: { type: 'string' }, 120 | }, 121 | required: ['propertyA', 'propertyB'], 122 | metadata: { 123 | metadataProperty: 'metatada', 124 | }, 125 | }) 126 | }) 127 | 128 | it('should not create required properties when there are none', () => { 129 | expect( 130 | schema.object(null, { 131 | propertyA: schema.string(), 132 | propertyB: schema.object({ 133 | innerPropertyC: schema.float64(), 134 | }), 135 | }), 136 | ).toEqual({ 137 | type: 'object', 138 | additionalProperties: true, 139 | properties: { 140 | propertyA: { type: 'string' }, 141 | propertyB: { 142 | type: 'object', 143 | additionalProperties: true, 144 | properties: { 145 | innerPropertyC: { type: 'number' }, 146 | }, 147 | required: ['innerPropertyC'], 148 | }, 149 | }, 150 | }) 151 | }) 152 | 153 | it('should create objects with no allowed additional properties', () => { 154 | expect( 155 | schema.sealed(null, { 156 | propertyA: schema.string(), 157 | propertyB: schema.object({ 158 | innerPropertyC: schema.float64(), 159 | }), 160 | }), 161 | ).toEqual({ 162 | type: 'object', 163 | additionalProperties: false, 164 | properties: { 165 | propertyA: { type: 'string' }, 166 | propertyB: { 167 | type: 'object', 168 | additionalProperties: true, 169 | properties: { 170 | innerPropertyC: { type: 'number' }, 171 | }, 172 | required: ['innerPropertyC'], 173 | }, 174 | }, 175 | }) 176 | }) 177 | }) 178 | -------------------------------------------------------------------------------- /test/rfc8927.nullable.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { 4 | // Nullable Types 5 | nullable, 6 | // Regular Types 7 | object, // Object with additional properties allowed 8 | } from '../index.js' 9 | 10 | // Test regular JSON Type Definition output 11 | describe('RFC 8927 support (nullable helper)', () => { 12 | it('should create nullable boolean types', () => { 13 | expect(nullable.boolean()).toEqual({ type: 'boolean', nullable: true }) 14 | expect(nullable.boolean({ title: 'flag' })).toEqual({ 15 | type: 'boolean', 16 | nullable: true, 17 | metadata: { 18 | title: 'flag', 19 | }, 20 | }) 21 | }) 22 | 23 | it('should create nullable string types', () => { 24 | expect(nullable.string()).toEqual({ type: 'string', nullable: true }) 25 | expect(nullable.string({ info: 'Information' })).toEqual({ 26 | type: 'string', 27 | nullable: true, 28 | metadata: { 29 | info: 'Information', 30 | }, 31 | }) 32 | }) 33 | 34 | it('should create nullable timestamp types', () => { 35 | expect(nullable.timestamp()).toEqual({ type: 'timestamp', nullable: true }) 36 | }) 37 | 38 | it('should create nullable number types', () => { 39 | // Aliases 40 | expect(nullable.number()).toEqual({ type: 'float64', nullable: true }) 41 | expect(nullable.integer()).toEqual({ type: 'float64', nullable: true }) 42 | // JTD types 43 | expect(nullable.float32()).toEqual({ type: 'float32', nullable: true }) 44 | expect(nullable.float64()).toEqual({ type: 'float64', nullable: true }) 45 | expect(nullable.int8()).toEqual({ type: 'int8', nullable: true }) 46 | expect(nullable.uint8()).toEqual({ type: 'uint8', nullable: true }) 47 | expect(nullable.int16()).toEqual({ type: 'int16', nullable: true }) 48 | expect(nullable.uint16()).toEqual({ type: 'uint16', nullable: true }) 49 | expect(nullable.int32()).toEqual({ type: 'int32', nullable: true }) 50 | expect(nullable.uint32()).toEqual({ type: 'uint32', nullable: true }) 51 | }) 52 | 53 | it('should create nullable enum types', () => { 54 | expect(nullable.values(['A', 'B', 'C'])).toEqual({ 55 | enum: ['A', 'B', 'C'], 56 | nullable: true, 57 | }) 58 | }) 59 | 60 | it('should create nullable elements form (arrays)', () => { 61 | expect(nullable.array(nullable.string())).toEqual({ 62 | elements: { type: 'string', nullable: true }, 63 | nullable: true, 64 | }) 65 | }) 66 | 67 | it('should create nullable properties form (objects)', () => { 68 | expect( 69 | nullable.object({ 70 | propertyA: nullable.string(), 71 | propertyB: object({ 72 | innerPropertyC: nullable.float64(), 73 | }), 74 | }), 75 | ).toEqual({ 76 | nullable: true, 77 | additionalProperties: true, 78 | properties: { 79 | propertyA: { type: 'string', nullable: true }, 80 | propertyB: { 81 | additionalProperties: true, 82 | properties: { 83 | innerPropertyC: { type: 'float64', nullable: true }, 84 | }, 85 | }, 86 | }, 87 | }) 88 | }) 89 | 90 | it('should create nullable optional properties', () => { 91 | expect( 92 | nullable.object( 93 | { 94 | propertyA: nullable.string(), 95 | propertyB: object({ 96 | innerPropertyC: nullable.float64(), 97 | }), 98 | }, 99 | { 100 | propertyC: nullable.string(), 101 | }, 102 | { 103 | metadataProperty: 'metatada', 104 | }, 105 | ), 106 | ).toEqual({ 107 | nullable: true, 108 | additionalProperties: true, 109 | properties: { 110 | propertyA: { type: 'string', nullable: true }, 111 | propertyB: { 112 | additionalProperties: true, 113 | properties: { 114 | innerPropertyC: { type: 'float64', nullable: true }, 115 | }, 116 | }, 117 | }, 118 | optionalProperties: { 119 | propertyC: { type: 'string', nullable: true }, 120 | }, 121 | metadata: { 122 | metadataProperty: 'metatada', 123 | }, 124 | }) 125 | }) 126 | 127 | it('should not create required properties when there are none', () => { 128 | expect( 129 | object(null, { 130 | propertyA: nullable.string(), 131 | propertyB: object({ 132 | innerPropertyC: nullable.float64(), 133 | }), 134 | }), 135 | ).toEqual({ 136 | additionalProperties: true, 137 | optionalProperties: { 138 | propertyA: { type: 'string', nullable: true }, 139 | propertyB: { 140 | additionalProperties: true, 141 | properties: { 142 | innerPropertyC: { type: 'float64', nullable: true }, 143 | }, 144 | }, 145 | }, 146 | }) 147 | }) 148 | 149 | it('should create nullable objects with no allowed additional properties', () => { 150 | expect( 151 | nullable.sealed(null, { 152 | propertyA: nullable.string(), 153 | propertyB: object({ 154 | innerPropertyC: nullable.float64(), 155 | }), 156 | }), 157 | ).toEqual({ 158 | nullable: true, 159 | additionalProperties: false, 160 | optionalProperties: { 161 | propertyA: { type: 'string', nullable: true }, 162 | propertyB: { 163 | additionalProperties: true, 164 | properties: { 165 | innerPropertyC: { type: 'float64', nullable: true }, 166 | }, 167 | }, 168 | }, 169 | }) 170 | }) 171 | }) 172 | -------------------------------------------------------------------------------- /test/rfc8927.test.js: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from 'vitest' 2 | 3 | import { 4 | array, 5 | boolean, 6 | // Types 7 | empty, 8 | float32, 9 | float64, 10 | int8, 11 | int16, 12 | int32, 13 | integer, // Alias to float64 for convenience 14 | match, // Helper to define both `discriminator` and `mapping`` 15 | number, // Alias to float64 for convenience 16 | object, // Object with additional properties allowed 17 | sealed, // Object with no additional properties allowed 18 | string, 19 | timestamp, 20 | uint8, 21 | uint16, 22 | uint32, 23 | values, // Alias to enum because that's a reserved keyword 24 | } from '../index.js' 25 | 26 | // Test regular JSON Type Definition output 27 | describe('RFC 8927 support', () => { 28 | it('should create empty forms', () => { 29 | expect(empty()).toEqual({}) 30 | }) 31 | 32 | it('should create boolean types', () => { 33 | expect(boolean()).toEqual({ type: 'boolean' }) 34 | expect(boolean({ title: 'flag' })).toEqual({ 35 | type: 'boolean', 36 | metadata: { 37 | title: 'flag', 38 | }, 39 | }) 40 | }) 41 | 42 | it('should create string types', () => { 43 | expect(string()).toEqual({ type: 'string' }) 44 | expect(string({ info: 'Information' })).toEqual({ 45 | type: 'string', 46 | metadata: { 47 | info: 'Information', 48 | }, 49 | }) 50 | }) 51 | 52 | it('should create timestamp types', () => { 53 | expect(timestamp()).toEqual({ type: 'timestamp' }) 54 | }) 55 | 56 | it('should create number types', () => { 57 | // Aliases 58 | expect(number()).toEqual({ type: 'float64' }) 59 | expect(integer()).toEqual({ type: 'float64' }) 60 | // JTD types 61 | expect(float32()).toEqual({ type: 'float32' }) 62 | expect(float64()).toEqual({ type: 'float64' }) 63 | expect(int8()).toEqual({ type: 'int8' }) 64 | expect(uint8()).toEqual({ type: 'uint8' }) 65 | expect(int16()).toEqual({ type: 'int16' }) 66 | expect(uint16()).toEqual({ type: 'uint16' }) 67 | expect(int32()).toEqual({ type: 'int32' }) 68 | expect(uint32()).toEqual({ type: 'uint32' }) 69 | }) 70 | 71 | it('should create enum types', () => { 72 | expect(values(['A', 'B', 'C'])).toEqual({ enum: ['A', 'B', 'C'] }) 73 | }) 74 | 75 | it('should create elements form (arrays)', () => { 76 | expect(array(string())).toEqual({ elements: { type: 'string' } }) 77 | }) 78 | 79 | it('should create properties form (objects)', () => { 80 | expect( 81 | object({ 82 | propertyA: string(), 83 | propertyB: object({ 84 | innerPropertyC: float64(), 85 | }), 86 | }), 87 | ).toEqual({ 88 | additionalProperties: true, 89 | properties: { 90 | propertyA: { type: 'string' }, 91 | propertyB: { 92 | additionalProperties: true, 93 | properties: { 94 | innerPropertyC: { type: 'float64' }, 95 | }, 96 | }, 97 | }, 98 | }) 99 | }) 100 | 101 | it('should create optional properties', () => { 102 | expect( 103 | object( 104 | { 105 | propertyA: string(), 106 | propertyB: object({ 107 | innerPropertyC: float64(), 108 | }), 109 | }, 110 | { 111 | propertyC: { type: 'string' }, 112 | }, 113 | { 114 | metadataProperty: 'metatada', 115 | }, 116 | ), 117 | ).toEqual({ 118 | additionalProperties: true, 119 | properties: { 120 | propertyA: { type: 'string' }, 121 | propertyB: { 122 | additionalProperties: true, 123 | properties: { 124 | innerPropertyC: { type: 'float64' }, 125 | }, 126 | }, 127 | }, 128 | optionalProperties: { 129 | propertyC: { type: 'string' }, 130 | }, 131 | metadata: { 132 | metadataProperty: 'metatada', 133 | }, 134 | }) 135 | }) 136 | 137 | it('should not create required properties when there are none', () => { 138 | expect( 139 | object(null, { 140 | propertyA: string(), 141 | propertyB: object({ 142 | innerPropertyC: float64(), 143 | }), 144 | }), 145 | ).toEqual({ 146 | additionalProperties: true, 147 | optionalProperties: { 148 | propertyA: { type: 'string' }, 149 | propertyB: { 150 | additionalProperties: true, 151 | properties: { 152 | innerPropertyC: { type: 'float64' }, 153 | }, 154 | }, 155 | }, 156 | }) 157 | }) 158 | 159 | it('should create objects with no allowed additional properties', () => { 160 | expect( 161 | sealed(null, { 162 | propertyA: string(), 163 | propertyB: object({ 164 | innerPropertyC: float64(), 165 | }), 166 | }), 167 | ).toEqual({ 168 | additionalProperties: false, 169 | optionalProperties: { 170 | propertyA: { type: 'string' }, 171 | propertyB: { 172 | additionalProperties: true, 173 | properties: { 174 | innerPropertyC: { type: 'float64' }, 175 | }, 176 | }, 177 | }, 178 | }) 179 | }) 180 | 181 | it('should create discriminator form (tagged unions)', () => { 182 | expect( 183 | match('event', { 184 | created: sealed({ when: timestamp() }), 185 | added: sealed({ when: timestamp(), what: string() }), 186 | }), 187 | ).toEqual({ 188 | discriminator: 'event', 189 | mapping: { 190 | created: { 191 | additionalProperties: false, 192 | properties: { 193 | when: { type: 'timestamp' }, 194 | }, 195 | }, 196 | added: { 197 | additionalProperties: false, 198 | properties: { 199 | when: { type: 'timestamp' }, 200 | what: { type: 'string' }, 201 | }, 202 | }, 203 | }, 204 | }) 205 | }) 206 | }) 207 | --------------------------------------------------------------------------------