@evolu/common 8.9.0 → 8.11.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/dist/src/Bytes.d.ts +647 -0
- package/dist/src/Bytes.d.ts.map +1 -0
- package/dist/src/{Binary.js → Bytes.js} +266 -16
- package/dist/src/Config.d.ts +142 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +181 -0
- package/dist/src/Console.d.ts +62 -7
- package/dist/src/Console.d.ts.map +1 -1
- package/dist/src/Console.js +20 -4
- package/dist/src/Crypto.d.ts +76 -4
- package/dist/src/Crypto.d.ts.map +1 -1
- package/dist/src/Crypto.js +55 -4
- package/dist/src/Error.d.ts +45 -0
- package/dist/src/Error.d.ts.map +1 -1
- package/dist/src/Error.js +69 -0
- package/dist/src/Fs.d.ts +376 -0
- package/dist/src/Fs.d.ts.map +1 -0
- package/dist/src/Fs.js +113 -0
- package/dist/src/Identicon.d.ts +2 -2
- package/dist/src/Identicon.js +2 -2
- package/dist/src/LeakDetector.d.ts +22 -3
- package/dist/src/LeakDetector.d.ts.map +1 -1
- package/dist/src/LeakDetector.js +12 -2
- package/dist/src/LockManager.d.ts +8 -0
- package/dist/src/LockManager.d.ts.map +1 -1
- package/dist/src/LockManager.js +6 -0
- package/dist/src/Number.d.ts +50 -7
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +47 -8
- package/dist/src/Object.d.ts +32 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +46 -0
- package/dist/src/Platform.d.ts +47 -7
- package/dist/src/Platform.d.ts.map +1 -1
- package/dist/src/Platform.js +24 -5
- package/dist/src/Random.d.ts +25 -2
- package/dist/src/Random.d.ts.map +1 -1
- package/dist/src/Random.js +14 -2
- package/dist/src/Resource.d.ts +156 -1
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Resource.js +201 -72
- package/dist/src/Schedule.d.ts +11 -10
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Schedule.js +1 -1
- package/dist/src/Sqlite.d.ts +132 -16
- package/dist/src/Sqlite.d.ts.map +1 -1
- package/dist/src/Sqlite.js +64 -10
- package/dist/src/Task.d.ts +15 -4
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +41 -15
- package/dist/src/Test.d.ts +9 -0
- package/dist/src/Test.d.ts.map +1 -1
- package/dist/src/Test.js +4 -0
- package/dist/src/Time.d.ts +179 -20
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +95 -6
- package/dist/src/Type.d.ts +3056 -1539
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +2548 -584
- package/dist/src/WebSocket.d.ts +164 -13
- package/dist/src/WebSocket.d.ts.map +1 -1
- package/dist/src/WebSocket.js +133 -24
- package/dist/src/Worker.d.ts +90 -8
- package/dist/src/Worker.d.ts.map +1 -1
- package/dist/src/Worker.js +28 -2
- package/dist/src/index.d.ts +9 -8
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +5 -4
- package/dist/src/intl/_en.d.ts +24 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +20 -0
- package/dist/src/intl/ar.d.ts +24 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +20 -0
- package/dist/src/intl/bn.d.ts +24 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +20 -0
- package/dist/src/intl/ca.d.ts +24 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +20 -0
- package/dist/src/intl/cs.d.ts +24 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +20 -0
- package/dist/src/intl/da.d.ts +24 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +20 -0
- package/dist/src/intl/de.d.ts +24 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +20 -0
- package/dist/src/intl/el.d.ts +24 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +20 -0
- package/dist/src/intl/es.d.ts +24 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +20 -0
- package/dist/src/intl/fa.d.ts +24 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +20 -0
- package/dist/src/intl/fi.d.ts +24 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +20 -0
- package/dist/src/intl/fil.d.ts +24 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +20 -0
- package/dist/src/intl/fr.d.ts +24 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +20 -0
- package/dist/src/intl/he.d.ts +24 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +20 -0
- package/dist/src/intl/hi.d.ts +24 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +20 -0
- package/dist/src/intl/hr.d.ts +24 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +20 -0
- package/dist/src/intl/hu.d.ts +22 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +18 -0
- package/dist/src/intl/id.d.ts +24 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +20 -0
- package/dist/src/intl/it.d.ts +24 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +20 -0
- package/dist/src/intl/ja.d.ts +24 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +20 -0
- package/dist/src/intl/ko.d.ts +24 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +20 -0
- package/dist/src/intl/ml.d.ts +24 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +20 -0
- package/dist/src/intl/mr.d.ts +24 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +20 -0
- package/dist/src/intl/ms.d.ts +24 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +20 -0
- package/dist/src/intl/nb.d.ts +22 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +18 -0
- package/dist/src/intl/nl.d.ts +24 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +20 -0
- package/dist/src/intl/pa.d.ts +24 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +20 -0
- package/dist/src/intl/pl.d.ts +23 -0
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +20 -0
- package/dist/src/intl/pt-BR.d.ts +24 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +20 -0
- package/dist/src/intl/pt.d.ts +24 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +20 -0
- package/dist/src/intl/ro.d.ts +24 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +20 -0
- package/dist/src/intl/sk.d.ts +24 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +20 -0
- package/dist/src/intl/sl.d.ts +24 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +20 -0
- package/dist/src/intl/sv.d.ts +24 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +20 -0
- package/dist/src/intl/sw.d.ts +21 -0
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +18 -0
- package/dist/src/intl/ta.d.ts +24 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +20 -0
- package/dist/src/intl/te.d.ts +24 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +20 -0
- package/dist/src/intl/th.d.ts +24 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +20 -0
- package/dist/src/intl/tr.d.ts +24 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +20 -0
- package/dist/src/intl/uk.d.ts +80 -57
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +174 -149
- package/dist/src/intl/ur.d.ts +24 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +20 -0
- package/dist/src/intl/vi.d.ts +24 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +20 -0
- package/dist/src/intl/zh-CN.d.ts +24 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +20 -0
- package/dist/src/intl/zh-TW.d.ts +24 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +20 -0
- package/dist/src/local-first/Db.d.ts +52 -3
- package/dist/src/local-first/Db.d.ts.map +1 -1
- package/dist/src/local-first/Db.js +412 -137
- package/dist/src/local-first/Evolu.d.ts +336 -211
- package/dist/src/local-first/Evolu.d.ts.map +1 -1
- package/dist/src/local-first/Evolu.js +102 -15
- package/dist/src/local-first/Owner.d.ts +13 -30
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +13 -30
- package/dist/src/local-first/Protocol.d.ts +95 -17
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +119 -39
- package/dist/src/local-first/Query.d.ts +8 -15
- package/dist/src/local-first/Query.d.ts.map +1 -1
- package/dist/src/local-first/Schema.d.ts +345 -21
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Schema.js +214 -17
- package/dist/src/local-first/Shared.d.ts +537 -22
- package/dist/src/local-first/Shared.d.ts.map +1 -1
- package/dist/src/local-first/Shared.js +1437 -234
- package/dist/src/local-first/Storage.d.ts +192 -14
- package/dist/src/local-first/Storage.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +82 -21
- package/dist/src/local-first/Timestamp.d.ts +392 -41
- package/dist/src/local-first/Timestamp.d.ts.map +1 -1
- package/dist/src/local-first/Timestamp.js +404 -82
- package/dist/src/local-first/index.d.ts +0 -1
- package/dist/src/local-first/index.d.ts.map +1 -1
- package/dist/src/local-first/index.js +0 -1
- package/package.json +1 -1
- package/src/Assert.test.ts +2 -5
- package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
- package/src/{Binary.ts → Bytes.ts} +652 -21
- package/src/Config.test.ts +668 -0
- package/src/Config.ts +410 -0
- package/src/Console.ts +62 -7
- package/src/Crypto.ts +76 -4
- package/src/Eq.test.ts +2 -3
- package/src/Error.test.ts +76 -3
- package/src/Error.ts +71 -0
- package/src/Fs.test.ts +105 -0
- package/src/Fs.ts +488 -0
- package/src/Identicon.ts +2 -2
- package/src/LeakDetector.ts +22 -3
- package/src/LockManager.ts +8 -0
- package/src/Number.test.ts +82 -18
- package/src/Number.ts +76 -8
- package/src/Object.test.ts +139 -10
- package/src/Object.ts +49 -0
- package/src/Platform.ts +50 -8
- package/src/Random.ts +25 -2
- package/src/Resource.test.ts +837 -0
- package/src/Resource.ts +235 -15
- package/src/Schedule.test.ts +50 -12
- package/src/Schedule.ts +24 -14
- package/src/Sqlite.ts +138 -18
- package/src/Task.test.ts +189 -8
- package/src/Task.ts +56 -17
- package/src/Test.ts +9 -0
- package/src/Time.test.ts +82 -11
- package/src/Time.ts +246 -24
- package/src/Type.test.ts +3994 -1119
- package/src/Type.ts +7258 -3842
- package/src/Types.test.ts +4 -14
- package/src/WebSocket.ts +313 -40
- package/src/Worker.ts +90 -8
- package/src/index.ts +18 -7
- package/src/intl/_en.ts +70 -0
- package/src/intl/ar.ts +71 -0
- package/src/intl/bn.ts +70 -0
- package/src/intl/ca.ts +70 -0
- package/src/intl/cs.ts +70 -0
- package/src/intl/da.ts +70 -0
- package/src/intl/de.ts +70 -0
- package/src/intl/el.ts +70 -0
- package/src/intl/es.ts +70 -0
- package/src/intl/fa.ts +70 -0
- package/src/intl/fi.ts +70 -0
- package/src/intl/fil.ts +70 -0
- package/src/intl/fr.ts +70 -0
- package/src/intl/he.ts +70 -0
- package/src/intl/hi.ts +70 -0
- package/src/intl/hr.ts +70 -0
- package/src/intl/hu.ts +69 -0
- package/src/intl/id.ts +70 -0
- package/src/intl/intl.test.ts +819 -1
- package/src/intl/it.ts +70 -0
- package/src/intl/ja.ts +70 -0
- package/src/intl/ko.ts +68 -0
- package/src/intl/ml.ts +70 -0
- package/src/intl/mr.ts +70 -0
- package/src/intl/ms.ts +71 -0
- package/src/intl/nb.ts +69 -0
- package/src/intl/nl.ts +70 -0
- package/src/intl/pa.ts +70 -0
- package/src/intl/pl.ts +63 -0
- package/src/intl/pt-BR.ts +70 -0
- package/src/intl/pt.ts +71 -0
- package/src/intl/ro.ts +70 -0
- package/src/intl/sk.ts +71 -0
- package/src/intl/sl.ts +70 -0
- package/src/intl/sv.ts +70 -0
- package/src/intl/sw.ts +62 -0
- package/src/intl/ta.ts +70 -0
- package/src/intl/te.ts +70 -0
- package/src/intl/th.ts +68 -0
- package/src/intl/tr.ts +70 -0
- package/src/intl/uk.ts +228 -155
- package/src/intl/ur.ts +70 -0
- package/src/intl/vi.ts +70 -0
- package/src/intl/zh-CN.ts +68 -0
- package/src/intl/zh-TW.ts +68 -0
- package/src/local-first/Db.ts +644 -339
- package/src/local-first/Evolu.test.ts +686 -21
- package/src/local-first/Evolu.ts +450 -228
- package/src/local-first/Owner.ts +13 -30
- package/src/local-first/Protocol.test.ts +618 -11
- package/src/local-first/Protocol.ts +197 -73
- package/src/local-first/Query.ts +8 -15
- package/src/local-first/Schema.test.ts +143 -0
- package/src/local-first/Schema.ts +374 -24
- package/src/local-first/Shared.test.ts +7731 -559
- package/src/local-first/Shared.ts +2036 -267
- package/src/local-first/Storage.ts +219 -33
- package/src/local-first/Timestamp.test.ts +344 -70
- package/src/local-first/Timestamp.ts +435 -119
- package/src/local-first/index.ts +0 -1
- package/dist/src/Binary.d.ts +0 -254
- package/dist/src/Binary.d.ts.map +0 -1
- package/dist/src/local-first/Error.d.ts +0 -12
- package/dist/src/local-first/Error.d.ts.map +0 -1
- package/dist/src/local-first/Error.js +0 -6
- package/dist/src/local-first/LocalAuth.d.ts +0 -150
- package/dist/src/local-first/LocalAuth.d.ts.map +0 -1
- package/dist/src/local-first/LocalAuth.js +0 -179
- package/src/local-first/Error.ts +0 -17
- package/src/local-first/LocalAuth.ts +0 -457
package/dist/src/Type.js
CHANGED
|
@@ -341,9 +341,9 @@
|
|
|
341
341
|
* parameter type. If application code claims an accessor-backed object or an
|
|
342
342
|
* object with excess properties is an Object Output, the assertion throws
|
|
343
343
|
* because the application contract is broken. `orThrow` and `orNull` preserve
|
|
344
|
-
* the assertion at their typed `Input` boundary
|
|
345
|
-
*
|
|
346
|
-
*
|
|
344
|
+
* the assertion at their typed `Input` boundary. After that boundary, `orThrow`
|
|
345
|
+
* throws returned validation errors with a formatted message and the original
|
|
346
|
+
* error as `cause`, while `orNull` maps them to `null`.
|
|
347
347
|
*
|
|
348
348
|
* Consequently, structural representation errors such as sparse Arrays,
|
|
349
349
|
* accessors, and excess properties normally do not enter user-facing validation
|
|
@@ -454,44 +454,129 @@ import * as bip39 from "@scure/bip39";
|
|
|
454
454
|
import { wordlist } from "@scure/bip39/wordlists/english.js";
|
|
455
455
|
import { createMutableArray, } from "./Array.js";
|
|
456
456
|
import { assert, assertNonNullable } from "./Assert.js";
|
|
457
|
+
import { eqData } from "./Eq.js";
|
|
457
458
|
import { identity } from "./Function.js";
|
|
458
|
-
import { createMutableRecord, getObjectKind, isPlainObject } from "./Object.js";
|
|
459
|
+
import { createMutableRecord, getObjectKind, isPlainObject, } from "./Object.js";
|
|
459
460
|
import { hasNodeBuffer } from "./Platform.js";
|
|
460
|
-
import { err, flatMapResult, getOk, getOrNull,
|
|
461
|
+
import { err, flatMapResult, getOk, getOrNull, ok, trySync, } from "./Result.js";
|
|
461
462
|
import { safelyStringifyUnknownValue } from "./String.js";
|
|
462
|
-
import {
|
|
463
|
+
import { isInstance, } from "./Types.js";
|
|
464
|
+
/**
|
|
465
|
+
* Converts an error from a {@link Type} into formatted issues with paths.
|
|
466
|
+
*
|
|
467
|
+
* Pass the Type that produced the error. This uses its nested and localized
|
|
468
|
+
* formatters without validating the input again. To retain every issue, decode
|
|
469
|
+
* with `{ errors: "all" }`; this function cannot recover errors omitted during
|
|
470
|
+
* validation. A root issue has an empty path.
|
|
471
|
+
*
|
|
472
|
+
* ### Example
|
|
473
|
+
*
|
|
474
|
+
* ```ts
|
|
475
|
+
* import {
|
|
476
|
+
* assertEqual,
|
|
477
|
+
* assertErr,
|
|
478
|
+
* IntFromString,
|
|
479
|
+
* object,
|
|
480
|
+
* typeErrorToIssues,
|
|
481
|
+
* } from "@evolu/common";
|
|
482
|
+
*
|
|
483
|
+
* const Settings = object({ port: IntFromString });
|
|
484
|
+
*
|
|
485
|
+
* const result = Settings.fromUnknown({ port: "http" }, { errors: "all" });
|
|
486
|
+
* assertErr(result);
|
|
487
|
+
*
|
|
488
|
+
* assertEqual(typeErrorToIssues(Settings, result.error), [
|
|
489
|
+
* {
|
|
490
|
+
* path: ["port"],
|
|
491
|
+
* message: 'The value "http" is not a decimal integer.',
|
|
492
|
+
* },
|
|
493
|
+
* ]);
|
|
494
|
+
* ```
|
|
495
|
+
*
|
|
496
|
+
* @group Core
|
|
497
|
+
*/
|
|
498
|
+
export const typeErrorToIssues = (type, error) => {
|
|
499
|
+
const runtimeType = type;
|
|
500
|
+
return runtimeType[getRuntimeTypeIssuesSymbol](error, "all", []).map((issue) => ({
|
|
501
|
+
path: issue.path,
|
|
502
|
+
message: formatRuntimeTypeIssue(issue),
|
|
503
|
+
}));
|
|
504
|
+
};
|
|
463
505
|
const outputValidationSymbol =
|
|
464
506
|
/*#__PURE__*/ globalThis.Symbol();
|
|
465
507
|
const getRuntimeTypeIssuesSymbol =
|
|
466
508
|
/*#__PURE__*/ globalThis.Symbol();
|
|
467
|
-
|
|
509
|
+
// Copying a short path in a loop is about twice as fast as spreading it.
|
|
510
|
+
const appendIssuePath = (path, key) => {
|
|
511
|
+
const issuePath = createMutableArray(path.length + 1);
|
|
512
|
+
for (let index = 0; index < path.length; index++) {
|
|
513
|
+
issuePath[index] = path[index];
|
|
514
|
+
}
|
|
515
|
+
issuePath[path.length] = key;
|
|
516
|
+
return issuePath;
|
|
517
|
+
};
|
|
518
|
+
const singleRuntimeTypeIssue = (formatterName, error, defaultFormatter, path) => [
|
|
468
519
|
{ name: formatterName, error, path, formatError: defaultFormatter },
|
|
469
520
|
];
|
|
470
|
-
const
|
|
471
|
-
...issue,
|
|
472
|
-
path: [key, ...issue.path],
|
|
473
|
-
}));
|
|
474
|
-
const createCollectionRuntimeTypeIssues = (name, issuesKind, defaultFormatter, getNestedType) => (error, mode) => {
|
|
521
|
+
const createCollectionRuntimeTypeIssues = (name, issuesKind, defaultFormatter, getNestedType) => (error, mode, path) => {
|
|
475
522
|
const reason = error.reason;
|
|
476
523
|
if (reason.kind !== issuesKind) {
|
|
477
|
-
return singleRuntimeTypeIssue(name, error, defaultFormatter);
|
|
524
|
+
return singleRuntimeTypeIssue(name, error, defaultFormatter, path);
|
|
478
525
|
}
|
|
479
526
|
const allIssues = reason.issues;
|
|
480
|
-
const
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
527
|
+
const issueCount = mode === "first" ? 1 : allIssues.length;
|
|
528
|
+
const result = [];
|
|
529
|
+
for (let index = 0; index < issueCount; index++) {
|
|
530
|
+
const issue = allIssues[index];
|
|
531
|
+
const issuePath = appendIssuePath(path, "key" in issue ? issue.key : issue.index);
|
|
532
|
+
if (issue.error === undefined) {
|
|
533
|
+
result.push({
|
|
534
|
+
name,
|
|
535
|
+
error: mode === "first"
|
|
536
|
+
? error
|
|
537
|
+
: {
|
|
538
|
+
type: name,
|
|
539
|
+
reason: { kind: issuesKind, issues: [issue] },
|
|
540
|
+
},
|
|
541
|
+
path: issuePath,
|
|
542
|
+
formatError: defaultFormatter,
|
|
543
|
+
});
|
|
544
|
+
continue;
|
|
485
545
|
}
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
}
|
|
546
|
+
const nestedIssues = getNestedType(issue)[getRuntimeTypeIssuesSymbol](issue.error, mode, issuePath);
|
|
547
|
+
// Nested issues are a new array, so one visited issue returns them.
|
|
548
|
+
if (issueCount === 1)
|
|
549
|
+
return nestedIssues;
|
|
550
|
+
for (const nestedIssue of nestedIssues)
|
|
551
|
+
result.push(nestedIssue);
|
|
552
|
+
}
|
|
553
|
+
return result;
|
|
493
554
|
};
|
|
494
555
|
const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
|
|
556
|
+
const formatRuntimeTypeIssue = (issue, formatIssue = formatDefaultRuntimeTypeIssue) => {
|
|
557
|
+
let message = formatIssue(issue);
|
|
558
|
+
if (issue.alternatives === undefined)
|
|
559
|
+
return message;
|
|
560
|
+
// Each alternative issue starts a "- " line; continuation lines of a
|
|
561
|
+
// multi-line message are indented.
|
|
562
|
+
for (const { index, name, issues } of issue.alternatives) {
|
|
563
|
+
for (const alternativeIssue of issues) {
|
|
564
|
+
let path = "";
|
|
565
|
+
for (const key of alternativeIssue.path) {
|
|
566
|
+
path +=
|
|
567
|
+
typeof key === "string"
|
|
568
|
+
? `[${JSON.stringify(key)}]`
|
|
569
|
+
: `[${globalThis.String(key)}]`;
|
|
570
|
+
}
|
|
571
|
+
const alternativeMessage = formatRuntimeTypeIssue(alternativeIssue, formatIssue);
|
|
572
|
+
// replaceAll is slow even without a match.
|
|
573
|
+
message += `\n- ${index}: ${name}${path}: ${alternativeMessage.includes("\n")
|
|
574
|
+
? alternativeMessage.replaceAll("\n", "\n ")
|
|
575
|
+
: alternativeMessage}`;
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return message;
|
|
579
|
+
};
|
|
495
580
|
export function assertType(type, value) {
|
|
496
581
|
if (type === undefined)
|
|
497
582
|
return;
|
|
@@ -530,6 +615,11 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
530
615
|
* that produced them. Different localized Type sets can coexist in separate
|
|
531
616
|
* application or dependency-injection scopes.
|
|
532
617
|
*
|
|
618
|
+
* A Union formatter supplies the summary of the failure. Retained member
|
|
619
|
+
* failures are appended using their own localized formatters, with member
|
|
620
|
+
* indexes, Type names, and paths identifying each alternative. The Union still
|
|
621
|
+
* produces one issue at its enclosing path.
|
|
622
|
+
*
|
|
533
623
|
* Localization is scoped to the selected Types instead of a package-wide
|
|
534
624
|
* translation registry. Static imports give bundlers an explicit dependency
|
|
535
625
|
* graph, so unrelated Types, locales, and formatters can be removed. Bundling
|
|
@@ -670,17 +760,30 @@ const withFormatError = (source, formatIssue, localizedTypeBySource) => {
|
|
|
670
760
|
const parent = source.parent
|
|
671
761
|
? withFormatError(source.parent, formatIssue, localizedTypeBySource)
|
|
672
762
|
: null;
|
|
673
|
-
const
|
|
763
|
+
const localizeIssue = (issue) => ({
|
|
674
764
|
...issue,
|
|
675
765
|
formatError: () => formatIssue(issue),
|
|
676
|
-
|
|
677
|
-
|
|
766
|
+
...(issue.alternatives === undefined
|
|
767
|
+
? {}
|
|
768
|
+
: {
|
|
769
|
+
alternatives: issue.alternatives.map((alternative) => ({
|
|
770
|
+
...alternative,
|
|
771
|
+
issues: alternative.issues.map(localizeIssue),
|
|
772
|
+
})),
|
|
773
|
+
}),
|
|
774
|
+
});
|
|
775
|
+
const getTypeIssues = (error, mode, path) => source[getRuntimeTypeIssuesSymbol](error, mode, path).map(localizeIssue);
|
|
776
|
+
const derived = createTypeNode(source.name, parent, source.fromUnknown, source.is, source[outputValidationSymbol], source[fromSymbol], source[encoderSymbol].parent ?? source[encoderSymbol], getTypeIssues, {
|
|
777
|
+
formatIssue,
|
|
778
|
+
check: source[checkSymbol],
|
|
779
|
+
refinements: source[refinementsSymbol],
|
|
780
|
+
});
|
|
678
781
|
// Localization changes error rendering, not conversion semantics. Preserve
|
|
679
782
|
// specialized public input operations such as json's single-parse Json
|
|
680
783
|
// boundary. Private operation chains on derived still support composition.
|
|
681
784
|
globalThis.Object.assign(derived, {
|
|
682
785
|
from: source.from,
|
|
683
|
-
orThrow: source.
|
|
786
|
+
orThrow: createRuntimeOrThrow(getTerminalRuntimeNode(source.from), derived.formatError),
|
|
684
787
|
orNull: source.orNull,
|
|
685
788
|
});
|
|
686
789
|
localizedTypeBySource.set(source, derived);
|
|
@@ -712,14 +815,27 @@ const localizeTypeReflection = (value, formatIssue, localizedTypeBySource) => {
|
|
|
712
815
|
return value;
|
|
713
816
|
const localized = globalThis.Object.create(globalThis.Object.getPrototypeOf(value));
|
|
714
817
|
for (const key of Reflect.ownKeys(value)) {
|
|
715
|
-
|
|
818
|
+
const property = value[key];
|
|
819
|
+
// Configured defaults are opaque data whose identity must be preserved.
|
|
820
|
+
localized[key] =
|
|
821
|
+
key === "value" && globalThis.Object.hasOwn(value, defaultPropertySymbol)
|
|
822
|
+
? property
|
|
823
|
+
: localizeTypeReflection(property, formatIssue, localizedTypeBySource);
|
|
716
824
|
}
|
|
717
825
|
return localized;
|
|
718
826
|
};
|
|
719
827
|
export function createType(name, fromUnknownOrParent, fromParentOrFormatError, formatError) {
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
828
|
+
if (typeof fromUnknownOrParent === "function") {
|
|
829
|
+
const fromUnknown = assertRefinementIdentity(fromUnknownOrParent);
|
|
830
|
+
return createRootType(name, fromUnknown, fromParentOrFormatError, {
|
|
831
|
+
// The asserted identity makes this an identity Type.
|
|
832
|
+
check: (value) => {
|
|
833
|
+
const result = fromUnknown(value);
|
|
834
|
+
return result.ok ? undefined : result.error;
|
|
835
|
+
},
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
return createChildType(name, fromUnknownOrParent, assertRefinementIdentity(fromParentOrFormatError), formatError);
|
|
723
839
|
}
|
|
724
840
|
const assertRefinementIdentity = (refinement) => (value) => {
|
|
725
841
|
const result = refinement(value);
|
|
@@ -728,11 +844,68 @@ const assertRefinementIdentity = (refinement) => (value) => {
|
|
|
728
844
|
}
|
|
729
845
|
return result;
|
|
730
846
|
};
|
|
731
|
-
|
|
847
|
+
/**
|
|
848
|
+
* Creates a root {@link Type} with a custom error for an existing validator.
|
|
849
|
+
*
|
|
850
|
+
* The source must use identity encoding. The new Type accepts its Output as
|
|
851
|
+
* Input and hides its parent boundaries. Validation and error collection are
|
|
852
|
+
* delegated automatically; the mapper receives the failure and original value.
|
|
853
|
+
* The formatter presents the mapped error as one issue.
|
|
854
|
+
*
|
|
855
|
+
* ### Example
|
|
856
|
+
*
|
|
857
|
+
* ```ts
|
|
858
|
+
* import {
|
|
859
|
+
* assertEqual,
|
|
860
|
+
* assertErr,
|
|
861
|
+
* createTypeWithError,
|
|
862
|
+
* Number,
|
|
863
|
+
* String,
|
|
864
|
+
* union,
|
|
865
|
+
* type TypeError,
|
|
866
|
+
* type UnionError,
|
|
867
|
+
* } from "@evolu/common";
|
|
868
|
+
*
|
|
869
|
+
* interface ValueError extends TypeError<"Value"> {
|
|
870
|
+
* readonly cause: UnionError;
|
|
871
|
+
* }
|
|
872
|
+
*
|
|
873
|
+
* const Value = createTypeWithError(
|
|
874
|
+
* "Value",
|
|
875
|
+
* union(String, Number),
|
|
876
|
+
* (cause): ValueError => ({ type: "Value", cause }),
|
|
877
|
+
* () => "Enter text or a number.",
|
|
878
|
+
* );
|
|
879
|
+
*
|
|
880
|
+
* const result = Value.fromUnknown(false, { errors: "all" });
|
|
881
|
+
* assertErr(result);
|
|
882
|
+
* assertEqual(result.error.cause.errors.length, 2);
|
|
883
|
+
* assertEqual(Value.formatError(result.error), "Enter text or a number.");
|
|
884
|
+
* ```
|
|
885
|
+
*
|
|
886
|
+
* @group Construction
|
|
887
|
+
*/
|
|
888
|
+
export const createTypeWithError = (name, type, mapError, formatError) => {
|
|
889
|
+
const check = type[checkSymbol];
|
|
890
|
+
return createRootType(name, (value, options) => {
|
|
891
|
+
const result = type.fromUnknown(value, options);
|
|
892
|
+
return result.ok ? result : err(mapError(result.error, value));
|
|
893
|
+
}, formatError, {
|
|
894
|
+
check: check &&
|
|
895
|
+
((value, options) => {
|
|
896
|
+
const error = check(value, options);
|
|
897
|
+
return error === undefined ? undefined : mapError(error, value);
|
|
898
|
+
}),
|
|
899
|
+
});
|
|
900
|
+
};
|
|
901
|
+
const createRootType = (name, fromUnknown, formatError, { getTypeIssues, isOutput, check, } = {}) => {
|
|
732
902
|
const runtimeFormatError = formatError;
|
|
733
903
|
const runtimeGetTypeIssues = getTypeIssues ??
|
|
734
|
-
((error) => singleRuntimeTypeIssue(error.type === "TypeOf" ? name : error.type, error, runtimeFormatError));
|
|
735
|
-
const is =
|
|
904
|
+
((error, _mode, path) => singleRuntimeTypeIssue(error.type === "TypeOf" ? name : error.type, error, runtimeFormatError, path));
|
|
905
|
+
const is = isOutput ??
|
|
906
|
+
(check
|
|
907
|
+
? (value) => check(value, firstValidationOptions) === undefined
|
|
908
|
+
: (value) => fromUnknown(value).ok);
|
|
736
909
|
const from = (value, options = firstValidationOptions) => {
|
|
737
910
|
assertTypeOutput(name, is, fromUnknown, value, options);
|
|
738
911
|
return ok(value);
|
|
@@ -746,7 +919,7 @@ const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
|
|
|
746
919
|
return value;
|
|
747
920
|
};
|
|
748
921
|
return {
|
|
749
|
-
|
|
922
|
+
"~evolu/instance": "Type",
|
|
750
923
|
name,
|
|
751
924
|
parent: null,
|
|
752
925
|
fromUnknown,
|
|
@@ -756,32 +929,134 @@ const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
|
|
|
756
929
|
to,
|
|
757
930
|
orThrow,
|
|
758
931
|
orNull: to,
|
|
759
|
-
"~standard":
|
|
932
|
+
"~standard": {
|
|
933
|
+
version: 1,
|
|
934
|
+
vendor: "evolu",
|
|
935
|
+
validate: (value) => {
|
|
936
|
+
const result = fromUnknown(value, allValidationOptions);
|
|
937
|
+
return result.ok
|
|
938
|
+
? { value: result.value }
|
|
939
|
+
: {
|
|
940
|
+
issues: runtimeGetTypeIssues(result.error, "all", []).map((issue) => ({
|
|
941
|
+
message: formatRuntimeTypeIssue(issue),
|
|
942
|
+
path: issue.path,
|
|
943
|
+
})),
|
|
944
|
+
};
|
|
945
|
+
},
|
|
946
|
+
},
|
|
760
947
|
[outputValidationSymbol]: fromUnknown,
|
|
761
948
|
[fromSymbol]: ok,
|
|
762
949
|
[encoderSymbol]: identity,
|
|
763
950
|
[getRuntimeTypeIssuesSymbol]: runtimeGetTypeIssues,
|
|
951
|
+
[checkSymbol]: check,
|
|
952
|
+
[refinementsSymbol]: undefined,
|
|
953
|
+
[arrayTypeSymbol]: undefined,
|
|
954
|
+
[setTypeSymbol]: undefined,
|
|
764
955
|
};
|
|
765
956
|
};
|
|
766
957
|
const createChildType = (name, parent, fromParent, formatOwnError,
|
|
767
958
|
// Wrapped child errors can delegate their structured issues.
|
|
768
|
-
getTypeIssuesOverride
|
|
959
|
+
getTypeIssuesOverride,
|
|
960
|
+
// A refinement `fromParent` only checks the parent Output and returns
|
|
961
|
+
// `ok()`, so the child can return the parent Result unchanged.
|
|
962
|
+
isRefinement = false) => {
|
|
769
963
|
const typeParent = parent;
|
|
770
964
|
const defaultFormatter = formatOwnError;
|
|
771
965
|
const getTypeIssues = getTypeIssuesOverride ??
|
|
772
966
|
(formatOwnError
|
|
773
|
-
? (error, mode) => error.type === name
|
|
774
|
-
? singleRuntimeTypeIssue(name, error, defaultFormatter)
|
|
775
|
-
: typeParent[getRuntimeTypeIssuesSymbol](error, mode)
|
|
967
|
+
? (error, mode, path) => error.type === name
|
|
968
|
+
? singleRuntimeTypeIssue(name, error, defaultFormatter, path)
|
|
969
|
+
: typeParent[getRuntimeTypeIssuesSymbol](error, mode, path)
|
|
776
970
|
: typeParent[getRuntimeTypeIssuesSymbol]);
|
|
971
|
+
// Validators get only the value and no receiver (refinements are read into
|
|
972
|
+
// a local before a call), so default parameters, `arguments`, and `this`
|
|
973
|
+
// behave as in a direct call.
|
|
777
974
|
const validate = fromParent;
|
|
778
|
-
|
|
779
|
-
const
|
|
975
|
+
// Returning the parent Result avoids allocating an equal Ok per layer.
|
|
976
|
+
const mapFromParent = isRefinement
|
|
977
|
+
? (result) => {
|
|
978
|
+
if (!result.ok)
|
|
979
|
+
return result;
|
|
980
|
+
const refined = validate(result.value);
|
|
981
|
+
return refined.ok ? result : refined;
|
|
982
|
+
}
|
|
983
|
+
: (result) => result.ok ? validate(result.value) : result;
|
|
984
|
+
// Parent operations are read once, so validation skips repeated property
|
|
985
|
+
// lookups on heterogeneous Type nodes.
|
|
986
|
+
const parentFromUnknown = typeParent.fromUnknown;
|
|
987
|
+
const parentValidateOutput = typeParent[outputValidationSymbol];
|
|
988
|
+
const parentFrom = typeParent[fromSymbol];
|
|
989
|
+
const fromParentOperation = (value, options = firstValidationOptions) => mapFromParent(parentFrom(value, options));
|
|
990
|
+
if (parentFrom.parent) {
|
|
991
|
+
fromParentOperation.parent = mapRuntimeOperations(parentFrom.parent, (operation) => mapRuntimeResult(operation, mapFromParent));
|
|
992
|
+
}
|
|
780
993
|
const from = createFromOperation(fromParentOperation);
|
|
781
|
-
|
|
782
|
-
|
|
994
|
+
// Refinement children form a chain whose refinements run in order after
|
|
995
|
+
// its base, the nearest ancestor that is not a refinement, so nested
|
|
996
|
+
// refinements need no call per level.
|
|
997
|
+
const parentRefinements = isRefinement
|
|
998
|
+
? typeParent[refinementsSymbol]
|
|
999
|
+
: undefined;
|
|
1000
|
+
const refinements = parentRefinements
|
|
1001
|
+
? [...parentRefinements, validate]
|
|
1002
|
+
: [validate];
|
|
1003
|
+
const refinementCount = refinements.length;
|
|
1004
|
+
let base = typeParent;
|
|
1005
|
+
if (isRefinement) {
|
|
1006
|
+
while (base[refinementsSymbol])
|
|
1007
|
+
base = base.parent;
|
|
1008
|
+
}
|
|
1009
|
+
const isBase = base.is;
|
|
1010
|
+
const is = (value) => {
|
|
1011
|
+
if (!isBase(value))
|
|
1012
|
+
return false;
|
|
1013
|
+
for (let index = 0; index < refinementCount; index++) {
|
|
1014
|
+
const refine = refinements[index];
|
|
1015
|
+
if (!refine(value).ok)
|
|
1016
|
+
return false;
|
|
1017
|
+
}
|
|
1018
|
+
return true;
|
|
1019
|
+
};
|
|
1020
|
+
const baseCheck = base[checkSymbol];
|
|
1021
|
+
// A child returns its parent Output unchanged, so a child of an identity
|
|
1022
|
+
// Type is one too.
|
|
1023
|
+
const check = baseCheck &&
|
|
1024
|
+
((value, options) => {
|
|
1025
|
+
const error = baseCheck(value, options);
|
|
1026
|
+
if (error !== undefined)
|
|
1027
|
+
return error;
|
|
1028
|
+
for (let index = 0; index < refinementCount; index++) {
|
|
1029
|
+
const refine = refinements[index];
|
|
1030
|
+
const result = refine(value);
|
|
1031
|
+
if (!result.ok)
|
|
1032
|
+
return result.error;
|
|
1033
|
+
}
|
|
1034
|
+
return undefined;
|
|
1035
|
+
});
|
|
1036
|
+
const validateBase = base.fromUnknown;
|
|
1037
|
+
// Like nested refinement children, it returns the base Result or the first
|
|
1038
|
+
// refinement Err. A single refinement is faster with its direct call.
|
|
1039
|
+
const fromUnknown = refinementCount > 1
|
|
1040
|
+
? (value, options = firstValidationOptions) => {
|
|
1041
|
+
const result = validateBase(value, options);
|
|
1042
|
+
if (!result.ok)
|
|
1043
|
+
return result;
|
|
1044
|
+
for (let index = 0; index < refinementCount; index++) {
|
|
1045
|
+
const refine = refinements[index];
|
|
1046
|
+
const refined = refine(result.value);
|
|
1047
|
+
if (!refined.ok)
|
|
1048
|
+
return refined;
|
|
1049
|
+
}
|
|
1050
|
+
return result;
|
|
1051
|
+
}
|
|
1052
|
+
: (value, options = firstValidationOptions) => mapFromParent(parentFromUnknown(value, options));
|
|
1053
|
+
// A root parent validates its Output with fromUnknown, and so does a child
|
|
1054
|
+
// of such a parent.
|
|
1055
|
+
const validateOutput = parentValidateOutput === parentFromUnknown
|
|
1056
|
+
? fromUnknown
|
|
1057
|
+
: (value, options = firstValidationOptions) => mapFromParent(parentValidateOutput(value, options));
|
|
783
1058
|
const to = identity;
|
|
784
|
-
return createTypeNode(name, typeParent, fromUnknown,
|
|
1059
|
+
return createTypeNode(name, typeParent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { check, refinements: isRefinement ? refinements : undefined });
|
|
785
1060
|
};
|
|
786
1061
|
export function transform(name, parent, output, { from: transformFrom, to: transformTo, }, formatOwnError) {
|
|
787
1062
|
const typeParent = parent;
|
|
@@ -802,20 +1077,20 @@ export function transform(name, parent, output, { from: transformFrom, to: trans
|
|
|
802
1077
|
return parentValue;
|
|
803
1078
|
};
|
|
804
1079
|
const defaultFormatter = formatOwnError;
|
|
805
|
-
const getTypeIssues = (error, mode) => {
|
|
1080
|
+
const getTypeIssues = (error, mode, path) => {
|
|
806
1081
|
if (error.type !== name) {
|
|
807
|
-
return typeParent[getRuntimeTypeIssuesSymbol](error, mode);
|
|
1082
|
+
return typeParent[getRuntimeTypeIssuesSymbol](error, mode, path);
|
|
808
1083
|
}
|
|
809
1084
|
if ("outputError" in error) {
|
|
810
|
-
return typeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
|
|
1085
|
+
return typeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode, path);
|
|
811
1086
|
}
|
|
812
|
-
return singleRuntimeTypeIssue(name, error, defaultFormatter);
|
|
1087
|
+
return singleRuntimeTypeIssue(name, error, defaultFormatter, path);
|
|
813
1088
|
};
|
|
814
1089
|
const validateOutput = (value, options) => {
|
|
815
1090
|
const result = typeOutput[outputValidationSymbol](value, options);
|
|
816
1091
|
return result.ok ? result : err({ type: name, outputError: result.error });
|
|
817
1092
|
};
|
|
818
|
-
return createTypeNode(name, typeParent, fromUnknown, typeOutput.is, validateOutput, from, to, getTypeIssues, { output: typeOutput });
|
|
1093
|
+
return createTypeNode(name, typeParent, fromUnknown, typeOutput.is, validateOutput, from, to, getTypeIssues, { additionalProperties: { output: typeOutput } });
|
|
819
1094
|
}
|
|
820
1095
|
const firstValidationOptions = { errors: "first" };
|
|
821
1096
|
const allValidationOptions = { errors: "all" };
|
|
@@ -825,7 +1100,39 @@ const fromSymbol =
|
|
|
825
1100
|
/*#__PURE__*/ globalThis.Symbol();
|
|
826
1101
|
const templateLiteralSyntaxSymbol =
|
|
827
1102
|
/*#__PURE__*/ globalThis.Symbol();
|
|
1103
|
+
// A Type node caches the Array and Set Types of itself as an element in these
|
|
1104
|
+
// own properties; a Type whose slot cannot be written, such as a frozen one,
|
|
1105
|
+
// uses the typeByElement WeakMaps. A WeakMap alone keeps each entry's value
|
|
1106
|
+
// alive through young-generation GCs, so every schema created at runtime would
|
|
1107
|
+
// be promoted.
|
|
1108
|
+
const arrayTypeSymbol = /*#__PURE__*/ globalThis.Symbol();
|
|
1109
|
+
const setTypeSymbol = /*#__PURE__*/ globalThis.Symbol();
|
|
1110
|
+
const checkSymbol =
|
|
1111
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
1112
|
+
const refinementsSymbol =
|
|
1113
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
1114
|
+
// The `fromUnknown` of an identity Type.
|
|
1115
|
+
const checkToFromUnknown = (check) => (value, options = firstValidationOptions) => {
|
|
1116
|
+
const error = check(value, options);
|
|
1117
|
+
return error === undefined ? ok(value) : err(error);
|
|
1118
|
+
};
|
|
1119
|
+
// The checks of identity Types, or `undefined` if any Type is not one.
|
|
1120
|
+
const getRuntimeChecks = (types) => {
|
|
1121
|
+
const checks = [];
|
|
1122
|
+
for (const type of types) {
|
|
1123
|
+
const check = type[checkSymbol];
|
|
1124
|
+
if (check === undefined)
|
|
1125
|
+
return undefined;
|
|
1126
|
+
checks.push(check);
|
|
1127
|
+
}
|
|
1128
|
+
return checks;
|
|
1129
|
+
};
|
|
828
1130
|
const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
|
|
1131
|
+
const createRuntimeOrThrow = (fromInput, formatError) => mapRuntimeResult(fromInput, (result) => {
|
|
1132
|
+
if (result.ok)
|
|
1133
|
+
return result.value;
|
|
1134
|
+
throw new Error(formatError(result.error), { cause: result.error });
|
|
1135
|
+
});
|
|
829
1136
|
// `map` must return a fresh operation because this function can attach `.parent`.
|
|
830
1137
|
const mapRuntimeOperations = (operation, map) => {
|
|
831
1138
|
const mapped = map(operation);
|
|
@@ -841,37 +1148,56 @@ const createFromOperation = (parent) => {
|
|
|
841
1148
|
from.parent = parent;
|
|
842
1149
|
return from;
|
|
843
1150
|
};
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
}
|
|
851
|
-
to.parent = toParent;
|
|
852
|
-
return to;
|
|
853
|
-
};
|
|
1151
|
+
// Composite Types read these operations of their Types on first use, so
|
|
1152
|
+
// construction allocates no arrays and validation skips property lookups on
|
|
1153
|
+
// heterogeneous Type nodes.
|
|
1154
|
+
const getFromUnknown = (type) => type.fromUnknown;
|
|
1155
|
+
const getValidateOutput = (type) => type[outputValidationSymbol];
|
|
1156
|
+
const getIs = (type) => type.is;
|
|
854
1157
|
function getTerminalRuntimeNode(node) {
|
|
855
1158
|
while (node.parent != null)
|
|
856
1159
|
node = node.parent;
|
|
857
1160
|
return node;
|
|
858
1161
|
}
|
|
859
|
-
const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, ownTo, getTypeIssues, additionalProperties, formatIssue = formatDefaultRuntimeTypeIssue) => {
|
|
1162
|
+
const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, ownTo, getTypeIssues, { additionalProperties, formatIssue = formatDefaultRuntimeTypeIssue, check, refinements, } = {}) => {
|
|
860
1163
|
const runtimeParent = parent;
|
|
861
|
-
const
|
|
862
|
-
|
|
863
|
-
|
|
1164
|
+
const parentTo = runtimeParent?.[encoderSymbol];
|
|
1165
|
+
// Operations defined directly here share this call's closure context, so a
|
|
1166
|
+
// Type node does not allocate one context per helper.
|
|
1167
|
+
let to = ownTo;
|
|
1168
|
+
if (parentTo) {
|
|
1169
|
+
to = (value) => parentTo(ownTo(value));
|
|
1170
|
+
const toParent = (value, options = firstValidationOptions) => ownTo(value, options);
|
|
1171
|
+
if (parentTo.parent) {
|
|
1172
|
+
toParent.parent = mapRuntimeOperations(parentTo.parent, (operation) => (value) => operation(ownTo(value)));
|
|
1173
|
+
}
|
|
1174
|
+
to.parent = toParent;
|
|
1175
|
+
}
|
|
864
1176
|
const runtimeFormatError = runtimeParent?.[getRuntimeTypeIssuesSymbol] === getTypeIssues
|
|
865
1177
|
? runtimeParent.formatError
|
|
866
|
-
: (error) =>
|
|
867
|
-
const typedFrom =
|
|
1178
|
+
: (error) => formatRuntimeTypeIssue(getTypeIssues(error, "first", [])[0], formatIssue);
|
|
1179
|
+
const typedFrom = (value, options = firstValidationOptions) => {
|
|
1180
|
+
assertTypeOutput(name, is, validateOutput, value, options);
|
|
1181
|
+
return from(value, options);
|
|
1182
|
+
};
|
|
1183
|
+
if (from.parent) {
|
|
1184
|
+
typedFrom.parent = addRuntimeAssertions(runtimeParent, from.parent);
|
|
1185
|
+
}
|
|
868
1186
|
const fromInput = getTerminalRuntimeNode(typedFrom);
|
|
869
|
-
const typedTo =
|
|
1187
|
+
const typedTo = (value) => {
|
|
870
1188
|
assertTypeOutput(name, is, validateOutput, value);
|
|
871
|
-
return
|
|
872
|
-
}
|
|
1189
|
+
return to(value);
|
|
1190
|
+
};
|
|
1191
|
+
if (to.parent) {
|
|
1192
|
+
typedTo.parent = mapRuntimeOperations(to.parent, (operation) => (value) => {
|
|
1193
|
+
assertTypeOutput(name, is, validateOutput, value);
|
|
1194
|
+
return operation(value);
|
|
1195
|
+
});
|
|
1196
|
+
}
|
|
1197
|
+
// A spread-free literal gets a fixed shape; spreading in the middle made
|
|
1198
|
+
// every later property a slow generic definition.
|
|
873
1199
|
const type = {
|
|
874
|
-
|
|
1200
|
+
"~evolu/instance": "Type",
|
|
875
1201
|
name,
|
|
876
1202
|
parent,
|
|
877
1203
|
fromUnknown,
|
|
@@ -879,41 +1205,55 @@ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, own
|
|
|
879
1205
|
is,
|
|
880
1206
|
from: typedFrom,
|
|
881
1207
|
to: typedTo,
|
|
882
|
-
orThrow:
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
1208
|
+
orThrow: (value, options = firstValidationOptions) => {
|
|
1209
|
+
const result = fromInput(value, options);
|
|
1210
|
+
if (result.ok)
|
|
1211
|
+
return result.value;
|
|
1212
|
+
throw new Error(runtimeFormatError(result.error), {
|
|
1213
|
+
cause: result.error,
|
|
1214
|
+
});
|
|
1215
|
+
},
|
|
1216
|
+
orNull: (value, options = firstValidationOptions) => getOrNull(fromInput(value, options)),
|
|
1217
|
+
"~standard": {
|
|
1218
|
+
version: 1,
|
|
1219
|
+
vendor: "evolu",
|
|
1220
|
+
validate: (value) => {
|
|
1221
|
+
const result = fromUnknown(value, allValidationOptions);
|
|
1222
|
+
return result.ok
|
|
1223
|
+
? { value: result.value }
|
|
1224
|
+
: {
|
|
1225
|
+
issues: getTypeIssues(result.error, "all", []).map((issue) => ({
|
|
1226
|
+
message: formatRuntimeTypeIssue(issue, formatIssue),
|
|
1227
|
+
path: issue.path,
|
|
1228
|
+
})),
|
|
1229
|
+
};
|
|
1230
|
+
},
|
|
1231
|
+
},
|
|
886
1232
|
[outputValidationSymbol]: validateOutput,
|
|
887
1233
|
[fromSymbol]: from,
|
|
888
1234
|
[encoderSymbol]: to,
|
|
889
1235
|
[getRuntimeTypeIssuesSymbol]: getTypeIssues,
|
|
1236
|
+
[checkSymbol]: check,
|
|
1237
|
+
[refinementsSymbol]: refinements,
|
|
1238
|
+
[arrayTypeSymbol]: undefined,
|
|
1239
|
+
[setTypeSymbol]: undefined,
|
|
890
1240
|
};
|
|
1241
|
+
if (additionalProperties !== undefined) {
|
|
1242
|
+
globalThis.Object.assign(type, additionalProperties);
|
|
1243
|
+
}
|
|
891
1244
|
return type;
|
|
892
1245
|
};
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
return result.ok
|
|
899
|
-
? { value: result.value }
|
|
900
|
-
: {
|
|
901
|
-
issues: getTypeIssues(result.error, "all").map((issue) => ({
|
|
902
|
-
message: formatIssue(issue),
|
|
903
|
-
path: issue.path,
|
|
904
|
-
})),
|
|
905
|
-
};
|
|
906
|
-
},
|
|
907
|
-
});
|
|
908
|
-
const addRuntimeAssertions = (name, is, validateOutput, parent, operation) => {
|
|
1246
|
+
// Each `from.parent` suffix asserts the Output of the next Type toward the
|
|
1247
|
+
// root before running the remaining pipeline.
|
|
1248
|
+
const addRuntimeAssertions = (boundary, operation) => {
|
|
1249
|
+
const { name, is } = boundary;
|
|
1250
|
+
const validateOutput = boundary[outputValidationSymbol];
|
|
909
1251
|
const asserted = (value, options = firstValidationOptions) => {
|
|
910
1252
|
assertTypeOutput(name, is, validateOutput, value, options);
|
|
911
1253
|
return operation(value, options);
|
|
912
1254
|
};
|
|
913
1255
|
if (operation.parent) {
|
|
914
|
-
|
|
915
|
-
const runtimeParent = typeParent;
|
|
916
|
-
asserted.parent = addRuntimeAssertions(runtimeParent.name, runtimeParent.is, runtimeParent[outputValidationSymbol], runtimeParent.parent, operation.parent);
|
|
1256
|
+
asserted.parent = addRuntimeAssertions(boundary.parent, operation.parent);
|
|
917
1257
|
}
|
|
918
1258
|
return asserted;
|
|
919
1259
|
};
|
|
@@ -922,18 +1262,26 @@ const addRuntimeAssertions = (name, is, validateOutput, parent, operation) => {
|
|
|
922
1262
|
*
|
|
923
1263
|
* @group Base
|
|
924
1264
|
*/
|
|
925
|
-
export const Unknown = /*#__PURE__*/ createRootType("Unknown", ok, identity);
|
|
1265
|
+
export const Unknown = /*#__PURE__*/ createRootType("Unknown", ok, identity, { isOutput: () => true, check: () => undefined });
|
|
926
1266
|
/**
|
|
927
1267
|
* A {@link Type} rejecting every value.
|
|
928
1268
|
*
|
|
929
1269
|
* @group Base
|
|
930
1270
|
*/
|
|
931
|
-
export const Never = /*#__PURE__*/ createRootType("Never", (value) => err({ type: "Never", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not valid for type Never
|
|
1271
|
+
export const Never = /*#__PURE__*/ createRootType("Never", (value) => err({ type: "Never", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not valid for type Never.`, {
|
|
1272
|
+
isOutput: () => false,
|
|
1273
|
+
check: (value) => ({ type: "Never", value }),
|
|
1274
|
+
});
|
|
932
1275
|
const createTypeOfType = (name) => {
|
|
933
1276
|
const typeOf = name.toLowerCase();
|
|
934
1277
|
return createRootType(name, (value) => typeof value === typeOf
|
|
935
1278
|
? ok(value)
|
|
936
|
-
: err({ type: "TypeOf", expected: name, value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not a ${typeOf}
|
|
1279
|
+
: err({ type: "TypeOf", expected: name, value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not a ${typeOf}.`, {
|
|
1280
|
+
isOutput: (value) => typeof value === typeOf,
|
|
1281
|
+
check: (value) => typeof value === typeOf
|
|
1282
|
+
? undefined
|
|
1283
|
+
: { type: "TypeOf", expected: name, value },
|
|
1284
|
+
});
|
|
937
1285
|
};
|
|
938
1286
|
/**
|
|
939
1287
|
* A JavaScript string {@link Type} without additional constraints.
|
|
@@ -1035,6 +1383,7 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
|
|
|
1035
1383
|
* >();
|
|
1036
1384
|
*
|
|
1037
1385
|
* assertOk(Age.fromUnknown(122), 122);
|
|
1386
|
+
*
|
|
1038
1387
|
* const invalid = Age.fromUnknown(200);
|
|
1039
1388
|
* assertErr(invalid);
|
|
1040
1389
|
* assertType(Data, invalid.error);
|
|
@@ -1060,6 +1409,46 @@ export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
|
|
|
1060
1409
|
* @group Base
|
|
1061
1410
|
*/
|
|
1062
1411
|
export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
|
|
1412
|
+
/**
|
|
1413
|
+
* Transforms a boolean spelled as text into a {@link Boolean}.
|
|
1414
|
+
*
|
|
1415
|
+
* This is useful for inputs that carry booleans as text, such as environment
|
|
1416
|
+
* variables, URL query parameters, and form fields. Exactly `true` and `false`
|
|
1417
|
+
* are accepted, the spellings JSON and JavaScript use, so a boolean has one
|
|
1418
|
+
* representation in every source.
|
|
1419
|
+
*
|
|
1420
|
+
* ### Example
|
|
1421
|
+
*
|
|
1422
|
+
* ```ts
|
|
1423
|
+
* import {
|
|
1424
|
+
* assertEqual,
|
|
1425
|
+
* assertErr,
|
|
1426
|
+
* assertOk,
|
|
1427
|
+
* BooleanFromString,
|
|
1428
|
+
* } from "@evolu/common";
|
|
1429
|
+
*
|
|
1430
|
+
* assertOk(BooleanFromString.fromUnknown("true"), true);
|
|
1431
|
+
* assertOk(BooleanFromString.fromUnknown("false"), false);
|
|
1432
|
+
* assertEqual(BooleanFromString.to(true), "true");
|
|
1433
|
+
*
|
|
1434
|
+
* const invalid = BooleanFromString.fromUnknown("yes");
|
|
1435
|
+
* assertErr(invalid, { type: "BooleanFromString", value: "yes" });
|
|
1436
|
+
* assertEqual(
|
|
1437
|
+
* BooleanFromString.formatError(invalid.error),
|
|
1438
|
+
* 'The value "yes" is not a boolean. Use true or false.',
|
|
1439
|
+
* );
|
|
1440
|
+
* ```
|
|
1441
|
+
*
|
|
1442
|
+
* @group Base
|
|
1443
|
+
*/
|
|
1444
|
+
export const BooleanFromString = /*#__PURE__*/ transform("BooleanFromString", String, Boolean, {
|
|
1445
|
+
from: (value) => value === "true"
|
|
1446
|
+
? ok(true)
|
|
1447
|
+
: value === "false"
|
|
1448
|
+
? ok(false)
|
|
1449
|
+
: err({ type: "BooleanFromString", value }),
|
|
1450
|
+
to: (value) => (value ? "true" : "false"),
|
|
1451
|
+
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a boolean. Use true or false.`);
|
|
1063
1452
|
/**
|
|
1064
1453
|
* A JavaScript symbol {@link Type}.
|
|
1065
1454
|
*
|
|
@@ -1085,18 +1474,25 @@ export const EvoluType = /*#__PURE__*/ createType("EvoluType", (value) => isInst
|
|
|
1085
1474
|
: err({ type: "EvoluType", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an Evolu Type.`);
|
|
1086
1475
|
export function objectTag(name, outputType) {
|
|
1087
1476
|
const formatError = (error) => `A value ${safelyStringifyUnknownValue(error.value)} does not have the expected object tag ${safelyStringifyUnknownValue(error.expected)}.`;
|
|
1477
|
+
const tag = `[object ${name}]`;
|
|
1088
1478
|
if (outputType === undefined) {
|
|
1089
|
-
return createRootType(name, (value) => hasObjectTag(value,
|
|
1479
|
+
return createRootType(name, (value) => hasObjectTag(value, tag)
|
|
1090
1480
|
? ok(value)
|
|
1091
|
-
: err({ type: "ObjectTag", expected: name, value }), formatError
|
|
1481
|
+
: err({ type: "ObjectTag", expected: name, value }), formatError, {
|
|
1482
|
+
isOutput: (value) => hasObjectTag(value, tag),
|
|
1483
|
+
check: (value) => hasObjectTag(value, tag)
|
|
1484
|
+
? undefined
|
|
1485
|
+
: { type: "ObjectTag", expected: name, value },
|
|
1486
|
+
});
|
|
1092
1487
|
}
|
|
1093
|
-
return globalThis.Object.assign(createChildType("ObjectTag", outputType, (value) => hasObjectTag(value,
|
|
1488
|
+
return globalThis.Object.assign(createChildType("ObjectTag", outputType, (value) => hasObjectTag(value, tag)
|
|
1094
1489
|
? ok(value)
|
|
1095
1490
|
: err({ type: "ObjectTag", expected: name, value }), formatError), { expected: name });
|
|
1096
1491
|
}
|
|
1097
|
-
|
|
1492
|
+
// Takes the complete `[object Name]` tag, so hot calls build no string.
|
|
1493
|
+
const hasObjectTag = (value, tag) => value !== null &&
|
|
1098
1494
|
(typeof value === "object" || typeof value === "function") &&
|
|
1099
|
-
globalThis.Object.prototype.toString.call(value) ===
|
|
1495
|
+
globalThis.Object.prototype.toString.call(value) === tag;
|
|
1100
1496
|
/**
|
|
1101
1497
|
* A realm-neutral JavaScript Date {@link Type} for trusted values.
|
|
1102
1498
|
*
|
|
@@ -1158,7 +1554,12 @@ export const instanceOf = (constructor) => {
|
|
|
1158
1554
|
const constructorName = concreteConstructor.name;
|
|
1159
1555
|
const is = (value) => globalThis.Function.prototype[globalThis.Symbol.hasInstance].call(concreteConstructor, value);
|
|
1160
1556
|
const fromUnknown = (value) => is(value) ? ok(value) : err({ type: "InstanceOf", constructorName, value });
|
|
1161
|
-
return globalThis.Object.assign(createRootType("InstanceOf", fromUnknown, (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an instance of ${error.constructorName}
|
|
1557
|
+
return globalThis.Object.assign(createRootType("InstanceOf", fromUnknown, (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an instance of ${error.constructorName}.`, {
|
|
1558
|
+
isOutput: is,
|
|
1559
|
+
check: (value) => is(value)
|
|
1560
|
+
? undefined
|
|
1561
|
+
: { type: "InstanceOf", constructorName, value },
|
|
1562
|
+
}), { constructor: concreteConstructor });
|
|
1162
1563
|
};
|
|
1163
1564
|
/**
|
|
1164
1565
|
* Literal {@link Type}.
|
|
@@ -1188,6 +1589,7 @@ export const instanceOf = (constructor) => {
|
|
|
1188
1589
|
*
|
|
1189
1590
|
* assertType<typeof Ready.Output, "ready">();
|
|
1190
1591
|
* assertOk(Ready.fromUnknown("ready"), "ready");
|
|
1592
|
+
*
|
|
1191
1593
|
* const invalid = Ready.fromUnknown("pending");
|
|
1192
1594
|
* assertErr(invalid);
|
|
1193
1595
|
* assertType(Data, invalid.error);
|
|
@@ -1202,9 +1604,6 @@ export const instanceOf = (constructor) => {
|
|
|
1202
1604
|
*/
|
|
1203
1605
|
export const literal = (expected) => {
|
|
1204
1606
|
const literalExpected = expected;
|
|
1205
|
-
const validate = (value) => value === literalExpected
|
|
1206
|
-
? ok(value)
|
|
1207
|
-
: err({ type: "Literal", expected: literalExpected, value });
|
|
1208
1607
|
const parent = typeof literalExpected === "string"
|
|
1209
1608
|
? String
|
|
1210
1609
|
: typeof literalExpected === "number"
|
|
@@ -1216,8 +1615,17 @@ export const literal = (expected) => {
|
|
|
1216
1615
|
: null;
|
|
1217
1616
|
const formatError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`;
|
|
1218
1617
|
return globalThis.Object.assign(parent
|
|
1219
|
-
? createChildType("Literal", parent,
|
|
1220
|
-
|
|
1618
|
+
? createChildType("Literal", parent, (value) => value === literalExpected
|
|
1619
|
+
? ok()
|
|
1620
|
+
: err({ type: "Literal", expected: literalExpected, value }), formatError, undefined, true)
|
|
1621
|
+
: createRootType("Literal", (value) => value === literalExpected
|
|
1622
|
+
? ok(value)
|
|
1623
|
+
: err({ type: "Literal", expected: literalExpected, value }), formatError, {
|
|
1624
|
+
isOutput: (value) => value === literalExpected,
|
|
1625
|
+
check: (value) => value === literalExpected
|
|
1626
|
+
? undefined
|
|
1627
|
+
: { type: "Literal", expected: literalExpected, value },
|
|
1628
|
+
}), {
|
|
1221
1629
|
expected: literalExpected,
|
|
1222
1630
|
[templateLiteralSyntaxSymbol]: true,
|
|
1223
1631
|
});
|
|
@@ -1239,17 +1647,35 @@ export function union(...typesOrLiterals) {
|
|
|
1239
1647
|
? typeOrLiteral
|
|
1240
1648
|
: literal(typeOrLiteral));
|
|
1241
1649
|
const inputMembers = members.map(getTerminalRuntimeNode);
|
|
1242
|
-
const
|
|
1243
|
-
const
|
|
1244
|
-
|
|
1245
|
-
|
|
1246
|
-
const
|
|
1247
|
-
const
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1650
|
+
const inputCheck = createUnionCheck(inputMembers);
|
|
1651
|
+
const inputFrom = inputCheck
|
|
1652
|
+
? checkToFromUnknown(inputCheck)
|
|
1653
|
+
: createUnionValidation(inputMembers, getFromUnknown);
|
|
1654
|
+
const getTypeIssues = createUnionRuntimeTypeIssues(members);
|
|
1655
|
+
const input = createTypeNode("Union", null, inputFrom, createUnionIs(inputMembers), inputCheck
|
|
1656
|
+
? inputFrom
|
|
1657
|
+
: createUnionValidation(inputMembers, getValidateOutput), ok, identity, createUnionRuntimeTypeIssues(inputMembers), { check: inputCheck });
|
|
1658
|
+
// Literal members accept exactly their `===` equal values, so a match skips
|
|
1659
|
+
// them.
|
|
1660
|
+
const isLiteralUnion = typesOrLiterals.every((typeOrLiteral) => typeOrLiteral === null || typeof typeOrLiteral !== "object");
|
|
1661
|
+
const isLiteral = (value) =>
|
|
1662
|
+
// oxlint-disable-next-line typescript/prefer-includes -- Like Literal validation, indexOf uses ===, so NaN never matches.
|
|
1663
|
+
typesOrLiterals.indexOf(value) !== -1;
|
|
1664
|
+
const checkMembers = createUnionCheck(members);
|
|
1665
|
+
const check = checkMembers && isLiteralUnion
|
|
1666
|
+
? (value, options) => isLiteral(value) ? undefined : checkMembers(value, options)
|
|
1667
|
+
: checkMembers;
|
|
1668
|
+
const fromUnknown = check
|
|
1669
|
+
? checkToFromUnknown(check)
|
|
1670
|
+
: createUnionValidation(members, getFromUnknown);
|
|
1671
|
+
const validateOutput = check
|
|
1672
|
+
? fromUnknown
|
|
1673
|
+
: createUnionValidation(members, getValidateOutput);
|
|
1674
|
+
const fromParent = createUnionValidation(members, (member, index) => {
|
|
1675
|
+
const memberFromInput = getTerminalRuntimeNode(member[fromSymbol]);
|
|
1676
|
+
const inputIs = inputMembers[index].is;
|
|
1677
|
+
return (value, options) => inputIs(value) ? memberFromInput(value, options) : undefined;
|
|
1678
|
+
});
|
|
1253
1679
|
const getOutputMember = (value) => members.find((member) => member.is(value));
|
|
1254
1680
|
const from = createFromOperation(fromParent);
|
|
1255
1681
|
const to = (value) => {
|
|
@@ -1257,25 +1683,99 @@ export function union(...typesOrLiterals) {
|
|
|
1257
1683
|
assertNonNullable(member);
|
|
1258
1684
|
return member[encoderSymbol](value);
|
|
1259
1685
|
};
|
|
1260
|
-
return createTypeNode("Union", input, fromUnknown,
|
|
1686
|
+
return createTypeNode("Union", input, fromUnknown, isLiteralUnion ? isLiteral : createUnionIs(members), validateOutput, from, to, getTypeIssues, {
|
|
1687
|
+
additionalProperties: { members, [templateLiteralSyntaxSymbol]: true },
|
|
1688
|
+
check,
|
|
1689
|
+
});
|
|
1261
1690
|
}
|
|
1262
|
-
const
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1691
|
+
const createUnionRuntimeTypeIssues = (members) => (error, _mode, path) => [
|
|
1692
|
+
{
|
|
1693
|
+
name: "Union",
|
|
1694
|
+
error,
|
|
1695
|
+
path,
|
|
1696
|
+
formatError: () => "A value does not match any allowed variant.",
|
|
1697
|
+
// Alternative issue paths are relative to the Union value.
|
|
1698
|
+
alternatives: error.errors.map(({ index, error }) => ({
|
|
1699
|
+
index,
|
|
1700
|
+
name: members[index].name,
|
|
1701
|
+
issues: members[index][getRuntimeTypeIssuesSymbol](error, "all", []),
|
|
1702
|
+
})),
|
|
1703
|
+
},
|
|
1704
|
+
];
|
|
1705
|
+
const createUnionIs = (members) => {
|
|
1706
|
+
// Two members, as in nullOr, are checked without a loop.
|
|
1707
|
+
if (members.length === 2) {
|
|
1708
|
+
const firstIs = members[0].is;
|
|
1709
|
+
const secondIs = members[1].is;
|
|
1710
|
+
return (value) => firstIs(value) || secondIs(value);
|
|
1273
1711
|
}
|
|
1274
|
-
|
|
1275
|
-
return
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1712
|
+
let memberIs;
|
|
1713
|
+
return (value) => {
|
|
1714
|
+
const isMembers = (memberIs ??= members.map(getIs));
|
|
1715
|
+
for (let index = 0; index < isMembers.length; index++) {
|
|
1716
|
+
if (isMembers[index](value))
|
|
1717
|
+
return true;
|
|
1718
|
+
}
|
|
1719
|
+
return false;
|
|
1720
|
+
};
|
|
1721
|
+
};
|
|
1722
|
+
// A Union of identity Types returns the Result of the first valid member, so
|
|
1723
|
+
// it is an identity Type too.
|
|
1724
|
+
const createUnionCheck = (members) => {
|
|
1725
|
+
const checks = getRuntimeChecks(members);
|
|
1726
|
+
if (checks === undefined)
|
|
1727
|
+
return undefined;
|
|
1728
|
+
return (value, options) => {
|
|
1729
|
+
// The same errors as createUnionValidation.
|
|
1730
|
+
let first;
|
|
1731
|
+
let errors;
|
|
1732
|
+
for (let index = 0; index < checks.length; index++) {
|
|
1733
|
+
const error = checks[index](value, options);
|
|
1734
|
+
if (error === undefined)
|
|
1735
|
+
return undefined;
|
|
1736
|
+
if (first === undefined) {
|
|
1737
|
+
first = { index, error };
|
|
1738
|
+
}
|
|
1739
|
+
else if (options.errors === "all") {
|
|
1740
|
+
(errors ??= [first]).push({ index, error });
|
|
1741
|
+
}
|
|
1742
|
+
}
|
|
1743
|
+
assertNonNullable(first);
|
|
1744
|
+
return {
|
|
1745
|
+
type: "Union",
|
|
1746
|
+
errors: (errors ?? [first]),
|
|
1747
|
+
};
|
|
1748
|
+
};
|
|
1749
|
+
};
|
|
1750
|
+
// A member validation returns `undefined` to skip its member.
|
|
1751
|
+
const createUnionValidation = (members, getMemberValidation) => {
|
|
1752
|
+
let memberValidations;
|
|
1753
|
+
return (value, options = firstValidationOptions) => {
|
|
1754
|
+
const validations = (memberValidations ??=
|
|
1755
|
+
members.map(getMemberValidation));
|
|
1756
|
+
// A later member often matches (nullOr), so the error array is created
|
|
1757
|
+
// only when a second member fails or the Union fails.
|
|
1758
|
+
let first;
|
|
1759
|
+
let errors;
|
|
1760
|
+
for (let index = 0; index < validations.length; index++) {
|
|
1761
|
+
const result = validations[index](value, options);
|
|
1762
|
+
if (result === undefined)
|
|
1763
|
+
continue;
|
|
1764
|
+
if (result.ok)
|
|
1765
|
+
return result;
|
|
1766
|
+
if (first === undefined) {
|
|
1767
|
+
first = { index, error: result.error };
|
|
1768
|
+
}
|
|
1769
|
+
else if (options.errors === "all") {
|
|
1770
|
+
(errors ??= [first]).push({ index, error: result.error });
|
|
1771
|
+
}
|
|
1772
|
+
}
|
|
1773
|
+
assertNonNullable(first);
|
|
1774
|
+
return err({
|
|
1775
|
+
type: "Union",
|
|
1776
|
+
errors: (errors ?? [first]),
|
|
1777
|
+
});
|
|
1778
|
+
};
|
|
1279
1779
|
};
|
|
1280
1780
|
/**
|
|
1281
1781
|
* Union {@link Type} containing the supplied Type and `undefined`.
|
|
@@ -1398,6 +1898,7 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" && "members" in t
|
|
|
1398
1898
|
* assertOk(result, ["cs", "CZ"]);
|
|
1399
1899
|
* const locale = result.value;
|
|
1400
1900
|
* assertType<typeof locale, SupportedLocale>();
|
|
1901
|
+
*
|
|
1401
1902
|
* const invalid = SupportedLocale.fromUnknown("cs/CZ");
|
|
1402
1903
|
* assertErr(invalid);
|
|
1403
1904
|
* assertType(Data, invalid.error);
|
|
@@ -1568,14 +2069,14 @@ const createTemplateLiteralParserType = (templateParts) => {
|
|
|
1568
2069
|
return result;
|
|
1569
2070
|
return err({ type: "TemplateLiteral", value: stringResult.value });
|
|
1570
2071
|
};
|
|
1571
|
-
const getTypeIssues = (error, mode) => {
|
|
2072
|
+
const getTypeIssues = (error, mode, path) => {
|
|
1572
2073
|
if (error.type !== "TemplateLiteral") {
|
|
1573
|
-
return String[getRuntimeTypeIssuesSymbol](error, mode);
|
|
2074
|
+
return String[getRuntimeTypeIssuesSymbol](error, mode, path);
|
|
1574
2075
|
}
|
|
1575
2076
|
if ("outputError" in error) {
|
|
1576
|
-
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
|
|
2077
|
+
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode, path);
|
|
1577
2078
|
}
|
|
1578
|
-
return singleRuntimeTypeIssue("TemplateLiteral", error, ((error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`));
|
|
2079
|
+
return singleRuntimeTypeIssue("TemplateLiteral", error, ((error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`), path);
|
|
1579
2080
|
};
|
|
1580
2081
|
const canonicalStringFromUnknown = (value, options = firstValidationOptions) => {
|
|
1581
2082
|
const stringResult = String.fromUnknown(value, options);
|
|
@@ -1584,7 +2085,7 @@ const createTemplateLiteralParserType = (templateParts) => {
|
|
|
1584
2085
|
: stringResult;
|
|
1585
2086
|
};
|
|
1586
2087
|
const canonicalStringFrom = createFromOperation((value, options = firstValidationOptions) => canonicalizeString(value, options));
|
|
1587
|
-
const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, reflection);
|
|
2088
|
+
const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, { additionalProperties: reflection });
|
|
1588
2089
|
const fromUnknown = (value, options = firstValidationOptions) => {
|
|
1589
2090
|
const stringResult = String.fromUnknown(value, options);
|
|
1590
2091
|
if (!stringResult.ok)
|
|
@@ -1614,7 +2115,7 @@ const createTemplateLiteralParserType = (templateParts) => {
|
|
|
1614
2115
|
};
|
|
1615
2116
|
fromCanonicalString.parent = fromString;
|
|
1616
2117
|
const from = createFromOperation(fromCanonicalString);
|
|
1617
|
-
const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, reflection);
|
|
2118
|
+
const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, { additionalProperties: reflection });
|
|
1618
2119
|
type.from.parent =
|
|
1619
2120
|
fromCanonicalString;
|
|
1620
2121
|
return type;
|
|
@@ -1756,9 +2257,7 @@ const getTemplateLiteralPartFraming = (part) => {
|
|
|
1756
2257
|
return getTemplateLiteralPartFraming(type.parent);
|
|
1757
2258
|
};
|
|
1758
2259
|
export function brand(name, parent, validate, formatError) {
|
|
1759
|
-
return createChildType(name, parent, validate
|
|
1760
|
-
? (value) => flatMapResult(validate(value), () => ok(value))
|
|
1761
|
-
: ok, formatError);
|
|
2260
|
+
return createChildType(name, parent, validate ?? (() => ok()), formatError, undefined, true);
|
|
1762
2261
|
}
|
|
1763
2262
|
/**
|
|
1764
2263
|
* Canonical ISO date-time {@link String}.
|
|
@@ -1785,6 +2284,7 @@ export function brand(name, parent, validate, formatError) {
|
|
|
1785
2284
|
*
|
|
1786
2285
|
* const value = "2023-01-01T12:00:00.000Z";
|
|
1787
2286
|
* assertOk(DateIso.fromUnknown(value), value);
|
|
2287
|
+
*
|
|
1788
2288
|
* const invalid = DateIso.fromUnknown("2023-01-01");
|
|
1789
2289
|
* assertErr(invalid);
|
|
1790
2290
|
* assertType(Data, invalid.error);
|
|
@@ -1837,126 +2337,790 @@ export const UInt64 = /*#__PURE__*/ brand("UInt64", BigInt, (value) => globalThi
|
|
|
1837
2337
|
? ok()
|
|
1838
2338
|
: err({ type: "UInt64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid unsigned 64-bit integer (UInt64).`);
|
|
1839
2339
|
/**
|
|
1840
|
-
*
|
|
1841
|
-
*
|
|
1842
|
-
*
|
|
2340
|
+
* Adds identifier validation in a naming convention to an existing string Type.
|
|
2341
|
+
*
|
|
2342
|
+
* An identifier is one or more ASCII words, each starting with a letter and
|
|
2343
|
+
* continuing with letters or digits. camelCase and PascalCase start every word
|
|
2344
|
+
* after the first with an uppercase letter, so `httpUrl` has two words and
|
|
2345
|
+
* `httpURL` has four. snake_case, kebab-case, and CONSTANT_CASE put exactly one
|
|
2346
|
+
* separator between words. Validation keeps the spelling and rejects empty
|
|
2347
|
+
* strings, whitespace, punctuation, and non-ASCII characters.
|
|
2348
|
+
*
|
|
2349
|
+
* Words never start with a digit. Identifier grammars in most languages forbid
|
|
2350
|
+
* a leading digit, and camelCase cannot mark a word boundary before one, so the
|
|
2351
|
+
* rule applies to every word and keeps every conversion exact. Join an
|
|
2352
|
+
* abbreviation such as `2FA` to the previous word, as in `MAX2FA_ATTEMPTS` and
|
|
2353
|
+
* `max2faAttempts`, or spell the number out, as in `TWO_FACTOR_SECRET`.
|
|
2354
|
+
*
|
|
2355
|
+
* Convert validated identifiers with functions such as
|
|
2356
|
+
* {@link camelCaseToSnakeCase}. Conversions preserve word boundaries, and
|
|
2357
|
+
* converting back restores the original spelling. They return only the
|
|
2358
|
+
* destination brand, dropping unrelated constraints such as input length.
|
|
1843
2359
|
*
|
|
1844
2360
|
* ### Example
|
|
1845
2361
|
*
|
|
1846
2362
|
* ```ts
|
|
1847
2363
|
* import {
|
|
1848
|
-
* assertEqual,
|
|
1849
2364
|
* assertErr,
|
|
1850
2365
|
* assertOk,
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
2366
|
+
* identifier,
|
|
2367
|
+
* maxLength,
|
|
1853
2368
|
* String,
|
|
1854
|
-
* capitalized,
|
|
1855
|
-
* type Brand,
|
|
1856
2369
|
* } from "@evolu/common";
|
|
1857
2370
|
*
|
|
1858
|
-
* const
|
|
1859
|
-
* type CapitalizedString = typeof CapitalizedString.Output;
|
|
1860
|
-
*
|
|
1861
|
-
* assertType<CapitalizedString, string & Brand<"Capitalized">>();
|
|
2371
|
+
* const EnvName = identifier("CONSTANT_CASE")(maxLength(16)(String));
|
|
1862
2372
|
*
|
|
1863
|
-
* assertOk(
|
|
1864
|
-
*
|
|
1865
|
-
* assertErr(invalid);
|
|
1866
|
-
* assertType(Data, invalid.error);
|
|
1867
|
-
* assertEqual(invalid.error, {
|
|
1868
|
-
* type: "Capitalized",
|
|
1869
|
-
* value: "evolu",
|
|
1870
|
-
* });
|
|
2373
|
+
* assertOk(EnvName.fromUnknown("HTTP2_PORT"), "HTTP2_PORT");
|
|
2374
|
+
* assertErr(EnvName.fromUnknown("HTTP_2_PORT"));
|
|
1871
2375
|
* ```
|
|
1872
2376
|
*
|
|
1873
2377
|
* @group String
|
|
1874
2378
|
*/
|
|
1875
|
-
export const
|
|
1876
|
-
const
|
|
1877
|
-
|
|
2379
|
+
export const identifier = (casing) => (parent) => {
|
|
2380
|
+
const name = identifierBrandByCasing[casing];
|
|
2381
|
+
const pattern = identifierPatternByCasing[casing];
|
|
2382
|
+
return brand(name, parent, (value) => pattern.test(value)
|
|
1878
2383
|
? ok()
|
|
1879
|
-
: err({ type:
|
|
1880
|
-
}
|
|
2384
|
+
: err({ type: name, value, casing }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a ${error.casing} identifier.`);
|
|
2385
|
+
};
|
|
2386
|
+
const identifierBrandByCasing = {
|
|
2387
|
+
camelCase: "CamelCaseIdentifier",
|
|
2388
|
+
PascalCase: "PascalCaseIdentifier",
|
|
2389
|
+
snake_case: "SnakeCaseIdentifier",
|
|
2390
|
+
"kebab-case": "KebabCaseIdentifier",
|
|
2391
|
+
CONSTANT_CASE: "ConstantCaseIdentifier",
|
|
2392
|
+
};
|
|
2393
|
+
const identifierPatternByCasing = {
|
|
2394
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/u,
|
|
2395
|
+
PascalCase: /^[A-Z][a-zA-Z0-9]*$/u,
|
|
2396
|
+
snake_case: /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/u,
|
|
2397
|
+
"kebab-case": /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/u,
|
|
2398
|
+
CONSTANT_CASE: /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
|
|
2399
|
+
};
|
|
1881
2400
|
/**
|
|
1882
|
-
*
|
|
2401
|
+
* A validated camelCase identifier, such as `http2Port`.
|
|
2402
|
+
*
|
|
2403
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
1883
2404
|
*
|
|
1884
2405
|
* @group String
|
|
1885
2406
|
*/
|
|
1886
|
-
export const
|
|
2407
|
+
export const CamelCaseIdentifier =
|
|
2408
|
+
/*#__PURE__*/ identifier("camelCase")(String);
|
|
1887
2409
|
/**
|
|
1888
|
-
*
|
|
1889
|
-
*
|
|
1890
|
-
* ### Example
|
|
2410
|
+
* A validated PascalCase identifier, such as `Http2Port`.
|
|
1891
2411
|
*
|
|
1892
|
-
*
|
|
1893
|
-
* import { assertOk, String, trimmed } from "@evolu/common";
|
|
2412
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
1894
2413
|
*
|
|
1895
|
-
*
|
|
2414
|
+
* @group String
|
|
2415
|
+
*/
|
|
2416
|
+
export const PascalCaseIdentifier =
|
|
2417
|
+
/*#__PURE__*/ identifier("PascalCase")(String);
|
|
2418
|
+
/**
|
|
2419
|
+
* A validated snake_case identifier, such as `http2_port`.
|
|
1896
2420
|
*
|
|
1897
|
-
*
|
|
1898
|
-
* ```
|
|
2421
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
1899
2422
|
*
|
|
1900
2423
|
* @group String
|
|
1901
2424
|
*/
|
|
1902
|
-
export const
|
|
1903
|
-
|
|
1904
|
-
: err({ type: "Trimmed", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`);
|
|
2425
|
+
export const SnakeCaseIdentifier =
|
|
2426
|
+
/*#__PURE__*/ identifier("snake_case")(String);
|
|
1905
2427
|
/**
|
|
1906
|
-
* A
|
|
2428
|
+
* A validated kebab-case identifier, such as `http2-port`.
|
|
1907
2429
|
*
|
|
1908
|
-
*
|
|
1909
|
-
* value. Use {@link trim} to normalize a string. Because an empty string is
|
|
1910
|
-
* valid, this Type is useful as an intermediate boundary for input controls
|
|
1911
|
-
* that trim their values before domain validation. If an empty string is
|
|
1912
|
-
* invalid, use {@link NonEmptyTrimmedString}.
|
|
2430
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
1913
2431
|
*
|
|
1914
2432
|
* @group String
|
|
1915
2433
|
*/
|
|
1916
|
-
export const
|
|
2434
|
+
export const KebabCaseIdentifier =
|
|
2435
|
+
/*#__PURE__*/ identifier("kebab-case")(String);
|
|
1917
2436
|
/**
|
|
1918
|
-
*
|
|
2437
|
+
* A validated CONSTANT_CASE identifier, such as `HTTP2_PORT`.
|
|
1919
2438
|
*
|
|
1920
|
-
*
|
|
2439
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
1921
2440
|
*
|
|
1922
|
-
*
|
|
1923
|
-
|
|
2441
|
+
* @group String
|
|
2442
|
+
*/
|
|
2443
|
+
export const ConstantCaseIdentifier =
|
|
2444
|
+
/*#__PURE__*/ identifier("CONSTANT_CASE")(String);
|
|
2445
|
+
/**
|
|
2446
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
1924
2447
|
*
|
|
1925
|
-
*
|
|
1926
|
-
*
|
|
2448
|
+
* Converts `http2Port` to `Http2Port`. Use {@link pascalCaseToCamelCase} to
|
|
2449
|
+
* recover the original spelling.
|
|
1927
2450
|
*
|
|
1928
2451
|
* @group String
|
|
1929
2452
|
*/
|
|
1930
|
-
export const
|
|
2453
|
+
export const camelCaseToPascalCase = (value) => (value.charAt(0).toUpperCase() + value.slice(1));
|
|
1931
2454
|
/**
|
|
1932
|
-
*
|
|
1933
|
-
*
|
|
1934
|
-
* ### Example
|
|
2455
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
1935
2456
|
*
|
|
1936
|
-
*
|
|
1937
|
-
*
|
|
2457
|
+
* Converts `http2Port` to `http2_port`. Use {@link snakeCaseToCamelCase} to
|
|
2458
|
+
* recover the original spelling.
|
|
1938
2459
|
*
|
|
1939
|
-
*
|
|
1940
|
-
|
|
2460
|
+
* @group String
|
|
2461
|
+
*/
|
|
2462
|
+
export const camelCaseToSnakeCase = (value) => value.replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
|
|
2463
|
+
/**
|
|
2464
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
1941
2465
|
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
1944
|
-
* ```
|
|
2466
|
+
* Converts `http2Port` to `http2-port`. Use {@link kebabCaseToCamelCase} to
|
|
2467
|
+
* recover the original spelling.
|
|
1945
2468
|
*
|
|
1946
2469
|
* @group String
|
|
1947
|
-
* @group Collection
|
|
1948
2470
|
*/
|
|
1949
|
-
export const
|
|
1950
|
-
const name = `MinLength${min}`;
|
|
1951
|
-
return brand(name, parent, (value) => value.length >= min
|
|
1952
|
-
? ok()
|
|
1953
|
-
: err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not meet the minimum length of ${error.min}.`);
|
|
1954
|
-
};
|
|
2471
|
+
export const camelCaseToKebabCase = (value) => snakeCaseToKebabCase(camelCaseToSnakeCase(value));
|
|
1955
2472
|
/**
|
|
1956
|
-
*
|
|
2473
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
1957
2474
|
*
|
|
1958
|
-
*
|
|
1959
|
-
*
|
|
2475
|
+
* Converts `http2Port` to `HTTP2_PORT`. Use {@link constantCaseToCamelCase} to
|
|
2476
|
+
* recover the original spelling.
|
|
2477
|
+
*
|
|
2478
|
+
* @group String
|
|
2479
|
+
*/
|
|
2480
|
+
export const camelCaseToConstantCase = (value) => snakeCaseToConstantCase(camelCaseToSnakeCase(value));
|
|
2481
|
+
/**
|
|
2482
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2483
|
+
*
|
|
2484
|
+
* Converts `Http2Port` to `http2Port`. Use {@link camelCaseToPascalCase} to
|
|
2485
|
+
* recover the original spelling.
|
|
2486
|
+
*
|
|
2487
|
+
* @group String
|
|
2488
|
+
*/
|
|
2489
|
+
export const pascalCaseToCamelCase = (value) => (value.charAt(0).toLowerCase() + value.slice(1));
|
|
2490
|
+
/**
|
|
2491
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2492
|
+
*
|
|
2493
|
+
* Converts `Http2Port` to `http2_port`. Use {@link snakeCaseToPascalCase} to
|
|
2494
|
+
* recover the original spelling.
|
|
2495
|
+
*
|
|
2496
|
+
* @group String
|
|
2497
|
+
*/
|
|
2498
|
+
export const pascalCaseToSnakeCase = (value) => camelCaseToSnakeCase(pascalCaseToCamelCase(value));
|
|
2499
|
+
/**
|
|
2500
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2501
|
+
*
|
|
2502
|
+
* Converts `Http2Port` to `http2-port`. Use {@link kebabCaseToPascalCase} to
|
|
2503
|
+
* recover the original spelling.
|
|
2504
|
+
*
|
|
2505
|
+
* @group String
|
|
2506
|
+
*/
|
|
2507
|
+
export const pascalCaseToKebabCase = (value) => snakeCaseToKebabCase(pascalCaseToSnakeCase(value));
|
|
2508
|
+
/**
|
|
2509
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2510
|
+
*
|
|
2511
|
+
* Converts `Http2Port` to `HTTP2_PORT`. Use {@link constantCaseToPascalCase} to
|
|
2512
|
+
* recover the original spelling.
|
|
2513
|
+
*
|
|
2514
|
+
* @group String
|
|
2515
|
+
*/
|
|
2516
|
+
export const pascalCaseToConstantCase = (value) => snakeCaseToConstantCase(pascalCaseToSnakeCase(value));
|
|
2517
|
+
/**
|
|
2518
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2519
|
+
*
|
|
2520
|
+
* Converts `http2_port` to `http2Port`. Use {@link camelCaseToSnakeCase} to
|
|
2521
|
+
* recover the original spelling.
|
|
2522
|
+
*
|
|
2523
|
+
* @group String
|
|
2524
|
+
*/
|
|
2525
|
+
export const snakeCaseToCamelCase = (value) => value.replaceAll(/_[a-z]/gu, (word) => word.charAt(1).toUpperCase());
|
|
2526
|
+
/**
|
|
2527
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2528
|
+
*
|
|
2529
|
+
* Converts `http2_port` to `Http2Port`. Use {@link pascalCaseToSnakeCase} to
|
|
2530
|
+
* recover the original spelling.
|
|
2531
|
+
*
|
|
2532
|
+
* @group String
|
|
2533
|
+
*/
|
|
2534
|
+
export const snakeCaseToPascalCase = (value) => camelCaseToPascalCase(snakeCaseToCamelCase(value));
|
|
2535
|
+
/**
|
|
2536
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2537
|
+
*
|
|
2538
|
+
* Converts `http2_port` to `http2-port`. Use {@link kebabCaseToSnakeCase} to
|
|
2539
|
+
* recover the original spelling.
|
|
2540
|
+
*
|
|
2541
|
+
* @group String
|
|
2542
|
+
*/
|
|
2543
|
+
export const snakeCaseToKebabCase = (value) => value.replaceAll("_", "-");
|
|
2544
|
+
/**
|
|
2545
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2546
|
+
*
|
|
2547
|
+
* Converts `http2_port` to `HTTP2_PORT`. Use {@link constantCaseToSnakeCase} to
|
|
2548
|
+
* recover the original spelling.
|
|
2549
|
+
*
|
|
2550
|
+
* @group String
|
|
2551
|
+
*/
|
|
2552
|
+
export const snakeCaseToConstantCase = (value) => value.toUpperCase();
|
|
2553
|
+
/**
|
|
2554
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2555
|
+
*
|
|
2556
|
+
* Converts `http2-port` to `http2Port`. Use {@link camelCaseToKebabCase} to
|
|
2557
|
+
* recover the original spelling.
|
|
2558
|
+
*
|
|
2559
|
+
* @group String
|
|
2560
|
+
*/
|
|
2561
|
+
export const kebabCaseToCamelCase = (value) => snakeCaseToCamelCase(kebabCaseToSnakeCase(value));
|
|
2562
|
+
/**
|
|
2563
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2564
|
+
*
|
|
2565
|
+
* Converts `http2-port` to `Http2Port`. Use {@link pascalCaseToKebabCase} to
|
|
2566
|
+
* recover the original spelling.
|
|
2567
|
+
*
|
|
2568
|
+
* @group String
|
|
2569
|
+
*/
|
|
2570
|
+
export const kebabCaseToPascalCase = (value) => camelCaseToPascalCase(kebabCaseToCamelCase(value));
|
|
2571
|
+
/**
|
|
2572
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2573
|
+
*
|
|
2574
|
+
* Converts `http2-port` to `http2_port`. Use {@link snakeCaseToKebabCase} to
|
|
2575
|
+
* recover the original spelling.
|
|
2576
|
+
*
|
|
2577
|
+
* @group String
|
|
2578
|
+
*/
|
|
2579
|
+
export const kebabCaseToSnakeCase = (value) => value.replaceAll("-", "_");
|
|
2580
|
+
/**
|
|
2581
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2582
|
+
*
|
|
2583
|
+
* Converts `http2-port` to `HTTP2_PORT`. Use {@link constantCaseToKebabCase} to
|
|
2584
|
+
* recover the original spelling.
|
|
2585
|
+
*
|
|
2586
|
+
* @group String
|
|
2587
|
+
*/
|
|
2588
|
+
export const kebabCaseToConstantCase = (value) => snakeCaseToConstantCase(kebabCaseToSnakeCase(value));
|
|
2589
|
+
/**
|
|
2590
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2591
|
+
*
|
|
2592
|
+
* Converts `HTTP2_PORT` to `http2Port`. Use {@link camelCaseToConstantCase} to
|
|
2593
|
+
* recover the original spelling.
|
|
2594
|
+
*
|
|
2595
|
+
* @group String
|
|
2596
|
+
*/
|
|
2597
|
+
export const constantCaseToCamelCase = (value) => snakeCaseToCamelCase(constantCaseToSnakeCase(value));
|
|
2598
|
+
/**
|
|
2599
|
+
* Decodes a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier} and
|
|
2600
|
+
* restores the original spelling when encoding.
|
|
2601
|
+
*
|
|
2602
|
+
* ### Example
|
|
2603
|
+
*
|
|
2604
|
+
* ```ts
|
|
2605
|
+
* import {
|
|
2606
|
+
* assertEqual,
|
|
2607
|
+
* assertOk,
|
|
2608
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
2609
|
+
* } from "@evolu/common";
|
|
2610
|
+
*
|
|
2611
|
+
* const Key = CamelCaseIdentifierFromConstantCaseIdentifier;
|
|
2612
|
+
* const result = Key.fromUnknown("HTTP2_PORT");
|
|
2613
|
+
* assertOk(result, "http2Port");
|
|
2614
|
+
* assertEqual(Key.to(result.value), "HTTP2_PORT");
|
|
2615
|
+
* ```
|
|
2616
|
+
*
|
|
2617
|
+
* @group String
|
|
2618
|
+
*/
|
|
2619
|
+
export const CamelCaseIdentifierFromConstantCaseIdentifier =
|
|
2620
|
+
/*#__PURE__*/ transform("CamelCaseIdentifierFromConstantCaseIdentifier", ConstantCaseIdentifier, CamelCaseIdentifier, {
|
|
2621
|
+
from: (value) => ok(constantCaseToCamelCase(value)),
|
|
2622
|
+
to: camelCaseToConstantCase,
|
|
2623
|
+
});
|
|
2624
|
+
/**
|
|
2625
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2626
|
+
*
|
|
2627
|
+
* Converts `HTTP2_PORT` to `Http2Port`. Use {@link pascalCaseToConstantCase} to
|
|
2628
|
+
* recover the original spelling.
|
|
2629
|
+
*
|
|
2630
|
+
* @group String
|
|
2631
|
+
*/
|
|
2632
|
+
export const constantCaseToPascalCase = (value) => camelCaseToPascalCase(constantCaseToCamelCase(value));
|
|
2633
|
+
/**
|
|
2634
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2635
|
+
*
|
|
2636
|
+
* Converts `HTTP2_PORT` to `http2_port`. Use {@link snakeCaseToConstantCase} to
|
|
2637
|
+
* recover the original spelling.
|
|
2638
|
+
*
|
|
2639
|
+
* @group String
|
|
2640
|
+
*/
|
|
2641
|
+
export const constantCaseToSnakeCase = (value) => value.toLowerCase();
|
|
2642
|
+
/**
|
|
2643
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2644
|
+
*
|
|
2645
|
+
* Converts `HTTP2_PORT` to `http2-port`. Use {@link kebabCaseToConstantCase} to
|
|
2646
|
+
* recover the original spelling.
|
|
2647
|
+
*
|
|
2648
|
+
* @group String
|
|
2649
|
+
*/
|
|
2650
|
+
export const constantCaseToKebabCase = (value) => snakeCaseToKebabCase(constantCaseToSnakeCase(value));
|
|
2651
|
+
/**
|
|
2652
|
+
* Adds capitalized text validation to an existing string Type.
|
|
2653
|
+
*
|
|
2654
|
+
* Narrows the output to TypeScript's `Capitalize<string>` while preserving the
|
|
2655
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2656
|
+
* {@link capitalize} to change its casing.
|
|
2657
|
+
*
|
|
2658
|
+
* ### Example
|
|
2659
|
+
*
|
|
2660
|
+
* ```ts
|
|
2661
|
+
* import {
|
|
2662
|
+
* assertErr,
|
|
2663
|
+
* assertOk,
|
|
2664
|
+
* capitalized,
|
|
2665
|
+
* maxLength,
|
|
2666
|
+
* String,
|
|
2667
|
+
* } from "@evolu/common";
|
|
2668
|
+
*
|
|
2669
|
+
* const Label = capitalized(maxLength(50)(String));
|
|
2670
|
+
* assertOk(Label.fromUnknown("Hello world"), "Hello world");
|
|
2671
|
+
* assertErr(Label.fromUnknown("hello world"));
|
|
2672
|
+
* ```
|
|
2673
|
+
*
|
|
2674
|
+
* @group String
|
|
2675
|
+
*/
|
|
2676
|
+
export const capitalized = (parent) => createType("Capitalized", parent, (value) => value === capitalize(value)
|
|
2677
|
+
? ok(value)
|
|
2678
|
+
: err({ type: "Capitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
|
|
2679
|
+
/**
|
|
2680
|
+
* Validates capitalized text as TypeScript's `Capitalize<string>`.
|
|
2681
|
+
*
|
|
2682
|
+
* The rest of the text can use any casing. Empty strings and text starting with
|
|
2683
|
+
* an uncased character, such as a digit or emoji, are valid. Use
|
|
2684
|
+
* {@link capitalize} to produce a capitalized value from any string.
|
|
2685
|
+
*
|
|
2686
|
+
* Capitalization applies to general text, including spaces and punctuation.
|
|
2687
|
+
* Both `hello` and `Hello` become `Hello`, so the original initial casing
|
|
2688
|
+
* cannot be recovered. Capitalization does not identify words or turn text into
|
|
2689
|
+
* an identifier.
|
|
2690
|
+
*
|
|
2691
|
+
* ### Example
|
|
2692
|
+
*
|
|
2693
|
+
* ```ts
|
|
2694
|
+
* import { assertErr, assertOk, CapitalizedString } from "@evolu/common";
|
|
2695
|
+
*
|
|
2696
|
+
* const text: CapitalizedString = "Hello world";
|
|
2697
|
+
* assertOk(CapitalizedString.fromUnknown(text), text);
|
|
2698
|
+
* assertErr(CapitalizedString.fromUnknown("hello world"));
|
|
2699
|
+
* assertOk(CapitalizedString.fromUnknown(""), "");
|
|
2700
|
+
* ```
|
|
2701
|
+
*
|
|
2702
|
+
* @group String
|
|
2703
|
+
*/
|
|
2704
|
+
export const CapitalizedString = /*#__PURE__*/ capitalized(String);
|
|
2705
|
+
/**
|
|
2706
|
+
* Uppercases the first Unicode code point and returns a
|
|
2707
|
+
* {@link CapitalizedString}.
|
|
2708
|
+
*
|
|
2709
|
+
* Preserves the remainder of the string and leaves an empty string unchanged.
|
|
2710
|
+
* Uses JavaScript's default Unicode casing without locale-specific rules.
|
|
2711
|
+
* Changing case can change the length, so input brands are not retained.
|
|
2712
|
+
*
|
|
2713
|
+
* ### Example
|
|
2714
|
+
*
|
|
2715
|
+
* ```ts
|
|
2716
|
+
* import { assertEqual, assertType, capitalize } from "@evolu/common";
|
|
2717
|
+
*
|
|
2718
|
+
* const text = capitalize("hello world");
|
|
2719
|
+
* assertEqual(text, "Hello world");
|
|
2720
|
+
* assertType<typeof text, "Hello world">();
|
|
2721
|
+
* assertEqual(capitalize(text), text);
|
|
2722
|
+
* ```
|
|
2723
|
+
*
|
|
2724
|
+
* @group String
|
|
2725
|
+
*/
|
|
2726
|
+
export const capitalize = (value) => {
|
|
2727
|
+
const [first = ""] = value;
|
|
2728
|
+
return (first.toUpperCase() + value.slice(first.length));
|
|
2729
|
+
};
|
|
2730
|
+
/**
|
|
2731
|
+
* Adds uncapitalized text validation to an existing string Type.
|
|
2732
|
+
*
|
|
2733
|
+
* Narrows the output to TypeScript's `Uncapitalize<string>` while preserving
|
|
2734
|
+
* the parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2735
|
+
* {@link uncapitalize} to change its casing.
|
|
2736
|
+
*
|
|
2737
|
+
* ### Example
|
|
2738
|
+
*
|
|
2739
|
+
* ```ts
|
|
2740
|
+
* import {
|
|
2741
|
+
* assertErr,
|
|
2742
|
+
* assertOk,
|
|
2743
|
+
* uncapitalized,
|
|
2744
|
+
* maxLength,
|
|
2745
|
+
* String,
|
|
2746
|
+
* } from "@evolu/common";
|
|
2747
|
+
*
|
|
2748
|
+
* const Label = uncapitalized(maxLength(50)(String));
|
|
2749
|
+
* assertOk(Label.fromUnknown("hello WORLD"), "hello WORLD");
|
|
2750
|
+
* assertErr(Label.fromUnknown("Hello WORLD"));
|
|
2751
|
+
* ```
|
|
2752
|
+
*
|
|
2753
|
+
* @group String
|
|
2754
|
+
*/
|
|
2755
|
+
export const uncapitalized = (parent) => createType("Uncapitalized", parent, (value) => value === uncapitalize(value)
|
|
2756
|
+
? ok(value)
|
|
2757
|
+
: err({ type: "Uncapitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must not start with an uppercase letter.`);
|
|
2758
|
+
/**
|
|
2759
|
+
* Validates uncapitalized text as TypeScript's `Uncapitalize<string>`.
|
|
2760
|
+
*
|
|
2761
|
+
* The rest of the text can use any casing. Empty strings and text starting with
|
|
2762
|
+
* an uncased character, such as a digit or emoji, are valid. Use
|
|
2763
|
+
* {@link uncapitalize} to produce an uncapitalized value from any string.
|
|
2764
|
+
*
|
|
2765
|
+
* ### Example
|
|
2766
|
+
*
|
|
2767
|
+
* ```ts
|
|
2768
|
+
* import { assertErr, assertOk, UncapitalizedString } from "@evolu/common";
|
|
2769
|
+
*
|
|
2770
|
+
* const text: UncapitalizedString = "hello WORLD";
|
|
2771
|
+
* assertOk(UncapitalizedString.fromUnknown(text), text);
|
|
2772
|
+
* assertErr(UncapitalizedString.fromUnknown("Hello WORLD"));
|
|
2773
|
+
* assertOk(UncapitalizedString.fromUnknown(""), "");
|
|
2774
|
+
* ```
|
|
2775
|
+
*
|
|
2776
|
+
* @group String
|
|
2777
|
+
*/
|
|
2778
|
+
export const UncapitalizedString = /*#__PURE__*/ uncapitalized(String);
|
|
2779
|
+
/**
|
|
2780
|
+
* Lowercases the first Unicode code point and returns a
|
|
2781
|
+
* {@link UncapitalizedString}.
|
|
2782
|
+
*
|
|
2783
|
+
* Preserves the remainder of the string and leaves an empty string unchanged.
|
|
2784
|
+
* Uses JavaScript's default Unicode casing without locale-specific rules.
|
|
2785
|
+
* Changing case can change the length, so input brands are not retained.
|
|
2786
|
+
*
|
|
2787
|
+
* ### Example
|
|
2788
|
+
*
|
|
2789
|
+
* ```ts
|
|
2790
|
+
* import { assertEqual, assertType, uncapitalize } from "@evolu/common";
|
|
2791
|
+
*
|
|
2792
|
+
* const text = uncapitalize("Hello WORLD");
|
|
2793
|
+
* assertEqual(text, "hello WORLD");
|
|
2794
|
+
* assertType<typeof text, "hello WORLD">();
|
|
2795
|
+
* assertEqual(uncapitalize(text), text);
|
|
2796
|
+
* ```
|
|
2797
|
+
*
|
|
2798
|
+
* @group String
|
|
2799
|
+
*/
|
|
2800
|
+
export const uncapitalize = (value) => {
|
|
2801
|
+
const [first = ""] = value;
|
|
2802
|
+
return (first.toLowerCase() + value.slice(first.length));
|
|
2803
|
+
};
|
|
2804
|
+
/**
|
|
2805
|
+
* Adds uppercased text validation to an existing string Type.
|
|
2806
|
+
*
|
|
2807
|
+
* Narrows the output to TypeScript's `Uppercase<string>` while preserving the
|
|
2808
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2809
|
+
* {@link uppercase} to change its casing.
|
|
2810
|
+
*
|
|
2811
|
+
* ### Example
|
|
2812
|
+
*
|
|
2813
|
+
* ```ts
|
|
2814
|
+
* import {
|
|
2815
|
+
* assertErr,
|
|
2816
|
+
* assertOk,
|
|
2817
|
+
* uppercased,
|
|
2818
|
+
* maxLength,
|
|
2819
|
+
* String,
|
|
2820
|
+
* } from "@evolu/common";
|
|
2821
|
+
*
|
|
2822
|
+
* const Label = uppercased(maxLength(50)(String));
|
|
2823
|
+
* assertOk(Label.fromUnknown("HELLO WORLD"), "HELLO WORLD");
|
|
2824
|
+
* assertErr(Label.fromUnknown("Hello world"));
|
|
2825
|
+
* ```
|
|
2826
|
+
*
|
|
2827
|
+
* @group String
|
|
2828
|
+
*/
|
|
2829
|
+
export const uppercased = (parent) => createType("Uppercased", parent, (value) => value === uppercase(value)
|
|
2830
|
+
? ok(value)
|
|
2831
|
+
: err({ type: "Uppercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be uppercased.`);
|
|
2832
|
+
/**
|
|
2833
|
+
* Validates uppercased text as TypeScript's `Uppercase<string>`.
|
|
2834
|
+
*
|
|
2835
|
+
* Checks the whole string using JavaScript's Unicode uppercase mapping. Empty
|
|
2836
|
+
* strings and uncased characters, such as digits and emoji, are valid. Use
|
|
2837
|
+
* {@link uppercase} to produce an uppercased value from any string.
|
|
2838
|
+
*
|
|
2839
|
+
* ### Example
|
|
2840
|
+
*
|
|
2841
|
+
* ```ts
|
|
2842
|
+
* import { assertErr, assertOk, UppercasedString } from "@evolu/common";
|
|
2843
|
+
*
|
|
2844
|
+
* const text: UppercasedString = "HELLO WORLD";
|
|
2845
|
+
* assertOk(UppercasedString.fromUnknown(text), text);
|
|
2846
|
+
* assertErr(UppercasedString.fromUnknown("Hello world"));
|
|
2847
|
+
* assertOk(UppercasedString.fromUnknown(""), "");
|
|
2848
|
+
* ```
|
|
2849
|
+
*
|
|
2850
|
+
* @group String
|
|
2851
|
+
*/
|
|
2852
|
+
export const UppercasedString = /*#__PURE__*/ uppercased(String);
|
|
2853
|
+
/**
|
|
2854
|
+
* Uppercases the whole string and returns a {@link UppercasedString}.
|
|
2855
|
+
*
|
|
2856
|
+
* Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
|
|
2857
|
+
* without locale-specific rules. Changing case can change the length, so input
|
|
2858
|
+
* brands are not retained.
|
|
2859
|
+
*
|
|
2860
|
+
* ### Example
|
|
2861
|
+
*
|
|
2862
|
+
* ```ts
|
|
2863
|
+
* import { assertEqual, assertType, uppercase } from "@evolu/common";
|
|
2864
|
+
*
|
|
2865
|
+
* const text = uppercase("Hello world");
|
|
2866
|
+
* assertEqual(text, "HELLO WORLD");
|
|
2867
|
+
* assertType<typeof text, "HELLO WORLD">();
|
|
2868
|
+
* assertEqual(uppercase(text), text);
|
|
2869
|
+
* ```
|
|
2870
|
+
*
|
|
2871
|
+
* @group String
|
|
2872
|
+
*/
|
|
2873
|
+
export const uppercase = (value) => value.toUpperCase();
|
|
2874
|
+
/**
|
|
2875
|
+
* Adds lowercased text validation to an existing string Type.
|
|
2876
|
+
*
|
|
2877
|
+
* Narrows the output to TypeScript's `Lowercase<string>` while preserving the
|
|
2878
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2879
|
+
* {@link lowercase} to change its casing.
|
|
2880
|
+
*
|
|
2881
|
+
* ### Example
|
|
2882
|
+
*
|
|
2883
|
+
* ```ts
|
|
2884
|
+
* import {
|
|
2885
|
+
* assertErr,
|
|
2886
|
+
* assertOk,
|
|
2887
|
+
* lowercased,
|
|
2888
|
+
* maxLength,
|
|
2889
|
+
* String,
|
|
2890
|
+
* } from "@evolu/common";
|
|
2891
|
+
*
|
|
2892
|
+
* const Label = lowercased(maxLength(50)(String));
|
|
2893
|
+
* assertOk(Label.fromUnknown("hello world"), "hello world");
|
|
2894
|
+
* assertErr(Label.fromUnknown("Hello WORLD"));
|
|
2895
|
+
* ```
|
|
2896
|
+
*
|
|
2897
|
+
* @group String
|
|
2898
|
+
*/
|
|
2899
|
+
export const lowercased = (parent) => createType("Lowercased", parent, (value) => value === lowercase(value)
|
|
2900
|
+
? ok(value)
|
|
2901
|
+
: err({ type: "Lowercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be lowercased.`);
|
|
2902
|
+
/**
|
|
2903
|
+
* Validates lowercased text as TypeScript's `Lowercase<string>`.
|
|
2904
|
+
*
|
|
2905
|
+
* Checks the whole string using JavaScript's Unicode lowercase mapping. Empty
|
|
2906
|
+
* strings and uncased characters, such as digits and emoji, are valid. Use
|
|
2907
|
+
* {@link lowercase} to produce a lowercased value from any string.
|
|
2908
|
+
*
|
|
2909
|
+
* ### Example
|
|
2910
|
+
*
|
|
2911
|
+
* ```ts
|
|
2912
|
+
* import { assertErr, assertOk, LowercasedString } from "@evolu/common";
|
|
2913
|
+
*
|
|
2914
|
+
* const text: LowercasedString = "hello world";
|
|
2915
|
+
* assertOk(LowercasedString.fromUnknown(text), text);
|
|
2916
|
+
* assertErr(LowercasedString.fromUnknown("Hello WORLD"));
|
|
2917
|
+
* assertOk(LowercasedString.fromUnknown(""), "");
|
|
2918
|
+
* ```
|
|
2919
|
+
*
|
|
2920
|
+
* @group String
|
|
2921
|
+
*/
|
|
2922
|
+
export const LowercasedString = /*#__PURE__*/ lowercased(String);
|
|
2923
|
+
/**
|
|
2924
|
+
* Lowercases the whole string and returns a {@link LowercasedString}.
|
|
2925
|
+
*
|
|
2926
|
+
* Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
|
|
2927
|
+
* without locale-specific rules. Changing case can change the length, so input
|
|
2928
|
+
* brands are not retained.
|
|
2929
|
+
*
|
|
2930
|
+
* ### Example
|
|
2931
|
+
*
|
|
2932
|
+
* ```ts
|
|
2933
|
+
* import { assertEqual, assertType, lowercase } from "@evolu/common";
|
|
2934
|
+
*
|
|
2935
|
+
* const text = lowercase("Hello WORLD");
|
|
2936
|
+
* assertEqual(text, "hello world");
|
|
2937
|
+
* assertType<typeof text, "hello world">();
|
|
2938
|
+
* assertEqual(lowercase(text), text);
|
|
2939
|
+
* ```
|
|
2940
|
+
*
|
|
2941
|
+
* @group String
|
|
2942
|
+
*/
|
|
2943
|
+
export const lowercase = (value) => value.toLowerCase();
|
|
2944
|
+
/**
|
|
2945
|
+
* String {@link Brand} without surrounding whitespace.
|
|
2946
|
+
*
|
|
2947
|
+
* ### Example
|
|
2948
|
+
*
|
|
2949
|
+
* ```ts
|
|
2950
|
+
* import { assertOk, String, trimmed } from "@evolu/common";
|
|
2951
|
+
*
|
|
2952
|
+
* const Trimmed = trimmed(String);
|
|
2953
|
+
*
|
|
2954
|
+
* assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
|
|
2955
|
+
* ```
|
|
2956
|
+
*
|
|
2957
|
+
* @group String
|
|
2958
|
+
*/
|
|
2959
|
+
export const trimmed = (parent) => brand("Trimmed", parent, (value) => value === value.trim()
|
|
2960
|
+
? ok()
|
|
2961
|
+
: err({ type: "Trimmed", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`);
|
|
2962
|
+
/**
|
|
2963
|
+
* A {@link String} without surrounding whitespace.
|
|
2964
|
+
*
|
|
2965
|
+
* This Type validates that a string is already trimmed; it does not modify the
|
|
2966
|
+
* value. Use {@link trim} to normalize a string. Because an empty string is
|
|
2967
|
+
* valid, this Type is useful as an intermediate boundary for input controls
|
|
2968
|
+
* that trim their values before domain validation. If an empty string is
|
|
2969
|
+
* invalid, use {@link NonEmptyTrimmedString}.
|
|
2970
|
+
*
|
|
2971
|
+
* @group String
|
|
2972
|
+
*/
|
|
2973
|
+
export const TrimmedString = /*#__PURE__*/ trimmed(String);
|
|
2974
|
+
/**
|
|
2975
|
+
* Trims a string and returns a {@link TrimmedString}.
|
|
2976
|
+
*
|
|
2977
|
+
* ### Example
|
|
2978
|
+
*
|
|
2979
|
+
* ```ts
|
|
2980
|
+
* import { assertEqual, trim } from "@evolu/common";
|
|
2981
|
+
*
|
|
2982
|
+
* assertEqual(trim(" Evolu "), "Evolu");
|
|
2983
|
+
* ```
|
|
2984
|
+
*
|
|
2985
|
+
* @group String
|
|
2986
|
+
*/
|
|
2987
|
+
export const trim = (value) => value.trim();
|
|
2988
|
+
/**
|
|
2989
|
+
* String {@link Brand} requiring an exact, case-sensitive prefix.
|
|
2990
|
+
*
|
|
2991
|
+
* Validation preserves the complete string, including the prefix. An empty
|
|
2992
|
+
* prefix accepts every string allowed by the parent Type. The prefix must be
|
|
2993
|
+
* one concrete string literal so different prefixes have distinct brands.
|
|
2994
|
+
*
|
|
2995
|
+
* ### Example
|
|
2996
|
+
*
|
|
2997
|
+
* ```ts
|
|
2998
|
+
* import {
|
|
2999
|
+
* assertEqual,
|
|
3000
|
+
* assertErr,
|
|
3001
|
+
* assertOk,
|
|
3002
|
+
* assertType,
|
|
3003
|
+
* maxLength,
|
|
3004
|
+
* startsWith,
|
|
3005
|
+
* String,
|
|
3006
|
+
* type Brand,
|
|
3007
|
+
* } from "@evolu/common";
|
|
3008
|
+
*
|
|
3009
|
+
* const EnvName = startsWith("APP_")(maxLength(64)(String));
|
|
3010
|
+
*
|
|
3011
|
+
* const name = EnvName.fromUnknown("APP_PORT");
|
|
3012
|
+
* assertOk(name, "APP_PORT");
|
|
3013
|
+
* assertType<
|
|
3014
|
+
* typeof name.value,
|
|
3015
|
+
* string & Brand<"MaxLength64"> & Brand<"StartsWithAPP_">
|
|
3016
|
+
* >();
|
|
3017
|
+
*
|
|
3018
|
+
* assertEqual(EnvName.to(name.value), "APP_PORT");
|
|
3019
|
+
*
|
|
3020
|
+
* assertErr(EnvName.fromUnknown("app_PORT"));
|
|
3021
|
+
* assertErr(EnvName.fromUnknown("APP_" + "X".repeat(61)));
|
|
3022
|
+
* ```
|
|
3023
|
+
*
|
|
3024
|
+
* @group String
|
|
3025
|
+
*/
|
|
3026
|
+
export const startsWith = (prefix) => {
|
|
3027
|
+
const name = `StartsWith${prefix}`;
|
|
3028
|
+
return (parent) => brand(name, parent, (value) => value.startsWith(prefix)
|
|
3029
|
+
? ok()
|
|
3030
|
+
: err({
|
|
3031
|
+
type: name,
|
|
3032
|
+
value,
|
|
3033
|
+
prefix,
|
|
3034
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must start with ${safelyStringifyUnknownValue(error.prefix)}.`);
|
|
3035
|
+
};
|
|
3036
|
+
/**
|
|
3037
|
+
* Decodes a prefixed string with another {@link Type} and restores the prefix
|
|
3038
|
+
* when encoding.
|
|
3039
|
+
*
|
|
3040
|
+
* Uses {@link startsWith} to validate an exact, case-sensitive prefix before
|
|
3041
|
+
* removing one occurrence. The wrapped Type validates and decodes the suffix;
|
|
3042
|
+
* its constraints apply to the suffix, and its Output is preserved. Encoding
|
|
3043
|
+
* prepends the prefix to the wrapped Type's canonical string representation. An
|
|
3044
|
+
* empty prefix leaves that representation unchanged.
|
|
3045
|
+
*
|
|
3046
|
+
* The prefix must be one concrete string literal. The wrapped Type must accept
|
|
3047
|
+
* a string Input and encode to strings; its Output can have another type, as
|
|
3048
|
+
* with {@link PortFromString}.
|
|
3049
|
+
*
|
|
3050
|
+
* ### Example
|
|
3051
|
+
*
|
|
3052
|
+
* ```ts
|
|
3053
|
+
* import {
|
|
3054
|
+
* assertEqual,
|
|
3055
|
+
* assertErr,
|
|
3056
|
+
* assertOk,
|
|
3057
|
+
* assertType,
|
|
3058
|
+
* ConstantCaseIdentifier,
|
|
3059
|
+
* prefixed,
|
|
3060
|
+
* PortFromString,
|
|
3061
|
+
* type Port,
|
|
3062
|
+
* } from "@evolu/common";
|
|
3063
|
+
*
|
|
3064
|
+
* const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
|
|
3065
|
+
*
|
|
3066
|
+
* const name = EnvName.fromUnknown("APP_PORT");
|
|
3067
|
+
* assertOk(name, "PORT");
|
|
3068
|
+
* assertType<typeof name.value, ConstantCaseIdentifier>();
|
|
3069
|
+
*
|
|
3070
|
+
* assertEqual(EnvName.to(name.value), "APP_PORT");
|
|
3071
|
+
*
|
|
3072
|
+
* assertErr(EnvName.fromUnknown("OTHER_PORT"));
|
|
3073
|
+
* assertErr(EnvName.fromUnknown("APP_port"));
|
|
3074
|
+
*
|
|
3075
|
+
* const PortSetting = prefixed("port:")(PortFromString);
|
|
3076
|
+
*
|
|
3077
|
+
* const port = PortSetting.fromUnknown("port:04000");
|
|
3078
|
+
* assertOk(port, 4000);
|
|
3079
|
+
* assertType<typeof port.value, Port>();
|
|
3080
|
+
*
|
|
3081
|
+
* assertEqual(PortSetting.to(port.value), "port:4000");
|
|
3082
|
+
* ```
|
|
3083
|
+
*
|
|
3084
|
+
* @group String
|
|
3085
|
+
*/
|
|
3086
|
+
export const prefixed = (prefix) => (type) => {
|
|
3087
|
+
// A literal prefix makes the generated names concrete and distinct.
|
|
3088
|
+
const source = startsWith(prefix)(String);
|
|
3089
|
+
return transform(`Prefixed${prefix}`, source, type, {
|
|
3090
|
+
// The Input guard permits any string; concatenation establishes the prefix brand.
|
|
3091
|
+
from: (value) => ok(value.slice(prefix.length)),
|
|
3092
|
+
to: (value) => `${prefix}${value}`,
|
|
3093
|
+
});
|
|
3094
|
+
};
|
|
3095
|
+
/**
|
|
3096
|
+
* Minimum-length {@link Brand} for values whose `length` is at least `min`.
|
|
3097
|
+
*
|
|
3098
|
+
* ### Example
|
|
3099
|
+
*
|
|
3100
|
+
* ```ts
|
|
3101
|
+
* import { assertOk, String, array, minLength } from "@evolu/common";
|
|
3102
|
+
*
|
|
3103
|
+
* const AtLeastThreeCharacters = minLength(3)(String);
|
|
3104
|
+
* const AtLeastTwoItems = minLength(2)(array(String));
|
|
3105
|
+
*
|
|
3106
|
+
* assertOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
|
|
3107
|
+
* assertOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
3108
|
+
* ```
|
|
3109
|
+
*
|
|
3110
|
+
* @group String
|
|
3111
|
+
* @group Collection
|
|
3112
|
+
*/
|
|
3113
|
+
export const minLength = (min) => (parent) => {
|
|
3114
|
+
const name = `MinLength${min}`;
|
|
3115
|
+
return brand(name, parent, (value) => value.length >= min
|
|
3116
|
+
? ok()
|
|
3117
|
+
: err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not meet the minimum length of ${error.min}.`);
|
|
3118
|
+
};
|
|
3119
|
+
/**
|
|
3120
|
+
* A non-empty {@link TrimmedString}.
|
|
3121
|
+
*
|
|
3122
|
+
* Use as the base Type for ordinary human-entered text, which should usually be
|
|
3123
|
+
* trimmed, non-empty, and bounded. Add a domain-appropriate maximum with
|
|
1960
3124
|
* {@link maxLength}, or use {@link NonEmptyTrimmedString100} or
|
|
1961
3125
|
* {@link NonEmptyTrimmedString1000}.
|
|
1962
3126
|
*
|
|
@@ -2055,6 +3219,7 @@ export const length = (exact) => (parent) => {
|
|
|
2055
3219
|
* assertType<UrlSafeString, string & Brand<"UrlSafeString">>();
|
|
2056
3220
|
*
|
|
2057
3221
|
* assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
|
|
3222
|
+
*
|
|
2058
3223
|
* const invalid = UrlSafeString.fromUnknown("not safe");
|
|
2059
3224
|
* assertErr(invalid);
|
|
2060
3225
|
* assertType(Data, invalid.error);
|
|
@@ -2183,7 +3348,13 @@ export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(byte
|
|
|
2183
3348
|
*/
|
|
2184
3349
|
export const base64UrlToUint8Array = (value) => base64UrlStringToUint8Array(value);
|
|
2185
3350
|
/**
|
|
2186
|
-
* A non-empty URL-safe
|
|
3351
|
+
* A non-empty file-system-safe and URL-safe token of at most 64 UTF-16 code
|
|
3352
|
+
* units.
|
|
3353
|
+
*
|
|
3354
|
+
* Evolu uses it for database file names, storage pool names, and log prefixes.
|
|
3355
|
+
* It accepts the {@link UrlSafeString} alphabet in any order, so it may start
|
|
3356
|
+
* with a digit, `-`, or `_`. It is not a language identifier; for
|
|
3357
|
+
* word-structured names, use {@link identifier}.
|
|
2187
3358
|
*
|
|
2188
3359
|
* @group String
|
|
2189
3360
|
*/
|
|
@@ -2643,6 +3814,7 @@ export const PositiveFiniteNumber = /*#__PURE__*/ positive(NonNegativeFiniteNumb
|
|
|
2643
3814
|
* assertType<Int, number & Brand<"Int">>();
|
|
2644
3815
|
*
|
|
2645
3816
|
* assertOk(Int.fromUnknown(42), 42);
|
|
3817
|
+
*
|
|
2646
3818
|
* const invalid = Int.fromUnknown(1.5);
|
|
2647
3819
|
* assertErr(invalid);
|
|
2648
3820
|
* assertType(Data, invalid.error);
|
|
@@ -2670,18 +3842,61 @@ export const NonNegativeInt = /*#__PURE__*/ nonNegative(Int);
|
|
|
2670
3842
|
/**
|
|
2671
3843
|
* Minimum {@link NonNegativeInt} value.
|
|
2672
3844
|
*
|
|
2673
|
-
* @group Number
|
|
2674
|
-
*/
|
|
2675
|
-
export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
|
|
2676
|
-
/**
|
|
2677
|
-
* Positive {@link Int}.
|
|
3845
|
+
* @group Number
|
|
3846
|
+
*/
|
|
3847
|
+
export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
|
|
3848
|
+
/**
|
|
3849
|
+
* Positive {@link Int}.
|
|
3850
|
+
*
|
|
3851
|
+
* Also satisfies {@link NonNegativeInt}, so it can be used wherever a
|
|
3852
|
+
* non-negative integer is required.
|
|
3853
|
+
*
|
|
3854
|
+
* @group Number
|
|
3855
|
+
*/
|
|
3856
|
+
export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
|
|
3857
|
+
/**
|
|
3858
|
+
* Transforms a decimal integer string into an {@link Int}.
|
|
3859
|
+
*
|
|
3860
|
+
* This is useful for inputs that carry numbers as text, such as environment
|
|
3861
|
+
* variables, URL query parameters, and form fields. The string must consist of
|
|
3862
|
+
* an optional minus sign and digits; the {@link Int} constraint then rejects
|
|
3863
|
+
* values outside the safe integer range.
|
|
3864
|
+
*
|
|
3865
|
+
* ### Example
|
|
3866
|
+
*
|
|
3867
|
+
* ```ts
|
|
3868
|
+
* import {
|
|
3869
|
+
* assertEqual,
|
|
3870
|
+
* assertErr,
|
|
3871
|
+
* assertOk,
|
|
3872
|
+
* assertSame,
|
|
3873
|
+
* IntFromString,
|
|
3874
|
+
* } from "@evolu/common";
|
|
3875
|
+
*
|
|
3876
|
+
* assertOk(IntFromString.fromUnknown("4000"), 4000);
|
|
3877
|
+
* assertOk(IntFromString.fromUnknown("-1"), -1);
|
|
3878
|
+
* assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
|
|
2678
3879
|
*
|
|
2679
|
-
*
|
|
2680
|
-
*
|
|
3880
|
+
* const negativeZero = IntFromString.orThrow("-0");
|
|
3881
|
+
* assertSame(negativeZero, -0);
|
|
3882
|
+
* assertEqual(IntFromString.to(negativeZero), "-0");
|
|
3883
|
+
*
|
|
3884
|
+
* const invalid = IntFromString.fromUnknown("4000.5");
|
|
3885
|
+
* assertErr(invalid, { type: "IntFromString", value: "4000.5" });
|
|
3886
|
+
* assertEqual(
|
|
3887
|
+
* IntFromString.formatError(invalid.error),
|
|
3888
|
+
* 'The value "4000.5" is not a decimal integer.',
|
|
3889
|
+
* );
|
|
3890
|
+
* ```
|
|
2681
3891
|
*
|
|
2682
3892
|
* @group Number
|
|
2683
3893
|
*/
|
|
2684
|
-
export const
|
|
3894
|
+
export const IntFromString = /*#__PURE__*/ transform("IntFromString", String, Int, {
|
|
3895
|
+
from: (value) => /^-?\d+$/u.test(value)
|
|
3896
|
+
? ok(globalThis.Number(value))
|
|
3897
|
+
: err({ type: "IntFromString", value }),
|
|
3898
|
+
to: (value) => globalThis.Object.is(value, -0) ? "-0" : globalThis.String(value),
|
|
3899
|
+
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a decimal integer.`);
|
|
2685
3900
|
/**
|
|
2686
3901
|
* Minimum {@link PositiveInt} value.
|
|
2687
3902
|
*
|
|
@@ -2805,6 +4020,55 @@ export const lessThanOrEqualTo = (max) => (parent) => {
|
|
|
2805
4020
|
? ok()
|
|
2806
4021
|
: err({ type: name, value, max }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be less than or equal to ${error.max}.`);
|
|
2807
4022
|
};
|
|
4023
|
+
/**
|
|
4024
|
+
* A TCP or UDP port as an integer from zero through 65535, inclusive.
|
|
4025
|
+
*
|
|
4026
|
+
* When binding a server, zero requests an automatically assigned port. Use
|
|
4027
|
+
* {@link PortFromString} for configuration values supplied as text.
|
|
4028
|
+
*
|
|
4029
|
+
* ### Example
|
|
4030
|
+
*
|
|
4031
|
+
* ```ts
|
|
4032
|
+
* import { assertErr, assertOk, Port } from "@evolu/common";
|
|
4033
|
+
*
|
|
4034
|
+
* assertOk(Port.fromUnknown(0), 0);
|
|
4035
|
+
* assertOk(Port.fromUnknown(4000), 4000);
|
|
4036
|
+
* assertOk(Port.fromUnknown(65535), 65535);
|
|
4037
|
+
* assertErr(Port.fromUnknown(-1));
|
|
4038
|
+
* assertErr(Port.fromUnknown(65536));
|
|
4039
|
+
* assertErr(Port.fromUnknown(4000.5));
|
|
4040
|
+
* ```
|
|
4041
|
+
*
|
|
4042
|
+
* @group Number
|
|
4043
|
+
*/
|
|
4044
|
+
export const Port = /*#__PURE__*/ brand("Port",
|
|
4045
|
+
/*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt));
|
|
4046
|
+
/**
|
|
4047
|
+
* Parses a decimal integer string and validates it as a {@link Port}.
|
|
4048
|
+
*
|
|
4049
|
+
* Uses {@link IntFromString} for decimal parsing, including its rejection of
|
|
4050
|
+
* whitespace, plus signs, fractions, and exponent notation.
|
|
4051
|
+
*
|
|
4052
|
+
* ### Example
|
|
4053
|
+
*
|
|
4054
|
+
* ```ts
|
|
4055
|
+
* import {
|
|
4056
|
+
* assertEqual,
|
|
4057
|
+
* assertErr,
|
|
4058
|
+
* assertOk,
|
|
4059
|
+
* PortFromString,
|
|
4060
|
+
* } from "@evolu/common";
|
|
4061
|
+
*
|
|
4062
|
+
* assertOk(PortFromString.fromUnknown("0"), 0);
|
|
4063
|
+
* assertOk(PortFromString.fromUnknown("4000"), 4000);
|
|
4064
|
+
* assertErr(PortFromString.fromUnknown("65536"));
|
|
4065
|
+
* assertErr(PortFromString.fromUnknown("http"));
|
|
4066
|
+
* assertEqual(PortFromString.to(PortFromString.orThrow("04000")), "4000");
|
|
4067
|
+
* ```
|
|
4068
|
+
*
|
|
4069
|
+
* @group Number
|
|
4070
|
+
*/
|
|
4071
|
+
export const PortFromString = /*#__PURE__*/ transform("PortFromString", IntFromString, Port, { from: ok, to: identity });
|
|
2808
4072
|
/**
|
|
2809
4073
|
* Finite {@link Number} from zero to one, inclusive.
|
|
2810
4074
|
*
|
|
@@ -3033,6 +4297,7 @@ export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPosi
|
|
|
3033
4297
|
* >();
|
|
3034
4298
|
*
|
|
3035
4299
|
* assertOk(Tenths.fromUnknown(0.3), 0.3);
|
|
4300
|
+
*
|
|
3036
4301
|
* const invalid = Tenths.fromUnknown(0.31);
|
|
3037
4302
|
* assertErr(invalid);
|
|
3038
4303
|
* assertType(Data, invalid.error);
|
|
@@ -3152,6 +4417,7 @@ export const between = (min, max) => (parent) => {
|
|
|
3152
4417
|
*
|
|
3153
4418
|
* const UserId = brand("UserId", String);
|
|
3154
4419
|
* const UserIds = array(UserId);
|
|
4420
|
+
*
|
|
3155
4421
|
* const result = UserIds.from.parent(["ada", "grace"]);
|
|
3156
4422
|
*
|
|
3157
4423
|
* assertType<
|
|
@@ -3160,6 +4426,7 @@ export const between = (min, max) => (parent) => {
|
|
|
3160
4426
|
* >();
|
|
3161
4427
|
* assertOk(result, ["ada", "grace"]);
|
|
3162
4428
|
* assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
|
|
4429
|
+
*
|
|
3163
4430
|
* const invalid = UserIds.fromUnknown("ada");
|
|
3164
4431
|
* assertErr(invalid);
|
|
3165
4432
|
* assertType(Data, invalid.error);
|
|
@@ -3211,9 +4478,13 @@ const isArrayCollection = (value, isElement) => {
|
|
|
3211
4478
|
};
|
|
3212
4479
|
const arrayRuntimeConfig = {
|
|
3213
4480
|
name: "Array",
|
|
4481
|
+
typeSymbol: arrayTypeSymbol,
|
|
3214
4482
|
typeByElement: arrayTypeByElement,
|
|
3215
4483
|
validate: validateArrayCollection,
|
|
3216
4484
|
validateItems: (value, validateElement, options) => validateArrayItems(value, validateElement, options, false),
|
|
4485
|
+
check: (value, checkElement, options) => Array.isArray(value)
|
|
4486
|
+
? checkIndexedArrayItems("Array", value, checkElement, options)
|
|
4487
|
+
: { type: "Array", reason: { kind: "NotArray", value } },
|
|
3217
4488
|
encode: encodeArrayCollection,
|
|
3218
4489
|
is: isArrayCollection,
|
|
3219
4490
|
formatError: ((error) => {
|
|
@@ -3238,23 +4509,10 @@ const validateArrayItems = (value, validate, options, checkStructure) => validat
|
|
|
3238
4509
|
// Encoding and Output membership stay feature-local because sharing those
|
|
3239
4510
|
// shorter hot paths measurably increases standalone bundles.
|
|
3240
4511
|
const validateIndexedArrayItems = (name, value, validate, options, checkStructure) => {
|
|
3241
|
-
let issues
|
|
4512
|
+
let issues = checkStructure
|
|
4513
|
+
? getArrayExcessPropertyIssues(value, options)
|
|
4514
|
+
: undefined;
|
|
3242
4515
|
let output;
|
|
3243
|
-
if (checkStructure) {
|
|
3244
|
-
for (const key of Reflect.ownKeys(value)) {
|
|
3245
|
-
if (key === "length")
|
|
3246
|
-
continue;
|
|
3247
|
-
if (typeof key === "string") {
|
|
3248
|
-
const index = globalThis.Number(key) >>> 0;
|
|
3249
|
-
if (index < value.length && globalThis.String(index) === key) {
|
|
3250
|
-
continue;
|
|
3251
|
-
}
|
|
3252
|
-
}
|
|
3253
|
-
(issues ??= []).push({ kind: "ExcessProperty", key });
|
|
3254
|
-
if (options.errors === "first")
|
|
3255
|
-
break;
|
|
3256
|
-
}
|
|
3257
|
-
}
|
|
3258
4516
|
for (let index = 0; (issues === undefined || options.errors === "all") && index < value.length; index++) {
|
|
3259
4517
|
let item;
|
|
3260
4518
|
if (checkStructure) {
|
|
@@ -3301,6 +4559,66 @@ const validateIndexedArrayItems = (name, value, validate, options, checkStructur
|
|
|
3301
4559
|
},
|
|
3302
4560
|
});
|
|
3303
4561
|
};
|
|
4562
|
+
// Like validateIndexedArrayItems for identity elements, which leave the value
|
|
4563
|
+
// unchanged, so it returns only the error.
|
|
4564
|
+
const checkIndexedArrayItems = (name, value, check, options) => {
|
|
4565
|
+
let issues = getArrayExcessPropertyIssues(value, options);
|
|
4566
|
+
for (let index = 0; (issues === undefined || options.errors === "all") && index < value.length; index++) {
|
|
4567
|
+
const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, index);
|
|
4568
|
+
if (descriptor === undefined) {
|
|
4569
|
+
(issues ??= []).push({ kind: "Hole", index });
|
|
4570
|
+
if (options.errors === "first")
|
|
4571
|
+
break;
|
|
4572
|
+
continue;
|
|
4573
|
+
}
|
|
4574
|
+
if (!("value" in descriptor)) {
|
|
4575
|
+
(issues ??= []).push({ kind: "Accessor", index });
|
|
4576
|
+
if (options.errors === "first")
|
|
4577
|
+
break;
|
|
4578
|
+
continue;
|
|
4579
|
+
}
|
|
4580
|
+
const error = check(descriptor.value, options, index);
|
|
4581
|
+
if (error === undefined)
|
|
4582
|
+
continue;
|
|
4583
|
+
(issues ??= []).push({ kind: "Element", index, error });
|
|
4584
|
+
if (options.errors === "first")
|
|
4585
|
+
break;
|
|
4586
|
+
}
|
|
4587
|
+
return issues === undefined
|
|
4588
|
+
? undefined
|
|
4589
|
+
: {
|
|
4590
|
+
type: name,
|
|
4591
|
+
reason: {
|
|
4592
|
+
kind: "Items",
|
|
4593
|
+
issues: issues,
|
|
4594
|
+
},
|
|
4595
|
+
};
|
|
4596
|
+
};
|
|
4597
|
+
// Own keys other than indexes and `length` are excess properties.
|
|
4598
|
+
const getArrayExcessPropertyIssues = (value, options) => {
|
|
4599
|
+
let issues;
|
|
4600
|
+
const keys = Reflect.ownKeys(value);
|
|
4601
|
+
for (let position = 0; position < keys.length; position++) {
|
|
4602
|
+
const key = keys[position];
|
|
4603
|
+
if (key === "length")
|
|
4604
|
+
continue;
|
|
4605
|
+
if (typeof key === "string") {
|
|
4606
|
+
// Ordinary arrays list their indexes first, in ascending order. Comparing
|
|
4607
|
+
// the key first reads `length` only for an index key.
|
|
4608
|
+
if (key === globalThis.String(position) && position < value.length) {
|
|
4609
|
+
continue;
|
|
4610
|
+
}
|
|
4611
|
+
const index = globalThis.Number(key) >>> 0;
|
|
4612
|
+
if (index < value.length && globalThis.String(index) === key) {
|
|
4613
|
+
continue;
|
|
4614
|
+
}
|
|
4615
|
+
}
|
|
4616
|
+
(issues ??= []).push({ kind: "ExcessProperty", key });
|
|
4617
|
+
if (options.errors === "first")
|
|
4618
|
+
break;
|
|
4619
|
+
}
|
|
4620
|
+
return issues;
|
|
4621
|
+
};
|
|
3304
4622
|
const copyArrayPrefix = (value, endIndex) => {
|
|
3305
4623
|
const output = createMutableArray(value.length);
|
|
3306
4624
|
for (let index = 0; index < endIndex; index++) {
|
|
@@ -3334,11 +4652,23 @@ export const set = (element) => createHomogeneousCollectionType(element, setRunt
|
|
|
3334
4652
|
// Array(String) +82/+63 B gzip, while Array(String)+Set(String) is 2501/2492 B
|
|
3335
4653
|
// and Set adds only +303/+304 B once Array is present. See TreeShaking.test.ts.
|
|
3336
4654
|
const createHomogeneousCollectionType = (typeElement, config) => {
|
|
3337
|
-
const
|
|
4655
|
+
const slot = typeElement[config.typeSymbol];
|
|
4656
|
+
// A copy of a Type, spread or with the Type as its prototype, has the slot
|
|
4657
|
+
// of the original.
|
|
4658
|
+
const cached = slot !== undefined && slot.element === typeElement
|
|
4659
|
+
? slot
|
|
4660
|
+
: config.typeByElement.get(typeElement);
|
|
3338
4661
|
if (cached)
|
|
3339
4662
|
return cached;
|
|
3340
|
-
const
|
|
3341
|
-
const
|
|
4663
|
+
const checkElement = typeElement[checkSymbol];
|
|
4664
|
+
const check = checkElement &&
|
|
4665
|
+
((value, options) => config.check(value, checkElement, options));
|
|
4666
|
+
const fromUnknown = check
|
|
4667
|
+
? checkToFromUnknown(check)
|
|
4668
|
+
: (value, options = firstValidationOptions) => config.validate(value, typeElement.fromUnknown, options);
|
|
4669
|
+
const validateOutput = check
|
|
4670
|
+
? fromUnknown
|
|
4671
|
+
: (value, options = firstValidationOptions) => config.validate(value, typeElement[outputValidationSymbol], options);
|
|
3342
4672
|
const parent = typeElement.parent
|
|
3343
4673
|
? createHomogeneousCollectionType(typeElement.parent, config)
|
|
3344
4674
|
: null;
|
|
@@ -3353,16 +4683,20 @@ const createHomogeneousCollectionType = (typeElement, config) => {
|
|
|
3353
4683
|
? identity
|
|
3354
4684
|
: (value) => config.encode(value, encodeElement);
|
|
3355
4685
|
const getTypeIssues = createCollectionRuntimeTypeIssues(config.name, "Items", config.formatError, () => typeElement);
|
|
3356
|
-
const type = createTypeNode(config.name, parent, fromUnknown, (value) => config.is(value, typeElement.is), validateOutput, from, to, getTypeIssues, { element: typeElement });
|
|
3357
|
-
|
|
4686
|
+
const type = createTypeNode(config.name, parent, fromUnknown, (value) => config.is(value, typeElement.is), validateOutput, from, to, getTypeIssues, { additionalProperties: { element: typeElement }, check });
|
|
4687
|
+
// A frozen element Type keeps its Collection Type in the WeakMap.
|
|
4688
|
+
if (!Reflect.set(typeElement, config.typeSymbol, type)) {
|
|
4689
|
+
config.typeByElement.set(typeElement, type);
|
|
4690
|
+
}
|
|
3358
4691
|
return type;
|
|
3359
4692
|
};
|
|
3360
4693
|
const setTypeByElement = /*#__PURE__*/ new WeakMap();
|
|
3361
4694
|
const setRuntimeConfig = {
|
|
3362
4695
|
name: "Set",
|
|
4696
|
+
typeSymbol: setTypeSymbol,
|
|
3363
4697
|
typeByElement: setTypeByElement,
|
|
3364
4698
|
validate: (value, validateElement, options) => {
|
|
3365
|
-
if (!hasObjectTag(value, "Set")) {
|
|
4699
|
+
if (!hasObjectTag(value, "[object Set]")) {
|
|
3366
4700
|
return err({
|
|
3367
4701
|
type: "Set",
|
|
3368
4702
|
reason: { kind: "NotSet", value },
|
|
@@ -3371,6 +4705,37 @@ const setRuntimeConfig = {
|
|
|
3371
4705
|
return validateSetItems(value, validateElement, options, true);
|
|
3372
4706
|
},
|
|
3373
4707
|
validateItems: (value, validateElement, options) => validateSetItems(value, validateElement, options, false),
|
|
4708
|
+
check: (value, checkElement, options) => {
|
|
4709
|
+
if (!hasObjectTag(value, "[object Set]")) {
|
|
4710
|
+
return { type: "Set", reason: { kind: "NotSet", value } };
|
|
4711
|
+
}
|
|
4712
|
+
// The same issues as validateSetItems, without building an Output.
|
|
4713
|
+
let issues;
|
|
4714
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
4715
|
+
(issues ??= []).push({ kind: "ExcessProperty", key });
|
|
4716
|
+
if (options.errors === "first")
|
|
4717
|
+
break;
|
|
4718
|
+
}
|
|
4719
|
+
let index = 0;
|
|
4720
|
+
for (const item of value) {
|
|
4721
|
+
if (issues !== undefined && options.errors === "first")
|
|
4722
|
+
break;
|
|
4723
|
+
const error = checkElement(item, options);
|
|
4724
|
+
if (error !== undefined) {
|
|
4725
|
+
(issues ??= []).push({ kind: "Element", index, error });
|
|
4726
|
+
}
|
|
4727
|
+
index++;
|
|
4728
|
+
}
|
|
4729
|
+
return issues === undefined
|
|
4730
|
+
? undefined
|
|
4731
|
+
: {
|
|
4732
|
+
type: "Set",
|
|
4733
|
+
reason: {
|
|
4734
|
+
kind: "Items",
|
|
4735
|
+
issues: issues,
|
|
4736
|
+
},
|
|
4737
|
+
};
|
|
4738
|
+
},
|
|
3374
4739
|
encode: (value, encodeElement) => {
|
|
3375
4740
|
let changed = false;
|
|
3376
4741
|
const output = new Set();
|
|
@@ -3383,7 +4748,7 @@ const setRuntimeConfig = {
|
|
|
3383
4748
|
return changed ? output : value;
|
|
3384
4749
|
},
|
|
3385
4750
|
is: (value, isElement) => {
|
|
3386
|
-
if (!hasObjectTag(value, "Set") ||
|
|
4751
|
+
if (!hasObjectTag(value, "[object Set]") ||
|
|
3387
4752
|
Reflect.ownKeys(value).length !== 0) {
|
|
3388
4753
|
return false;
|
|
3389
4754
|
}
|
|
@@ -3474,7 +4839,7 @@ export const map = (key, value) => {
|
|
|
3474
4839
|
if (cached)
|
|
3475
4840
|
return cached;
|
|
3476
4841
|
const validate = (input, validateKey, validateValue, options) => {
|
|
3477
|
-
if (!hasObjectTag(input, "Map")) {
|
|
4842
|
+
if (!hasObjectTag(input, "[object Map]")) {
|
|
3478
4843
|
return err({
|
|
3479
4844
|
type: "Map",
|
|
3480
4845
|
reason: { kind: "NotMap", value: input },
|
|
@@ -3482,8 +4847,90 @@ export const map = (key, value) => {
|
|
|
3482
4847
|
}
|
|
3483
4848
|
return validateMapEntries(input, validateKey, validateValue, options, true);
|
|
3484
4849
|
};
|
|
3485
|
-
const
|
|
3486
|
-
const
|
|
4850
|
+
const checkKey = typeKey[checkSymbol];
|
|
4851
|
+
const checkValue = typeValue[checkSymbol];
|
|
4852
|
+
// Like validate for identity keys and values, which leave the Map unchanged,
|
|
4853
|
+
// so it returns only the error. A custom iterator can repeat a key, so
|
|
4854
|
+
// collisions are still detected.
|
|
4855
|
+
const check = checkKey &&
|
|
4856
|
+
checkValue &&
|
|
4857
|
+
((input, options) => {
|
|
4858
|
+
if (!hasObjectTag(input, "[object Map]")) {
|
|
4859
|
+
return {
|
|
4860
|
+
type: "Map",
|
|
4861
|
+
reason: { kind: "NotMap", value: input },
|
|
4862
|
+
};
|
|
4863
|
+
}
|
|
4864
|
+
const entries = input;
|
|
4865
|
+
let issues;
|
|
4866
|
+
const entryByKey = new Map();
|
|
4867
|
+
for (const key of Reflect.ownKeys(entries)) {
|
|
4868
|
+
(issues ??= []).push({ kind: "ExcessProperty", key });
|
|
4869
|
+
if (options.errors === "first")
|
|
4870
|
+
break;
|
|
4871
|
+
}
|
|
4872
|
+
let index = 0;
|
|
4873
|
+
for (const [inputKey, inputValue] of entries) {
|
|
4874
|
+
if (issues !== undefined && options.errors === "first")
|
|
4875
|
+
break;
|
|
4876
|
+
const keyError = checkKey(inputKey, options);
|
|
4877
|
+
if (keyError !== undefined) {
|
|
4878
|
+
(issues ??= []).push({
|
|
4879
|
+
kind: "Key",
|
|
4880
|
+
index,
|
|
4881
|
+
key: inputKey,
|
|
4882
|
+
error: keyError,
|
|
4883
|
+
});
|
|
4884
|
+
if (options.errors === "first")
|
|
4885
|
+
break;
|
|
4886
|
+
}
|
|
4887
|
+
const valueError = checkValue(inputValue, options);
|
|
4888
|
+
if (valueError !== undefined) {
|
|
4889
|
+
(issues ??= []).push({
|
|
4890
|
+
kind: "Value",
|
|
4891
|
+
index,
|
|
4892
|
+
key: inputKey,
|
|
4893
|
+
error: valueError,
|
|
4894
|
+
});
|
|
4895
|
+
if (options.errors === "first")
|
|
4896
|
+
break;
|
|
4897
|
+
}
|
|
4898
|
+
if (keyError === undefined) {
|
|
4899
|
+
const previous = entryByKey.get(inputKey);
|
|
4900
|
+
if (previous === undefined) {
|
|
4901
|
+
entryByKey.set(inputKey, { index, key: inputKey });
|
|
4902
|
+
}
|
|
4903
|
+
else {
|
|
4904
|
+
(issues ??= []).push({
|
|
4905
|
+
kind: "Collision",
|
|
4906
|
+
index,
|
|
4907
|
+
key: inputKey,
|
|
4908
|
+
previousIndex: previous.index,
|
|
4909
|
+
previousKey: previous.key,
|
|
4910
|
+
outputKey: inputKey,
|
|
4911
|
+
});
|
|
4912
|
+
if (options.errors === "first")
|
|
4913
|
+
break;
|
|
4914
|
+
}
|
|
4915
|
+
}
|
|
4916
|
+
index++;
|
|
4917
|
+
}
|
|
4918
|
+
return issues === undefined
|
|
4919
|
+
? undefined
|
|
4920
|
+
: {
|
|
4921
|
+
type: "Map",
|
|
4922
|
+
reason: {
|
|
4923
|
+
kind: "Entries",
|
|
4924
|
+
issues: issues,
|
|
4925
|
+
},
|
|
4926
|
+
};
|
|
4927
|
+
});
|
|
4928
|
+
const fromUnknown = check
|
|
4929
|
+
? checkToFromUnknown(check)
|
|
4930
|
+
: (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
|
|
4931
|
+
const validateOutput = check
|
|
4932
|
+
? fromUnknown
|
|
4933
|
+
: (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
|
|
3487
4934
|
const formatError = (error) => {
|
|
3488
4935
|
if (error.reason.kind === "NotMap")
|
|
3489
4936
|
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Map.`;
|
|
@@ -3524,7 +4971,7 @@ export const map = (key, value) => {
|
|
|
3524
4971
|
return changed ? output : input;
|
|
3525
4972
|
};
|
|
3526
4973
|
const is = (input) => {
|
|
3527
|
-
if (!hasObjectTag(input, "Map")) {
|
|
4974
|
+
if (!hasObjectTag(input, "[object Map]")) {
|
|
3528
4975
|
return false;
|
|
3529
4976
|
}
|
|
3530
4977
|
if (Reflect.ownKeys(input).length !== 0)
|
|
@@ -3535,26 +4982,36 @@ export const map = (key, value) => {
|
|
|
3535
4982
|
}
|
|
3536
4983
|
return true;
|
|
3537
4984
|
};
|
|
3538
|
-
const getTypeIssues = (error, mode) => {
|
|
4985
|
+
const getTypeIssues = (error, mode, path) => {
|
|
3539
4986
|
const mapError = error;
|
|
3540
4987
|
if (mapError.reason.kind !== "Entries") {
|
|
3541
|
-
return singleRuntimeTypeIssue("Map", error, formatError);
|
|
4988
|
+
return singleRuntimeTypeIssue("Map", error, formatError, path);
|
|
3542
4989
|
}
|
|
3543
4990
|
const allIssues = mapError.reason.issues;
|
|
3544
4991
|
const issues = mode === "first" ? [allIssues[0]] : allIssues;
|
|
3545
|
-
|
|
4992
|
+
const result = [];
|
|
4993
|
+
for (const issue of issues) {
|
|
3546
4994
|
if (issue.kind === "Key" || issue.kind === "Value") {
|
|
3547
|
-
|
|
4995
|
+
for (const nestedIssue of (issue.kind === "Key" ? typeKey : typeValue)[getRuntimeTypeIssuesSymbol](issue.error, mode, appendIssuePath(appendIssuePath(path, issue.index), issue.kind === "Key" ? "key" : "value"))) {
|
|
4996
|
+
result.push(nestedIssue);
|
|
4997
|
+
}
|
|
4998
|
+
continue;
|
|
3548
4999
|
}
|
|
3549
|
-
|
|
3550
|
-
|
|
3551
|
-
:
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
5000
|
+
result.push({
|
|
5001
|
+
name: "Map",
|
|
5002
|
+
error: mode === "first"
|
|
5003
|
+
? error
|
|
5004
|
+
: {
|
|
5005
|
+
type: "Map",
|
|
5006
|
+
reason: { kind: "Entries", issues: [issue] },
|
|
5007
|
+
},
|
|
5008
|
+
path: appendIssuePath(path, issue.kind === "ExcessProperty" ? issue.key : issue.index),
|
|
5009
|
+
formatError: formatError,
|
|
5010
|
+
});
|
|
5011
|
+
}
|
|
5012
|
+
return result;
|
|
3556
5013
|
};
|
|
3557
|
-
const type = createTypeNode("Map", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { key: typeKey, value: typeValue });
|
|
5014
|
+
const type = createTypeNode("Map", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { additionalProperties: { key: typeKey, value: typeValue }, check });
|
|
3558
5015
|
if (typeByValue === undefined) {
|
|
3559
5016
|
typeByValue = new WeakMap();
|
|
3560
5017
|
mapTypeByValueByKey.set(typeKey, typeByValue);
|
|
@@ -3667,8 +5124,33 @@ const createTupleType = (typeElements) => {
|
|
|
3667
5124
|
}
|
|
3668
5125
|
return validateTupleItems(value, typeElements, validateElement, options, true);
|
|
3669
5126
|
};
|
|
3670
|
-
const
|
|
3671
|
-
const
|
|
5127
|
+
const elementChecks = getRuntimeChecks(typeElements);
|
|
5128
|
+
const checkElement = elementChecks &&
|
|
5129
|
+
((item, options, index) => elementChecks[index](item, options));
|
|
5130
|
+
// The same errors as validate, for identity elements.
|
|
5131
|
+
const check = checkElement &&
|
|
5132
|
+
((value, options) => {
|
|
5133
|
+
if (!Array.isArray(value)) {
|
|
5134
|
+
return { type: "Tuple", reason: { kind: "NotArray", value } };
|
|
5135
|
+
}
|
|
5136
|
+
if (value.length !== expectedLength) {
|
|
5137
|
+
return {
|
|
5138
|
+
type: "Tuple",
|
|
5139
|
+
reason: {
|
|
5140
|
+
kind: "InvalidLength",
|
|
5141
|
+
expected: expectedLength,
|
|
5142
|
+
actual: value.length,
|
|
5143
|
+
},
|
|
5144
|
+
};
|
|
5145
|
+
}
|
|
5146
|
+
return checkIndexedArrayItems("Tuple", value, checkElement, options);
|
|
5147
|
+
});
|
|
5148
|
+
const fromUnknown = check
|
|
5149
|
+
? checkToFromUnknown(check)
|
|
5150
|
+
: (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element.fromUnknown(item, elementOptions), options);
|
|
5151
|
+
const validateOutput = check
|
|
5152
|
+
? fromUnknown
|
|
5153
|
+
: (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element[outputValidationSymbol](item, elementOptions), options);
|
|
3672
5154
|
const formatError = (error) => {
|
|
3673
5155
|
if (error.reason.kind === "NotArray")
|
|
3674
5156
|
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a tuple.`;
|
|
@@ -3727,29 +5209,29 @@ const createTupleType = (typeElements) => {
|
|
|
3727
5209
|
return true;
|
|
3728
5210
|
};
|
|
3729
5211
|
const getTypeIssues = createCollectionRuntimeTypeIssues("Tuple", "Items", formatError, (issue) => typeElements[issue.index]);
|
|
3730
|
-
return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { elements: typeElements });
|
|
5212
|
+
return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { additionalProperties: { elements: typeElements }, check });
|
|
3731
5213
|
};
|
|
3732
5214
|
const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
|
|
3733
5215
|
/**
|
|
3734
|
-
* Decimal
|
|
5216
|
+
* Decimal integer string from `"0"` to `"9"`.
|
|
3735
5217
|
*
|
|
3736
5218
|
* @group String
|
|
3737
5219
|
*/
|
|
3738
5220
|
export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
3739
5221
|
/**
|
|
3740
|
-
* Decimal
|
|
5222
|
+
* Decimal integer string from `"1"` to `"9"`.
|
|
3741
5223
|
*
|
|
3742
5224
|
* @group String
|
|
3743
5225
|
*/
|
|
3744
5226
|
export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
3745
5227
|
/**
|
|
3746
|
-
* Decimal string from `"1"` to `"6"`.
|
|
5228
|
+
* Decimal integer string from `"1"` to `"6"`.
|
|
3747
5229
|
*
|
|
3748
5230
|
* @group String
|
|
3749
5231
|
*/
|
|
3750
5232
|
export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
|
|
3751
5233
|
/**
|
|
3752
|
-
* Decimal string from `"1"` to `"23"
|
|
5234
|
+
* Decimal integer string from `"1"` to `"23"`, without a leading zero.
|
|
3753
5235
|
*
|
|
3754
5236
|
* @group String
|
|
3755
5237
|
*/
|
|
@@ -3757,7 +5239,7 @@ export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
|
|
|
3757
5239
|
/*#__PURE__*/ templateLiteral("1", Digit),
|
|
3758
5240
|
/*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")));
|
|
3759
5241
|
/**
|
|
3760
|
-
* Decimal string from `"1"` to `"51"
|
|
5242
|
+
* Decimal integer string from `"1"` to `"51"`, without a leading zero.
|
|
3761
5243
|
*
|
|
3762
5244
|
* @group String
|
|
3763
5245
|
*/
|
|
@@ -3765,31 +5247,32 @@ export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
|
|
|
3765
5247
|
/*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
3766
5248
|
/*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
|
|
3767
5249
|
/**
|
|
3768
|
-
* Decimal string from `"1"` to `"99"
|
|
5250
|
+
* Decimal integer string from `"1"` to `"99"`, without a leading zero.
|
|
3769
5251
|
*
|
|
3770
5252
|
* @group String
|
|
3771
5253
|
*/
|
|
3772
5254
|
export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
|
|
3773
5255
|
/*#__PURE__*/ templateLiteral(Digit1To9, Digit));
|
|
3774
5256
|
/**
|
|
3775
|
-
* Decimal string from `"1"` to `"59"
|
|
5257
|
+
* Decimal integer string from `"1"` to `"59"`, without a leading zero.
|
|
3776
5258
|
*
|
|
3777
5259
|
* @group String
|
|
3778
5260
|
*/
|
|
3779
5261
|
export const Digit1To59 = /*#__PURE__*/ union(Digit1To9,
|
|
3780
5262
|
/*#__PURE__*/ templateLiteral(
|
|
3781
5263
|
/*#__PURE__*/ union("1", "2", "3", "4", "5"), Digit));
|
|
3782
|
-
const createObjectRuntimeTypeIssues = (
|
|
5264
|
+
const createObjectRuntimeTypeIssues = (props, recordType) => (error, mode, path) => {
|
|
3783
5265
|
const objectError = error;
|
|
3784
5266
|
if (objectError.reason.kind !== "Properties") {
|
|
3785
|
-
return singleRuntimeTypeIssue("Object", error,
|
|
5267
|
+
return singleRuntimeTypeIssue("Object", error, formatObjectError, path);
|
|
3786
5268
|
}
|
|
3787
5269
|
const propertyErrors = objectError.reason.errors;
|
|
3788
5270
|
const keys = Reflect.ownKeys(propertyErrors);
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
const
|
|
3792
|
-
|
|
5271
|
+
assertNonNullable(keys[0]);
|
|
5272
|
+
const visitCount = mode === "first" ? 1 : keys.length;
|
|
5273
|
+
const result = [];
|
|
5274
|
+
for (let index = 0; index < visitCount; index++) {
|
|
5275
|
+
const key = keys[index];
|
|
3793
5276
|
const propertyError = propertyErrors[key];
|
|
3794
5277
|
const property = typeof key === "string" &&
|
|
3795
5278
|
props !== undefined &&
|
|
@@ -3809,51 +5292,86 @@ const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (
|
|
|
3809
5292
|
reason: { kind: "Properties", errors },
|
|
3810
5293
|
};
|
|
3811
5294
|
}
|
|
3812
|
-
|
|
3813
|
-
|
|
3814
|
-
|
|
5295
|
+
result.push({
|
|
5296
|
+
name: "Object",
|
|
5297
|
+
error: ownError,
|
|
5298
|
+
path: appendIssuePath(path, key),
|
|
5299
|
+
// The issue error has this first key, so its message needs no key
|
|
5300
|
+
// enumeration of the error record.
|
|
5301
|
+
formatError: () => formatObjectPropertyError(key, propertyError),
|
|
5302
|
+
});
|
|
5303
|
+
continue;
|
|
3815
5304
|
}
|
|
3816
|
-
|
|
3817
|
-
|
|
5305
|
+
// A record property error adds its own key.
|
|
5306
|
+
const nestedIssues = property === undefined
|
|
5307
|
+
? recordType[getRuntimeTypeIssuesSymbol](propertyError, mode, path.slice())
|
|
5308
|
+
: objectPropertyToType(property)[getRuntimeTypeIssuesSymbol](propertyError, mode, appendIssuePath(path, key));
|
|
5309
|
+
// Nested issues are a new array, so one visited issue returns them.
|
|
5310
|
+
if (visitCount === 1)
|
|
5311
|
+
return nestedIssues;
|
|
5312
|
+
for (const issue of nestedIssues)
|
|
5313
|
+
result.push(issue);
|
|
5314
|
+
}
|
|
5315
|
+
return result;
|
|
5316
|
+
};
|
|
5317
|
+
const formatObjectError = (error) => {
|
|
5318
|
+
if (error.reason.kind !== "Properties")
|
|
5319
|
+
return formatPlainObjectRootError(error.reason);
|
|
5320
|
+
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
5321
|
+
assertNonNullable(key);
|
|
5322
|
+
return formatObjectPropertyError(key, error.reason.errors[key]);
|
|
5323
|
+
};
|
|
5324
|
+
const formatObjectPropertyError = (key, propertyError) => {
|
|
5325
|
+
assertNonNullable(propertyError);
|
|
5326
|
+
if (propertyError.type === "ObjectPropertyAccess") {
|
|
5327
|
+
switch (propertyError.reason) {
|
|
5328
|
+
case "Accessor":
|
|
5329
|
+
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
5330
|
+
case "NonEnumerable":
|
|
5331
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3818
5332
|
}
|
|
3819
|
-
|
|
3820
|
-
|
|
5333
|
+
}
|
|
5334
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
5335
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
5336
|
+
if (typeof key === "symbol")
|
|
5337
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
5338
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
5339
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
5340
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3821
5341
|
};
|
|
3822
5342
|
const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
3823
5343
|
? `A value ${safelyStringifyUnknownValue(reason.value)} is not an object.`
|
|
3824
5344
|
: "The value is an object, but an Object Output must be a plain object or have a null prototype.";
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
|
|
3828
|
-
|
|
3829
|
-
|
|
3830
|
-
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
|
|
3836
|
-
|
|
3837
|
-
|
|
3838
|
-
|
|
3839
|
-
|
|
3840
|
-
|
|
3841
|
-
|
|
3842
|
-
|
|
5345
|
+
// Ordinary objects list string keys before symbols, so without symbols the
|
|
5346
|
+
// names are the own keys, and cheaper than Reflect.ownKeys. A Proxy may list
|
|
5347
|
+
// symbols first, and Reflect.ownKeys keeps that order.
|
|
5348
|
+
const getOwnKeys = (value) => {
|
|
5349
|
+
const names = globalThis.Object.getOwnPropertyNames(value);
|
|
5350
|
+
return globalThis.Object.getOwnPropertySymbols(value).length === 0
|
|
5351
|
+
? names
|
|
5352
|
+
: Reflect.ownKeys(value);
|
|
5353
|
+
};
|
|
5354
|
+
// An `is` walk in own-key order fails at the first symbol key, after checking
|
|
5355
|
+
// the names before it. Ordinary objects list symbols last, but a Proxy may
|
|
5356
|
+
// list one first. Walking names is faster than walking mixed own keys.
|
|
5357
|
+
const getNamesBeforeSymbol = (value) => {
|
|
5358
|
+
const names = [];
|
|
5359
|
+
for (const key of Reflect.ownKeys(value)) {
|
|
5360
|
+
if (typeof key !== "string")
|
|
5361
|
+
break;
|
|
5362
|
+
names.push(key);
|
|
5363
|
+
}
|
|
5364
|
+
return names;
|
|
5365
|
+
};
|
|
5366
|
+
const checkPlainObject = (value, options) => {
|
|
3843
5367
|
if (value === null || typeof value !== "object") {
|
|
3844
|
-
return
|
|
3845
|
-
type: "Object",
|
|
3846
|
-
reason: { kind: "NotObject", value },
|
|
3847
|
-
});
|
|
5368
|
+
return { type: "Object", reason: { kind: "NotObject", value } };
|
|
3848
5369
|
}
|
|
3849
5370
|
if (!isPlainObject(value)) {
|
|
3850
|
-
return
|
|
3851
|
-
type: "Object",
|
|
3852
|
-
reason: { kind: "UnexpectedPrototype", value },
|
|
3853
|
-
});
|
|
5371
|
+
return { type: "Object", reason: { kind: "UnexpectedPrototype", value } };
|
|
3854
5372
|
}
|
|
3855
5373
|
let errors;
|
|
3856
|
-
for (const key of
|
|
5374
|
+
for (const key of getOwnKeys(value)) {
|
|
3857
5375
|
let propertyError;
|
|
3858
5376
|
if (typeof key !== "string") {
|
|
3859
5377
|
propertyError = { type: "ObjectExcessProperty" };
|
|
@@ -3882,57 +5400,38 @@ const _Object = /*#__PURE__*/ createRootType("Object", (value, options = firstVa
|
|
|
3882
5400
|
break;
|
|
3883
5401
|
}
|
|
3884
5402
|
return errors === undefined
|
|
3885
|
-
?
|
|
3886
|
-
:
|
|
5403
|
+
? undefined
|
|
5404
|
+
: {
|
|
3887
5405
|
type: "Object",
|
|
3888
5406
|
reason: { kind: "Properties", errors },
|
|
3889
|
-
}
|
|
3890
|
-
}
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
3898
|
-
|
|
3899
|
-
|
|
3900
|
-
|
|
3901
|
-
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
3916
|
-
|
|
3917
|
-
assertNonNullable(key);
|
|
3918
|
-
const propertyError = error.reason.errors[key];
|
|
3919
|
-
assertNonNullable(propertyError);
|
|
3920
|
-
if (propertyError.type === "ObjectPropertyAccess") {
|
|
3921
|
-
switch (propertyError.reason) {
|
|
3922
|
-
case "Accessor":
|
|
3923
|
-
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3924
|
-
case "NonEnumerable":
|
|
3925
|
-
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3926
|
-
}
|
|
3927
|
-
}
|
|
3928
|
-
if (propertyError.type === "ObjectMissingProperty")
|
|
3929
|
-
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
3930
|
-
if (typeof key === "symbol")
|
|
3931
|
-
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3932
|
-
if (propertyError.type === "ObjectExcessProperty")
|
|
3933
|
-
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3934
|
-
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3935
|
-
})));
|
|
5407
|
+
};
|
|
5408
|
+
};
|
|
5409
|
+
/**
|
|
5410
|
+
* A {@link Type} for readonly plain objects with unknown property values.
|
|
5411
|
+
*
|
|
5412
|
+
* `Object` is the runtime counterpart of a `Readonly<Record<string, unknown>>`
|
|
5413
|
+
* data boundary. Its prototype rule uses the realm-neutral structural heuristic
|
|
5414
|
+
* described by {@link isPlainObject}. A matching custom root prototype can be
|
|
5415
|
+
* classified as plain; other custom prototypes and class instances are
|
|
5416
|
+
* rejected. Every own property must have a string key and be an enumerable data
|
|
5417
|
+
* property. Accessors, non-enumerable properties, and symbol properties are
|
|
5418
|
+
* rejected without reading their values.
|
|
5419
|
+
*
|
|
5420
|
+
* Use {@link object} when property names are fixed, {@link record} when keys and
|
|
5421
|
+
* values have their own Types, and {@link instanceOf} when an instance belongs
|
|
5422
|
+
* to the domain.
|
|
5423
|
+
*
|
|
5424
|
+
* @group Base
|
|
5425
|
+
*/
|
|
5426
|
+
const _Object = /*#__PURE__*/ createRootType("Object", (value, options = firstValidationOptions) => {
|
|
5427
|
+
const error = checkPlainObject(value, options);
|
|
5428
|
+
return error === undefined
|
|
5429
|
+
? ok(value)
|
|
5430
|
+
: err(error);
|
|
5431
|
+
}, formatObjectError, {
|
|
5432
|
+
getTypeIssues: /*#__PURE__*/ createObjectRuntimeTypeIssues(),
|
|
5433
|
+
check: checkPlainObject,
|
|
5434
|
+
});
|
|
3936
5435
|
// Avoid a local `Object` binding because Babel's CommonJS transform injects
|
|
3937
5436
|
// `Object.defineProperty` before it is initialized:
|
|
3938
5437
|
// https://github.com/babel/babel/issues/16943
|
|
@@ -4026,6 +5525,7 @@ export { _Object as Object };
|
|
|
4026
5525
|
* const valueType = typeof value;
|
|
4027
5526
|
*
|
|
4028
5527
|
* assertEqual(valueType, "function");
|
|
5528
|
+
*
|
|
4029
5529
|
* const called = trySync(
|
|
4030
5530
|
* () => {
|
|
4031
5531
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -4061,6 +5561,7 @@ export { _Object as Object };
|
|
|
4061
5561
|
* const valueType = typeof value;
|
|
4062
5562
|
*
|
|
4063
5563
|
* assertEqual(valueType, "function");
|
|
5564
|
+
*
|
|
4064
5565
|
* const called = trySync(
|
|
4065
5566
|
* () => {
|
|
4066
5567
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -4094,8 +5595,71 @@ export const record = (key, value) => {
|
|
|
4094
5595
|
}
|
|
4095
5596
|
return validateRecordEntries(input, validateKey, validateValue, options);
|
|
4096
5597
|
};
|
|
4097
|
-
const
|
|
4098
|
-
const
|
|
5598
|
+
const checkKey = typeKey[checkSymbol];
|
|
5599
|
+
const checkValue = typeValue[checkSymbol];
|
|
5600
|
+
// The same errors as validate for identity keys and values. Own keys are
|
|
5601
|
+
// unique, so identity keys never collide.
|
|
5602
|
+
const check = checkKey &&
|
|
5603
|
+
checkValue &&
|
|
5604
|
+
((input, options) => {
|
|
5605
|
+
if (input === null || typeof input !== "object") {
|
|
5606
|
+
return { type: "Record", reason: { kind: "NotRecord", value: input } };
|
|
5607
|
+
}
|
|
5608
|
+
if (!isPlainObject(input)) {
|
|
5609
|
+
return {
|
|
5610
|
+
type: "Record",
|
|
5611
|
+
reason: { kind: "NotPlainRecord", value: input },
|
|
5612
|
+
};
|
|
5613
|
+
}
|
|
5614
|
+
let issues;
|
|
5615
|
+
for (const inputKey of getOwnKeys(input)) {
|
|
5616
|
+
const keyError = checkKey(inputKey, options);
|
|
5617
|
+
if (keyError !== undefined) {
|
|
5618
|
+
(issues ??= []).push({ kind: "Key", key: inputKey, error: keyError });
|
|
5619
|
+
if (options.errors === "first")
|
|
5620
|
+
break;
|
|
5621
|
+
}
|
|
5622
|
+
const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, inputKey);
|
|
5623
|
+
assert(descriptor !== undefined, "Record property descriptor is missing.");
|
|
5624
|
+
if (!("value" in descriptor)) {
|
|
5625
|
+
(issues ??= []).push({ kind: "Accessor", key: inputKey });
|
|
5626
|
+
if (options.errors === "first")
|
|
5627
|
+
break;
|
|
5628
|
+
}
|
|
5629
|
+
else if (!descriptor.enumerable) {
|
|
5630
|
+
(issues ??= []).push({ kind: "NonEnumerable", key: inputKey });
|
|
5631
|
+
if (options.errors === "first")
|
|
5632
|
+
break;
|
|
5633
|
+
}
|
|
5634
|
+
else {
|
|
5635
|
+
const valueError = checkValue(descriptor.value, options);
|
|
5636
|
+
if (valueError !== undefined) {
|
|
5637
|
+
(issues ??= []).push({
|
|
5638
|
+
kind: "Value",
|
|
5639
|
+
key: inputKey,
|
|
5640
|
+
error: valueError,
|
|
5641
|
+
});
|
|
5642
|
+
if (options.errors === "first")
|
|
5643
|
+
break;
|
|
5644
|
+
}
|
|
5645
|
+
}
|
|
5646
|
+
}
|
|
5647
|
+
return issues === undefined
|
|
5648
|
+
? undefined
|
|
5649
|
+
: {
|
|
5650
|
+
type: "Record",
|
|
5651
|
+
reason: {
|
|
5652
|
+
kind: "Entries",
|
|
5653
|
+
issues: issues,
|
|
5654
|
+
},
|
|
5655
|
+
};
|
|
5656
|
+
});
|
|
5657
|
+
const fromUnknown = check
|
|
5658
|
+
? checkToFromUnknown(check)
|
|
5659
|
+
: (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
|
|
5660
|
+
const validateOutput = check
|
|
5661
|
+
? fromUnknown
|
|
5662
|
+
: (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
|
|
4099
5663
|
const formatError = (error) => {
|
|
4100
5664
|
if (error.reason.kind === "NotRecord")
|
|
4101
5665
|
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Record.`;
|
|
@@ -4149,8 +5713,16 @@ export const record = (key, value) => {
|
|
|
4149
5713
|
return false;
|
|
4150
5714
|
if (!isPlainObject(input))
|
|
4151
5715
|
return false;
|
|
4152
|
-
|
|
4153
|
-
|
|
5716
|
+
// Separate name and symbol enumerations cost dictionary-mode inputs, such
|
|
5717
|
+
// as null-prototype records, about 10% over one Reflect.ownKeys, which is
|
|
5718
|
+
// much slower for ordinary objects.
|
|
5719
|
+
const hasSymbols = globalThis.Object.getOwnPropertySymbols(input).length !== 0;
|
|
5720
|
+
const names = hasSymbols
|
|
5721
|
+
? getNamesBeforeSymbol(input)
|
|
5722
|
+
: globalThis.Object.getOwnPropertyNames(input);
|
|
5723
|
+
for (let index = 0; index < names.length; index++) {
|
|
5724
|
+
const key = names[index];
|
|
5725
|
+
if (!typeKey.is(key))
|
|
4154
5726
|
return false;
|
|
4155
5727
|
const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
|
|
4156
5728
|
if (descriptor === undefined ||
|
|
@@ -4161,17 +5733,17 @@ export const record = (key, value) => {
|
|
|
4161
5733
|
if (!typeValue.is(descriptor.value))
|
|
4162
5734
|
return false;
|
|
4163
5735
|
}
|
|
4164
|
-
return
|
|
5736
|
+
return !hasSymbols;
|
|
4165
5737
|
};
|
|
4166
5738
|
const getTypeIssues = createCollectionRuntimeTypeIssues("Record", "Entries", formatError, (issue) => (issue.kind === "Key" ? typeKey : typeValue));
|
|
4167
|
-
return createTypeNode("Record", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { key: typeKey, value: typeValue });
|
|
5739
|
+
return createTypeNode("Record", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { additionalProperties: { key: typeKey, value: typeValue }, check });
|
|
4168
5740
|
};
|
|
4169
5741
|
const validateRecordEntries = (input, validateKey, validateValue, options) => {
|
|
4170
5742
|
let issues;
|
|
4171
5743
|
const output = createMutableRecord();
|
|
4172
5744
|
const inputKeyByOutputKey = createMutableRecord();
|
|
4173
5745
|
let changed = false;
|
|
4174
|
-
for (const inputKey of
|
|
5746
|
+
for (const inputKey of getOwnKeys(input)) {
|
|
4175
5747
|
const keyResult = validateKey(inputKey, options);
|
|
4176
5748
|
if (!keyResult.ok) {
|
|
4177
5749
|
(issues ??= []).push({
|
|
@@ -4279,17 +5851,139 @@ const validateRecordEntries = (input, validateKey, validateValue, options) => {
|
|
|
4279
5851
|
* @group Objects
|
|
4280
5852
|
*/
|
|
4281
5853
|
export const optional = (type) => createOptionalProperty(type);
|
|
4282
|
-
const optionalPropertySymbol =
|
|
5854
|
+
const optionalPropertySymbol =
|
|
5855
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
4283
5856
|
const createOptionalProperty = (type) => ({
|
|
4284
5857
|
type,
|
|
4285
5858
|
[optionalPropertySymbol]: true,
|
|
4286
5859
|
});
|
|
5860
|
+
export function withDefault(property, value, { strategy } = {}) {
|
|
5861
|
+
assert(strategy === undefined || strategy === "preserve", 'withDefault strategy must be omitted or "preserve".');
|
|
5862
|
+
const optionalInput = optionalPropertySymbol in property;
|
|
5863
|
+
const type = (optionalInput ? property.type : property);
|
|
5864
|
+
assert(type.is(value), `withDefault value must be an Output of ${type.name}.`);
|
|
5865
|
+
const originals = new Set([
|
|
5866
|
+
...(optionalInput ? ["missing"] : []),
|
|
5867
|
+
...(type.is(null) ? ["null"] : []),
|
|
5868
|
+
...(type.is(undefined) ? ["undefined"] : []),
|
|
5869
|
+
]);
|
|
5870
|
+
const supplied = createRootType("DefaultSupplied", (candidate) => (strategy === undefined && globalThis.Object.is(candidate, value)) ||
|
|
5871
|
+
(candidate !== null && candidate !== undefined && type.is(candidate))
|
|
5872
|
+
? ok(candidate)
|
|
5873
|
+
: err({ type: "DefaultValue" }), () => "The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.");
|
|
5874
|
+
const defaultValue = createRootType("DefaultValue", (candidate) => type.is(candidate) &&
|
|
5875
|
+
(globalThis.Object.is(candidate, value) ||
|
|
5876
|
+
(Data.is(value) && Data.is(candidate) && eqData(candidate, value)))
|
|
5877
|
+
? ok(candidate)
|
|
5878
|
+
: err({ type: "DefaultValue" }), () => "A preserved default must equal the configured default value.");
|
|
5879
|
+
const output = (strategy === undefined
|
|
5880
|
+
? supplied
|
|
5881
|
+
: discriminatedUnion("defaultUsed", object({ value: supplied, defaultUsed: literal(false) }), object({
|
|
5882
|
+
value: defaultValue,
|
|
5883
|
+
defaultUsed: literal(true),
|
|
5884
|
+
original: createRootType("DefaultOriginal", (candidate) => typeof candidate === "string" && originals.has(candidate)
|
|
5885
|
+
? ok(candidate)
|
|
5886
|
+
: err({ type: "DefaultOriginal" }), () => "The original absence must be handled by this default declaration."),
|
|
5887
|
+
})));
|
|
5888
|
+
const ownGetTypeIssues = output[getRuntimeTypeIssuesSymbol];
|
|
5889
|
+
const outputWithIssues = {
|
|
5890
|
+
...output,
|
|
5891
|
+
[outputValidationSymbol]: (value, options) => {
|
|
5892
|
+
const result = output[outputValidationSymbol](value, options);
|
|
5893
|
+
return result.ok
|
|
5894
|
+
? result
|
|
5895
|
+
: err({ type: "WithDefault", outputError: result.error });
|
|
5896
|
+
},
|
|
5897
|
+
[getRuntimeTypeIssuesSymbol]: (error, mode, path) => error.type === "WithDefault" && "outputError" in error
|
|
5898
|
+
? ownGetTypeIssues(error.outputError, mode, path)
|
|
5899
|
+
: type[getRuntimeTypeIssuesSymbol](error, mode, path),
|
|
5900
|
+
// Output validation wraps errors, so a copied check would not match it.
|
|
5901
|
+
[checkSymbol]: undefined,
|
|
5902
|
+
[refinementsSymbol]: undefined,
|
|
5903
|
+
};
|
|
5904
|
+
const operations = {
|
|
5905
|
+
output: outputWithIssues,
|
|
5906
|
+
createObject: createDefaultObjectType,
|
|
5907
|
+
partial: (type) => createOptionalProperty(strategy === "preserve"
|
|
5908
|
+
? withDefault(type, value, {
|
|
5909
|
+
strategy,
|
|
5910
|
+
})
|
|
5911
|
+
: withDefault(type, value)),
|
|
5912
|
+
decode: (candidate) => {
|
|
5913
|
+
const original = candidate === missingDefaultValue
|
|
5914
|
+
? "missing"
|
|
5915
|
+
: candidate === null
|
|
5916
|
+
? "null"
|
|
5917
|
+
: candidate === undefined
|
|
5918
|
+
? "undefined"
|
|
5919
|
+
: undefined;
|
|
5920
|
+
const effective = original === undefined ? candidate : value;
|
|
5921
|
+
return strategy === undefined
|
|
5922
|
+
? effective
|
|
5923
|
+
: original === undefined
|
|
5924
|
+
? { value: effective, defaultUsed: false }
|
|
5925
|
+
: { value: effective, defaultUsed: true, original };
|
|
5926
|
+
},
|
|
5927
|
+
encode: (candidate) => {
|
|
5928
|
+
if (strategy === undefined)
|
|
5929
|
+
return candidate;
|
|
5930
|
+
const preserved = candidate;
|
|
5931
|
+
if (!preserved.defaultUsed)
|
|
5932
|
+
return preserved.value;
|
|
5933
|
+
switch (preserved.original) {
|
|
5934
|
+
case "missing":
|
|
5935
|
+
return missingDefaultValue;
|
|
5936
|
+
case "null":
|
|
5937
|
+
return null;
|
|
5938
|
+
case "undefined":
|
|
5939
|
+
return undefined;
|
|
5940
|
+
}
|
|
5941
|
+
},
|
|
5942
|
+
};
|
|
5943
|
+
if (optionalInput) {
|
|
5944
|
+
return {
|
|
5945
|
+
type,
|
|
5946
|
+
value,
|
|
5947
|
+
strategy: strategy ?? "replace",
|
|
5948
|
+
[defaultPropertySymbol]: operations,
|
|
5949
|
+
};
|
|
5950
|
+
}
|
|
5951
|
+
const fromOwn = (value) => ok(operations.decode(value));
|
|
5952
|
+
const fromParent = mapRuntimeOperations(type[fromSymbol], (operation) => (value, options) => flatMapResult(operation(value, options), fromOwn));
|
|
5953
|
+
return createTypeNode("WithDefault", type, (value, options) => flatMapResult(type.fromUnknown(value, options), fromOwn), operations.output.is, operations.output[outputValidationSymbol], createFromOperation(fromParent), operations.encode, operations.output[getRuntimeTypeIssuesSymbol]);
|
|
5954
|
+
}
|
|
5955
|
+
const defaultPropertySymbol =
|
|
5956
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
5957
|
+
const missingDefaultValue = /*#__PURE__*/ globalThis.Symbol();
|
|
4287
5958
|
export function object(props, recordType) {
|
|
4288
5959
|
return createObjectType(snapshotObjectProps(props), recordType);
|
|
4289
5960
|
}
|
|
4290
|
-
const createObjectType = (props, recordType
|
|
5961
|
+
const createObjectType = (props, recordType,
|
|
5962
|
+
// A parent Object Type reuses the key order of its child.
|
|
5963
|
+
keys = globalThis.Object.keys(props)) => {
|
|
4291
5964
|
const runtimeProps = props;
|
|
4292
|
-
const
|
|
5965
|
+
const keyCount = keys.length;
|
|
5966
|
+
const propertyTypes = createMutableArray(keyCount);
|
|
5967
|
+
const optionalFlags = createMutableArray(keyCount);
|
|
5968
|
+
for (let index = 0; index < keyCount; index++) {
|
|
5969
|
+
const property = runtimeProps[keys[index]];
|
|
5970
|
+
if (isDefaultProperty(property)) {
|
|
5971
|
+
return property[defaultPropertySymbol].createObject(props, recordType);
|
|
5972
|
+
}
|
|
5973
|
+
const isOptional = isOptionalProperty(property);
|
|
5974
|
+
const type = isOptional ? property.type : property;
|
|
5975
|
+
propertyTypes[index] = type;
|
|
5976
|
+
optionalFlags[index] = isOptional;
|
|
5977
|
+
}
|
|
5978
|
+
const propertyChecks = propertyTypes.map((type) => type[checkSymbol]);
|
|
5979
|
+
const recordCheck = recordType?.value[checkSymbol];
|
|
5980
|
+
// When every property and Record value Type is an identity Type, validation
|
|
5981
|
+
// never changes a value.
|
|
5982
|
+
const isIdentity = !propertyChecks.includes(undefined) &&
|
|
5983
|
+
(recordType === undefined || recordCheck !== undefined);
|
|
5984
|
+
let isOutputs;
|
|
5985
|
+
// Returns `undefined` for a valid input it did not change, so a valid
|
|
5986
|
+
// identity Object allocates no Result.
|
|
4293
5987
|
const validate = (value, options, exactOutput) => {
|
|
4294
5988
|
if (value === null || typeof value !== "object") {
|
|
4295
5989
|
return err({
|
|
@@ -4304,116 +5998,158 @@ const createObjectType = (props, recordType) => {
|
|
|
4304
5998
|
reason: { kind: "UnexpectedPrototype", value: input },
|
|
4305
5999
|
});
|
|
4306
6000
|
}
|
|
6001
|
+
const ownKeys = getOwnKeys(input);
|
|
6002
|
+
// Descriptors by visit index let a copy-on-write Output reuse the values
|
|
6003
|
+
// already read. Only a property without a check can change its value.
|
|
6004
|
+
const descriptors = isIdentity
|
|
6005
|
+
? undefined
|
|
6006
|
+
: createMutableArray(keyCount + ownKeys.length);
|
|
4307
6007
|
let errors;
|
|
4308
6008
|
let output;
|
|
4309
|
-
|
|
4310
|
-
|
|
4311
|
-
|
|
4312
|
-
|
|
4313
|
-
|
|
4314
|
-
|
|
4315
|
-
|
|
4316
|
-
|
|
4317
|
-
|
|
4318
|
-
};
|
|
4319
|
-
for (const [key] of inputDescriptorByKey) {
|
|
4320
|
-
const property = typeof key === "string" && globalThis.Object.hasOwn(runtimeProps, key)
|
|
4321
|
-
? runtimeProps[key]
|
|
4322
|
-
: undefined;
|
|
6009
|
+
// Own keys that are exactly the declared keys in their order leave the
|
|
6010
|
+
// undeclared-key loop nothing to visit. A first-mode stop leaves the flag
|
|
6011
|
+
// unchecked, but then that loop does not run either.
|
|
6012
|
+
let ownKeysAreDeclared = ownKeys.length === keyCount;
|
|
6013
|
+
for (let index = 0; (errors === undefined || options.errors !== "first") && index < keyCount; index++) {
|
|
6014
|
+
const key = keys[index];
|
|
6015
|
+
if (ownKeysAreDeclared && ownKeys[index] !== key) {
|
|
6016
|
+
ownKeysAreDeclared = false;
|
|
6017
|
+
}
|
|
4323
6018
|
const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
|
|
4324
|
-
|
|
6019
|
+
if (descriptors !== undefined)
|
|
6020
|
+
descriptors[index] = descriptor;
|
|
6021
|
+
let error;
|
|
4325
6022
|
if (descriptor === undefined) {
|
|
4326
|
-
|
|
4327
|
-
if (isOptionalProperty(property)) {
|
|
6023
|
+
if (optionalFlags[index])
|
|
4328
6024
|
continue;
|
|
6025
|
+
error = { type: "ObjectMissingProperty" };
|
|
6026
|
+
}
|
|
6027
|
+
else if (!("value" in descriptor)) {
|
|
6028
|
+
error = {
|
|
6029
|
+
type: "ObjectPropertyAccess",
|
|
6030
|
+
reason: "Accessor",
|
|
6031
|
+
};
|
|
6032
|
+
}
|
|
6033
|
+
else if (!descriptor.enumerable) {
|
|
6034
|
+
error = {
|
|
6035
|
+
type: "ObjectPropertyAccess",
|
|
6036
|
+
reason: "NonEnumerable",
|
|
6037
|
+
};
|
|
6038
|
+
}
|
|
6039
|
+
else {
|
|
6040
|
+
const propertyValue = descriptor.value;
|
|
6041
|
+
const check = propertyChecks[index];
|
|
6042
|
+
if (check !== undefined) {
|
|
6043
|
+
const propertyError = check(propertyValue, options);
|
|
6044
|
+
if (propertyError === undefined) {
|
|
6045
|
+
if (output !== undefined && errors === undefined) {
|
|
6046
|
+
output[key] = propertyValue;
|
|
6047
|
+
}
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
error = propertyError;
|
|
4329
6051
|
}
|
|
4330
|
-
|
|
4331
|
-
|
|
4332
|
-
|
|
6052
|
+
else {
|
|
6053
|
+
const result = exactOutput
|
|
6054
|
+
? propertyTypes[index][outputValidationSymbol](propertyValue, options)
|
|
6055
|
+
: propertyTypes[index].fromUnknown(propertyValue, options);
|
|
6056
|
+
if (result.ok) {
|
|
6057
|
+
if (errors !== undefined || exactOutput)
|
|
6058
|
+
continue;
|
|
6059
|
+
if (output === undefined) {
|
|
6060
|
+
if (globalThis.Object.is(result.value, propertyValue))
|
|
6061
|
+
continue;
|
|
6062
|
+
output = copyVisitedProperties(descriptors, ownKeys, index);
|
|
6063
|
+
}
|
|
6064
|
+
output[key] = result.value;
|
|
6065
|
+
continue;
|
|
6066
|
+
}
|
|
6067
|
+
error = result.error;
|
|
6068
|
+
}
|
|
6069
|
+
}
|
|
6070
|
+
errors ??= createMutableRecord();
|
|
6071
|
+
errors[key] = error;
|
|
6072
|
+
}
|
|
6073
|
+
const ownKeyEnd = ownKeysAreDeclared ? 0 : ownKeys.length;
|
|
6074
|
+
for (let position = 0; (errors === undefined || options.errors !== "first") &&
|
|
6075
|
+
position < ownKeyEnd; position++) {
|
|
6076
|
+
const key = ownKeys[position];
|
|
6077
|
+
const index = keyCount + position;
|
|
6078
|
+
if (typeof key === "string" &&
|
|
6079
|
+
// Own keys usually follow the declared order.
|
|
6080
|
+
((position < keyCount && key === keys[position]) ||
|
|
6081
|
+
globalThis.Object.hasOwn(runtimeProps, key))) {
|
|
6082
|
+
if (descriptors !== undefined)
|
|
6083
|
+
descriptors[index] = undefined;
|
|
4333
6084
|
continue;
|
|
4334
6085
|
}
|
|
4335
|
-
|
|
4336
|
-
|
|
6086
|
+
const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
|
|
6087
|
+
assert(descriptor !== undefined, "Object property is missing.");
|
|
6088
|
+
if (descriptors !== undefined)
|
|
6089
|
+
descriptors[index] = descriptor;
|
|
6090
|
+
let error;
|
|
6091
|
+
if (recordType === undefined) {
|
|
6092
|
+
error = {
|
|
4337
6093
|
type: "ObjectExcessProperty",
|
|
4338
|
-
}
|
|
4339
|
-
if (options.errors === "first")
|
|
4340
|
-
break;
|
|
4341
|
-
continue;
|
|
6094
|
+
};
|
|
4342
6095
|
}
|
|
4343
|
-
if (
|
|
4344
|
-
|
|
6096
|
+
else if (typeof key !== "string") {
|
|
6097
|
+
error = createRecordPropertyError({
|
|
4345
6098
|
kind: "Key",
|
|
4346
6099
|
key,
|
|
4347
|
-
error: {
|
|
4348
|
-
|
|
4349
|
-
expected: "String",
|
|
4350
|
-
value: key,
|
|
4351
|
-
},
|
|
4352
|
-
}));
|
|
4353
|
-
if (options.errors === "first")
|
|
4354
|
-
break;
|
|
4355
|
-
continue;
|
|
6100
|
+
error: { type: "TypeOf", expected: "String", value: key },
|
|
6101
|
+
});
|
|
4356
6102
|
}
|
|
4357
|
-
if (!("value" in descriptor)) {
|
|
4358
|
-
|
|
6103
|
+
else if (!("value" in descriptor)) {
|
|
6104
|
+
error = {
|
|
4359
6105
|
type: "ObjectPropertyAccess",
|
|
4360
6106
|
reason: "Accessor",
|
|
4361
6107
|
};
|
|
4362
|
-
setError(key, propertyError);
|
|
4363
|
-
if (options.errors === "first")
|
|
4364
|
-
break;
|
|
4365
|
-
continue;
|
|
4366
6108
|
}
|
|
4367
|
-
if (!descriptor.enumerable) {
|
|
4368
|
-
|
|
6109
|
+
else if (!descriptor.enumerable) {
|
|
6110
|
+
error = {
|
|
4369
6111
|
type: "ObjectPropertyAccess",
|
|
4370
6112
|
reason: "NonEnumerable",
|
|
4371
6113
|
};
|
|
4372
|
-
setError(key, propertyError);
|
|
4373
|
-
if (options.errors === "first")
|
|
4374
|
-
break;
|
|
4375
|
-
continue;
|
|
4376
|
-
}
|
|
4377
|
-
const propertyValue = descriptor.value;
|
|
4378
|
-
const propertyType = property === undefined
|
|
4379
|
-
? recordType.value
|
|
4380
|
-
: objectPropertyToType(property);
|
|
4381
|
-
const result = exactOutput
|
|
4382
|
-
? propertyType[outputValidationSymbol](propertyValue, options)
|
|
4383
|
-
: propertyType.fromUnknown(propertyValue, options);
|
|
4384
|
-
if (!result.ok) {
|
|
4385
|
-
setError(key, property === undefined
|
|
4386
|
-
? createRecordPropertyError({
|
|
4387
|
-
kind: "Value",
|
|
4388
|
-
key,
|
|
4389
|
-
error: result.error,
|
|
4390
|
-
})
|
|
4391
|
-
: result.error);
|
|
4392
|
-
if (options.errors === "first")
|
|
4393
|
-
break;
|
|
4394
|
-
continue;
|
|
4395
6114
|
}
|
|
4396
|
-
|
|
4397
|
-
|
|
4398
|
-
|
|
4399
|
-
|
|
4400
|
-
|
|
4401
|
-
|
|
4402
|
-
|
|
4403
|
-
|
|
4404
|
-
|
|
4405
|
-
|
|
4406
|
-
for (const [previousKey, previousDescriptor] of inputDescriptorByKey) {
|
|
4407
|
-
if (previousKey === key)
|
|
4408
|
-
break;
|
|
4409
|
-
if (previousDescriptor !== undefined &&
|
|
4410
|
-
"value" in previousDescriptor &&
|
|
4411
|
-
previousDescriptor.enumerable) {
|
|
4412
|
-
output[previousKey] = previousDescriptor.value;
|
|
6115
|
+
else {
|
|
6116
|
+
const propertyValue = descriptor.value;
|
|
6117
|
+
let valueError;
|
|
6118
|
+
if (recordCheck !== undefined) {
|
|
6119
|
+
const checkError = recordCheck(propertyValue, options);
|
|
6120
|
+
if (checkError === undefined) {
|
|
6121
|
+
if (output !== undefined && errors === undefined) {
|
|
6122
|
+
output[key] = propertyValue;
|
|
6123
|
+
}
|
|
6124
|
+
continue;
|
|
4413
6125
|
}
|
|
6126
|
+
valueError = checkError;
|
|
4414
6127
|
}
|
|
6128
|
+
else {
|
|
6129
|
+
const result = exactOutput
|
|
6130
|
+
? recordType.value[outputValidationSymbol](propertyValue, options)
|
|
6131
|
+
: recordType.value.fromUnknown(propertyValue, options);
|
|
6132
|
+
if (result.ok) {
|
|
6133
|
+
if (errors !== undefined || exactOutput)
|
|
6134
|
+
continue;
|
|
6135
|
+
if (output === undefined) {
|
|
6136
|
+
if (globalThis.Object.is(result.value, propertyValue))
|
|
6137
|
+
continue;
|
|
6138
|
+
output = copyVisitedProperties(descriptors, ownKeys, index);
|
|
6139
|
+
}
|
|
6140
|
+
output[key] = result.value;
|
|
6141
|
+
continue;
|
|
6142
|
+
}
|
|
6143
|
+
valueError = result.error;
|
|
6144
|
+
}
|
|
6145
|
+
error = createRecordPropertyError({
|
|
6146
|
+
kind: "Value",
|
|
6147
|
+
key,
|
|
6148
|
+
error: valueError,
|
|
6149
|
+
});
|
|
4415
6150
|
}
|
|
4416
|
-
|
|
6151
|
+
errors ??= createMutableRecord();
|
|
6152
|
+
errors[key] = error;
|
|
4417
6153
|
}
|
|
4418
6154
|
if (errors !== undefined) {
|
|
4419
6155
|
return err({
|
|
@@ -4421,69 +6157,70 @@ const createObjectType = (props, recordType) => {
|
|
|
4421
6157
|
reason: { kind: "Properties", errors },
|
|
4422
6158
|
});
|
|
4423
6159
|
}
|
|
4424
|
-
|
|
4425
|
-
return ok(input);
|
|
4426
|
-
return ok(output);
|
|
6160
|
+
return output === undefined ? undefined : ok(output);
|
|
4427
6161
|
};
|
|
4428
|
-
|
|
4429
|
-
|
|
4430
|
-
const
|
|
4431
|
-
|
|
4432
|
-
|
|
4433
|
-
|
|
4434
|
-
|
|
4435
|
-
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
switch (propertyError.reason) {
|
|
4439
|
-
case "Accessor":
|
|
4440
|
-
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
4441
|
-
case "NonEnumerable":
|
|
4442
|
-
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
4443
|
-
}
|
|
6162
|
+
// Properties visited before `end` are valid and unchanged, so a new Output
|
|
6163
|
+
// starts with their values.
|
|
6164
|
+
const copyVisitedProperties = (descriptors, ownKeys, end) => {
|
|
6165
|
+
const output = globalThis.Object.create(null);
|
|
6166
|
+
for (let index = 0; index < end; index++) {
|
|
6167
|
+
const descriptor = descriptors[index];
|
|
6168
|
+
if (descriptor === undefined)
|
|
6169
|
+
continue;
|
|
6170
|
+
output[index < keyCount ? keys[index] : ownKeys[index - keyCount]] =
|
|
6171
|
+
descriptor.value;
|
|
4444
6172
|
}
|
|
4445
|
-
|
|
4446
|
-
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
4447
|
-
if (typeof key === "symbol")
|
|
4448
|
-
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
4449
|
-
if (propertyError.type === "ObjectExcessProperty")
|
|
4450
|
-
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
4451
|
-
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
6173
|
+
return output;
|
|
4452
6174
|
};
|
|
4453
|
-
|
|
6175
|
+
// An identity Object builds no Output, so its validation returns an Err or
|
|
6176
|
+
// `undefined`.
|
|
6177
|
+
const check = isIdentity
|
|
6178
|
+
? (value, options) => {
|
|
6179
|
+
const result = validate(value, options, false);
|
|
6180
|
+
return result?.ok === false ? result.error : undefined;
|
|
6181
|
+
}
|
|
6182
|
+
: undefined;
|
|
6183
|
+
const fromUnknown = (value, options = firstValidationOptions) => validate(value, options, false) ??
|
|
6184
|
+
ok(value);
|
|
6185
|
+
const validateOutput = isIdentity
|
|
6186
|
+
? fromUnknown
|
|
6187
|
+
: (value, options = firstValidationOptions) => validate(value, options, true) ??
|
|
6188
|
+
ok(value);
|
|
4454
6189
|
let hasNonRootType = false;
|
|
4455
6190
|
let canSkipTo = recordType === undefined || recordType.value[encoderSymbol] === identity;
|
|
4456
|
-
for (
|
|
4457
|
-
const
|
|
4458
|
-
const type = objectPropertyToType(property);
|
|
6191
|
+
for (let index = 0; index < keyCount; index++) {
|
|
6192
|
+
const type = propertyTypes[index];
|
|
4459
6193
|
if (type[encoderSymbol] !== identity)
|
|
4460
6194
|
canSkipTo = false;
|
|
4461
|
-
|
|
4462
|
-
if (rootType !== type)
|
|
6195
|
+
if (type.parent != null)
|
|
4463
6196
|
hasNonRootType = true;
|
|
4464
|
-
rootProps[key] = isOptionalProperty(property)
|
|
4465
|
-
? optional(rootType)
|
|
4466
|
-
: rootType;
|
|
4467
6197
|
}
|
|
4468
|
-
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
6198
|
+
let parent = null;
|
|
6199
|
+
if (hasNonRootType || recordType?.parent) {
|
|
6200
|
+
const rootProps = createMutableRecord();
|
|
6201
|
+
for (let index = 0; index < keyCount; index++) {
|
|
6202
|
+
const rootType = getTerminalRuntimeNode(propertyTypes[index]);
|
|
6203
|
+
rootProps[keys[index]] = optionalFlags[index]
|
|
6204
|
+
? optional(rootType)
|
|
6205
|
+
: rootType;
|
|
6206
|
+
}
|
|
6207
|
+
parent = createObjectType(rootProps, (recordType?.parent ?? recordType), keys);
|
|
4474
6208
|
}
|
|
6209
|
+
let inputFroms;
|
|
4475
6210
|
const recordValueFromInput = recordType
|
|
4476
6211
|
? getTerminalRuntimeNode(recordType.value[fromSymbol])
|
|
4477
6212
|
: undefined;
|
|
4478
6213
|
const fromParent = parent
|
|
4479
6214
|
? (value, options = firstValidationOptions) => {
|
|
6215
|
+
const propertyFroms = (inputFroms ??= propertyTypes.map((type) => getTerminalRuntimeNode(type[fromSymbol])));
|
|
4480
6216
|
let errors;
|
|
4481
6217
|
let output;
|
|
4482
|
-
for (
|
|
6218
|
+
for (let index = 0; index < keyCount; index++) {
|
|
6219
|
+
const key = keys[index];
|
|
4483
6220
|
if (!globalThis.Object.hasOwn(value, key))
|
|
4484
6221
|
continue;
|
|
4485
6222
|
const propertyValue = value[key];
|
|
4486
|
-
const result =
|
|
6223
|
+
const result = propertyFroms[index](propertyValue, options);
|
|
4487
6224
|
if (result.ok) {
|
|
4488
6225
|
if (!globalThis.Object.is(result.value, propertyValue)) {
|
|
4489
6226
|
(output ??= createMutableRecord(value))[key] = result.value;
|
|
@@ -4531,11 +6268,12 @@ const createObjectType = (props, recordType) => {
|
|
|
4531
6268
|
? identity
|
|
4532
6269
|
: (value) => {
|
|
4533
6270
|
let output;
|
|
4534
|
-
for (
|
|
6271
|
+
for (let index = 0; index < keyCount; index++) {
|
|
6272
|
+
const key = keys[index];
|
|
4535
6273
|
if (!globalThis.Object.hasOwn(value, key))
|
|
4536
6274
|
continue;
|
|
4537
6275
|
const propertyValue = value[key];
|
|
4538
|
-
const encoded =
|
|
6276
|
+
const encoded = propertyTypes[index][encoderSymbol](propertyValue);
|
|
4539
6277
|
if (!globalThis.Object.is(encoded, propertyValue)) {
|
|
4540
6278
|
(output ??= createMutableRecord(value))[key] = encoded;
|
|
4541
6279
|
}
|
|
@@ -4553,57 +6291,139 @@ const createObjectType = (props, recordType) => {
|
|
|
4553
6291
|
}
|
|
4554
6292
|
return output ?? value;
|
|
4555
6293
|
};
|
|
4556
|
-
const
|
|
4557
|
-
|
|
4558
|
-
|
|
4559
|
-
|
|
4560
|
-
return false;
|
|
4561
|
-
for (const key of keys) {
|
|
4562
|
-
const property = runtimeProps[key];
|
|
4563
|
-
const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, key);
|
|
6294
|
+
const isDeclared = (value) => {
|
|
6295
|
+
const isProperties = (isOutputs ??= propertyTypes.map(getIs));
|
|
6296
|
+
for (let index = 0; index < keyCount; index++) {
|
|
6297
|
+
const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, keys[index]);
|
|
4564
6298
|
if (descriptor === undefined) {
|
|
4565
|
-
if (!
|
|
6299
|
+
if (!optionalFlags[index])
|
|
4566
6300
|
return false;
|
|
4567
6301
|
continue;
|
|
4568
6302
|
}
|
|
4569
6303
|
if (!("value" in descriptor) ||
|
|
4570
6304
|
!descriptor.enumerable ||
|
|
4571
|
-
!
|
|
6305
|
+
!isProperties[index](descriptor.value)) {
|
|
4572
6306
|
return false;
|
|
4573
6307
|
}
|
|
4574
6308
|
}
|
|
4575
|
-
|
|
4576
|
-
|
|
4577
|
-
|
|
4578
|
-
|
|
4579
|
-
|
|
4580
|
-
|
|
6309
|
+
return true;
|
|
6310
|
+
};
|
|
6311
|
+
// Separate predicates keep each small; one shared by both cases was up to
|
|
6312
|
+
// 5% slower in either.
|
|
6313
|
+
const is = recordType === undefined
|
|
6314
|
+
? (value) => {
|
|
6315
|
+
if (value === null || typeof value !== "object")
|
|
4581
6316
|
return false;
|
|
4582
|
-
|
|
4583
|
-
if (descriptor === undefined ||
|
|
4584
|
-
!("value" in descriptor) ||
|
|
4585
|
-
!descriptor.enumerable) {
|
|
6317
|
+
if (!isPlainObject(value) || !isDeclared(value))
|
|
4586
6318
|
return false;
|
|
6319
|
+
// Checking names has no effects without a Record, so symbols, which
|
|
6320
|
+
// fail, are counted after them.
|
|
6321
|
+
const names = globalThis.Object.getOwnPropertyNames(value);
|
|
6322
|
+
for (let position = 0; position < names.length; position++) {
|
|
6323
|
+
const key = names[position];
|
|
6324
|
+
if (!(position < keyCount && key === keys[position]) &&
|
|
6325
|
+
!globalThis.Object.hasOwn(runtimeProps, key)) {
|
|
6326
|
+
return false;
|
|
6327
|
+
}
|
|
4587
6328
|
}
|
|
4588
|
-
|
|
6329
|
+
return globalThis.Object.getOwnPropertySymbols(value).length === 0;
|
|
6330
|
+
}
|
|
6331
|
+
: (value) => {
|
|
6332
|
+
if (value === null || typeof value !== "object")
|
|
6333
|
+
return false;
|
|
6334
|
+
if (!isPlainObject(value) || !isDeclared(value))
|
|
4589
6335
|
return false;
|
|
6336
|
+
const hasSymbols = globalThis.Object.getOwnPropertySymbols(value).length !== 0;
|
|
6337
|
+
const names = hasSymbols
|
|
6338
|
+
? getNamesBeforeSymbol(value)
|
|
6339
|
+
: globalThis.Object.getOwnPropertyNames(value);
|
|
6340
|
+
for (let position = 0; position < names.length; position++) {
|
|
6341
|
+
const key = names[position];
|
|
6342
|
+
if ((position < keyCount && key === keys[position]) ||
|
|
6343
|
+
globalThis.Object.hasOwn(runtimeProps, key)) {
|
|
6344
|
+
continue;
|
|
6345
|
+
}
|
|
6346
|
+
const descriptor = globalThis.Object.getOwnPropertyDescriptor(value, key);
|
|
6347
|
+
if (descriptor === undefined ||
|
|
6348
|
+
!("value" in descriptor) ||
|
|
6349
|
+
!descriptor.enumerable) {
|
|
6350
|
+
return false;
|
|
6351
|
+
}
|
|
6352
|
+
if (!recordType.value.is(descriptor.value))
|
|
6353
|
+
return false;
|
|
6354
|
+
}
|
|
6355
|
+
return !hasSymbols;
|
|
6356
|
+
};
|
|
6357
|
+
const getTypeIssues = createObjectRuntimeTypeIssues(runtimeProps, recordType);
|
|
6358
|
+
return createTypeNode("Object", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, {
|
|
6359
|
+
additionalProperties: recordType
|
|
6360
|
+
? {
|
|
6361
|
+
props: runtimeProps,
|
|
6362
|
+
record: recordType,
|
|
6363
|
+
}
|
|
6364
|
+
: { props: runtimeProps },
|
|
6365
|
+
check,
|
|
6366
|
+
});
|
|
6367
|
+
};
|
|
6368
|
+
const createDefaultObjectType = (props, recordType) => {
|
|
6369
|
+
const inputProps = createMutableRecord();
|
|
6370
|
+
const outputProps = createMutableRecord();
|
|
6371
|
+
const defaults = new Map();
|
|
6372
|
+
for (const [key, property] of globalThis.Object.entries(props)) {
|
|
6373
|
+
if (isDefaultProperty(property)) {
|
|
6374
|
+
const defaultProperty = property;
|
|
6375
|
+
inputProps[key] = createOptionalProperty(defaultProperty.type);
|
|
6376
|
+
outputProps[key] = defaultProperty[defaultPropertySymbol].output;
|
|
6377
|
+
defaults.set(key, defaultProperty[defaultPropertySymbol]);
|
|
4590
6378
|
}
|
|
4591
|
-
|
|
6379
|
+
else {
|
|
6380
|
+
inputProps[key] = property;
|
|
6381
|
+
const type = objectPropertyToType(property);
|
|
6382
|
+
const output = createRootType(type.name, type[outputValidationSymbol], type.formatError, { getTypeIssues: type[getRuntimeTypeIssuesSymbol] });
|
|
6383
|
+
outputProps[key] =
|
|
6384
|
+
optionalPropertySymbol in property
|
|
6385
|
+
? createOptionalProperty(output)
|
|
6386
|
+
: output;
|
|
6387
|
+
}
|
|
6388
|
+
}
|
|
6389
|
+
const source = createObjectType(inputProps, recordType);
|
|
6390
|
+
// The object parent validates only its input structure and root value Types.
|
|
6391
|
+
const parent = getTerminalRuntimeNode(source);
|
|
6392
|
+
const output = createObjectType(outputProps, recordType);
|
|
6393
|
+
const finish = (value) => {
|
|
6394
|
+
const result = createMutableRecord(value);
|
|
6395
|
+
for (const [key, operations] of defaults) {
|
|
6396
|
+
result[key] = operations.decode(globalThis.Object.hasOwn(result, key)
|
|
6397
|
+
? result[key]
|
|
6398
|
+
: missingDefaultValue);
|
|
6399
|
+
}
|
|
6400
|
+
return ok(result);
|
|
4592
6401
|
};
|
|
4593
|
-
const
|
|
4594
|
-
|
|
4595
|
-
|
|
4596
|
-
|
|
4597
|
-
|
|
6402
|
+
const fromInput = getTerminalRuntimeNode(source[fromSymbol]);
|
|
6403
|
+
const fromParent = (value, options) => flatMapResult(fromInput(value, options), finish);
|
|
6404
|
+
const to = (value) => {
|
|
6405
|
+
const result = createMutableRecord(value);
|
|
6406
|
+
for (const [key, operations] of defaults) {
|
|
6407
|
+
const original = operations.encode(result[key]);
|
|
6408
|
+
if (original === missingDefaultValue)
|
|
6409
|
+
delete result[key];
|
|
6410
|
+
else
|
|
6411
|
+
result[key] = original;
|
|
4598
6412
|
}
|
|
4599
|
-
|
|
6413
|
+
return source[encoderSymbol](result);
|
|
6414
|
+
};
|
|
6415
|
+
return createTypeNode("Object", parent, (value, options) => flatMapResult(source.fromUnknown(value, options), finish), output.is, output[outputValidationSymbol], createFromOperation(fromParent), to, createObjectRuntimeTypeIssues(props, recordType), {
|
|
6416
|
+
additionalProperties: recordType
|
|
6417
|
+
? { props, record: recordType }
|
|
6418
|
+
: { props },
|
|
6419
|
+
});
|
|
4600
6420
|
};
|
|
4601
6421
|
// Read descriptors instead of spreading so accessors are not invoked,
|
|
4602
6422
|
// non-enumerable declarations are retained, and later mutations are isolated.
|
|
4603
6423
|
const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
|
|
4604
6424
|
const errorMessage = "Object schema properties must be own string-keyed data properties.";
|
|
4605
6425
|
assert(isPlainObject(props), errorMessage);
|
|
4606
|
-
for (const key of
|
|
6426
|
+
for (const key of getOwnKeys(props)) {
|
|
4607
6427
|
const descriptor = globalThis.Object.getOwnPropertyDescriptor(props, key);
|
|
4608
6428
|
assert(typeof key === "string" &&
|
|
4609
6429
|
descriptor !== undefined &&
|
|
@@ -4613,7 +6433,12 @@ const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
|
|
|
4613
6433
|
return runtimeProps;
|
|
4614
6434
|
};
|
|
4615
6435
|
const isOptionalProperty = (property) => optionalPropertySymbol in property;
|
|
4616
|
-
const
|
|
6436
|
+
const isDefaultProperty = (property) => defaultPropertySymbol in property;
|
|
6437
|
+
const objectPropertyToType = (property) => isDefaultProperty(property)
|
|
6438
|
+
? property[defaultPropertySymbol].output
|
|
6439
|
+
: isOptionalProperty(property)
|
|
6440
|
+
? property.type
|
|
6441
|
+
: property;
|
|
4617
6442
|
const createRecordPropertyError = (issue) => ({
|
|
4618
6443
|
type: "Record",
|
|
4619
6444
|
reason: { kind: "Entries", issues: [issue] },
|
|
@@ -4622,7 +6447,8 @@ const createRecordPropertyError = (issue) => ({
|
|
|
4622
6447
|
* Object {@link Type} with every property optional.
|
|
4623
6448
|
*
|
|
4624
6449
|
* No property is required, but every present property must still satisfy its
|
|
4625
|
-
* Type.
|
|
6450
|
+
* Type. For {@link withDefault} properties, this disables the missing-property
|
|
6451
|
+
* default; defaults for present `null` or `undefined` values still apply.
|
|
4626
6452
|
*
|
|
4627
6453
|
* ### Example
|
|
4628
6454
|
*
|
|
@@ -4650,9 +6476,11 @@ export const partial = (props, ..._validation) => {
|
|
|
4650
6476
|
const partialProps = createMutableRecord();
|
|
4651
6477
|
for (const key of globalThis.Object.keys(source)) {
|
|
4652
6478
|
const property = source[key];
|
|
4653
|
-
partialProps[key] =
|
|
4654
|
-
? property
|
|
4655
|
-
:
|
|
6479
|
+
partialProps[key] = isDefaultProperty(property)
|
|
6480
|
+
? property[defaultPropertySymbol].partial(property.type)
|
|
6481
|
+
: isOptionalProperty(property)
|
|
6482
|
+
? property
|
|
6483
|
+
: createOptionalProperty(property);
|
|
4656
6484
|
}
|
|
4657
6485
|
return createObjectType(partialProps);
|
|
4658
6486
|
};
|
|
@@ -4693,7 +6521,7 @@ export const nullableToOptional = (props, ..._validation) => {
|
|
|
4693
6521
|
const optionalProps = createMutableRecord();
|
|
4694
6522
|
for (const key of globalThis.Object.keys(source)) {
|
|
4695
6523
|
const property = source[key];
|
|
4696
|
-
if (isOptionalProperty(property)) {
|
|
6524
|
+
if (isOptionalProperty(property) || isDefaultProperty(property)) {
|
|
4697
6525
|
optionalProps[key] = property;
|
|
4698
6526
|
continue;
|
|
4699
6527
|
}
|
|
@@ -4732,6 +6560,101 @@ export const omit = (objectType, ...keys) => {
|
|
|
4732
6560
|
}
|
|
4733
6561
|
return createObjectType(props, runtimeObjectType.record);
|
|
4734
6562
|
};
|
|
6563
|
+
/**
|
|
6564
|
+
* Renames a strict {@link object} Type's encoded keys using a string codec.
|
|
6565
|
+
*
|
|
6566
|
+
* The object declaration uses semantic keys. Each key must be a valid Output of
|
|
6567
|
+
* the key Type. Its canonical encoding becomes the external property name.
|
|
6568
|
+
* Decoding accepts those exact names; aliases and unknown properties are
|
|
6569
|
+
* rejected. Values, optionality, and semantic Output stay with the object Type.
|
|
6570
|
+
* Only the outer keys change. Nested field codecs keep their own behavior.
|
|
6571
|
+
*
|
|
6572
|
+
* Construction rejects invalid schema keys, duplicate encodings, and key codecs
|
|
6573
|
+
* that do not decode their encodings back to the declared keys. These are
|
|
6574
|
+
* schema mistakes; invalid external values return normal Type errors. Error
|
|
6575
|
+
* paths use the external names, including missing required properties.
|
|
6576
|
+
*
|
|
6577
|
+
* ### Example
|
|
6578
|
+
*
|
|
6579
|
+
* ```ts
|
|
6580
|
+
* import {
|
|
6581
|
+
* assertEqual,
|
|
6582
|
+
* assertErr,
|
|
6583
|
+
* assertOk,
|
|
6584
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
6585
|
+
* object,
|
|
6586
|
+
* objectKeys,
|
|
6587
|
+
* optional,
|
|
6588
|
+
* PortFromString,
|
|
6589
|
+
* prefixed,
|
|
6590
|
+
* } from "@evolu/common";
|
|
6591
|
+
*
|
|
6592
|
+
* const Key = prefixed("APP_")(
|
|
6593
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
6594
|
+
* );
|
|
6595
|
+
* const Settings = objectKeys(Key)(
|
|
6596
|
+
* object({ port: optional(PortFromString) }),
|
|
6597
|
+
* );
|
|
6598
|
+
*
|
|
6599
|
+
* const result = Settings.fromUnknown({ APP_PORT: "04000" });
|
|
6600
|
+
* assertOk(result, { port: 4000 });
|
|
6601
|
+
*
|
|
6602
|
+
* assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
|
|
6603
|
+
*
|
|
6604
|
+
* assertErr(Settings.fromUnknown({ APP_POTR: "4000" }));
|
|
6605
|
+
* ```
|
|
6606
|
+
*
|
|
6607
|
+
* @group Objects
|
|
6608
|
+
*/
|
|
6609
|
+
export const objectKeys = (key) => (type) => {
|
|
6610
|
+
const runtimeKey = key;
|
|
6611
|
+
const runtimeOutput = type;
|
|
6612
|
+
const props = createMutableRecord();
|
|
6613
|
+
const outputKeyByInputKey = new Map();
|
|
6614
|
+
const inputKeyByOutputKey = new Map();
|
|
6615
|
+
for (const outputKey of globalThis.Object.keys(type.props)) {
|
|
6616
|
+
assert(runtimeKey.is(outputKey), `Invalid object schema key ${safelyStringifyUnknownValue(outputKey)} for ${key.name}.`);
|
|
6617
|
+
const inputKey = runtimeKey.to(outputKey);
|
|
6618
|
+
assert(!outputKeyByInputKey.has(inputKey), `Duplicate encoded object key ${safelyStringifyUnknownValue(inputKey)}.`);
|
|
6619
|
+
const decoded = runtimeKey.fromUnknown(inputKey, firstValidationOptions);
|
|
6620
|
+
assert(decoded.ok && decoded.value === outputKey, "An object key codec must decode its encoding to the declared key.");
|
|
6621
|
+
props[inputKey] = type.props[outputKey];
|
|
6622
|
+
outputKeyByInputKey.set(inputKey, outputKey);
|
|
6623
|
+
inputKeyByOutputKey.set(outputKey, inputKey);
|
|
6624
|
+
}
|
|
6625
|
+
const input = createObjectType(props);
|
|
6626
|
+
const fromUnknown = (value, options = firstValidationOptions) => {
|
|
6627
|
+
const result = input.fromUnknown(value, options);
|
|
6628
|
+
if (!result.ok)
|
|
6629
|
+
return err({ type: "ObjectKeys", error: result.error });
|
|
6630
|
+
const output = createMutableRecord();
|
|
6631
|
+
for (const [inputKey, value] of globalThis.Object.entries(result.value)) {
|
|
6632
|
+
output[outputKeyByInputKey.get(inputKey)] = value;
|
|
6633
|
+
}
|
|
6634
|
+
return ok(output);
|
|
6635
|
+
};
|
|
6636
|
+
const to = (value) => {
|
|
6637
|
+
const encoded = runtimeOutput[encoderSymbol](value);
|
|
6638
|
+
const input = createMutableRecord();
|
|
6639
|
+
for (const [key, value] of globalThis.Object.entries(encoded)) {
|
|
6640
|
+
input[inputKeyByOutputKey.get(key)] = value;
|
|
6641
|
+
}
|
|
6642
|
+
return input;
|
|
6643
|
+
};
|
|
6644
|
+
const validateOutput = (value, options) => {
|
|
6645
|
+
const result = runtimeOutput[outputValidationSymbol](value, options);
|
|
6646
|
+
return result.ok
|
|
6647
|
+
? result
|
|
6648
|
+
: err({ type: "ObjectKeys", outputError: result.error });
|
|
6649
|
+
};
|
|
6650
|
+
const getTypeIssues = (error, mode, path) => {
|
|
6651
|
+
if ("outputError" in error) {
|
|
6652
|
+
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode, path);
|
|
6653
|
+
}
|
|
6654
|
+
return input[getRuntimeTypeIssuesSymbol](error.error, mode, path);
|
|
6655
|
+
};
|
|
6656
|
+
return createTypeNode("ObjectKeys", Unknown, fromUnknown, runtimeOutput.is, validateOutput, createFromOperation(fromUnknown), to, getTypeIssues, { additionalProperties: { key, output: type } });
|
|
6657
|
+
};
|
|
4735
6658
|
export function result(okType, errorType) {
|
|
4736
6659
|
return discriminatedUnion("ok", createObjectType({
|
|
4737
6660
|
ok: literal(true),
|
|
@@ -4788,21 +6711,28 @@ export function discriminatedUnion(...keyOrMembers) {
|
|
|
4788
6711
|
membersByDiscriminator.set(value, member);
|
|
4789
6712
|
}
|
|
4790
6713
|
const routeUnknown = (value) => {
|
|
4791
|
-
|
|
4792
|
-
? err({ type: "Object", reason: { kind: "NotObject", value } })
|
|
4793
|
-
: !isPlainObject(value)
|
|
4794
|
-
? err({
|
|
4795
|
-
type: "Object",
|
|
4796
|
-
reason: { kind: "UnexpectedPrototype", value },
|
|
4797
|
-
})
|
|
4798
|
-
: ok(value);
|
|
4799
|
-
if (!objectResult.ok) {
|
|
6714
|
+
if (value === null || typeof value !== "object") {
|
|
4800
6715
|
return err({
|
|
4801
6716
|
type: "DiscriminatedUnion",
|
|
4802
|
-
reason: {
|
|
6717
|
+
reason: {
|
|
6718
|
+
kind: "Object",
|
|
6719
|
+
error: { type: "Object", reason: { kind: "NotObject", value } },
|
|
6720
|
+
},
|
|
6721
|
+
});
|
|
6722
|
+
}
|
|
6723
|
+
if (!isPlainObject(value)) {
|
|
6724
|
+
return err({
|
|
6725
|
+
type: "DiscriminatedUnion",
|
|
6726
|
+
reason: {
|
|
6727
|
+
kind: "Object",
|
|
6728
|
+
error: {
|
|
6729
|
+
type: "Object",
|
|
6730
|
+
reason: { kind: "UnexpectedPrototype", value },
|
|
6731
|
+
},
|
|
6732
|
+
},
|
|
4803
6733
|
});
|
|
4804
6734
|
}
|
|
4805
|
-
const input =
|
|
6735
|
+
const input = value;
|
|
4806
6736
|
const descriptor = globalThis.Object.getOwnPropertyDescriptor(input, key);
|
|
4807
6737
|
let discriminator;
|
|
4808
6738
|
if (descriptor === undefined) {
|
|
@@ -4843,16 +6773,15 @@ export function discriminatedUnion(...keyOrMembers) {
|
|
|
4843
6773
|
}
|
|
4844
6774
|
return ok(member);
|
|
4845
6775
|
};
|
|
4846
|
-
const
|
|
4847
|
-
|
|
4848
|
-
:
|
|
4849
|
-
|
|
4850
|
-
|
|
4851
|
-
|
|
4852
|
-
|
|
4853
|
-
|
|
4854
|
-
|
|
4855
|
-
});
|
|
6776
|
+
const createMemberError = (member, error) => ({
|
|
6777
|
+
type: "DiscriminatedUnion",
|
|
6778
|
+
reason: {
|
|
6779
|
+
kind: "Member",
|
|
6780
|
+
discriminator: member.props[key].expected,
|
|
6781
|
+
error,
|
|
6782
|
+
},
|
|
6783
|
+
});
|
|
6784
|
+
const wrapMemberResult = (member, result) => result.ok ? result : err(createMemberError(member, result.error));
|
|
4856
6785
|
const validateUnknown = (value, options = firstValidationOptions, useParent, exactOutput) => {
|
|
4857
6786
|
const routeResult = routeUnknown(value);
|
|
4858
6787
|
if (!routeResult.ok)
|
|
@@ -4891,24 +6820,34 @@ export function discriminatedUnion(...keyOrMembers) {
|
|
|
4891
6820
|
}
|
|
4892
6821
|
};
|
|
4893
6822
|
const defaultFormatter = formatError;
|
|
4894
|
-
const getTypeIssues = (error, mode) => {
|
|
6823
|
+
const getTypeIssues = (error, mode, path) => {
|
|
4895
6824
|
const discriminatedUnionError = error;
|
|
4896
6825
|
if (discriminatedUnionError.reason.kind === "Member") {
|
|
4897
6826
|
const reason = discriminatedUnionError.reason;
|
|
4898
6827
|
const member = membersByDiscriminator.get(reason.discriminator);
|
|
4899
6828
|
assertNonNullable(member);
|
|
4900
|
-
return member[getRuntimeTypeIssuesSymbol](reason.error, mode);
|
|
6829
|
+
return member[getRuntimeTypeIssuesSymbol](reason.error, mode, path);
|
|
4901
6830
|
}
|
|
4902
6831
|
if (discriminatedUnionError.reason.kind === "PropertyAccess" ||
|
|
4903
6832
|
discriminatedUnionError.reason.kind === "Discriminator") {
|
|
4904
|
-
return singleRuntimeTypeIssue("DiscriminatedUnion", discriminatedUnionError, defaultFormatter,
|
|
6833
|
+
return singleRuntimeTypeIssue("DiscriminatedUnion", discriminatedUnionError, defaultFormatter, appendIssuePath(path, discriminatedUnionError.reason.key));
|
|
4905
6834
|
}
|
|
4906
|
-
return singleRuntimeTypeIssue("DiscriminatedUnion", error, defaultFormatter);
|
|
6835
|
+
return singleRuntimeTypeIssue("DiscriminatedUnion", error, defaultFormatter, path);
|
|
4907
6836
|
};
|
|
4908
6837
|
const parentFromUnknown = (value, options) => validateUnknown(value, options, true, false);
|
|
4909
6838
|
const parentValidateOutput = (value, options) => validateUnknown(value, options, true, true);
|
|
4910
6839
|
const parentTo = (value) => route(value).parent[encoderSymbol](value);
|
|
4911
|
-
const parent = createTypeNode("DiscriminatedUnion", null, parentFromUnknown, (value) => getOutputMember(value)?.parent.is(value) ?? false, parentValidateOutput, ok, parentTo, getTypeIssues, { key, members });
|
|
6840
|
+
const parent = createTypeNode("DiscriminatedUnion", null, parentFromUnknown, (value) => getOutputMember(value)?.parent.is(value) ?? false, parentValidateOutput, ok, parentTo, getTypeIssues, { additionalProperties: { key, members } });
|
|
6841
|
+
// Identity members return the input itself, and so does routing.
|
|
6842
|
+
const check = getRuntimeChecks(members) &&
|
|
6843
|
+
((value, options) => {
|
|
6844
|
+
const routeResult = routeUnknown(value);
|
|
6845
|
+
if (!routeResult.ok)
|
|
6846
|
+
return routeResult.error;
|
|
6847
|
+
const member = routeResult.value;
|
|
6848
|
+
const error = member[checkSymbol](value, options);
|
|
6849
|
+
return error === undefined ? undefined : createMemberError(member, error);
|
|
6850
|
+
});
|
|
4912
6851
|
const fromUnknown = (value, options) => validateUnknown(value, options, false, false);
|
|
4913
6852
|
const validateOutput = (value, options) => validateUnknown(value, options, false, true);
|
|
4914
6853
|
const fromParent = (value, options = firstValidationOptions) => {
|
|
@@ -4918,7 +6857,7 @@ export function discriminatedUnion(...keyOrMembers) {
|
|
|
4918
6857
|
};
|
|
4919
6858
|
const from = createFromOperation(fromParent);
|
|
4920
6859
|
const to = (value) => route(value)[encoderSymbol](value);
|
|
4921
|
-
return createTypeNode("DiscriminatedUnion", parent, fromUnknown, (value) => getOutputMember(value)?.is(value) ?? false, validateOutput, from, to, getTypeIssues, { key, members });
|
|
6860
|
+
return createTypeNode("DiscriminatedUnion", parent, fromUnknown, (value) => getOutputMember(value)?.is(value) ?? false, validateOutput, from, to, getTypeIssues, { additionalProperties: { key, members }, check });
|
|
4922
6861
|
}
|
|
4923
6862
|
export function lazy(getType) {
|
|
4924
6863
|
let resolution = { state: "unresolved" };
|
|
@@ -4960,11 +6899,11 @@ export function lazy(getType) {
|
|
|
4960
6899
|
};
|
|
4961
6900
|
const parentFromUnknown = (value, options = firstValidationOptions) => resolve().root.fromUnknown(value, options);
|
|
4962
6901
|
const parentTo = (value) => resolve().root[encoderSymbol](value);
|
|
4963
|
-
const parent = createTypeNode("Lazy", null, parentFromUnknown, (value) => resolve().root.is(value), (value, options) => resolve().root[outputValidationSymbol](value, options), ok, parentTo, (error, mode) => resolve().root[getRuntimeTypeIssuesSymbol](error, mode));
|
|
6902
|
+
const parent = createTypeNode("Lazy", null, parentFromUnknown, (value) => resolve().root.is(value), (value, options) => resolve().root[outputValidationSymbol](value, options), ok, parentTo, (error, mode, path) => resolve().root[getRuntimeTypeIssuesSymbol](error, mode, path));
|
|
4964
6903
|
const fromUnknown = (value, options = firstValidationOptions) => resolve().target.fromUnknown(value, options);
|
|
4965
6904
|
const fromParent = (value, options = firstValidationOptions) => resolve().targetFromInput(value, options);
|
|
4966
6905
|
const from = createFromOperation(fromParent);
|
|
4967
|
-
const type = createTypeNode("Lazy", parent, fromUnknown, (value) => resolve().target.is(value), (value, options) => resolve().target[outputValidationSymbol](value, options), from, (value) => resolve().target[encoderSymbol](value), (error, mode) => resolve().target[getRuntimeTypeIssuesSymbol](error, mode));
|
|
6906
|
+
const type = createTypeNode("Lazy", parent, fromUnknown, (value) => resolve().target.is(value), (value, options) => resolve().target[outputValidationSymbol](value, options), from, (value) => resolve().target[encoderSymbol](value), (error, mode, path) => resolve().target[getRuntimeTypeIssuesSymbol](error, mode, path));
|
|
4968
6907
|
lazyTypeNodes.add(parent);
|
|
4969
6908
|
lazyTypeNodes.add(type);
|
|
4970
6909
|
return type;
|
|
@@ -5035,10 +6974,17 @@ const validateData = (value, options = firstValidationOptions) => {
|
|
|
5035
6974
|
const kind = getObjectKind(value);
|
|
5036
6975
|
if (kind === "Array") {
|
|
5037
6976
|
const array = value;
|
|
5038
|
-
|
|
6977
|
+
const keys = Reflect.ownKeys(array);
|
|
6978
|
+
for (let position = 0; position < keys.length; position++) {
|
|
6979
|
+
const key = keys[position];
|
|
5039
6980
|
if (key === "length")
|
|
5040
6981
|
continue;
|
|
5041
6982
|
if (typeof key === "string") {
|
|
6983
|
+
// Ordinary arrays list their indexes first, in ascending order.
|
|
6984
|
+
// Comparing the key first reads `length` only for an index key.
|
|
6985
|
+
if (key === globalThis.String(position) && position < array.length) {
|
|
6986
|
+
continue;
|
|
6987
|
+
}
|
|
5042
6988
|
const index = globalThis.Number(key) >>> 0;
|
|
5043
6989
|
if (index < array.length && globalThis.String(index) === key) {
|
|
5044
6990
|
continue;
|
|
@@ -5073,7 +7019,7 @@ const validateData = (value, options = firstValidationOptions) => {
|
|
|
5073
7019
|
}
|
|
5074
7020
|
}
|
|
5075
7021
|
else if (kind === "Object") {
|
|
5076
|
-
for (const key of
|
|
7022
|
+
for (const key of getOwnKeys(value)) {
|
|
5077
7023
|
const childPath = dataChildPath(path, key);
|
|
5078
7024
|
if (typeof key === "symbol") {
|
|
5079
7025
|
if (addIssue({
|
|
@@ -5187,7 +7133,7 @@ const validateData = (value, options = firstValidationOptions) => {
|
|
|
5187
7133
|
},
|
|
5188
7134
|
});
|
|
5189
7135
|
};
|
|
5190
|
-
const getDataRuntimeTypeIssues = (error, mode) => {
|
|
7136
|
+
const getDataRuntimeTypeIssues = (error, mode, path) => {
|
|
5191
7137
|
const dataError = error;
|
|
5192
7138
|
const issues = mode === "first"
|
|
5193
7139
|
? [dataError.reason.issues[0]]
|
|
@@ -5200,7 +7146,8 @@ const getDataRuntimeTypeIssues = (error, mode) => {
|
|
|
5200
7146
|
type: "Data",
|
|
5201
7147
|
reason: { kind: "Issues", issues: [issue] },
|
|
5202
7148
|
},
|
|
5203
|
-
path
|
|
7149
|
+
// A root issue shares the path array of its error.
|
|
7150
|
+
path: path.length === 0 ? issue.path : [...path, ...issue.path],
|
|
5204
7151
|
formatError: ((error) => {
|
|
5205
7152
|
const issue = error.reason.issues[0];
|
|
5206
7153
|
switch (issue.kind) {
|
|
@@ -5244,7 +7191,12 @@ const getDataRuntimeTypeIssues = (error, mode) => {
|
|
|
5244
7191
|
*
|
|
5245
7192
|
* @group Base
|
|
5246
7193
|
*/
|
|
5247
|
-
export const Data = /*#__PURE__*/ createTypeNode("Data", null, validateData, (value) => validateData(value).ok, validateData, ok, identity, getDataRuntimeTypeIssues
|
|
7194
|
+
export const Data = /*#__PURE__*/ createTypeNode("Data", null, validateData, (value) => validateData(value).ok, validateData, ok, identity, getDataRuntimeTypeIssues, {
|
|
7195
|
+
check: (value, options) => {
|
|
7196
|
+
const result = validateData(value, options);
|
|
7197
|
+
return result.ok ? undefined : result.error;
|
|
7198
|
+
},
|
|
7199
|
+
});
|
|
5248
7200
|
const emptyJsonValuePath =
|
|
5249
7201
|
/*#__PURE__*/ globalThis.Object.freeze([]);
|
|
5250
7202
|
const jsonValuePathToArray = (path) => {
|
|
@@ -5327,10 +7279,17 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
|
|
|
5327
7279
|
}
|
|
5328
7280
|
const children = [];
|
|
5329
7281
|
if (isArray) {
|
|
5330
|
-
|
|
7282
|
+
const keys = Reflect.ownKeys(value);
|
|
7283
|
+
for (let position = 0; position < keys.length; position++) {
|
|
7284
|
+
const key = keys[position];
|
|
5331
7285
|
if (key === "length")
|
|
5332
7286
|
continue;
|
|
5333
7287
|
if (typeof key === "string") {
|
|
7288
|
+
// Ordinary arrays list their indexes first, in ascending order.
|
|
7289
|
+
// Comparing the key first reads `length` only for an index key.
|
|
7290
|
+
if (key === globalThis.String(position) && position < value.length) {
|
|
7291
|
+
continue;
|
|
7292
|
+
}
|
|
5334
7293
|
const index = globalThis.Number(key) >>> 0;
|
|
5335
7294
|
if (index < value.length && globalThis.String(index) === key) {
|
|
5336
7295
|
continue;
|
|
@@ -5374,7 +7333,7 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
|
|
|
5374
7333
|
}
|
|
5375
7334
|
}
|
|
5376
7335
|
else {
|
|
5377
|
-
for (const key of
|
|
7336
|
+
for (const key of getOwnKeys(value)) {
|
|
5378
7337
|
const childPath = jsonValueChildPath(path, key);
|
|
5379
7338
|
if (typeof key === "symbol") {
|
|
5380
7339
|
if (addIssue({
|
|
@@ -5429,7 +7388,7 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
|
|
|
5429
7388
|
},
|
|
5430
7389
|
});
|
|
5431
7390
|
};
|
|
5432
|
-
const getJsonValueRuntimeTypeIssues = (error, mode) => {
|
|
7391
|
+
const getJsonValueRuntimeTypeIssues = (error, mode, path) => {
|
|
5433
7392
|
const jsonValueError = error;
|
|
5434
7393
|
const issues = mode === "first"
|
|
5435
7394
|
? [jsonValueError.reason.issues[0]]
|
|
@@ -5442,7 +7401,7 @@ const getJsonValueRuntimeTypeIssues = (error, mode) => {
|
|
|
5442
7401
|
type: "JsonValue",
|
|
5443
7402
|
reason: { kind: "Issues", issues: [issue] },
|
|
5444
7403
|
},
|
|
5445
|
-
path: issue.path,
|
|
7404
|
+
path: path.length === 0 ? issue.path : [...path, ...issue.path],
|
|
5446
7405
|
formatError: ((error) => {
|
|
5447
7406
|
const issue = error.reason.issues[0];
|
|
5448
7407
|
switch (issue.kind) {
|
|
@@ -5559,7 +7518,12 @@ const stringifyJsonValue = (value) => {
|
|
|
5559
7518
|
* @group JSON
|
|
5560
7519
|
*/
|
|
5561
7520
|
export const JsonValue =
|
|
5562
|
-
/*#__PURE__*/ createTypeNode("JsonValue", null, validateJsonValue, (value) => validateJsonValue(value).ok, validateJsonValue, ok, identity, getJsonValueRuntimeTypeIssues
|
|
7521
|
+
/*#__PURE__*/ createTypeNode("JsonValue", null, validateJsonValue, (value) => validateJsonValue(value).ok, validateJsonValue, ok, identity, getJsonValueRuntimeTypeIssues, {
|
|
7522
|
+
check: (value, options) => {
|
|
7523
|
+
const result = validateJsonValue(value, options);
|
|
7524
|
+
return result.ok ? undefined : result.error;
|
|
7525
|
+
},
|
|
7526
|
+
});
|
|
5563
7527
|
/**
|
|
5564
7528
|
* Exact top-level JSON array {@link Type}.
|
|
5565
7529
|
*
|
|
@@ -5695,9 +7659,9 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
|
|
|
5695
7659
|
*/
|
|
5696
7660
|
export const json = (type, name, ..._validation) => {
|
|
5697
7661
|
const runtimeType = type;
|
|
5698
|
-
const getTypeIssues = (error, mode) => error.type === name
|
|
5699
|
-
? runtimeType[getRuntimeTypeIssuesSymbol](error.error, mode)
|
|
5700
|
-
: Json[getRuntimeTypeIssuesSymbol](error, mode);
|
|
7662
|
+
const getTypeIssues = (error, mode, path) => error.type === name
|
|
7663
|
+
? runtimeType[getRuntimeTypeIssuesSymbol](error.error, mode, path)
|
|
7664
|
+
: Json[getRuntimeTypeIssuesSymbol](error, mode, path);
|
|
5701
7665
|
const jsonToTypeOutput = (value, options = firstValidationOptions) => runtimeType.fromUnknown(parseJson(value), options);
|
|
5702
7666
|
const jsonStringToBrandedJson = (value, options) => {
|
|
5703
7667
|
const jsonValueResult = jsonToJsonValueResult(value);
|
|
@@ -5746,7 +7710,7 @@ export const json = (type, name, ..._validation) => {
|
|
|
5746
7710
|
const BrandedJson = {
|
|
5747
7711
|
...typeNode,
|
|
5748
7712
|
from,
|
|
5749
|
-
orThrow:
|
|
7713
|
+
orThrow: createRuntimeOrThrow(fromInput, typeNode.formatError),
|
|
5750
7714
|
orNull: mapRuntimeResult(fromInput, getOrNull),
|
|
5751
7715
|
};
|
|
5752
7716
|
return [
|