functionalscript 0.46.0 → 0.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/fjs/basen/cbase32/module.f.mjs +18 -10
- package/fjs/basen/cbase32/proof.f.d.mts +3 -0
- package/fjs/basen/cbase32/proof.f.mjs +24 -2
- package/fjs/basen/proof.f.mjs +8 -16
- package/fjs/bnf/ll1/proof.f.d.mts +2 -0
- package/fjs/bnf/ll1/proof.f.mjs +33 -18
- package/fjs/bnf/module.f.d.mts +2 -2
- package/fjs/bnf/module.f.mjs +2 -2
- package/fjs/cas/proof.f.mjs +4 -26
- package/fjs/ci/common/module.f.mjs +6 -0
- package/fjs/ci/config/module.f.d.mts +7 -7
- package/fjs/ci/config/module.f.mjs +7 -7
- package/fjs/cli/module.f.mjs +14 -10
- package/fjs/crypto/vdf/module.f.d.mts +0 -1
- package/fjs/crypto/vdf/module.f.mjs +3 -14
- package/fjs/dev/module.f.mjs +10 -20
- package/fjs/djs/parser/module.f.d.mts +101 -53
- package/fjs/djs/parser/module.f.mjs +732 -525
- package/fjs/djs/parser/proof.f.d.mts +6 -0
- package/fjs/djs/parser/proof.f.mjs +315 -11
- package/fjs/djs/parser/types.d.ts +29 -2
- package/fjs/djs/tokenizer/module.f.mjs +122 -105
- package/fjs/djs/tokenizer/proof.f.mjs +17 -0
- package/fjs/djs/transpiler/module.f.mjs +4 -5
- package/fjs/djs/transpiler/proof.f.d.mts +2 -0
- package/fjs/djs/transpiler/proof.f.mjs +25 -0
- package/fjs/edag/amnesia/module.f.d.mts +20 -0
- package/fjs/edag/amnesia/module.f.mjs +303 -0
- package/fjs/edag/amnesia/proof.f.d.mts +79 -0
- package/fjs/edag/amnesia/proof.f.mjs +573 -0
- package/fjs/edag/amnesia/types.d.ts +15 -0
- package/fjs/edag/module.f.d.mts +410 -0
- package/fjs/edag/module.f.mjs +559 -0
- package/fjs/edag/proof.f.d.mts +116 -0
- package/fjs/edag/proof.f.mjs +686 -0
- package/fjs/edag/types.d.ts +55 -0
- package/fjs/effects/node/memory/module.d.mts +37 -3
- package/fjs/effects/node/memory/module.mjs +38 -4
- package/fjs/effects/node/memory/proof.d.mts +5 -2
- package/fjs/effects/node/memory/proof.mjs +36 -6
- package/fjs/effects/node/module.d.mts +9 -1
- package/fjs/effects/node/module.f.d.mts +39 -0
- package/fjs/effects/node/module.f.mjs +46 -0
- package/fjs/effects/node/module.mjs +234 -32
- package/fjs/effects/node/types.d.ts +28 -2
- package/fjs/effects/node/virtual/module.f.d.mts +19 -10
- package/fjs/effects/node/virtual/module.f.mjs +246 -32
- package/fjs/effects/node/virtual/proof.f.d.mts +17 -2
- package/fjs/effects/node/virtual/proof.f.mjs +232 -8
- package/fjs/effects/node/virtual/types.d.ts +57 -1
- package/fjs/emergent_testing/browser/proof.d.mts +55 -0
- package/fjs/emergent_testing/browser/proof.mjs +352 -0
- package/fjs/emergent_testing/browser/species.proof.d.mts +4 -0
- package/fjs/emergent_testing/browser/species.proof.mjs +45 -0
- package/fjs/emergent_testing/browser.d.mts +79 -0
- package/fjs/emergent_testing/browser.mjs +432 -0
- package/fjs/emergent_testing/proof.f.d.mts +1 -1
- package/fjs/emergent_testing/proof.f.mjs +2 -2
- package/fjs/js/string_escape/module.f.d.mts +47 -0
- package/fjs/js/string_escape/module.f.mjs +95 -0
- package/fjs/js/string_escape/proof.f.d.mts +8 -0
- package/fjs/js/string_escape/proof.f.mjs +68 -0
- package/fjs/js/tokenizer/module.f.mjs +14 -12
- package/fjs/mcp/cas/module.f.d.mts +1 -7
- package/fjs/mcp/cas/module.f.mjs +24 -15
- package/fjs/mcp/evo/module.f.mjs +9 -24
- package/fjs/mcp/module.f.d.mts +3 -1
- package/fjs/mcp/module.f.mjs +4 -2
- package/fjs/media/json/rtti/module.f.d.mts +2 -2
- package/fjs/media/json/rtti/module.f.mjs +1 -1
- package/fjs/media/json/schema/module.f.d.mts +15 -5
- package/fjs/media/json/schema/module.f.mjs +42 -15
- package/fjs/media/json/schema/proof.f.d.mts +10 -1
- package/fjs/media/json/schema/proof.f.mjs +88 -16
- package/fjs/media/json/serializer/module.f.mjs +14 -26
- package/fjs/media/json/types.d.ts +1 -1
- package/fjs/media/lock/module.f.d.mts +8 -3
- package/fjs/media/lock/module.f.mjs +9 -3
- package/fjs/media/lock/proof.f.d.mts +1 -1
- package/fjs/media/lock/proof.f.mjs +2 -2
- package/fjs/media/lock/types.d.ts +1 -3
- package/fjs/media/module.f.d.mts +20 -13
- package/fjs/media/module.f.mjs +29 -16
- package/fjs/media/note/module.f.d.mts +14 -7
- package/fjs/media/note/module.f.mjs +14 -7
- package/fjs/media/note/proof.f.mjs +1 -1
- package/fjs/media/proof.f.mjs +5 -5
- package/fjs/media/revision/module.f.d.mts +8 -2
- package/fjs/media/revision/module.f.mjs +9 -3
- package/fjs/media/revision/proof.f.mjs +1 -1
- package/fjs/media/revision/types.d.ts +1 -1
- package/fjs/media/type/module.f.d.mts +20 -0
- package/fjs/media/type/module.f.mjs +74 -0
- package/fjs/media/type/proof.f.d.mts +5 -0
- package/fjs/media/type/proof.f.mjs +24 -1
- package/fjs/module.f.mjs +6 -0
- package/fjs/path/module.f.d.mts +60 -3
- package/fjs/path/module.f.mjs +138 -14
- package/fjs/path/proof.f.d.mts +3 -0
- package/fjs/path/proof.f.mjs +298 -2
- package/fjs/protocol/json_rpc/module.f.d.mts +65 -28
- package/fjs/protocol/json_rpc/module.f.mjs +49 -13
- package/fjs/protocol/json_rpc/proof.f.d.mts +4 -0
- package/fjs/protocol/json_rpc/proof.f.mjs +29 -0
- package/fjs/protocol/json_rpc/types.d.ts +12 -2
- package/fjs/protocol/mcp/module.f.d.mts +89 -54
- package/fjs/protocol/mcp/module.f.mjs +110 -56
- package/fjs/protocol/mcp/proof.f.d.mts +13 -0
- package/fjs/protocol/mcp/proof.f.mjs +107 -7
- package/fjs/protocol/mcp/stdio/module.f.mjs +3 -4
- package/fjs/protocol/mcp/stdio/proof.f.mjs +2 -2
- package/fjs/protocol/mcp/types.d.ts +14 -2
- package/fjs/text/utf8/module.f.mjs +27 -1
- package/fjs/text/utf8/proof.f.mjs +37 -0
- package/fjs/types/array/module.f.mjs +18 -11
- package/fjs/types/array/proof.f.mjs +27 -1
- package/fjs/types/bigfloat/module.f.d.mts +48 -9
- package/fjs/types/bigfloat/module.f.mjs +147 -59
- package/fjs/types/bigfloat/proof.f.d.mts +2 -0
- package/fjs/types/bigfloat/proof.f.mjs +136 -4
- package/fjs/types/bigfloat/types.d.ts +17 -0
- package/fjs/types/function/module.f.d.mts +11 -2
- package/fjs/types/function/module.f.mjs +20 -2
- package/fjs/types/function/proof.f.mjs +5 -1
- package/fjs/types/phantom/types.d.ts +21 -0
- package/fjs/types/result/module.f.d.mts +4 -4
- package/fjs/types/result/module.f.mjs +2 -2
- package/fjs/types/result/proof.f.d.mts +6 -2
- package/fjs/types/result/proof.f.mjs +17 -1
- package/fjs/types/rtti/common/module.f.d.mts +78 -5
- package/fjs/types/rtti/common/module.f.mjs +168 -3
- package/fjs/types/rtti/common/proof.f.d.mts +14 -0
- package/fjs/types/rtti/common/proof.f.mjs +61 -2
- package/fjs/types/rtti/common/types.d.ts +23 -1
- package/fjs/types/rtti/data/module.f.d.mts +57 -1
- package/fjs/types/rtti/data/module.f.mjs +241 -47
- package/fjs/types/rtti/data/proof.f.d.mts +10 -0
- package/fjs/types/rtti/data/proof.f.mjs +272 -39
- package/fjs/types/rtti/data/types.d.ts +14 -7
- package/fjs/types/rtti/host.proof.d.mts +33 -0
- package/fjs/types/rtti/host.proof.mjs +170 -0
- package/fjs/types/rtti/module.f.d.mts +51 -6
- package/fjs/types/rtti/module.f.mjs +50 -3
- package/fjs/types/rtti/parse/module.f.d.mts +31 -24
- package/fjs/types/rtti/parse/module.f.mjs +197 -59
- package/fjs/types/rtti/parse/proof.f.d.mts +37 -2
- package/fjs/types/rtti/parse/proof.f.mjs +171 -47
- package/fjs/types/rtti/proof.f.d.mts +6 -0
- package/fjs/types/rtti/proof.f.mjs +42 -1
- package/fjs/types/rtti/ts/module.f.d.mts +9 -3
- package/fjs/types/rtti/ts/module.f.mjs +68 -15
- package/fjs/types/rtti/ts/proof.f.d.mts +42 -2
- package/fjs/types/rtti/ts/proof.f.mjs +139 -6
- package/fjs/types/rtti/ts/types.d.ts +164 -16
- package/fjs/types/rtti/types.d.ts +31 -3
- package/fjs/types/rtti/validate/module.f.d.mts +120 -0
- package/fjs/types/rtti/validate/module.f.mjs +351 -0
- package/fjs/types/rtti/validate/proof.f.d.mts +154 -0
- package/fjs/types/rtti/validate/proof.f.mjs +781 -0
- package/fjs/types/ts/types.d.ts +4 -0
- package/fjs/web/module.f.d.mts +115 -0
- package/fjs/web/module.f.mjs +581 -0
- package/fjs/web/proof.f.d.mts +39 -0
- package/fjs/web/proof.f.mjs +413 -0
- package/fjs/web/types.d.ts +39 -0
- package/fjs/website/browser-prepare.d.mts +5 -0
- package/fjs/website/browser-prepare.mjs +115 -0
- package/fjs/website/browser-source.d.mts +37 -0
- package/fjs/website/browser-source.mjs +202 -0
- package/fjs/website/browser-source.proof.d.mts +31 -0
- package/fjs/website/browser-source.proof.mjs +162 -0
- package/fjs/website/browser.d.mts +5 -0
- package/fjs/website/browser.mjs +8 -0
- package/fjs/website/module.f.d.mts +3 -3
- package/fjs/website/module.f.mjs +62 -9
- package/fjs/website/proof.f.d.mts +4 -0
- package/fjs/website/proof.f.mjs +18 -3
- package/package.json +2 -3
|
@@ -0,0 +1,781 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ValidationError, ValidateE, Validate } from '../common/types.ts'
|
|
3
|
+
* @import { Type } from '../types.ts'
|
|
4
|
+
* @import { Equal } from '../../ts/types.ts'
|
|
5
|
+
* @import { Ts, Unknown } from '../ts/types.ts'
|
|
6
|
+
* @import { Unknown as DjsUnknown } from '../../../djs/types.ts'
|
|
7
|
+
* @import { Assert } from '../../../asserts/types.ts'
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { validate } from './module.f.mjs'
|
|
11
|
+
import { parse } from '../parse/module.f.mjs'
|
|
12
|
+
import { toData, validate as dataValidate } from '../data/module.f.mjs'
|
|
13
|
+
import { boolean, number, string, bigint, unknown, array, never, open, record, rest, or, option } from '../module.f.mjs'
|
|
14
|
+
import { unwrap } from '../../result/module.f.mjs'
|
|
15
|
+
import { assert, assertEq, assertStructurallySame } from '../../../asserts/module.f.mjs'
|
|
16
|
+
|
|
17
|
+
/** @type {(r: readonly [string, unknown]) => void} */
|
|
18
|
+
const assertOk = ([k]) => { assertEq(k, 'ok', 'expected ok') }
|
|
19
|
+
|
|
20
|
+
/** @type {(r: readonly [string, unknown]) => void} */
|
|
21
|
+
const assertError = ([k]) => { assertEq(k, 'error', 'expected error') }
|
|
22
|
+
|
|
23
|
+
/** @type {(expected: readonly string[]) => (r: readonly [string, unknown]) => void} */
|
|
24
|
+
const assertErrorPath = expected =>
|
|
25
|
+
r => {
|
|
26
|
+
assert(r[0] === 'error', 'expected error')
|
|
27
|
+
const e = /** @type {ValidationError} */ (r[1])
|
|
28
|
+
assertStructurallySame(e.path, expected, 'unexpected error path')
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Both readers with their payload type erased, so a table can hold rows of mixed schemas. */
|
|
32
|
+
|
|
33
|
+
/** @type {(t: Type) => ValidateE} */
|
|
34
|
+
const v = t => /** @type {any} */ (validate(t))
|
|
35
|
+
|
|
36
|
+
/** @type {(t: Type) => ValidateE} */
|
|
37
|
+
const p = t => /** @type {any} */ (parse(t))
|
|
38
|
+
|
|
39
|
+
/** The data form's reader, over the same erased signature. */
|
|
40
|
+
/** @type {(t: Type) => ValidateE} */
|
|
41
|
+
const d = t => dataValidate(toData(t))
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* A rest that is its own container, so nothing about it is inline: the
|
|
45
|
+
* conversion keeps `rest: "recursiveRest"` rather than recognizing that no
|
|
46
|
+
* finite array inhabits it, and every reader accepts a hole past the prefix
|
|
47
|
+
* accordingly. It is one of the two rests {@link emptyRests} must *not*
|
|
48
|
+
* recognize.
|
|
49
|
+
*
|
|
50
|
+
* @typedef {() => readonly ['rest', readonly [_RecursiveRest], typeof never]} _RecursiveRest
|
|
51
|
+
*/
|
|
52
|
+
|
|
53
|
+
/** @type {_RecursiveRest} */
|
|
54
|
+
const recursiveRest = () => ['rest', [recursiveRest], never]
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* The other one: a pure `or` cycle. `toData(orCycleA)` **is** `never`, yet as a
|
|
58
|
+
* rest it converts to a reference and stays, so a test on the rest's own
|
|
59
|
+
* canonical data would answer the opposite of the criterion.
|
|
60
|
+
*
|
|
61
|
+
* @typedef {() => readonly ['or', _OrCycleB]} _OrCycleA
|
|
62
|
+
* @typedef {() => readonly ['or', _OrCycleA]} _OrCycleB
|
|
63
|
+
*/
|
|
64
|
+
|
|
65
|
+
/** @type {_OrCycleA} */
|
|
66
|
+
const orCycleA = () => ['or', orCycleB]
|
|
67
|
+
|
|
68
|
+
/** @type {_OrCycleB} */
|
|
69
|
+
const orCycleB = () => ['or', orCycleA]
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Two separately constructed copies of one recursive rule. Converting a rest
|
|
73
|
+
* reserves its rule name first, so the container's copy is named `r0` where
|
|
74
|
+
* converting the container alone names it `r` — which is what rules `equal`
|
|
75
|
+
* out as the comparison behind {@link emptyRests}.
|
|
76
|
+
*
|
|
77
|
+
* @typedef {() => readonly ['or', undefined, () => readonly ['array', _SelfList]]} _SelfList
|
|
78
|
+
*/
|
|
79
|
+
|
|
80
|
+
/** @type {_SelfList} */
|
|
81
|
+
const selfList0 = () => ['or', undefined, array(selfList0)]
|
|
82
|
+
|
|
83
|
+
/** @type {_SelfList} */
|
|
84
|
+
const selfList1 = () => ['or', undefined, array(selfList1)]
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* The acceptance table. Rows cover both container kinds, the closed default
|
|
88
|
+
* and a stated rest on both, the short-array rule, primitives, `or`, and
|
|
89
|
+
* misses — every reader of a schema has to answer them the same way.
|
|
90
|
+
*
|
|
91
|
+
* @type {readonly (readonly [Type, Unknown])[]}
|
|
92
|
+
*/
|
|
93
|
+
const rows = [
|
|
94
|
+
[number, 42],
|
|
95
|
+
[number, '42'],
|
|
96
|
+
[string, 42],
|
|
97
|
+
[boolean, false],
|
|
98
|
+
[bigint, 7n],
|
|
99
|
+
[unknown, { a: [1, 'x'] }],
|
|
100
|
+
[/** @type {const} */ (42), 42],
|
|
101
|
+
[/** @type {const} */ (42), 43],
|
|
102
|
+
[array(number), [1, 2, 3]],
|
|
103
|
+
[array(number), [1, 'two']],
|
|
104
|
+
[array(number), {}],
|
|
105
|
+
// an enumerable non-index key is an entry every reader walks, so it is
|
|
106
|
+
// held to the element type like any other — and a key is an index only in
|
|
107
|
+
// the canonical spelling, whatever `Number` makes of it
|
|
108
|
+
[array(number), Object.assign([1], { foo: 2 })],
|
|
109
|
+
[array(number), Object.assign([1], { foo: 'x' })],
|
|
110
|
+
[array(number), Object.assign([1], { '-1': 'x' })],
|
|
111
|
+
[array(number), Object.assign([1], { '01': 'x' })],
|
|
112
|
+
// an empty element set is the empty array, not "any number of holes": the
|
|
113
|
+
// data form normalizes such a rest away, which leaves the exact-length
|
|
114
|
+
// pattern, and the thunk readers bound the length to match
|
|
115
|
+
[array(or()), []],
|
|
116
|
+
[array(or()), new Array(1)],
|
|
117
|
+
[array(number), [, ,]],
|
|
118
|
+
[record(number), { a: 1 }],
|
|
119
|
+
[record(number), { a: 'one' }],
|
|
120
|
+
[record(number), []],
|
|
121
|
+
// the closed default, on both kinds
|
|
122
|
+
[[/** @type {const} */ (42)], [42, 'extra']],
|
|
123
|
+
[[/** @type {const} */ (42)], [42]],
|
|
124
|
+
[[/** @type {const} */ (42)], [42, undefined]],
|
|
125
|
+
[[/** @type {const} */ (42)], [42, ,]],
|
|
126
|
+
[[/** @type {const} */ (42)], Object.assign([42], { foo: 1 })],
|
|
127
|
+
[[/** @type {const} */ (42)], []],
|
|
128
|
+
[{ a: /** @type {const} */ (42) }, { a: 42, b: 'x' }],
|
|
129
|
+
[{ a: /** @type {const} */ (42) }, { a: 42 }],
|
|
130
|
+
// a key declared `unknown` is a member the schema has, so the canonical
|
|
131
|
+
// form must not drop it the way an `open` struct's is dropped
|
|
132
|
+
[{ a: unknown }, { a: 1 }],
|
|
133
|
+
[{ a: unknown }, { a: 1, b: 2 }],
|
|
134
|
+
// and the same rows under `open`, which is the form that admits them
|
|
135
|
+
[open([/** @type {const} */ (42)]), [42, 'extra']],
|
|
136
|
+
[open({ a: /** @type {const} */ (42) }), { a: 42, b: 'x' }],
|
|
137
|
+
[open([]), [1]],
|
|
138
|
+
[open({}), { a: 1 }],
|
|
139
|
+
// closedness is about *undeclared* members and leaves the short-array rule
|
|
140
|
+
// alone
|
|
141
|
+
[[number, option(string)], [42]],
|
|
142
|
+
// the rule is per position, not "the last one": every trailing position
|
|
143
|
+
// whose set admits `undefined` may be absent, so an array may stop at the
|
|
144
|
+
// last required one
|
|
145
|
+
[[number, bigint, option(string), option(null)], [2, 4n]],
|
|
146
|
+
[[number, bigint, option(string), option(null)], [2, 4n, 'x']],
|
|
147
|
+
[[number, bigint, option(string), option(null)], [2, 4n, 'x', null]],
|
|
148
|
+
[[number, bigint, option(string), option(null)], [2]],
|
|
149
|
+
[[number, bigint, option(string), option(null)], [2, 4n, 5]],
|
|
150
|
+
[{ a: number, b: option(string) }, { a: 1 }],
|
|
151
|
+
[{ a: number }, { a: 'one' }],
|
|
152
|
+
// a hole in a tuple schema is a declared position whose schema is
|
|
153
|
+
// `undefined`, so the schema's length is what it declares — the reading
|
|
154
|
+
// the data form has always had, and the one `Object.entries` lost
|
|
155
|
+
[new Array(1), [1, 2, 3]],
|
|
156
|
+
[new Array(1), new Array(1)],
|
|
157
|
+
[new Array(1), [undefined]],
|
|
158
|
+
[new Array(1), [1]],
|
|
159
|
+
[new Array(1), []],
|
|
160
|
+
[[, number], [9, 5]],
|
|
161
|
+
[[, number], [undefined, 5]],
|
|
162
|
+
// and a non-index enumerable own property is no position at all: a tuple
|
|
163
|
+
// schema is read by index, so `foo` declares nothing — which leaves a
|
|
164
|
+
// value's own `foo` an undeclared member like any other
|
|
165
|
+
[Object.assign([number], { foo: string }), [1]],
|
|
166
|
+
[Object.assign([number], { foo: string }), Object.assign([1], { foo: 'x' })],
|
|
167
|
+
[open(Object.assign([number], { foo: string })), Object.assign([1], { foo: 'x' })],
|
|
168
|
+
// a stated rest: what an undeclared member must be
|
|
169
|
+
[rest([number], string), [1, 'x', 'y']],
|
|
170
|
+
[rest([number], string), [1, 2]],
|
|
171
|
+
// a hole past the prefix is no member, so it meets no rest — which is what
|
|
172
|
+
// the `| undefined` in the rendered tail says
|
|
173
|
+
[rest([number], string), [1, ,]],
|
|
174
|
+
// An index the prototype supplies, and a key past the index range, are
|
|
175
|
+
// members too — both need in-place mutation to build, so their rows run
|
|
176
|
+
// through the same three readers in `../host.proof.mjs`.
|
|
177
|
+
[rest({ a: number }, string), { a: 1, b: 'x' }],
|
|
178
|
+
[rest({ a: number }, string), { a: 1, b: 2 }],
|
|
179
|
+
// a stated rest with nothing to answer for: the struct kind has no length,
|
|
180
|
+
// so it fits whatever the rest is
|
|
181
|
+
[rest({ a: number }, string), { a: 1 }],
|
|
182
|
+
// an unconstrained rest is `open`
|
|
183
|
+
[rest([number], unknown), [1, 'x']],
|
|
184
|
+
[rest({ a: number }, unknown), { a: 1, b: 'x' }],
|
|
185
|
+
// an empty one is the bare form, so the length is bounded again
|
|
186
|
+
[rest([number], never), [1, ,]],
|
|
187
|
+
[rest([number], or()), [1, ,]],
|
|
188
|
+
[rest([number], [or()]), [1, ,]],
|
|
189
|
+
[rest([number], [or()]), [1, 2]],
|
|
190
|
+
// …and a rest the conversion keeps is not empty, however few values it
|
|
191
|
+
// has: these two are the pair that tells the criterion from an emptiness
|
|
192
|
+
// analysis
|
|
193
|
+
[rest([number], recursiveRest), [1, ,]],
|
|
194
|
+
[rest([number], orCycleA), [1, ,]],
|
|
195
|
+
[rest([selfList0], [selfList1, never]), [undefined, ,]],
|
|
196
|
+
[or(number, string), true],
|
|
197
|
+
[or(number, string), 'hello'],
|
|
198
|
+
[option(number), undefined],
|
|
199
|
+
[option(number), null],
|
|
200
|
+
[{ user: { name: string, age: number } }, { user: { name: 'A', age: 'old' } }],
|
|
201
|
+
]
|
|
202
|
+
|
|
203
|
+
export const proof = {
|
|
204
|
+
// ── the three properties this module exists for ──────────────────────────
|
|
205
|
+
//
|
|
206
|
+
// `parse` rebuilds: it materializes a declared-but-absent member as
|
|
207
|
+
// `undefined`, drops an undeclared one, and returns a new object. For a
|
|
208
|
+
// caller whose value is a document whose bytes are its identity — a
|
|
209
|
+
// content-addressed store, a signed payload — each of those is a
|
|
210
|
+
// different document. `validate` answers the same question about the
|
|
211
|
+
// value it was handed and hands it back.
|
|
212
|
+
verbatim: {
|
|
213
|
+
// An absent optional member stays absent. `'b' in out` is the
|
|
214
|
+
// assertion, not `out.b === undefined`: `parse` satisfies the latter.
|
|
215
|
+
absentOptionalStaysAbsent: () => {
|
|
216
|
+
const schema = { a: number, b: option(string) }
|
|
217
|
+
const input = { a: 1 }
|
|
218
|
+
const out = unwrap(validate(schema)(input))
|
|
219
|
+
assert(!('b' in out), 'an absent optional member must stay absent')
|
|
220
|
+
// The contrast that motivates the module.
|
|
221
|
+
assert('b' in unwrap(parse(schema)(input)), 'parse materializes it')
|
|
222
|
+
},
|
|
223
|
+
// An undeclared member survives — where the schema admits one at all.
|
|
224
|
+
// `parse` accepts the same values and does not carry the member into
|
|
225
|
+
// what it builds.
|
|
226
|
+
undeclaredMemberSurvives: () => {
|
|
227
|
+
const schema = open({ a: number })
|
|
228
|
+
const struct = { a: 1, b: 'extra' }
|
|
229
|
+
assertStructurallySame(unwrap(validate(schema)(struct)), { a: 1, b: 'extra' })
|
|
230
|
+
assert(!('b' in unwrap(parse(schema)(struct))), 'parse drops it')
|
|
231
|
+
// The same on the other kind: a longer array keeps its tail.
|
|
232
|
+
const tuple = [1, 'extra']
|
|
233
|
+
assertStructurallySame(unwrap(validate(open([number]))(tuple)), [1, 'extra'])
|
|
234
|
+
},
|
|
235
|
+
// On success the result *is* the argument. This is the property the
|
|
236
|
+
// other two follow from, and the mirror of `../parse/proof.f.mjs`'s
|
|
237
|
+
// `freshArray` / `freshRecord`.
|
|
238
|
+
referenceIdentity: () => {
|
|
239
|
+
/** @type {(t: Type, value: Unknown) => void} */
|
|
240
|
+
const same = (t, value) => {
|
|
241
|
+
const r = v(t)(value)
|
|
242
|
+
assert(r[0] === 'ok', 'expected ok')
|
|
243
|
+
assert(Object.is(r[1], value), 'expected the original value')
|
|
244
|
+
}
|
|
245
|
+
const arr = [1, 2, 3]
|
|
246
|
+
same(array(number), arr)
|
|
247
|
+
same([number, number, number], arr)
|
|
248
|
+
same(unknown, arr)
|
|
249
|
+
const obj = { a: 1, b: 2 }
|
|
250
|
+
same(record(number), obj)
|
|
251
|
+
same({ a: number, b: number }, obj)
|
|
252
|
+
same(or(string, record(number)), obj)
|
|
253
|
+
const nested = { xs: [{ a: 1 }] }
|
|
254
|
+
same({ xs: array({ a: number }) }, nested)
|
|
255
|
+
assert(Object.is(unwrap(validate({ xs: array({ a: number }) })(nested)).xs, nested.xs),
|
|
256
|
+
'nested containers are not rebuilt either')
|
|
257
|
+
},
|
|
258
|
+
},
|
|
259
|
+
// Acceptance is `parse`'s, exactly: the two readers differ in what a
|
|
260
|
+
// success carries and in nothing else.
|
|
261
|
+
sameAcceptanceAsParse: () => {
|
|
262
|
+
for (const [t, value] of rows) {
|
|
263
|
+
const rv = v(t)(value)
|
|
264
|
+
const rp = p(t)(value)
|
|
265
|
+
assertEq(rv[0], rp[0], 'validate and parse must agree on acceptance')
|
|
266
|
+
if (rv[0] === 'error') {
|
|
267
|
+
assert(rp[0] === 'error', 'expected error')
|
|
268
|
+
assertStructurallySame(rv[1], rp[1], 'the two readers report the same error')
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
},
|
|
272
|
+
// The same table against the third reader, the data form's — the one that
|
|
273
|
+
// consumes `toData` output rather than the thunk graph. A schema denotes
|
|
274
|
+
// one set of values, so a conversion that changed which values a schema
|
|
275
|
+
// admits would be a bug in `toData`, and this is where it shows up: an
|
|
276
|
+
// open tuple whose data form was exact-length passed every row above and
|
|
277
|
+
// still disagreed here. Acceptance only: the data form reaches a value
|
|
278
|
+
// through the canonical union rather than through the schema's syntax, so
|
|
279
|
+
// it reports a miss as its own kind-wise failure rather than repeating
|
|
280
|
+
// `or`'s `no match`.
|
|
281
|
+
sameAcceptanceInTheDataForm: () => {
|
|
282
|
+
for (const [t, value] of rows) {
|
|
283
|
+
assertEq(d(t)(value)[0], p(t)(value)[0], 'the data form must accept what `parse` accepts')
|
|
284
|
+
}
|
|
285
|
+
},
|
|
286
|
+
// The two proofs above compare the readers with one another, so a
|
|
287
|
+
// regression all three shared would pass them both. The table's rows for
|
|
288
|
+
// a schema with several optional positions therefore carry their expected
|
|
289
|
+
// answer here as well.
|
|
290
|
+
//
|
|
291
|
+
// The rule is *per position*, not "the last one": a position is required
|
|
292
|
+
// exactly when its set excludes `undefined`, independently of the others.
|
|
293
|
+
// A dense prefix alone would only show that an optional *suffix* may be
|
|
294
|
+
// truncated, so the cases below also omit position 2 while position 3 is
|
|
295
|
+
// present, and omit a required position with everything after it present.
|
|
296
|
+
//
|
|
297
|
+
// Every case runs against the `open` form too. A stated rest is a separate
|
|
298
|
+
// reader on all three — `restContainerValidate`/`restContainerParse`, and
|
|
299
|
+
// its own conversion in the data form — and it widens *which values are
|
|
300
|
+
// members*, not which positions are required, so it must answer these
|
|
301
|
+
// identically. The one case where opening does change the answer is at the
|
|
302
|
+
// end.
|
|
303
|
+
optionalPositions: () => {
|
|
304
|
+
const t = /** @type {const} */ ([number, bigint, option(string), option(null)])
|
|
305
|
+
/** @type {(rtti: Type) => (check: (r: readonly [string, unknown]) => void) => (value: Unknown) => void} */
|
|
306
|
+
const every = rtti =>
|
|
307
|
+
check =>
|
|
308
|
+
value => {
|
|
309
|
+
for (const read of [v, p, d]) { check(read(rtti)(value)) }
|
|
310
|
+
}
|
|
311
|
+
for (const rtti of [t, open(t)]) {
|
|
312
|
+
const accepted = every(rtti)(assertOk)
|
|
313
|
+
const rejected = every(rtti)(assertError)
|
|
314
|
+
accepted([2, 4n]) // stops at the last required position
|
|
315
|
+
accepted([2, 4n, 'x']) // the first optional present
|
|
316
|
+
accepted([2, 4n, 'x', null]) // both present
|
|
317
|
+
// Omission is independent, not just truncation: an absent member
|
|
318
|
+
// reads as `undefined` wherever it sits, so position 2 may be
|
|
319
|
+
// missing while position 3 is present. A hole and an explicit
|
|
320
|
+
// `undefined` are the same value, so both spellings are accepted.
|
|
321
|
+
accepted([2, 4n, , null]) //< a hole at position 2
|
|
322
|
+
accepted([2, 4n, undefined, null]) //< the same value, spelled densely
|
|
323
|
+
rejected([2]) // `bigint` excludes `undefined`
|
|
324
|
+
rejected([2, 4n, 5]) // an optional that is present is still checked
|
|
325
|
+
// The mirror of the two rows above: `bigint` excludes `undefined`,
|
|
326
|
+
// so omitting position 1 fails however much of the rest is present.
|
|
327
|
+
rejected([2, , 'x', null]) //< a hole at position 1
|
|
328
|
+
}
|
|
329
|
+
// What opening does change: an element past the declared positions is
|
|
330
|
+
// a member of the open set and not of the bare, closed one.
|
|
331
|
+
const extra = /** @type {const} */ ([2, 4n, 'x', null, 'extra'])
|
|
332
|
+
every(t)(assertError)(extra)
|
|
333
|
+
every(open(t))(assertOk)(extra)
|
|
334
|
+
// How stopping short composes with running long: the two rules are
|
|
335
|
+
// independent, so the open form's accepted lengths run from the last
|
|
336
|
+
// required position upwards without a gap or a cap — 2, 3, 4, 5, and
|
|
337
|
+
// on. The bare form caps the top at the declared count and leaves the
|
|
338
|
+
// bottom where it is.
|
|
339
|
+
every(open(t))(assertOk)([2, 4n, 'x', null, 'a', 'b'])
|
|
340
|
+
},
|
|
341
|
+
// An interior omittable position may be absent with a *required* position
|
|
342
|
+
// after it, which is the sharpest witness that absence is per position
|
|
343
|
+
// rather than truncation: `optionalPositions`'s hole is followed by
|
|
344
|
+
// another omittable position, so it cannot say this. Absence is also
|
|
345
|
+
// positional, not a shift — `[, 5]` holds `5` at position 1 and is
|
|
346
|
+
// accepted, while `[5]` holds it at position 0 and is not.
|
|
347
|
+
interiorOptionBeforeRequired: () => {
|
|
348
|
+
const t = /** @type {const} */ ([option(string), number])
|
|
349
|
+
/** @type {(rtti: Type) => (check: (r: readonly [string, unknown]) => void) => (value: Unknown) => void} */
|
|
350
|
+
const every = rtti =>
|
|
351
|
+
check =>
|
|
352
|
+
value => {
|
|
353
|
+
for (const read of [v, p, d]) { check(read(rtti)(value)) }
|
|
354
|
+
}
|
|
355
|
+
// Open too, for the reason `optionalPositions` runs both: a stated
|
|
356
|
+
// rest is its own reader on all three, and this schema is not one of
|
|
357
|
+
// the shapes the trailing-option cases there already put through it.
|
|
358
|
+
for (const rtti of [t, open(t)]) {
|
|
359
|
+
every(rtti)(assertOk)([, 5]) //< a hole at position 0
|
|
360
|
+
every(rtti)(assertOk)([undefined, 5]) //< the same value, spelled densely
|
|
361
|
+
every(rtti)(assertOk)(['x', 5])
|
|
362
|
+
every(rtti)(assertError)([5]) //< `number` at position 1 is required
|
|
363
|
+
}
|
|
364
|
+
// And the one answer opening changes here as well.
|
|
365
|
+
const extra = /** @type {const} */ (['x', 5, 'extra'])
|
|
366
|
+
every(t)(assertError)(extra)
|
|
367
|
+
every(open(t))(assertOk)(extra)
|
|
368
|
+
},
|
|
369
|
+
// The two tables above pin that the three readers *agree*; these pin what
|
|
370
|
+
// they agree on, which is what the changelog entry claims.
|
|
371
|
+
sparseTuple: {
|
|
372
|
+
holeIsDeclaredUndefined: () => {
|
|
373
|
+
assertError(validate([, number])([9, 5]))
|
|
374
|
+
assertOk(validate([, number])([undefined, 5]))
|
|
375
|
+
assertError(validate(new Array(1))([1, 2, 3]))
|
|
376
|
+
assertOk(validate(new Array(1))([undefined]))
|
|
377
|
+
},
|
|
378
|
+
// A hole is a position, so a sparse schema is as long as it looks:
|
|
379
|
+
// `declared.length` is the schema's length, not its key count.
|
|
380
|
+
arityIsTheSchemaLength: () => {
|
|
381
|
+
assertOk(validate(new Array(1))([undefined]))
|
|
382
|
+
assertError(validate(new Array(1))([1]))
|
|
383
|
+
},
|
|
384
|
+
// A tuple schema is read by index, so `foo` declares no position — it
|
|
385
|
+
// is not matched against anything, and on the value side it is an
|
|
386
|
+
// undeclared member like any other, which the closed form rejects and
|
|
387
|
+
// `open` admits.
|
|
388
|
+
nonIndexPropertyIsNotDeclared: () => {
|
|
389
|
+
const schema = Object.assign([number], { foo: string })
|
|
390
|
+
assertOk(validate(schema)([1]))
|
|
391
|
+
assertError(validate(schema)(Object.assign([1], { foo: 'x' })))
|
|
392
|
+
assertOk(validate(open(schema))(Object.assign([1], { foo: 'x' })))
|
|
393
|
+
assertError(validate(schema)(['x']))
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
boolean: {
|
|
397
|
+
ok: () => {
|
|
398
|
+
/** @typedef {Assert<Equal<Ts<typeof boolean>, boolean>>} _RoundTrip */
|
|
399
|
+
assertOk(validate(boolean)(true))
|
|
400
|
+
assertOk(validate(boolean)(false))
|
|
401
|
+
},
|
|
402
|
+
error: () => {
|
|
403
|
+
assertError(validate(boolean)(0))
|
|
404
|
+
assertError(validate(boolean)('true'))
|
|
405
|
+
assertError(validate(boolean)(null))
|
|
406
|
+
},
|
|
407
|
+
},
|
|
408
|
+
number: {
|
|
409
|
+
ok: () => {
|
|
410
|
+
/** @typedef {Assert<Equal<Ts<typeof number>, number>>} _RoundTrip */
|
|
411
|
+
assertOk(validate(number)(42))
|
|
412
|
+
},
|
|
413
|
+
error: () => {
|
|
414
|
+
assertError(validate(number)('42'))
|
|
415
|
+
assertError(validate(number)(42n))
|
|
416
|
+
},
|
|
417
|
+
},
|
|
418
|
+
string: {
|
|
419
|
+
ok: () => {
|
|
420
|
+
/** @typedef {Assert<Equal<Ts<typeof string>, string>>} _RoundTrip */
|
|
421
|
+
assertOk(validate(string)('hello'))
|
|
422
|
+
},
|
|
423
|
+
error: () => {
|
|
424
|
+
assertError(validate(string)(42))
|
|
425
|
+
assertError(validate(string)(null))
|
|
426
|
+
},
|
|
427
|
+
},
|
|
428
|
+
bigint: {
|
|
429
|
+
ok: () => {
|
|
430
|
+
/** @typedef {Assert<Equal<Ts<typeof bigint>, bigint>>} _RoundTrip */
|
|
431
|
+
assertOk(validate(bigint)(4n))
|
|
432
|
+
},
|
|
433
|
+
error: () => {
|
|
434
|
+
assertError(validate(bigint)(4))
|
|
435
|
+
assertError(validate(bigint)('4'))
|
|
436
|
+
},
|
|
437
|
+
},
|
|
438
|
+
unknown: {
|
|
439
|
+
ok: () => {
|
|
440
|
+
/** @typedef {Assert<Equal<Ts<typeof unknown>, DjsUnknown>>} _RoundTrip */
|
|
441
|
+
assertOk(validate(unknown)(null))
|
|
442
|
+
assertOk(validate(unknown)(42))
|
|
443
|
+
assertOk(validate(unknown)('hello'))
|
|
444
|
+
assertOk(validate(unknown)(true))
|
|
445
|
+
assertOk(validate(unknown)({}))
|
|
446
|
+
assertOk(validate(unknown)([]))
|
|
447
|
+
},
|
|
448
|
+
},
|
|
449
|
+
const: {
|
|
450
|
+
null: {
|
|
451
|
+
ok: () => assertOk(validate(null)(null)),
|
|
452
|
+
error: () => {
|
|
453
|
+
assertError(validate(null)(undefined))
|
|
454
|
+
assertError(validate(null)(0))
|
|
455
|
+
},
|
|
456
|
+
},
|
|
457
|
+
undefined: {
|
|
458
|
+
ok: () => assertOk(validate(undefined)(undefined)),
|
|
459
|
+
error: () => assertError(validate(undefined)(null)),
|
|
460
|
+
},
|
|
461
|
+
number: {
|
|
462
|
+
ok: () => assertOk(validate(42)(42)),
|
|
463
|
+
error: () => assertError(validate(42)(43)),
|
|
464
|
+
},
|
|
465
|
+
nan: {
|
|
466
|
+
ok: () => assertOk(validate(NaN)(NaN)),
|
|
467
|
+
error: () => {
|
|
468
|
+
assertError(validate(NaN)(0))
|
|
469
|
+
assertError(validate(0)(NaN))
|
|
470
|
+
assertError(validate(42)(NaN))
|
|
471
|
+
},
|
|
472
|
+
},
|
|
473
|
+
infinity: {
|
|
474
|
+
ok: () => {
|
|
475
|
+
assertOk(validate(Infinity)(Infinity))
|
|
476
|
+
assertOk(validate(-Infinity)(-Infinity))
|
|
477
|
+
},
|
|
478
|
+
error: () => {
|
|
479
|
+
assertError(validate(Infinity)(-Infinity))
|
|
480
|
+
assertError(validate(Infinity)(0))
|
|
481
|
+
},
|
|
482
|
+
},
|
|
483
|
+
signedZero: {
|
|
484
|
+
// `Object.is` distinguishes +0 and -0; `===` treats them equal.
|
|
485
|
+
distinct: () => {
|
|
486
|
+
assertError(validate(0)(-0))
|
|
487
|
+
assertError(validate(-0)(0))
|
|
488
|
+
},
|
|
489
|
+
self: () => {
|
|
490
|
+
assertOk(validate(0)(0))
|
|
491
|
+
assertOk(validate(-0)(-0))
|
|
492
|
+
},
|
|
493
|
+
},
|
|
494
|
+
string: {
|
|
495
|
+
ok: () => assertOk(validate('hello')('hello')),
|
|
496
|
+
error: () => assertError(validate('hello')('world')),
|
|
497
|
+
},
|
|
498
|
+
bigint: {
|
|
499
|
+
ok: () => assertOk(validate(7n)(7n)),
|
|
500
|
+
error: () => assertError(validate(7n)(8n)),
|
|
501
|
+
},
|
|
502
|
+
boolean: {
|
|
503
|
+
ok: () => assertOk(validate(true)(true)),
|
|
504
|
+
error: () => assertError(validate(true)(false)),
|
|
505
|
+
},
|
|
506
|
+
tuple: {
|
|
507
|
+
ok: () => assertStructurallySame(
|
|
508
|
+
unwrap(validate([42, 'hello'])([42, 'hello'])), [42, 'hello']),
|
|
509
|
+
// A tuple is CLOSED, so a longer array is not one of its values —
|
|
510
|
+
// see "Structs and tuples are closed" in ../README.md. Under
|
|
511
|
+
// `open` it is, and it comes back as it went in.
|
|
512
|
+
extraItemsRejected: () => {
|
|
513
|
+
const long = [42, 1, 2, 3]
|
|
514
|
+
assertError(validate([42])(long))
|
|
515
|
+
assert(Object.is(unwrap(validate(open([42]))(long)), long),
|
|
516
|
+
'the longer array comes back as it went in')
|
|
517
|
+
},
|
|
518
|
+
// An absent member reads as `undefined`, so a position is required
|
|
519
|
+
// exactly when its set excludes `undefined` — and nothing is
|
|
520
|
+
// filled in, so the array keeps its length.
|
|
521
|
+
shortArrayKeepsItsLength: () => {
|
|
522
|
+
const short = [42]
|
|
523
|
+
const out = unwrap(validate([number, option(string)])(short))
|
|
524
|
+
assert(Object.is(out, short), 'expected the original array')
|
|
525
|
+
assertEq(short.length, 1, 'no gap is filled')
|
|
526
|
+
},
|
|
527
|
+
empty: () => assertOk(validate([])([])),
|
|
528
|
+
error: () => {
|
|
529
|
+
assertError(validate([42])([99]))
|
|
530
|
+
assertError(validate([42])({}))
|
|
531
|
+
// `42` excludes `undefined`, so position 0 is required.
|
|
532
|
+
assertError(validate([42])([]))
|
|
533
|
+
},
|
|
534
|
+
},
|
|
535
|
+
struct: {
|
|
536
|
+
// `validate` takes a `const` type parameter, so a struct literal
|
|
537
|
+
// keeps its literal members without an `@type {const}` cast at the
|
|
538
|
+
// call site: this is a reader for `{ a: 42, b: 'hello' }`, not for
|
|
539
|
+
// `{ a: number, b: string }`. Dropping the modifier is what makes
|
|
540
|
+
// the assertion fail.
|
|
541
|
+
ok: () => {
|
|
542
|
+
const v = validate({ a: 42, b: 'hello' })
|
|
543
|
+
/** @typedef {Assert<Equal<typeof v, Validate<{ readonly a: 42, readonly b: 'hello' }>>>} _ConstParameter */
|
|
544
|
+
assertStructurallySame(unwrap(v({ a: 42, b: 'hello' })), { a: 42, b: 'hello' })
|
|
545
|
+
},
|
|
546
|
+
error: () => {
|
|
547
|
+
assertError(validate({ a: 42 })({ a: 99 }))
|
|
548
|
+
assertError(validate({ a: 42 })([]))
|
|
549
|
+
},
|
|
550
|
+
},
|
|
551
|
+
},
|
|
552
|
+
array: {
|
|
553
|
+
empty: () => {
|
|
554
|
+
const input = /** @type {readonly number[]} */ ([])
|
|
555
|
+
assert(Object.is(unwrap(validate(array(number))(input)), input), 'expected the original array')
|
|
556
|
+
},
|
|
557
|
+
ok: () => assertStructurallySame(unwrap(validate(array(number))([1, 2, 3])), [1, 2, 3]),
|
|
558
|
+
error: () => {
|
|
559
|
+
assertError(validate(array(number))([1, 'two', 3]))
|
|
560
|
+
assertError(validate(array(number))({}))
|
|
561
|
+
assertError(validate(array(number))(null))
|
|
562
|
+
},
|
|
563
|
+
nested: () => {
|
|
564
|
+
assertOk(validate(array(array(boolean)))([[true, false], [false]]))
|
|
565
|
+
assertError(validate(array(array(boolean)))([[true, 42]]))
|
|
566
|
+
},
|
|
567
|
+
},
|
|
568
|
+
record: {
|
|
569
|
+
empty: () => {
|
|
570
|
+
const input = /** @type {{ readonly[K in string]?: number }} */ ({})
|
|
571
|
+
assert(Object.is(unwrap(validate(record(number))(input)), input), 'expected the original record')
|
|
572
|
+
},
|
|
573
|
+
ok: () => assertStructurallySame(
|
|
574
|
+
unwrap(validate(record(string))({ a: 'hello', b: 'world' })),
|
|
575
|
+
{ a: 'hello', b: 'world' },
|
|
576
|
+
),
|
|
577
|
+
error: () => {
|
|
578
|
+
assertError(validate(record(number))({ a: 1, b: 'two' }))
|
|
579
|
+
assertError(validate(record(number))(null))
|
|
580
|
+
assertError(validate(record(number))([]))
|
|
581
|
+
},
|
|
582
|
+
},
|
|
583
|
+
constThunk: {
|
|
584
|
+
primitive: () => {
|
|
585
|
+
const t = () => /** @type {const} */ (['const', 7n])
|
|
586
|
+
assertOk(validate(t)(7n))
|
|
587
|
+
assertError(validate(t)(8n))
|
|
588
|
+
},
|
|
589
|
+
},
|
|
590
|
+
or: {
|
|
591
|
+
consts: {
|
|
592
|
+
ok: () => {
|
|
593
|
+
const t = or(false, 42, 'hello')
|
|
594
|
+
assertOk(validate(t)(false))
|
|
595
|
+
assertOk(validate(t)(42))
|
|
596
|
+
assertOk(validate(t)('hello'))
|
|
597
|
+
},
|
|
598
|
+
error: () => {
|
|
599
|
+
const t = or(false, 42, 'hello')
|
|
600
|
+
assertError(validate(t)(true))
|
|
601
|
+
assertError(validate(t)(43))
|
|
602
|
+
assertError(validate(t)('world'))
|
|
603
|
+
assertError(validate(t)(null))
|
|
604
|
+
},
|
|
605
|
+
},
|
|
606
|
+
thunks: {
|
|
607
|
+
ok: () => {
|
|
608
|
+
const t = or(number, string)
|
|
609
|
+
assertOk(validate(t)(42))
|
|
610
|
+
assertOk(validate(t)('hello'))
|
|
611
|
+
},
|
|
612
|
+
error: () => {
|
|
613
|
+
const t = or(number, string)
|
|
614
|
+
assertError(validate(t)(true))
|
|
615
|
+
assertError(validate(t)(null))
|
|
616
|
+
},
|
|
617
|
+
},
|
|
618
|
+
// The first matching variant wins, and it returns the value itself —
|
|
619
|
+
// so, unlike `parse`, which variant matched is not observable in the
|
|
620
|
+
// result. `parse` here returns a length-1 array; `validate` returns
|
|
621
|
+
// the length-3 one it was given.
|
|
622
|
+
firstMatchWins: () => {
|
|
623
|
+
const t = or(open([number]), array(number))
|
|
624
|
+
const input = [1, 2, 3]
|
|
625
|
+
assert(Object.is(unwrap(validate(t)(input)), input), 'expected the original array')
|
|
626
|
+
assertStructurallySame(unwrap(parse(t)(input)), [1])
|
|
627
|
+
},
|
|
628
|
+
},
|
|
629
|
+
option: {
|
|
630
|
+
ok: () => {
|
|
631
|
+
const t = option(number)
|
|
632
|
+
assertOk(validate(t)(42))
|
|
633
|
+
assertOk(validate(t)(undefined))
|
|
634
|
+
},
|
|
635
|
+
error: () => {
|
|
636
|
+
const t = option(number)
|
|
637
|
+
assertError(validate(t)(null))
|
|
638
|
+
assertError(validate(t)('42'))
|
|
639
|
+
},
|
|
640
|
+
},
|
|
641
|
+
path: {
|
|
642
|
+
rootMismatch: () => assertErrorPath([])(validate(number)('not a number')),
|
|
643
|
+
arrayIndex: () => assertErrorPath(['1'])(validate(array(number))([1, 'two', 3])),
|
|
644
|
+
recordKey: () => assertErrorPath(['b'])(validate(record(number))({ a: 1, b: 'two', c: 3 })),
|
|
645
|
+
nestedArray: () => assertErrorPath(['0', '1'])(
|
|
646
|
+
validate(array(array(number)))([[1, 'x'], [2, 3]])
|
|
647
|
+
),
|
|
648
|
+
tupleIndex: () => assertErrorPath(['1'])(
|
|
649
|
+
validate([number, number])([1, 'two'])
|
|
650
|
+
),
|
|
651
|
+
structKey: () => assertErrorPath(['b'])(
|
|
652
|
+
validate({ a: number, b: number })({ a: 1, b: 'two' })
|
|
653
|
+
),
|
|
654
|
+
deepStruct: () => {
|
|
655
|
+
const schema = /** @type {const} */ ({ user: { name: string, age: number } })
|
|
656
|
+
assertErrorPath(['user', 'age'])(validate(schema)({ user: { name: 'A', age: 'old' } }))
|
|
657
|
+
},
|
|
658
|
+
recursiveSchema: () => {
|
|
659
|
+
/** @typedef {readonly _A[]} _A */
|
|
660
|
+
const list = () => /** @type {const} */ (['array', list])
|
|
661
|
+
const r = validate(list)([/** @type {_A} */ (/** @type {unknown} */ ([[42]]))])
|
|
662
|
+
assertErrorPath(['0', '0', '0'])(r)
|
|
663
|
+
},
|
|
664
|
+
orRoot: () => assertErrorPath([])(validate(or(number, string))(true)),
|
|
665
|
+
},
|
|
666
|
+
recursive: {
|
|
667
|
+
arrayOfArrays: () => {
|
|
668
|
+
/** @typedef {readonly _A[]} _A */
|
|
669
|
+
const list = () => /** @type {const} */ (['array', list])
|
|
670
|
+
/** @typedef {Assert<Equal<_A, Ts<typeof list>>>} _ListRoundTrip */
|
|
671
|
+
const x = validate(list)
|
|
672
|
+
assertOk(x([]))
|
|
673
|
+
assertOk(x([[], []]))
|
|
674
|
+
assertOk(x([[[], []], []]))
|
|
675
|
+
assertError(x([42]))
|
|
676
|
+
assertError(x(null))
|
|
677
|
+
},
|
|
678
|
+
recordOfRecords: () => {
|
|
679
|
+
const tree = () => /** @type {const} */ (['record', tree])
|
|
680
|
+
/** @typedef {{ readonly[K in string]?: _A }} _A */
|
|
681
|
+
/** @typedef {Assert<Equal<_A, Ts<typeof tree>>>} _TreeRoundTrip */
|
|
682
|
+
const x = validate(tree)
|
|
683
|
+
assertOk(x({}))
|
|
684
|
+
assertOk(x({ a: {}, b: { c: {} } }))
|
|
685
|
+
assertError(x({ a: 42 }))
|
|
686
|
+
},
|
|
687
|
+
},
|
|
688
|
+
// A stated rest widens acceptance and nothing else: a success still carries
|
|
689
|
+
// the very value it was given. The acceptance half is in the table above,
|
|
690
|
+
// run through all three readers; what is left to pin here is that
|
|
691
|
+
// `validate` stays verbatim on that form too.
|
|
692
|
+
rest: {
|
|
693
|
+
verbatim: () => {
|
|
694
|
+
const value = [1, 'x', 'y']
|
|
695
|
+
assert(Object.is(unwrap(validate(rest([number], string))(value)), value),
|
|
696
|
+
'the value comes back as it went in')
|
|
697
|
+
const struct = { a: 1, b: 'x' }
|
|
698
|
+
assert(Object.is(unwrap(validate(rest({ a: number }, string))(struct)), struct),
|
|
699
|
+
'and so does an object with rest-matching keys')
|
|
700
|
+
},
|
|
701
|
+
// An absent optional member still stays absent — a container's rest
|
|
702
|
+
// says nothing about a member it declares.
|
|
703
|
+
absentOptionalStaysAbsent: () => {
|
|
704
|
+
const out = unwrap(validate({ a: number, b: option(string) })({ a: 1 }))
|
|
705
|
+
assert(!('b' in out), 'an absent optional member must stay absent')
|
|
706
|
+
},
|
|
707
|
+
path: () => {
|
|
708
|
+
assertErrorPath(['1'])(validate([number, number])([1, 'two']))
|
|
709
|
+
assertErrorPath(['b'])(validate(rest({ a: number }, string))({ a: 1, b: 2 }))
|
|
710
|
+
// The rejection of an undeclared member is about the container, so
|
|
711
|
+
// it is reported at the container.
|
|
712
|
+
assertErrorPath([])(validate({ a: number })({ a: 1, b: 2 }))
|
|
713
|
+
},
|
|
714
|
+
notAContainer: () => {
|
|
715
|
+
assertError(validate(rest([number], string))({}))
|
|
716
|
+
assertError(validate(rest({ a: number }, string))([]))
|
|
717
|
+
},
|
|
718
|
+
},
|
|
719
|
+
// The three-reader table pins that the readers *agree* on the empty-rest
|
|
720
|
+
// criterion; these assert the verdict outright, since a row alone passes
|
|
721
|
+
// whenever all three move together. Each spelling is named rather than
|
|
722
|
+
// covered by "an independently constructed empty rest": an implementation
|
|
723
|
+
// recognizing empty unions but not `[or()]` passes the `or()` row while
|
|
724
|
+
// keeping that spelling's disagreement, and one keyed on the exported
|
|
725
|
+
// `never`'s identity passes the converse.
|
|
726
|
+
emptyRests: {
|
|
727
|
+
dropped: () => {
|
|
728
|
+
for (const r of [never, or(), [or()]]) {
|
|
729
|
+
assertError(validate(rest([number], r))([42, ,]))
|
|
730
|
+
}
|
|
731
|
+
// The name collision: converting the rest reserves `r`, so the
|
|
732
|
+
// container's own rule is named `r0` — which `equal` reads as a
|
|
733
|
+
// different schema and `subset` both ways does not.
|
|
734
|
+
assertError(v(rest([selfList0], [selfList1, never]))([undefined, ,]))
|
|
735
|
+
},
|
|
736
|
+
kept: () => {
|
|
737
|
+
// A rest the conversion keeps is not empty however few values it
|
|
738
|
+
// has: `recursiveRest` catches an emptiness analysis that reaches
|
|
739
|
+
// container cycles, `orCycleA` one that tests the rest's own
|
|
740
|
+
// canonical data, and `unknown` one that reads the absence of a
|
|
741
|
+
// `rest` key as elimination.
|
|
742
|
+
// `v` rather than `validate`: `Ts<>` walks a recursive schema
|
|
743
|
+
// structurally, and these two exist to be recursive.
|
|
744
|
+
assertOk(v(rest([number], recursiveRest))([42, ,]))
|
|
745
|
+
assertOk(v(rest([number], orCycleA))([42, ,]))
|
|
746
|
+
assertOk(validate(open([]))([1]))
|
|
747
|
+
},
|
|
748
|
+
},
|
|
749
|
+
// The two ways an array can reach past a closed prefix, told apart. The
|
|
750
|
+
// acceptance table pins that the three readers agree on both; these assert
|
|
751
|
+
// the verdict, since a row alone passes whenever all three move together.
|
|
752
|
+
// The hole is rejected by *length* — it is no member, so the member check
|
|
753
|
+
// alone would let it through — and the explicit `undefined` by the member
|
|
754
|
+
// check, since it is a member and the schema declares no position for it.
|
|
755
|
+
// A closed tuple therefore has exactly one spelling per value.
|
|
756
|
+
beyondAClosedPrefix: () => {
|
|
757
|
+
for (const read of [v, p, d]) {
|
|
758
|
+
assertError(read([/** @type {const} */ (42)])([42, ,]))
|
|
759
|
+
assertError(read([/** @type {const} */ (42)])([42, undefined]))
|
|
760
|
+
assertOk(read([/** @type {const} */ (42)])([42]))
|
|
761
|
+
}
|
|
762
|
+
},
|
|
763
|
+
// The walk is bounded by what the value and its prototypes carry rather
|
|
764
|
+
// than by `length`: a sparse array as long as the index space allows
|
|
765
|
+
// answers at once, where materializing the range exhausted memory first.
|
|
766
|
+
// The verdicts are the ordinary ones — a bare tuple is too short for it,
|
|
767
|
+
// a rest with nothing present past the prefix admits it.
|
|
768
|
+
lengthDoesNotBoundTheWalk: () => {
|
|
769
|
+
const big = new Array(2 ** 32 - 1)
|
|
770
|
+
assertError(v([option(string)])(big))
|
|
771
|
+
assertOk(v(rest([], string))(big))
|
|
772
|
+
},
|
|
773
|
+
arrayOptional: () => {
|
|
774
|
+
const a = /** @type {const} */([number, option(string)])
|
|
775
|
+
const v = validate(a)
|
|
776
|
+
assertOk(v([5]))
|
|
777
|
+
assertError(v(["n"]))
|
|
778
|
+
assertOk(v([6, "3"]))
|
|
779
|
+
assertError(v([6, 9]))
|
|
780
|
+
}
|
|
781
|
+
}
|