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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "functionalscript",
3
- "version": "0.46.0",
3
+ "version": "0.46.1",
4
4
  "type": "module",
5
5
  "files": [
6
6
  "**/*.js",