functionalscript 0.46.0 → 0.46.1
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.
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation of unknown values against RTTI schemas — the verbatim
|
|
3
|
+
* reader.
|
|
4
|
+
*
|
|
5
|
+
* The main entry point is `validate(rtti)`, which takes a schema `Type` and
|
|
6
|
+
* returns a `Validate<T>` function. When called with an unknown value, it
|
|
7
|
+
* returns a `Result` that is either `['ok', value]` — **the value it was
|
|
8
|
+
* given** — or `['error', { path, message }]`.
|
|
9
|
+
*
|
|
10
|
+
* ## What distinguishes it from `parse`
|
|
11
|
+
*
|
|
12
|
+
* `../parse/module.f.mjs` answers "read this value as `T`" and builds a fresh
|
|
13
|
+
* value holding exactly what the schema declares. `validate` answers "is this
|
|
14
|
+
* value a `T`?" about the value itself, so on success the caller keeps the
|
|
15
|
+
* object it passed in — same reference, same members, same serialization:
|
|
16
|
+
*
|
|
17
|
+
* ```js
|
|
18
|
+
* const schema = { a: number, b: option(string) }
|
|
19
|
+
* parse(schema)({ a: 1, extra: 'x' }) // ['ok', { a: 1, b: undefined }]
|
|
20
|
+
* validate(schema)({ a: 1, extra: 'x' }) // ['ok', { a: 1, extra: 'x' }]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The two agree on **acceptance**: every value one accepts the other accepts,
|
|
24
|
+
* with the same error `path` and `message`. They differ only in what a success
|
|
25
|
+
* carries. `./proof.f.mjs` pins that agreement as a table rather than leaving
|
|
26
|
+
* it to convention. Which reader a caller wants, and why both exist, is in
|
|
27
|
+
* "The two schema-form readers" in `../README.md`.
|
|
28
|
+
*
|
|
29
|
+
* ## Structs and tuples are open
|
|
30
|
+
*
|
|
31
|
+
* Openness is the shared rule, not a `parse` detail — see "Structs and tuples
|
|
32
|
+
* are open" in `../README.md`. `validate` iterates the *schema's* entries, so
|
|
33
|
+
* an undeclared key or a longer array is never visited: it is accepted, and it
|
|
34
|
+
* is still there afterwards because the value is returned as-is. An absent
|
|
35
|
+
* member reads as `undefined`, so a member is required exactly when its set
|
|
36
|
+
* excludes `undefined`.
|
|
37
|
+
*
|
|
38
|
+
* **Do not add a length check for tuples here.** `Ts<readonly [42]>` is the
|
|
39
|
+
* exact tuple only because TypeScript cannot express the open one (see
|
|
40
|
+
* `../ts/types.ts` `TupleTs`); reading that rendering as the value model is
|
|
41
|
+
* what produced #1622, whose check lived in this module's ancestor and was
|
|
42
|
+
* deleted with it. A schema that wants exact members says so — see
|
|
43
|
+
* `../todo/close-type.md`.
|
|
44
|
+
*
|
|
45
|
+
* ## Dispatch strategy
|
|
46
|
+
*
|
|
47
|
+
* Schema recognition is delegated to `visit` in `../common/module.f.mjs`,
|
|
48
|
+
* which routes each `Type` variant to a handler in the `Visitor` record
|
|
49
|
+
* defined below; nothing here walks the `Type` ADT itself. The container
|
|
50
|
+
* handlers drive `eachEntry` in its no-accumulator mode — the mode its JSDoc
|
|
51
|
+
* describes for "a caller whose whole question is 'did every entry
|
|
52
|
+
* succeed?'" — so a validation allocates nothing per entry. The data form's
|
|
53
|
+
* `validate` (`../data/module.f.mjs`) is the same shape over `Data`.
|
|
54
|
+
*
|
|
55
|
+
* ## Recursion safety
|
|
56
|
+
*
|
|
57
|
+
* For `array` and `record` schemas, the inner item validator is instantiated
|
|
58
|
+
* lazily — only after confirming the container is non-empty. This prevents
|
|
59
|
+
* infinite recursion when validating recursive schemas like
|
|
60
|
+
* `const list = () => ['array', list]`.
|
|
61
|
+
*
|
|
62
|
+
* See `./types.ts` for the `Path`/`Result`/`Validate`/`ValidationError`
|
|
63
|
+
* type-level API.
|
|
64
|
+
*
|
|
65
|
+
* @module
|
|
66
|
+
*
|
|
67
|
+
* @import { Unknown } from '../ts/types.ts'
|
|
68
|
+
* @import { Info1, Struct, Tag1, Tuple, Type } from '../types.ts'
|
|
69
|
+
* @import { Container, IsContainer, Validate, Visitor } from '../common/types.ts'
|
|
70
|
+
*/
|
|
71
|
+
import type { Type } from '../types.ts';
|
|
72
|
+
import type { Validate } from '../common/types.ts';
|
|
73
|
+
/**
|
|
74
|
+
* Creates a validator function for the given RTTI schema: a `Thunk` for
|
|
75
|
+
* tag-based schemas, or a `Const` (primitive literal, tuple, or struct) for
|
|
76
|
+
* exact-value schemas.
|
|
77
|
+
*
|
|
78
|
+
* The returned function takes an unknown value and returns either
|
|
79
|
+
* `['ok', value]` — the very value it was given, not a reconstruction — or
|
|
80
|
+
* `['error', { path, message }]` describing the failure location.
|
|
81
|
+
*
|
|
82
|
+
* Use it when the question is "is this value of this shape?" and the value has
|
|
83
|
+
* to survive the question intact. Use `../parse/module.f.mjs` when the answer
|
|
84
|
+
* wanted is a value built to the schema.
|
|
85
|
+
*
|
|
86
|
+
* @example
|
|
87
|
+
* ```js
|
|
88
|
+
* const v = validate(array(number))
|
|
89
|
+
* const input = [1, 2, 3]
|
|
90
|
+
* v(input) // ['ok', input] — the same array, not a copy
|
|
91
|
+
* v([1, 'two']) // ['error', { path: ['1'], message: 'unexpected value' }]
|
|
92
|
+
*
|
|
93
|
+
* // open, and the extras are still there afterwards
|
|
94
|
+
* validate([number, number])([1, 2, 3]) // ['ok', [1, 2, 3]]
|
|
95
|
+
* validate({ a: number })({ a: 1, b: 2 }) // ['ok', { a: 1, b: 2 }]
|
|
96
|
+
*
|
|
97
|
+
* // an absent optional member stays absent
|
|
98
|
+
* validate({ a: number, b: option(string) })({ a: 1 }) // ['ok', { a: 1 }]
|
|
99
|
+
* ```
|
|
100
|
+
*
|
|
101
|
+
* @type {<T extends Type>(rtti: T) => Validate<T>}
|
|
102
|
+
*/
|
|
103
|
+
export declare const validate: <T extends Type>(rtti: T) => Validate<T>;
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Runtime validation of unknown values against RTTI schemas — the verbatim
|
|
3
|
+
* reader.
|
|
4
|
+
*
|
|
5
|
+
* The main entry point is `validate(rtti)`, which takes a schema `Type` and
|
|
6
|
+
* returns a `Validate<T>` function. When called with an unknown value, it
|
|
7
|
+
* returns a `Result` that is either `['ok', value]` — **the value it was
|
|
8
|
+
* given** — or `['error', { path, message }]`.
|
|
9
|
+
*
|
|
10
|
+
* ## What distinguishes it from `parse`
|
|
11
|
+
*
|
|
12
|
+
* `../parse/module.f.mjs` answers "read this value as `T`" and builds a fresh
|
|
13
|
+
* value holding exactly what the schema declares. `validate` answers "is this
|
|
14
|
+
* value a `T`?" about the value itself, so on success the caller keeps the
|
|
15
|
+
* object it passed in — same reference, same members, same serialization:
|
|
16
|
+
*
|
|
17
|
+
* ```js
|
|
18
|
+
* const schema = { a: number, b: option(string) }
|
|
19
|
+
* parse(schema)({ a: 1, extra: 'x' }) // ['ok', { a: 1, b: undefined }]
|
|
20
|
+
* validate(schema)({ a: 1, extra: 'x' }) // ['ok', { a: 1, extra: 'x' }]
|
|
21
|
+
* ```
|
|
22
|
+
*
|
|
23
|
+
* The two agree on **acceptance**: every value one accepts the other accepts,
|
|
24
|
+
* with the same error `path` and `message`. They differ only in what a success
|
|
25
|
+
* carries. `./proof.f.mjs` pins that agreement as a table rather than leaving
|
|
26
|
+
* it to convention. Which reader a caller wants, and why both exist, is in
|
|
27
|
+
* "The two schema-form readers" in `../README.md`.
|
|
28
|
+
*
|
|
29
|
+
* ## Structs and tuples are open
|
|
30
|
+
*
|
|
31
|
+
* Openness is the shared rule, not a `parse` detail — see "Structs and tuples
|
|
32
|
+
* are open" in `../README.md`. `validate` iterates the *schema's* entries, so
|
|
33
|
+
* an undeclared key or a longer array is never visited: it is accepted, and it
|
|
34
|
+
* is still there afterwards because the value is returned as-is. An absent
|
|
35
|
+
* member reads as `undefined`, so a member is required exactly when its set
|
|
36
|
+
* excludes `undefined`.
|
|
37
|
+
*
|
|
38
|
+
* **Do not add a length check for tuples here.** `Ts<readonly [42]>` is the
|
|
39
|
+
* exact tuple only because TypeScript cannot express the open one (see
|
|
40
|
+
* `../ts/types.ts` `TupleTs`); reading that rendering as the value model is
|
|
41
|
+
* what produced #1622, whose check lived in this module's ancestor and was
|
|
42
|
+
* deleted with it. A schema that wants exact members says so — see
|
|
43
|
+
* `../todo/close-type.md`.
|
|
44
|
+
*
|
|
45
|
+
* ## Dispatch strategy
|
|
46
|
+
*
|
|
47
|
+
* Schema recognition is delegated to `visit` in `../common/module.f.mjs`,
|
|
48
|
+
* which routes each `Type` variant to a handler in the `Visitor` record
|
|
49
|
+
* defined below; nothing here walks the `Type` ADT itself. The container
|
|
50
|
+
* handlers drive `eachEntry` in its no-accumulator mode — the mode its JSDoc
|
|
51
|
+
* describes for "a caller whose whole question is 'did every entry
|
|
52
|
+
* succeed?'" — so a validation allocates nothing per entry. The data form's
|
|
53
|
+
* `validate` (`../data/module.f.mjs`) is the same shape over `Data`.
|
|
54
|
+
*
|
|
55
|
+
* ## Recursion safety
|
|
56
|
+
*
|
|
57
|
+
* For `array` and `record` schemas, the inner item validator is instantiated
|
|
58
|
+
* lazily — only after confirming the container is non-empty. This prevents
|
|
59
|
+
* infinite recursion when validating recursive schemas like
|
|
60
|
+
* `const list = () => ['array', list]`.
|
|
61
|
+
*
|
|
62
|
+
* See `./types.ts` for the `Path`/`Result`/`Validate`/`ValidationError`
|
|
63
|
+
* type-level API.
|
|
64
|
+
*
|
|
65
|
+
* @module
|
|
66
|
+
*
|
|
67
|
+
* @import { Unknown } from '../ts/types.ts'
|
|
68
|
+
* @import { Info1, Struct, Tag1, Tuple, Type } from '../types.ts'
|
|
69
|
+
* @import { Container, IsContainer, Validate, Visitor } from '../common/types.ts'
|
|
70
|
+
*/
|
|
71
|
+
|
|
72
|
+
import { ok } from '../../result/module.f.mjs'
|
|
73
|
+
import {
|
|
74
|
+
constPrimitiveValidate,
|
|
75
|
+
eachEntry,
|
|
76
|
+
isArray,
|
|
77
|
+
isObject,
|
|
78
|
+
orVisit,
|
|
79
|
+
primitive0Validate,
|
|
80
|
+
verror,
|
|
81
|
+
visit,
|
|
82
|
+
} from '../common/module.f.mjs'
|
|
83
|
+
|
|
84
|
+
const { entries } = Object
|
|
85
|
+
|
|
86
|
+
/** `validate` has nothing to collect from a successful entry — only pass/fail matters. */
|
|
87
|
+
const noAccumulate = () => undefined
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Builds a validator for `array` or `record` schemas.
|
|
91
|
+
* The inner item validator is instantiated lazily (only when the container is
|
|
92
|
+
* non-empty) to avoid infinite recursion with recursive schemas.
|
|
93
|
+
*/
|
|
94
|
+
const containerValidate =
|
|
95
|
+
/**
|
|
96
|
+
* @template {Tag1} K
|
|
97
|
+
* @param {IsContainer<Container<K>>} isContainer
|
|
98
|
+
* @returns {<I extends Type>(item: I) => Validate<Info1<K, I>>}
|
|
99
|
+
*/
|
|
100
|
+
isContainer =>
|
|
101
|
+
item => value => {
|
|
102
|
+
if (!isContainer(value)) {
|
|
103
|
+
return verror('unexpected value')
|
|
104
|
+
}
|
|
105
|
+
const e = entries(value)
|
|
106
|
+
if (e.length === 0) {
|
|
107
|
+
return /** @type {any} */ (ok(value))
|
|
108
|
+
}
|
|
109
|
+
// Note: we shouldn't instantiate `itemValidate` until we make sure `entries` is not empty.
|
|
110
|
+
// Otherwise, we can get infinite recursion on empty arrays and objects
|
|
111
|
+
const itemValidate = validate(item)
|
|
112
|
+
const r = eachEntry(e, (_k, v) => itemValidate(v), undefined, noAccumulate)
|
|
113
|
+
// `value` is Container<K>, but Ts<Info1<K,I>> = readonly Ts<I>[] | Record<string,Ts<I>>.
|
|
114
|
+
// TypeScript can't narrow the container's element types through the validation loop.
|
|
115
|
+
return r[0] === 'error' ? r : /** @type {any} */ (ok(value))
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const arrayValidate = containerValidate(isArray)
|
|
119
|
+
|
|
120
|
+
const recordValidate = containerValidate(isObject)
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Builds a validator for `Tuple` or `Struct` const schemas. It iterates the
|
|
124
|
+
* *schema's* entries, which is what makes both kinds open: a longer array or
|
|
125
|
+
* an undeclared key is never visited, so it is accepted — and, the value being
|
|
126
|
+
* returned as it came, it survives.
|
|
127
|
+
*/
|
|
128
|
+
const constContainerValidate =
|
|
129
|
+
/**
|
|
130
|
+
* @template {Unknown} C
|
|
131
|
+
* @param {IsContainer<C>} isContainer
|
|
132
|
+
* @param {(value: C, k: string) => Unknown} getItem
|
|
133
|
+
* @returns {<T extends Tuple | Struct>(rtti: T) => Validate<T>}
|
|
134
|
+
*/
|
|
135
|
+
(isContainer, getItem) =>
|
|
136
|
+
rtti => {
|
|
137
|
+
// Depends on `rtti` alone, so it is computed once per schema rather
|
|
138
|
+
// than once per validated value.
|
|
139
|
+
const rttiEntries = entries(rtti)
|
|
140
|
+
return value => {
|
|
141
|
+
if (!isContainer(value)) {
|
|
142
|
+
return verror('unexpected value')
|
|
143
|
+
}
|
|
144
|
+
const r = eachEntry(
|
|
145
|
+
rttiEntries,
|
|
146
|
+
(k, v) => /** @type {any} */ (validate(v))(getItem(value, k)),
|
|
147
|
+
undefined,
|
|
148
|
+
noAccumulate,
|
|
149
|
+
)
|
|
150
|
+
// `value` is C (Unknown container), but Ts<T> for T extends Tuple|Struct is not
|
|
151
|
+
// structurally equivalent to C — TypeScript can't narrow element types through the loop.
|
|
152
|
+
return r[0] === 'error' ? r : /** @type {any} */ (ok(value))
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
const tupleValidate = constContainerValidate(
|
|
157
|
+
isArray,
|
|
158
|
+
(value, k) => value[Number(k)],
|
|
159
|
+
)
|
|
160
|
+
|
|
161
|
+
const structValidate = constContainerValidate(
|
|
162
|
+
isObject,
|
|
163
|
+
(value, k) => value[k],
|
|
164
|
+
)
|
|
165
|
+
|
|
166
|
+
const orValidate =
|
|
167
|
+
/**
|
|
168
|
+
* @template {readonly Type[]} T
|
|
169
|
+
* @param {T} rtti
|
|
170
|
+
* @returns {Validate<() => readonly ['or', ...T]>}
|
|
171
|
+
*/
|
|
172
|
+
rtti =>
|
|
173
|
+
/** @type {any} */ (orVisit(/** @type {any} */ (validate))(rtti))
|
|
174
|
+
|
|
175
|
+
const validateVisitor = /** @type {any} */ ({
|
|
176
|
+
tuple: tupleValidate,
|
|
177
|
+
struct: structValidate,
|
|
178
|
+
array: arrayValidate,
|
|
179
|
+
record: recordValidate,
|
|
180
|
+
or: orValidate,
|
|
181
|
+
constPrimitive: constPrimitiveValidate,
|
|
182
|
+
primitive0: primitive0Validate,
|
|
183
|
+
unknown: () => ok,
|
|
184
|
+
})
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Creates a validator function for the given RTTI schema: a `Thunk` for
|
|
188
|
+
* tag-based schemas, or a `Const` (primitive literal, tuple, or struct) for
|
|
189
|
+
* exact-value schemas.
|
|
190
|
+
*
|
|
191
|
+
* The returned function takes an unknown value and returns either
|
|
192
|
+
* `['ok', value]` — the very value it was given, not a reconstruction — or
|
|
193
|
+
* `['error', { path, message }]` describing the failure location.
|
|
194
|
+
*
|
|
195
|
+
* Use it when the question is "is this value of this shape?" and the value has
|
|
196
|
+
* to survive the question intact. Use `../parse/module.f.mjs` when the answer
|
|
197
|
+
* wanted is a value built to the schema.
|
|
198
|
+
*
|
|
199
|
+
* @example
|
|
200
|
+
* ```js
|
|
201
|
+
* const v = validate(array(number))
|
|
202
|
+
* const input = [1, 2, 3]
|
|
203
|
+
* v(input) // ['ok', input] — the same array, not a copy
|
|
204
|
+
* v([1, 'two']) // ['error', { path: ['1'], message: 'unexpected value' }]
|
|
205
|
+
*
|
|
206
|
+
* // open, and the extras are still there afterwards
|
|
207
|
+
* validate([number, number])([1, 2, 3]) // ['ok', [1, 2, 3]]
|
|
208
|
+
* validate({ a: number })({ a: 1, b: 2 }) // ['ok', { a: 1, b: 2 }]
|
|
209
|
+
*
|
|
210
|
+
* // an absent optional member stays absent
|
|
211
|
+
* validate({ a: number, b: option(string) })({ a: 1 }) // ['ok', { a: 1 }]
|
|
212
|
+
* ```
|
|
213
|
+
*
|
|
214
|
+
* @type {<T extends Type>(rtti: T) => Validate<T>}
|
|
215
|
+
*/
|
|
216
|
+
export const validate = rtti =>
|
|
217
|
+
(visit(validateVisitor)(rtti))
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ValidationError, ValidateE } 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
|
+
export declare const proof: {
|
|
10
|
+
verbatim: {
|
|
11
|
+
absentOptionalStaysAbsent: () => void;
|
|
12
|
+
undeclaredMemberSurvives: () => void;
|
|
13
|
+
referenceIdentity: () => void;
|
|
14
|
+
};
|
|
15
|
+
sameAcceptanceAsParse: () => void;
|
|
16
|
+
boolean: {
|
|
17
|
+
ok: () => void;
|
|
18
|
+
error: () => void;
|
|
19
|
+
};
|
|
20
|
+
number: {
|
|
21
|
+
ok: () => void;
|
|
22
|
+
error: () => void;
|
|
23
|
+
};
|
|
24
|
+
string: {
|
|
25
|
+
ok: () => void;
|
|
26
|
+
error: () => void;
|
|
27
|
+
};
|
|
28
|
+
bigint: {
|
|
29
|
+
ok: () => void;
|
|
30
|
+
error: () => void;
|
|
31
|
+
};
|
|
32
|
+
unknown: {
|
|
33
|
+
ok: () => void;
|
|
34
|
+
};
|
|
35
|
+
const: {
|
|
36
|
+
null: {
|
|
37
|
+
ok: () => void;
|
|
38
|
+
error: () => void;
|
|
39
|
+
};
|
|
40
|
+
undefined: {
|
|
41
|
+
ok: () => void;
|
|
42
|
+
error: () => void;
|
|
43
|
+
};
|
|
44
|
+
number: {
|
|
45
|
+
ok: () => void;
|
|
46
|
+
error: () => void;
|
|
47
|
+
};
|
|
48
|
+
nan: {
|
|
49
|
+
ok: () => void;
|
|
50
|
+
error: () => void;
|
|
51
|
+
};
|
|
52
|
+
infinity: {
|
|
53
|
+
ok: () => void;
|
|
54
|
+
error: () => void;
|
|
55
|
+
};
|
|
56
|
+
signedZero: {
|
|
57
|
+
distinct: () => void;
|
|
58
|
+
self: () => void;
|
|
59
|
+
};
|
|
60
|
+
string: {
|
|
61
|
+
ok: () => void;
|
|
62
|
+
error: () => void;
|
|
63
|
+
};
|
|
64
|
+
bigint: {
|
|
65
|
+
ok: () => void;
|
|
66
|
+
error: () => void;
|
|
67
|
+
};
|
|
68
|
+
boolean: {
|
|
69
|
+
ok: () => void;
|
|
70
|
+
error: () => void;
|
|
71
|
+
};
|
|
72
|
+
tuple: {
|
|
73
|
+
ok: () => void;
|
|
74
|
+
extraItemsAcceptedAndKept: () => void;
|
|
75
|
+
shortArrayKeepsItsLength: () => void;
|
|
76
|
+
empty: () => void;
|
|
77
|
+
error: () => void;
|
|
78
|
+
};
|
|
79
|
+
struct: {
|
|
80
|
+
ok: () => void;
|
|
81
|
+
error: () => void;
|
|
82
|
+
};
|
|
83
|
+
};
|
|
84
|
+
array: {
|
|
85
|
+
empty: () => void;
|
|
86
|
+
ok: () => void;
|
|
87
|
+
error: () => void;
|
|
88
|
+
nested: () => void;
|
|
89
|
+
};
|
|
90
|
+
record: {
|
|
91
|
+
empty: () => void;
|
|
92
|
+
ok: () => void;
|
|
93
|
+
error: () => void;
|
|
94
|
+
};
|
|
95
|
+
constThunk: {
|
|
96
|
+
primitive: () => void;
|
|
97
|
+
};
|
|
98
|
+
or: {
|
|
99
|
+
consts: {
|
|
100
|
+
ok: () => void;
|
|
101
|
+
error: () => void;
|
|
102
|
+
};
|
|
103
|
+
thunks: {
|
|
104
|
+
ok: () => void;
|
|
105
|
+
error: () => void;
|
|
106
|
+
};
|
|
107
|
+
firstMatchWins: () => void;
|
|
108
|
+
};
|
|
109
|
+
option: {
|
|
110
|
+
ok: () => void;
|
|
111
|
+
error: () => void;
|
|
112
|
+
};
|
|
113
|
+
path: {
|
|
114
|
+
rootMismatch: () => void;
|
|
115
|
+
arrayIndex: () => void;
|
|
116
|
+
recordKey: () => void;
|
|
117
|
+
nestedArray: () => void;
|
|
118
|
+
tupleIndex: () => void;
|
|
119
|
+
structKey: () => void;
|
|
120
|
+
deepStruct: () => void;
|
|
121
|
+
recursiveSchema: () => void;
|
|
122
|
+
orRoot: () => void;
|
|
123
|
+
};
|
|
124
|
+
recursive: {
|
|
125
|
+
arrayOfArrays: () => void;
|
|
126
|
+
recordOfRecords: () => void;
|
|
127
|
+
};
|
|
128
|
+
};
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @import { ValidationError, ValidateE } 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 { boolean, number, string, bigint, unknown, array, record, or, option } from '../module.f.mjs'
|
|
13
|
+
import { unwrap } from '../../result/module.f.mjs'
|
|
14
|
+
import { assert, assertEq, assertStructurallySame } from '../../../asserts/module.f.mjs'
|
|
15
|
+
|
|
16
|
+
/** @type {(r: readonly [string, unknown]) => void} */
|
|
17
|
+
const assertOk = ([k]) => { assertEq(k, 'ok', 'expected ok') }
|
|
18
|
+
|
|
19
|
+
/** @type {(r: readonly [string, unknown]) => void} */
|
|
20
|
+
const assertError = ([k]) => { assertEq(k, 'error', 'expected error') }
|
|
21
|
+
|
|
22
|
+
/** @type {(expected: readonly string[]) => (r: readonly [string, unknown]) => void} */
|
|
23
|
+
const assertErrorPath = expected =>
|
|
24
|
+
r => {
|
|
25
|
+
assert(r[0] === 'error', 'expected error')
|
|
26
|
+
const e = /** @type {ValidationError} */ (r[1])
|
|
27
|
+
assertStructurallySame(e.path, expected, 'unexpected error path')
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Both readers with their payload type erased, so a table can hold rows of mixed schemas. */
|
|
31
|
+
|
|
32
|
+
/** @type {(t: Type) => ValidateE} */
|
|
33
|
+
const v = t => /** @type {any} */ (validate(t))
|
|
34
|
+
|
|
35
|
+
/** @type {(t: Type) => ValidateE} */
|
|
36
|
+
const p = t => /** @type {any} */ (parse(t))
|
|
37
|
+
|
|
38
|
+
export const proof = {
|
|
39
|
+
// ── the three properties this module exists for ──────────────────────────
|
|
40
|
+
//
|
|
41
|
+
// `parse` rebuilds: it materializes a declared-but-absent member as
|
|
42
|
+
// `undefined`, drops an undeclared one, and returns a new object. For a
|
|
43
|
+
// caller whose value is a document whose bytes are its identity — a
|
|
44
|
+
// content-addressed store, a signed payload — each of those is a
|
|
45
|
+
// different document. `validate` answers the same question about the
|
|
46
|
+
// value it was handed and hands it back.
|
|
47
|
+
verbatim: {
|
|
48
|
+
// An absent optional member stays absent. `'b' in out` is the
|
|
49
|
+
// assertion, not `out.b === undefined`: `parse` satisfies the latter.
|
|
50
|
+
absentOptionalStaysAbsent: () => {
|
|
51
|
+
const schema = { a: number, b: option(string) }
|
|
52
|
+
const input = { a: 1 }
|
|
53
|
+
const out = unwrap(validate(schema)(input))
|
|
54
|
+
assert(!('b' in out), 'an absent optional member must stay absent')
|
|
55
|
+
// The contrast that motivates the module.
|
|
56
|
+
assert('b' in unwrap(parse(schema)(input)), 'parse materializes it')
|
|
57
|
+
},
|
|
58
|
+
// An undeclared member survives. `parse` accepts it too — structs and
|
|
59
|
+
// tuples are open — but does not carry it into what it builds.
|
|
60
|
+
undeclaredMemberSurvives: () => {
|
|
61
|
+
const schema = { a: number }
|
|
62
|
+
const struct = { a: 1, b: 'extra' }
|
|
63
|
+
assertStructurallySame(unwrap(validate(schema)(struct)), { a: 1, b: 'extra' })
|
|
64
|
+
assert(!('b' in unwrap(parse(schema)(struct))), 'parse drops it')
|
|
65
|
+
// The same on the other kind: a longer array keeps its tail.
|
|
66
|
+
const tuple = [1, 'extra']
|
|
67
|
+
assertStructurallySame(unwrap(validate([number])(tuple)), [1, 'extra'])
|
|
68
|
+
},
|
|
69
|
+
// On success the result *is* the argument. This is the property the
|
|
70
|
+
// other two follow from, and the mirror of `../parse/proof.f.mjs`'s
|
|
71
|
+
// `freshArray` / `freshRecord`.
|
|
72
|
+
referenceIdentity: () => {
|
|
73
|
+
/** @type {(t: Type, value: Unknown) => void} */
|
|
74
|
+
const same = (t, value) => {
|
|
75
|
+
const r = v(t)(value)
|
|
76
|
+
assert(r[0] === 'ok', 'expected ok')
|
|
77
|
+
assert(Object.is(r[1], value), 'expected the original value')
|
|
78
|
+
}
|
|
79
|
+
const arr = [1, 2, 3]
|
|
80
|
+
same(array(number), arr)
|
|
81
|
+
same([number, number, number], arr)
|
|
82
|
+
same(unknown, arr)
|
|
83
|
+
const obj = { a: 1, b: 2 }
|
|
84
|
+
same(record(number), obj)
|
|
85
|
+
same({ a: number }, obj)
|
|
86
|
+
same(or(string, record(number)), obj)
|
|
87
|
+
const nested = { xs: [{ a: 1 }] }
|
|
88
|
+
same({ xs: array({ a: number }) }, nested)
|
|
89
|
+
assert(Object.is(unwrap(validate({ xs: array({ a: number }) })(nested)).xs, nested.xs),
|
|
90
|
+
'nested containers are not rebuilt either')
|
|
91
|
+
},
|
|
92
|
+
},
|
|
93
|
+
// Acceptance is `parse`'s, exactly: the two readers differ in what a
|
|
94
|
+
// success carries and in nothing else. Rows cover both container kinds,
|
|
95
|
+
// openness on both, the short-array rule, primitives, `or`, and misses.
|
|
96
|
+
sameAcceptanceAsParse: () => {
|
|
97
|
+
/** @type {readonly (readonly [Type, Unknown])[]} */
|
|
98
|
+
const rows = [
|
|
99
|
+
[number, 42],
|
|
100
|
+
[number, '42'],
|
|
101
|
+
[string, 42],
|
|
102
|
+
[boolean, false],
|
|
103
|
+
[bigint, 7n],
|
|
104
|
+
[unknown, { a: [1, 'x'] }],
|
|
105
|
+
[/** @type {const} */ (42), 42],
|
|
106
|
+
[/** @type {const} */ (42), 43],
|
|
107
|
+
[array(number), [1, 2, 3]],
|
|
108
|
+
[array(number), [1, 'two']],
|
|
109
|
+
[array(number), {}],
|
|
110
|
+
[record(number), { a: 1 }],
|
|
111
|
+
[record(number), { a: 'one' }],
|
|
112
|
+
[record(number), []],
|
|
113
|
+
// the four openness rows
|
|
114
|
+
[[/** @type {const} */ (42)], [42, 'extra']],
|
|
115
|
+
[{ a: /** @type {const} */ (42) }, { a: 42, b: 'x' }],
|
|
116
|
+
[[number, option(string)], [42]],
|
|
117
|
+
[[/** @type {const} */ (42)], []],
|
|
118
|
+
[{ a: number, b: option(string) }, { a: 1 }],
|
|
119
|
+
[{ a: number }, { a: 'one' }],
|
|
120
|
+
[or(number, string), true],
|
|
121
|
+
[or(number, string), 'hello'],
|
|
122
|
+
[option(number), undefined],
|
|
123
|
+
[option(number), null],
|
|
124
|
+
[{ user: { name: string, age: number } }, { user: { name: 'A', age: 'old' } }],
|
|
125
|
+
]
|
|
126
|
+
for (const [t, value] of rows) {
|
|
127
|
+
const rv = v(t)(value)
|
|
128
|
+
const rp = p(t)(value)
|
|
129
|
+
assertEq(rv[0], rp[0], 'validate and parse must agree on acceptance')
|
|
130
|
+
if (rv[0] === 'error') {
|
|
131
|
+
assert(rp[0] === 'error', 'expected error')
|
|
132
|
+
assertStructurallySame(rv[1], rp[1], 'the two readers report the same error')
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
},
|
|
136
|
+
boolean: {
|
|
137
|
+
ok: () => {
|
|
138
|
+
/** @typedef {Assert<Equal<Ts<typeof boolean>, boolean>>} _RoundTrip */
|
|
139
|
+
assertOk(validate(boolean)(true))
|
|
140
|
+
assertOk(validate(boolean)(false))
|
|
141
|
+
},
|
|
142
|
+
error: () => {
|
|
143
|
+
assertError(validate(boolean)(0))
|
|
144
|
+
assertError(validate(boolean)('true'))
|
|
145
|
+
assertError(validate(boolean)(null))
|
|
146
|
+
},
|
|
147
|
+
},
|
|
148
|
+
number: {
|
|
149
|
+
ok: () => {
|
|
150
|
+
/** @typedef {Assert<Equal<Ts<typeof number>, number>>} _RoundTrip */
|
|
151
|
+
assertOk(validate(number)(42))
|
|
152
|
+
},
|
|
153
|
+
error: () => {
|
|
154
|
+
assertError(validate(number)('42'))
|
|
155
|
+
assertError(validate(number)(42n))
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
string: {
|
|
159
|
+
ok: () => {
|
|
160
|
+
/** @typedef {Assert<Equal<Ts<typeof string>, string>>} _RoundTrip */
|
|
161
|
+
assertOk(validate(string)('hello'))
|
|
162
|
+
},
|
|
163
|
+
error: () => {
|
|
164
|
+
assertError(validate(string)(42))
|
|
165
|
+
assertError(validate(string)(null))
|
|
166
|
+
},
|
|
167
|
+
},
|
|
168
|
+
bigint: {
|
|
169
|
+
ok: () => {
|
|
170
|
+
/** @typedef {Assert<Equal<Ts<typeof bigint>, bigint>>} _RoundTrip */
|
|
171
|
+
assertOk(validate(bigint)(4n))
|
|
172
|
+
},
|
|
173
|
+
error: () => {
|
|
174
|
+
assertError(validate(bigint)(4))
|
|
175
|
+
assertError(validate(bigint)('4'))
|
|
176
|
+
},
|
|
177
|
+
},
|
|
178
|
+
unknown: {
|
|
179
|
+
ok: () => {
|
|
180
|
+
/** @typedef {Assert<Equal<Ts<typeof unknown>, DjsUnknown>>} _RoundTrip */
|
|
181
|
+
assertOk(validate(unknown)(null))
|
|
182
|
+
assertOk(validate(unknown)(42))
|
|
183
|
+
assertOk(validate(unknown)('hello'))
|
|
184
|
+
assertOk(validate(unknown)(true))
|
|
185
|
+
assertOk(validate(unknown)({}))
|
|
186
|
+
assertOk(validate(unknown)([]))
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
const: {
|
|
190
|
+
null: {
|
|
191
|
+
ok: () => assertOk(validate(null)(null)),
|
|
192
|
+
error: () => {
|
|
193
|
+
assertError(validate(null)(undefined))
|
|
194
|
+
assertError(validate(null)(0))
|
|
195
|
+
},
|
|
196
|
+
},
|
|
197
|
+
undefined: {
|
|
198
|
+
ok: () => assertOk(validate(undefined)(undefined)),
|
|
199
|
+
error: () => assertError(validate(undefined)(null)),
|
|
200
|
+
},
|
|
201
|
+
number: {
|
|
202
|
+
ok: () => assertOk(validate(/** @type {const} */ (42))(42)),
|
|
203
|
+
error: () => assertError(validate(/** @type {const} */ (42))(43)),
|
|
204
|
+
},
|
|
205
|
+
nan: {
|
|
206
|
+
ok: () => assertOk(validate(NaN)(NaN)),
|
|
207
|
+
error: () => {
|
|
208
|
+
assertError(validate(NaN)(0))
|
|
209
|
+
assertError(validate(/** @type {const} */ (0))(NaN))
|
|
210
|
+
assertError(validate(/** @type {const} */ (42))(NaN))
|
|
211
|
+
},
|
|
212
|
+
},
|
|
213
|
+
infinity: {
|
|
214
|
+
ok: () => {
|
|
215
|
+
assertOk(validate(Infinity)(Infinity))
|
|
216
|
+
assertOk(validate(-Infinity)(-Infinity))
|
|
217
|
+
},
|
|
218
|
+
error: () => {
|
|
219
|
+
assertError(validate(Infinity)(-Infinity))
|
|
220
|
+
assertError(validate(Infinity)(0))
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
signedZero: {
|
|
224
|
+
// `Object.is` distinguishes +0 and -0; `===` treats them equal.
|
|
225
|
+
distinct: () => {
|
|
226
|
+
assertError(validate(/** @type {const} */ (0))(-0))
|
|
227
|
+
assertError(validate(-0)(0))
|
|
228
|
+
},
|
|
229
|
+
self: () => {
|
|
230
|
+
assertOk(validate(/** @type {const} */ (0))(0))
|
|
231
|
+
assertOk(validate(-0)(-0))
|
|
232
|
+
},
|
|
233
|
+
},
|
|
234
|
+
string: {
|
|
235
|
+
ok: () => assertOk(validate(/** @type {const} */ ('hello'))('hello')),
|
|
236
|
+
error: () => assertError(validate(/** @type {const} */ ('hello'))('world')),
|
|
237
|
+
},
|
|
238
|
+
bigint: {
|
|
239
|
+
ok: () => assertOk(validate(/** @type {const} */ (7n))(7n)),
|
|
240
|
+
error: () => assertError(validate(/** @type {const} */ (7n))(8n)),
|
|
241
|
+
},
|
|
242
|
+
boolean: {
|
|
243
|
+
ok: () => assertOk(validate(/** @type {const} */ (true))(true)),
|
|
244
|
+
error: () => assertError(validate(/** @type {const} */ (true))(false)),
|
|
245
|
+
},
|
|
246
|
+
tuple: {
|
|
247
|
+
ok: () => {
|
|
248
|
+
const t = /** @type {const} */ ([42, 'hello'])
|
|
249
|
+
assertStructurallySame(unwrap(validate(t)([42, 'hello'])), [42, 'hello'])
|
|
250
|
+
},
|
|
251
|
+
// A tuple is OPEN, and the extras are still there afterwards. This
|
|
252
|
+
// is deliberate — see "Structs and tuples are open" in
|
|
253
|
+
// ../README.md. Do not restore #1622's length check on the
|
|
254
|
+
// strength of `Ts<readonly [42]>` being an exact tuple; that
|
|
255
|
+
// mapping is exact only because TypeScript could not express the
|
|
256
|
+
// open one (see ../ts/types.ts `TupleTs`).
|
|
257
|
+
extraItemsAcceptedAndKept: () => {
|
|
258
|
+
const long = [42, 1, 2, 3]
|
|
259
|
+
assert(Object.is(unwrap(validate(/** @type {const} */ ([42]))(long)), long),
|
|
260
|
+
'the longer array comes back as it went in')
|
|
261
|
+
},
|
|
262
|
+
// An absent member reads as `undefined`, so a position is required
|
|
263
|
+
// exactly when its set excludes `undefined` — and nothing is
|
|
264
|
+
// filled in, so the array keeps its length.
|
|
265
|
+
shortArrayKeepsItsLength: () => {
|
|
266
|
+
const short = [42]
|
|
267
|
+
const out = unwrap(validate([number, option(string)])(short))
|
|
268
|
+
assert(Object.is(out, short), 'expected the original array')
|
|
269
|
+
assertEq(short.length, 1, 'no gap is filled')
|
|
270
|
+
},
|
|
271
|
+
empty: () => assertOk(validate(/** @type {const} */ ([]))([])),
|
|
272
|
+
error: () => {
|
|
273
|
+
assertError(validate(/** @type {const} */ ([42]))([99]))
|
|
274
|
+
assertError(validate(/** @type {const} */ ([42]))({}))
|
|
275
|
+
// `42` excludes `undefined`, so position 0 is required.
|
|
276
|
+
assertError(validate(/** @type {const} */ ([42]))([]))
|
|
277
|
+
},
|
|
278
|
+
},
|
|
279
|
+
struct: {
|
|
280
|
+
ok: () => {
|
|
281
|
+
const t = /** @type {const} */ ({ a: 42, b: 'hello' })
|
|
282
|
+
assertStructurallySame(unwrap(validate(t)({ a: 42, b: 'hello' })), { a: 42, b: 'hello' })
|
|
283
|
+
},
|
|
284
|
+
error: () => {
|
|
285
|
+
assertError(validate(/** @type {const} */ ({ a: 42 }))({ a: 99 }))
|
|
286
|
+
assertError(validate(/** @type {const} */ ({ a: 42 }))([]))
|
|
287
|
+
},
|
|
288
|
+
},
|
|
289
|
+
},
|
|
290
|
+
array: {
|
|
291
|
+
empty: () => {
|
|
292
|
+
const input = /** @type {readonly number[]} */ ([])
|
|
293
|
+
assert(Object.is(unwrap(validate(array(number))(input)), input), 'expected the original array')
|
|
294
|
+
},
|
|
295
|
+
ok: () => assertStructurallySame(unwrap(validate(array(number))([1, 2, 3])), [1, 2, 3]),
|
|
296
|
+
error: () => {
|
|
297
|
+
assertError(validate(array(number))([1, 'two', 3]))
|
|
298
|
+
assertError(validate(array(number))({}))
|
|
299
|
+
assertError(validate(array(number))(null))
|
|
300
|
+
},
|
|
301
|
+
nested: () => {
|
|
302
|
+
assertOk(validate(array(array(boolean)))([[true, false], [false]]))
|
|
303
|
+
assertError(validate(array(array(boolean)))([[true, 42]]))
|
|
304
|
+
},
|
|
305
|
+
},
|
|
306
|
+
record: {
|
|
307
|
+
empty: () => {
|
|
308
|
+
const input = /** @type {{ readonly[K in string]?: number }} */ ({})
|
|
309
|
+
assert(Object.is(unwrap(validate(record(number))(input)), input), 'expected the original record')
|
|
310
|
+
},
|
|
311
|
+
ok: () => assertStructurallySame(
|
|
312
|
+
unwrap(validate(record(string))({ a: 'hello', b: 'world' })),
|
|
313
|
+
{ a: 'hello', b: 'world' },
|
|
314
|
+
),
|
|
315
|
+
error: () => {
|
|
316
|
+
assertError(validate(record(number))({ a: 1, b: 'two' }))
|
|
317
|
+
assertError(validate(record(number))(null))
|
|
318
|
+
assertError(validate(record(number))([]))
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
constThunk: {
|
|
322
|
+
primitive: () => {
|
|
323
|
+
const t = () => /** @type {const} */ (['const', 7n])
|
|
324
|
+
assertOk(validate(t)(7n))
|
|
325
|
+
assertError(validate(t)(8n))
|
|
326
|
+
},
|
|
327
|
+
},
|
|
328
|
+
or: {
|
|
329
|
+
consts: {
|
|
330
|
+
ok: () => {
|
|
331
|
+
const t = or(.../** @type {const} */ ([false, 42, 'hello']))
|
|
332
|
+
assertOk(validate(t)(false))
|
|
333
|
+
assertOk(validate(t)(42))
|
|
334
|
+
assertOk(validate(t)('hello'))
|
|
335
|
+
},
|
|
336
|
+
error: () => {
|
|
337
|
+
const t = or(.../** @type {const} */ ([false, 42, 'hello']))
|
|
338
|
+
assertError(validate(t)(true))
|
|
339
|
+
assertError(validate(t)(43))
|
|
340
|
+
assertError(validate(t)('world'))
|
|
341
|
+
assertError(validate(t)(null))
|
|
342
|
+
},
|
|
343
|
+
},
|
|
344
|
+
thunks: {
|
|
345
|
+
ok: () => {
|
|
346
|
+
const t = or(number, string)
|
|
347
|
+
assertOk(validate(t)(42))
|
|
348
|
+
assertOk(validate(t)('hello'))
|
|
349
|
+
},
|
|
350
|
+
error: () => {
|
|
351
|
+
const t = or(number, string)
|
|
352
|
+
assertError(validate(t)(true))
|
|
353
|
+
assertError(validate(t)(null))
|
|
354
|
+
},
|
|
355
|
+
},
|
|
356
|
+
// The first matching variant wins, and it returns the value itself —
|
|
357
|
+
// so, unlike `parse`, which variant matched is not observable in the
|
|
358
|
+
// result. `parse` here returns a length-1 array; `validate` returns
|
|
359
|
+
// the length-3 one it was given.
|
|
360
|
+
firstMatchWins: () => {
|
|
361
|
+
const t = or(/** @type {const} */ ([number]), array(number))
|
|
362
|
+
const input = [1, 2, 3]
|
|
363
|
+
assert(Object.is(unwrap(validate(t)(input)), input), 'expected the original array')
|
|
364
|
+
assertStructurallySame(unwrap(parse(t)(input)), [1])
|
|
365
|
+
},
|
|
366
|
+
},
|
|
367
|
+
option: {
|
|
368
|
+
ok: () => {
|
|
369
|
+
const t = option(number)
|
|
370
|
+
assertOk(validate(t)(42))
|
|
371
|
+
assertOk(validate(t)(undefined))
|
|
372
|
+
},
|
|
373
|
+
error: () => {
|
|
374
|
+
const t = option(number)
|
|
375
|
+
assertError(validate(t)(null))
|
|
376
|
+
assertError(validate(t)('42'))
|
|
377
|
+
},
|
|
378
|
+
},
|
|
379
|
+
path: {
|
|
380
|
+
rootMismatch: () => assertErrorPath([])(validate(number)('not a number')),
|
|
381
|
+
arrayIndex: () => assertErrorPath(['1'])(validate(array(number))([1, 'two', 3])),
|
|
382
|
+
recordKey: () => assertErrorPath(['b'])(validate(record(number))({ a: 1, b: 'two', c: 3 })),
|
|
383
|
+
nestedArray: () => assertErrorPath(['0', '1'])(
|
|
384
|
+
validate(array(array(number)))([[1, 'x'], [2, 3]])
|
|
385
|
+
),
|
|
386
|
+
tupleIndex: () => assertErrorPath(['1'])(
|
|
387
|
+
validate(/** @type {const} */ ([number, number]))([1, 'two'])
|
|
388
|
+
),
|
|
389
|
+
structKey: () => assertErrorPath(['b'])(
|
|
390
|
+
validate(/** @type {const} */ ({ a: number, b: number }))({ a: 1, b: 'two' })
|
|
391
|
+
),
|
|
392
|
+
deepStruct: () => {
|
|
393
|
+
const schema = /** @type {const} */ ({ user: { name: string, age: number } })
|
|
394
|
+
assertErrorPath(['user', 'age'])(validate(schema)({ user: { name: 'A', age: 'old' } }))
|
|
395
|
+
},
|
|
396
|
+
recursiveSchema: () => {
|
|
397
|
+
/** @typedef {readonly _A[]} _A */
|
|
398
|
+
const list = () => /** @type {const} */ (['array', list])
|
|
399
|
+
const r = validate(list)([/** @type {_A} */ (/** @type {unknown} */ ([[42]]))])
|
|
400
|
+
assertErrorPath(['0', '0', '0'])(r)
|
|
401
|
+
},
|
|
402
|
+
orRoot: () => assertErrorPath([])(validate(or(number, string))(true)),
|
|
403
|
+
},
|
|
404
|
+
recursive: {
|
|
405
|
+
arrayOfArrays: () => {
|
|
406
|
+
/** @typedef {readonly _A[]} _A */
|
|
407
|
+
const list = () => /** @type {const} */ (['array', list])
|
|
408
|
+
/** @typedef {Assert<Equal<_A, Ts<typeof list>>>} _ListRoundTrip */
|
|
409
|
+
const x = validate(list)
|
|
410
|
+
assertOk(x([]))
|
|
411
|
+
assertOk(x([[], []]))
|
|
412
|
+
assertOk(x([[[], []], []]))
|
|
413
|
+
assertError(x([42]))
|
|
414
|
+
assertError(x(null))
|
|
415
|
+
},
|
|
416
|
+
recordOfRecords: () => {
|
|
417
|
+
const tree = () => /** @type {const} */ (['record', tree])
|
|
418
|
+
/** @typedef {{ readonly[K in string]?: _A }} _A */
|
|
419
|
+
/** @typedef {Assert<Equal<_A, Ts<typeof tree>>>} _TreeRoundTrip */
|
|
420
|
+
const x = validate(tree)
|
|
421
|
+
assertOk(x({}))
|
|
422
|
+
assertOk(x({ a: {}, b: { c: {} } }))
|
|
423
|
+
assertError(x({ a: 42 }))
|
|
424
|
+
},
|
|
425
|
+
},
|
|
426
|
+
}
|