@evolu/common 8.9.0 → 8.10.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/Fs.d.ts +302 -0
- package/dist/src/Fs.d.ts.map +1 -0
- package/dist/src/Fs.js +111 -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 +41 -0
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/Time.d.ts +74 -12
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +40 -2
- package/dist/src/Type.d.ts +1451 -79
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1303 -96
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- 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/Protocol.d.ts +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +1 -1
- package/dist/src/local-first/Schema.d.ts +10 -0
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +1 -1
- package/dist/src/local-first/Timestamp.js +1 -1
- package/package.json +1 -1
- package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
- package/src/{Binary.ts → Bytes.ts} +652 -21
- package/src/Config.test.ts +672 -0
- package/src/Config.ts +410 -0
- package/src/Fs.test.ts +105 -0
- package/src/Fs.ts +414 -0
- package/src/Number.test.ts +82 -18
- package/src/Number.ts +76 -8
- package/src/Object.test.ts +115 -1
- package/src/Object.ts +44 -0
- package/src/Sqlite.ts +1 -1
- package/src/Time.test.ts +82 -11
- package/src/Time.ts +141 -16
- package/src/Type.test.ts +3114 -157
- package/src/Type.ts +2553 -196
- package/src/index.ts +3 -1
- 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/Protocol.test.ts +1 -1
- package/src/local-first/Protocol.ts +1 -1
- package/src/local-first/Schema.ts +11 -0
- package/src/local-first/Storage.ts +1 -1
- package/src/local-first/Timestamp.ts +1 -1
- package/dist/src/Binary.d.ts +0 -254
- package/dist/src/Binary.d.ts.map +0 -1
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,12 +454,54 @@ 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
463
|
import { instance, 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 =
|
|
@@ -492,6 +534,23 @@ const createCollectionRuntimeTypeIssues = (name, issuesKind, defaultFormatter, g
|
|
|
492
534
|
});
|
|
493
535
|
};
|
|
494
536
|
const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
|
|
537
|
+
const formatRuntimeTypeIssue = (issue, formatIssue = formatDefaultRuntimeTypeIssue) => {
|
|
538
|
+
const summary = formatIssue(issue);
|
|
539
|
+
if (issue.alternatives === undefined)
|
|
540
|
+
return summary;
|
|
541
|
+
return [
|
|
542
|
+
summary,
|
|
543
|
+
...issue.alternatives.flatMap(({ index, name, issues }) => issues.map((issue) => {
|
|
544
|
+
const path = issue.path
|
|
545
|
+
.map((key) => typeof key === "string"
|
|
546
|
+
? `[${JSON.stringify(key)}]`
|
|
547
|
+
: `[${globalThis.String(key)}]`)
|
|
548
|
+
.join("");
|
|
549
|
+
const message = formatRuntimeTypeIssue(issue, formatIssue).replaceAll("\n", "\n ");
|
|
550
|
+
return `- ${index}: ${name}${path}: ${message}`;
|
|
551
|
+
})),
|
|
552
|
+
].join("\n");
|
|
553
|
+
};
|
|
495
554
|
export function assertType(type, value) {
|
|
496
555
|
if (type === undefined)
|
|
497
556
|
return;
|
|
@@ -530,6 +589,11 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
530
589
|
* that produced them. Different localized Type sets can coexist in separate
|
|
531
590
|
* application or dependency-injection scopes.
|
|
532
591
|
*
|
|
592
|
+
* A Union formatter supplies the summary of the failure. Retained member
|
|
593
|
+
* failures are appended using their own localized formatters, with member
|
|
594
|
+
* indexes, Type names, and paths identifying each alternative. The Union still
|
|
595
|
+
* produces one issue at its enclosing path.
|
|
596
|
+
*
|
|
533
597
|
* Localization is scoped to the selected Types instead of a package-wide
|
|
534
598
|
* translation registry. Static imports give bundlers an explicit dependency
|
|
535
599
|
* graph, so unrelated Types, locales, and formatters can be removed. Bundling
|
|
@@ -670,17 +734,26 @@ const withFormatError = (source, formatIssue, localizedTypeBySource) => {
|
|
|
670
734
|
const parent = source.parent
|
|
671
735
|
? withFormatError(source.parent, formatIssue, localizedTypeBySource)
|
|
672
736
|
: null;
|
|
673
|
-
const
|
|
737
|
+
const localizeIssue = (issue) => ({
|
|
674
738
|
...issue,
|
|
675
739
|
formatError: () => formatIssue(issue),
|
|
676
|
-
|
|
740
|
+
...(issue.alternatives === undefined
|
|
741
|
+
? {}
|
|
742
|
+
: {
|
|
743
|
+
alternatives: issue.alternatives.map((alternative) => ({
|
|
744
|
+
...alternative,
|
|
745
|
+
issues: alternative.issues.map(localizeIssue),
|
|
746
|
+
})),
|
|
747
|
+
}),
|
|
748
|
+
});
|
|
749
|
+
const getTypeIssues = (error, mode) => source[getRuntimeTypeIssuesSymbol](error, mode).map(localizeIssue);
|
|
677
750
|
const derived = createTypeNode(source.name, parent, source.fromUnknown, source.is, source[outputValidationSymbol], source[fromSymbol], source[encoderSymbol].parent ?? source[encoderSymbol], getTypeIssues, undefined, formatIssue);
|
|
678
751
|
// Localization changes error rendering, not conversion semantics. Preserve
|
|
679
752
|
// specialized public input operations such as json's single-parse Json
|
|
680
753
|
// boundary. Private operation chains on derived still support composition.
|
|
681
754
|
globalThis.Object.assign(derived, {
|
|
682
755
|
from: source.from,
|
|
683
|
-
orThrow: source.
|
|
756
|
+
orThrow: createRuntimeOrThrow(getTerminalRuntimeNode(source.from), derived.formatError),
|
|
684
757
|
orNull: source.orNull,
|
|
685
758
|
});
|
|
686
759
|
localizedTypeBySource.set(source, derived);
|
|
@@ -712,7 +785,12 @@ const localizeTypeReflection = (value, formatIssue, localizedTypeBySource) => {
|
|
|
712
785
|
return value;
|
|
713
786
|
const localized = globalThis.Object.create(globalThis.Object.getPrototypeOf(value));
|
|
714
787
|
for (const key of Reflect.ownKeys(value)) {
|
|
715
|
-
|
|
788
|
+
const property = value[key];
|
|
789
|
+
// Configured defaults are opaque data whose identity must be preserved.
|
|
790
|
+
localized[key] =
|
|
791
|
+
key === "value" && globalThis.Object.hasOwn(value, defaultPropertySymbol)
|
|
792
|
+
? property
|
|
793
|
+
: localizeTypeReflection(property, formatIssue, localizedTypeBySource);
|
|
716
794
|
}
|
|
717
795
|
return localized;
|
|
718
796
|
};
|
|
@@ -728,6 +806,51 @@ const assertRefinementIdentity = (refinement) => (value) => {
|
|
|
728
806
|
}
|
|
729
807
|
return result;
|
|
730
808
|
};
|
|
809
|
+
/**
|
|
810
|
+
* Creates a root {@link Type} with a custom error for an existing validator.
|
|
811
|
+
*
|
|
812
|
+
* The source must use identity encoding. The new Type accepts its Output as
|
|
813
|
+
* Input and hides its parent boundaries. Validation and error collection are
|
|
814
|
+
* delegated automatically; the mapper receives the failure and original value.
|
|
815
|
+
* The formatter presents the mapped error as one issue.
|
|
816
|
+
*
|
|
817
|
+
* ### Example
|
|
818
|
+
*
|
|
819
|
+
* ```ts
|
|
820
|
+
* import {
|
|
821
|
+
* assertEqual,
|
|
822
|
+
* assertErr,
|
|
823
|
+
* createTypeWithError,
|
|
824
|
+
* Number,
|
|
825
|
+
* String,
|
|
826
|
+
* union,
|
|
827
|
+
* type TypeError,
|
|
828
|
+
* type UnionError,
|
|
829
|
+
* } from "@evolu/common";
|
|
830
|
+
*
|
|
831
|
+
* interface ValueError extends TypeError<"Value"> {
|
|
832
|
+
* readonly cause: UnionError;
|
|
833
|
+
* }
|
|
834
|
+
*
|
|
835
|
+
* const Value = createTypeWithError(
|
|
836
|
+
* "Value",
|
|
837
|
+
* union(String, Number),
|
|
838
|
+
* (cause): ValueError => ({ type: "Value", cause }),
|
|
839
|
+
* () => "Enter text or a number.",
|
|
840
|
+
* );
|
|
841
|
+
*
|
|
842
|
+
* const result = Value.fromUnknown(false, { errors: "all" });
|
|
843
|
+
* assertErr(result);
|
|
844
|
+
* assertEqual(result.error.cause.errors.length, 2);
|
|
845
|
+
* assertEqual(Value.formatError(result.error), "Enter text or a number.");
|
|
846
|
+
* ```
|
|
847
|
+
*
|
|
848
|
+
* @group Construction
|
|
849
|
+
*/
|
|
850
|
+
export const createTypeWithError = (name, type, mapError, formatError) => createRootType(name, (value, options) => {
|
|
851
|
+
const result = type.fromUnknown(value, options);
|
|
852
|
+
return result.ok ? result : err(mapError(result.error, value));
|
|
853
|
+
}, formatError);
|
|
731
854
|
const createRootType = (name, fromUnknown, formatError, getTypeIssues) => {
|
|
732
855
|
const runtimeFormatError = formatError;
|
|
733
856
|
const runtimeGetTypeIssues = getTypeIssues ??
|
|
@@ -826,6 +949,11 @@ const fromSymbol =
|
|
|
826
949
|
const templateLiteralSyntaxSymbol =
|
|
827
950
|
/*#__PURE__*/ globalThis.Symbol();
|
|
828
951
|
const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
|
|
952
|
+
const createRuntimeOrThrow = (fromInput, formatError) => mapRuntimeResult(fromInput, (result) => {
|
|
953
|
+
if (result.ok)
|
|
954
|
+
return result.value;
|
|
955
|
+
throw new Error(formatError(result.error), { cause: result.error });
|
|
956
|
+
});
|
|
829
957
|
// `map` must return a fresh operation because this function can attach `.parent`.
|
|
830
958
|
const mapRuntimeOperations = (operation, map) => {
|
|
831
959
|
const mapped = map(operation);
|
|
@@ -863,7 +991,7 @@ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, own
|
|
|
863
991
|
: ownTo;
|
|
864
992
|
const runtimeFormatError = runtimeParent?.[getRuntimeTypeIssuesSymbol] === getTypeIssues
|
|
865
993
|
? runtimeParent.formatError
|
|
866
|
-
: (error) =>
|
|
994
|
+
: (error) => formatRuntimeTypeIssue(getTypeIssues(error, "first")[0], formatIssue);
|
|
867
995
|
const typedFrom = addRuntimeAssertions(name, is, validateOutput, parent, from);
|
|
868
996
|
const fromInput = getTerminalRuntimeNode(typedFrom);
|
|
869
997
|
const typedTo = mapRuntimeOperations(to, (operation) => (value) => {
|
|
@@ -879,7 +1007,7 @@ const createTypeNode = (name, parent, fromUnknown, is, validateOutput, from, own
|
|
|
879
1007
|
is,
|
|
880
1008
|
from: typedFrom,
|
|
881
1009
|
to: typedTo,
|
|
882
|
-
orThrow:
|
|
1010
|
+
orThrow: createRuntimeOrThrow(fromInput, runtimeFormatError),
|
|
883
1011
|
orNull: mapRuntimeResult(fromInput, getOrNull),
|
|
884
1012
|
"~standard": createStandardSchemaProps(fromUnknown, getTypeIssues, formatIssue),
|
|
885
1013
|
...additionalProperties,
|
|
@@ -899,7 +1027,7 @@ const createStandardSchemaProps = (fromUnknown, getTypeIssues, formatIssue) => (
|
|
|
899
1027
|
? { value: result.value }
|
|
900
1028
|
: {
|
|
901
1029
|
issues: getTypeIssues(result.error, "all").map((issue) => ({
|
|
902
|
-
message:
|
|
1030
|
+
message: formatRuntimeTypeIssue(issue, formatIssue),
|
|
903
1031
|
path: issue.path,
|
|
904
1032
|
})),
|
|
905
1033
|
};
|
|
@@ -1035,6 +1163,7 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
|
|
|
1035
1163
|
* >();
|
|
1036
1164
|
*
|
|
1037
1165
|
* assertOk(Age.fromUnknown(122), 122);
|
|
1166
|
+
*
|
|
1038
1167
|
* const invalid = Age.fromUnknown(200);
|
|
1039
1168
|
* assertErr(invalid);
|
|
1040
1169
|
* assertType(Data, invalid.error);
|
|
@@ -1060,6 +1189,46 @@ export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
|
|
|
1060
1189
|
* @group Base
|
|
1061
1190
|
*/
|
|
1062
1191
|
export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
|
|
1192
|
+
/**
|
|
1193
|
+
* Transforms a boolean spelled as text into a {@link Boolean}.
|
|
1194
|
+
*
|
|
1195
|
+
* This is useful for inputs that carry booleans as text, such as environment
|
|
1196
|
+
* variables, URL query parameters, and form fields. Exactly `true` and `false`
|
|
1197
|
+
* are accepted, the spellings JSON and JavaScript use, so a boolean has one
|
|
1198
|
+
* representation in every source.
|
|
1199
|
+
*
|
|
1200
|
+
* ### Example
|
|
1201
|
+
*
|
|
1202
|
+
* ```ts
|
|
1203
|
+
* import {
|
|
1204
|
+
* assertEqual,
|
|
1205
|
+
* assertErr,
|
|
1206
|
+
* assertOk,
|
|
1207
|
+
* BooleanFromString,
|
|
1208
|
+
* } from "@evolu/common";
|
|
1209
|
+
*
|
|
1210
|
+
* assertOk(BooleanFromString.fromUnknown("true"), true);
|
|
1211
|
+
* assertOk(BooleanFromString.fromUnknown("false"), false);
|
|
1212
|
+
* assertEqual(BooleanFromString.to(true), "true");
|
|
1213
|
+
*
|
|
1214
|
+
* const invalid = BooleanFromString.fromUnknown("yes");
|
|
1215
|
+
* assertErr(invalid, { type: "BooleanFromString", value: "yes" });
|
|
1216
|
+
* assertEqual(
|
|
1217
|
+
* BooleanFromString.formatError(invalid.error),
|
|
1218
|
+
* 'The value "yes" is not a boolean. Use true or false.',
|
|
1219
|
+
* );
|
|
1220
|
+
* ```
|
|
1221
|
+
*
|
|
1222
|
+
* @group Base
|
|
1223
|
+
*/
|
|
1224
|
+
export const BooleanFromString = /*#__PURE__*/ transform("BooleanFromString", String, Boolean, {
|
|
1225
|
+
from: (value) => value === "true"
|
|
1226
|
+
? ok(true)
|
|
1227
|
+
: value === "false"
|
|
1228
|
+
? ok(false)
|
|
1229
|
+
: err({ type: "BooleanFromString", value }),
|
|
1230
|
+
to: (value) => (value ? "true" : "false"),
|
|
1231
|
+
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a boolean. Use true or false.`);
|
|
1063
1232
|
/**
|
|
1064
1233
|
* A JavaScript symbol {@link Type}.
|
|
1065
1234
|
*
|
|
@@ -1188,6 +1357,7 @@ export const instanceOf = (constructor) => {
|
|
|
1188
1357
|
*
|
|
1189
1358
|
* assertType<typeof Ready.Output, "ready">();
|
|
1190
1359
|
* assertOk(Ready.fromUnknown("ready"), "ready");
|
|
1360
|
+
*
|
|
1191
1361
|
* const invalid = Ready.fromUnknown("pending");
|
|
1192
1362
|
* assertErr(invalid);
|
|
1193
1363
|
* assertType(Data, invalid.error);
|
|
@@ -1241,9 +1411,8 @@ export function union(...typesOrLiterals) {
|
|
|
1241
1411
|
const inputMembers = members.map(getTerminalRuntimeNode);
|
|
1242
1412
|
const inputFrom = createUnionValidation(inputMembers, (member, value, options) => member.fromUnknown(value, options));
|
|
1243
1413
|
const inputValidateOutput = createUnionValidation(inputMembers, (member, value, options) => member[outputValidationSymbol](value, options));
|
|
1244
|
-
const
|
|
1245
|
-
const
|
|
1246
|
-
const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, getTypeIssues);
|
|
1414
|
+
const getTypeIssues = createUnionRuntimeTypeIssues(members);
|
|
1415
|
+
const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, createUnionRuntimeTypeIssues(inputMembers));
|
|
1247
1416
|
const fromUnknown = createUnionValidation(members, (member, value, options) => member.fromUnknown(value, options));
|
|
1248
1417
|
const validateOutput = createUnionValidation(members, (member, value, options) => member[outputValidationSymbol](value, options));
|
|
1249
1418
|
const memberFromInputs = members.map((member) => getTerminalRuntimeNode(member[fromSymbol]));
|
|
@@ -1259,6 +1428,19 @@ export function union(...typesOrLiterals) {
|
|
|
1259
1428
|
};
|
|
1260
1429
|
return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
|
|
1261
1430
|
}
|
|
1431
|
+
const createUnionRuntimeTypeIssues = (members) => (error) => [
|
|
1432
|
+
{
|
|
1433
|
+
name: "Union",
|
|
1434
|
+
error,
|
|
1435
|
+
path: [],
|
|
1436
|
+
formatError: () => "A value does not match any allowed variant.",
|
|
1437
|
+
alternatives: error.errors.map(({ index, error }) => ({
|
|
1438
|
+
index,
|
|
1439
|
+
name: members[index].name,
|
|
1440
|
+
issues: members[index][getRuntimeTypeIssuesSymbol](error, "all"),
|
|
1441
|
+
})),
|
|
1442
|
+
},
|
|
1443
|
+
];
|
|
1262
1444
|
const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
|
|
1263
1445
|
let errors;
|
|
1264
1446
|
for (let index = 0; index < members.length; index++) {
|
|
@@ -1398,6 +1580,7 @@ const isRuntimeUnionTypeNode = (type) => type.name === "Union" && "members" in t
|
|
|
1398
1580
|
* assertOk(result, ["cs", "CZ"]);
|
|
1399
1581
|
* const locale = result.value;
|
|
1400
1582
|
* assertType<typeof locale, SupportedLocale>();
|
|
1583
|
+
*
|
|
1401
1584
|
* const invalid = SupportedLocale.fromUnknown("cs/CZ");
|
|
1402
1585
|
* assertErr(invalid);
|
|
1403
1586
|
* assertType(Data, invalid.error);
|
|
@@ -1775,115 +1958,673 @@ export function brand(name, parent, validate, formatError) {
|
|
|
1775
1958
|
*
|
|
1776
1959
|
* ```ts
|
|
1777
1960
|
* import {
|
|
1778
|
-
* assertEqual,
|
|
1961
|
+
* assertEqual,
|
|
1962
|
+
* assertErr,
|
|
1963
|
+
* assertOk,
|
|
1964
|
+
* assertType,
|
|
1965
|
+
* Data,
|
|
1966
|
+
* DateIso,
|
|
1967
|
+
* } from "@evolu/common";
|
|
1968
|
+
*
|
|
1969
|
+
* const value = "2023-01-01T12:00:00.000Z";
|
|
1970
|
+
* assertOk(DateIso.fromUnknown(value), value);
|
|
1971
|
+
*
|
|
1972
|
+
* const invalid = DateIso.fromUnknown("2023-01-01");
|
|
1973
|
+
* assertErr(invalid);
|
|
1974
|
+
* assertType(Data, invalid.error);
|
|
1975
|
+
* assertEqual(invalid.error, {
|
|
1976
|
+
* type: "DateIso",
|
|
1977
|
+
* value: "2023-01-01",
|
|
1978
|
+
* });
|
|
1979
|
+
* ```
|
|
1980
|
+
*
|
|
1981
|
+
* @group String
|
|
1982
|
+
*/
|
|
1983
|
+
export const DateIso = /*#__PURE__*/ brand("DateIso", String, (value) => value.length === 24 && new globalThis.Date(value).toJSON() === value
|
|
1984
|
+
? ok()
|
|
1985
|
+
: err({ type: "DateIso", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a canonical ISO date-time string.`);
|
|
1986
|
+
/**
|
|
1987
|
+
* Safely transforms a {@link Date} into a canonical {@link DateIso}.
|
|
1988
|
+
*
|
|
1989
|
+
* ### Example
|
|
1990
|
+
*
|
|
1991
|
+
* ```ts
|
|
1992
|
+
* import { assertEqual, assertOk, DateIsoFromDate } from "@evolu/common";
|
|
1993
|
+
*
|
|
1994
|
+
* const date = new Date("2025-01-01T12:00:00.000Z");
|
|
1995
|
+
* const result = DateIsoFromDate.fromUnknown(date);
|
|
1996
|
+
*
|
|
1997
|
+
* assertOk(result, "2025-01-01T12:00:00.000Z");
|
|
1998
|
+
* assertEqual(DateIsoFromDate.to(result.value), date);
|
|
1999
|
+
* ```
|
|
2000
|
+
*
|
|
2001
|
+
* @group String
|
|
2002
|
+
*/
|
|
2003
|
+
export const DateIsoFromDate = /*#__PURE__*/ transform("DateIsoFromDate", Date, DateIso, {
|
|
2004
|
+
from: (value) => trySync(() => globalThis.Date.prototype.toISOString.call(value), () => ({ type: "DateIsoFromDate", value })),
|
|
2005
|
+
to: (value) => new globalThis.Date(value),
|
|
2006
|
+
}, () => "The Date cannot be represented as DateIso.");
|
|
2007
|
+
/**
|
|
2008
|
+
* Signed 64-bit {@link BigInt}.
|
|
2009
|
+
*
|
|
2010
|
+
* @group Number
|
|
2011
|
+
*/
|
|
2012
|
+
export const Int64 = /*#__PURE__*/ brand("Int64", BigInt, (value) => globalThis.BigInt.asIntN(64, value) === value
|
|
2013
|
+
? ok()
|
|
2014
|
+
: err({ type: "Int64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid signed 64-bit integer (Int64).`);
|
|
2015
|
+
/**
|
|
2016
|
+
* Unsigned 64-bit {@link BigInt}.
|
|
2017
|
+
*
|
|
2018
|
+
* @group Number
|
|
2019
|
+
*/
|
|
2020
|
+
export const UInt64 = /*#__PURE__*/ brand("UInt64", BigInt, (value) => globalThis.BigInt.asUintN(64, value) === value
|
|
2021
|
+
? ok()
|
|
2022
|
+
: err({ type: "UInt64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid unsigned 64-bit integer (UInt64).`);
|
|
2023
|
+
/**
|
|
2024
|
+
* Adds identifier validation in a naming convention to an existing string Type.
|
|
2025
|
+
*
|
|
2026
|
+
* An identifier is one or more ASCII words, each starting with a letter and
|
|
2027
|
+
* continuing with letters or digits. camelCase and PascalCase start every word
|
|
2028
|
+
* after the first with an uppercase letter, so `httpUrl` has two words and
|
|
2029
|
+
* `httpURL` has four. snake_case, kebab-case, and CONSTANT_CASE put exactly one
|
|
2030
|
+
* separator between words. Validation keeps the spelling and rejects empty
|
|
2031
|
+
* strings, whitespace, punctuation, and non-ASCII characters.
|
|
2032
|
+
*
|
|
2033
|
+
* Words never start with a digit. Identifier grammars in most languages forbid
|
|
2034
|
+
* a leading digit, and camelCase cannot mark a word boundary before one, so the
|
|
2035
|
+
* rule applies to every word and keeps every conversion exact. Join an
|
|
2036
|
+
* abbreviation such as `2FA` to the previous word, as in `MAX2FA_ATTEMPTS` and
|
|
2037
|
+
* `max2faAttempts`, or spell the number out, as in `TWO_FACTOR_SECRET`.
|
|
2038
|
+
*
|
|
2039
|
+
* Convert validated identifiers with functions such as
|
|
2040
|
+
* {@link camelCaseToSnakeCase}. Conversions preserve word boundaries, and
|
|
2041
|
+
* converting back restores the original spelling. They return only the
|
|
2042
|
+
* destination brand, dropping unrelated constraints such as input length.
|
|
2043
|
+
*
|
|
2044
|
+
* ### Example
|
|
2045
|
+
*
|
|
2046
|
+
* ```ts
|
|
2047
|
+
* import {
|
|
2048
|
+
* assertErr,
|
|
2049
|
+
* assertOk,
|
|
2050
|
+
* identifier,
|
|
2051
|
+
* maxLength,
|
|
2052
|
+
* String,
|
|
2053
|
+
* } from "@evolu/common";
|
|
2054
|
+
*
|
|
2055
|
+
* const EnvName = identifier("CONSTANT_CASE")(maxLength(16)(String));
|
|
2056
|
+
*
|
|
2057
|
+
* assertOk(EnvName.fromUnknown("HTTP2_PORT"), "HTTP2_PORT");
|
|
2058
|
+
* assertErr(EnvName.fromUnknown("HTTP_2_PORT"));
|
|
2059
|
+
* ```
|
|
2060
|
+
*
|
|
2061
|
+
* @group String
|
|
2062
|
+
*/
|
|
2063
|
+
export const identifier = (casing) => (parent) => {
|
|
2064
|
+
const name = identifierBrandByCasing[casing];
|
|
2065
|
+
const pattern = identifierPatternByCasing[casing];
|
|
2066
|
+
return brand(name, parent, (value) => pattern.test(value)
|
|
2067
|
+
? ok()
|
|
2068
|
+
: err({ type: name, value, casing }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a ${error.casing} identifier.`);
|
|
2069
|
+
};
|
|
2070
|
+
const identifierBrandByCasing = {
|
|
2071
|
+
camelCase: "CamelCaseIdentifier",
|
|
2072
|
+
PascalCase: "PascalCaseIdentifier",
|
|
2073
|
+
snake_case: "SnakeCaseIdentifier",
|
|
2074
|
+
"kebab-case": "KebabCaseIdentifier",
|
|
2075
|
+
CONSTANT_CASE: "ConstantCaseIdentifier",
|
|
2076
|
+
};
|
|
2077
|
+
const identifierPatternByCasing = {
|
|
2078
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/u,
|
|
2079
|
+
PascalCase: /^[A-Z][a-zA-Z0-9]*$/u,
|
|
2080
|
+
snake_case: /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/u,
|
|
2081
|
+
"kebab-case": /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/u,
|
|
2082
|
+
CONSTANT_CASE: /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
|
|
2083
|
+
};
|
|
2084
|
+
/**
|
|
2085
|
+
* A validated camelCase identifier, such as `http2Port`.
|
|
2086
|
+
*
|
|
2087
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
2088
|
+
*
|
|
2089
|
+
* @group String
|
|
2090
|
+
*/
|
|
2091
|
+
export const CamelCaseIdentifier =
|
|
2092
|
+
/*#__PURE__*/ identifier("camelCase")(String);
|
|
2093
|
+
/**
|
|
2094
|
+
* A validated PascalCase identifier, such as `Http2Port`.
|
|
2095
|
+
*
|
|
2096
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
2097
|
+
*
|
|
2098
|
+
* @group String
|
|
2099
|
+
*/
|
|
2100
|
+
export const PascalCaseIdentifier =
|
|
2101
|
+
/*#__PURE__*/ identifier("PascalCase")(String);
|
|
2102
|
+
/**
|
|
2103
|
+
* A validated snake_case identifier, such as `http2_port`.
|
|
2104
|
+
*
|
|
2105
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
2106
|
+
*
|
|
2107
|
+
* @group String
|
|
2108
|
+
*/
|
|
2109
|
+
export const SnakeCaseIdentifier =
|
|
2110
|
+
/*#__PURE__*/ identifier("snake_case")(String);
|
|
2111
|
+
/**
|
|
2112
|
+
* A validated kebab-case identifier, such as `http2-port`.
|
|
2113
|
+
*
|
|
2114
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
2115
|
+
*
|
|
2116
|
+
* @group String
|
|
2117
|
+
*/
|
|
2118
|
+
export const KebabCaseIdentifier =
|
|
2119
|
+
/*#__PURE__*/ identifier("kebab-case")(String);
|
|
2120
|
+
/**
|
|
2121
|
+
* A validated CONSTANT_CASE identifier, such as `HTTP2_PORT`.
|
|
2122
|
+
*
|
|
2123
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
2124
|
+
*
|
|
2125
|
+
* @group String
|
|
2126
|
+
*/
|
|
2127
|
+
export const ConstantCaseIdentifier =
|
|
2128
|
+
/*#__PURE__*/ identifier("CONSTANT_CASE")(String);
|
|
2129
|
+
/**
|
|
2130
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2131
|
+
*
|
|
2132
|
+
* Converts `http2Port` to `Http2Port`. Use {@link pascalCaseToCamelCase} to
|
|
2133
|
+
* recover the original spelling.
|
|
2134
|
+
*
|
|
2135
|
+
* @group String
|
|
2136
|
+
*/
|
|
2137
|
+
export const camelCaseToPascalCase = (value) => (value.charAt(0).toUpperCase() + value.slice(1));
|
|
2138
|
+
/**
|
|
2139
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2140
|
+
*
|
|
2141
|
+
* Converts `http2Port` to `http2_port`. Use {@link snakeCaseToCamelCase} to
|
|
2142
|
+
* recover the original spelling.
|
|
2143
|
+
*
|
|
2144
|
+
* @group String
|
|
2145
|
+
*/
|
|
2146
|
+
export const camelCaseToSnakeCase = (value) => value.replaceAll(/[A-Z]/gu, (letter) => `_${letter.toLowerCase()}`);
|
|
2147
|
+
/**
|
|
2148
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2149
|
+
*
|
|
2150
|
+
* Converts `http2Port` to `http2-port`. Use {@link kebabCaseToCamelCase} to
|
|
2151
|
+
* recover the original spelling.
|
|
2152
|
+
*
|
|
2153
|
+
* @group String
|
|
2154
|
+
*/
|
|
2155
|
+
export const camelCaseToKebabCase = (value) => snakeCaseToKebabCase(camelCaseToSnakeCase(value));
|
|
2156
|
+
/**
|
|
2157
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2158
|
+
*
|
|
2159
|
+
* Converts `http2Port` to `HTTP2_PORT`. Use {@link constantCaseToCamelCase} to
|
|
2160
|
+
* recover the original spelling.
|
|
2161
|
+
*
|
|
2162
|
+
* @group String
|
|
2163
|
+
*/
|
|
2164
|
+
export const camelCaseToConstantCase = (value) => snakeCaseToConstantCase(camelCaseToSnakeCase(value));
|
|
2165
|
+
/**
|
|
2166
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2167
|
+
*
|
|
2168
|
+
* Converts `Http2Port` to `http2Port`. Use {@link camelCaseToPascalCase} to
|
|
2169
|
+
* recover the original spelling.
|
|
2170
|
+
*
|
|
2171
|
+
* @group String
|
|
2172
|
+
*/
|
|
2173
|
+
export const pascalCaseToCamelCase = (value) => (value.charAt(0).toLowerCase() + value.slice(1));
|
|
2174
|
+
/**
|
|
2175
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2176
|
+
*
|
|
2177
|
+
* Converts `Http2Port` to `http2_port`. Use {@link snakeCaseToPascalCase} to
|
|
2178
|
+
* recover the original spelling.
|
|
2179
|
+
*
|
|
2180
|
+
* @group String
|
|
2181
|
+
*/
|
|
2182
|
+
export const pascalCaseToSnakeCase = (value) => camelCaseToSnakeCase(pascalCaseToCamelCase(value));
|
|
2183
|
+
/**
|
|
2184
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2185
|
+
*
|
|
2186
|
+
* Converts `Http2Port` to `http2-port`. Use {@link kebabCaseToPascalCase} to
|
|
2187
|
+
* recover the original spelling.
|
|
2188
|
+
*
|
|
2189
|
+
* @group String
|
|
2190
|
+
*/
|
|
2191
|
+
export const pascalCaseToKebabCase = (value) => snakeCaseToKebabCase(pascalCaseToSnakeCase(value));
|
|
2192
|
+
/**
|
|
2193
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2194
|
+
*
|
|
2195
|
+
* Converts `Http2Port` to `HTTP2_PORT`. Use {@link constantCaseToPascalCase} to
|
|
2196
|
+
* recover the original spelling.
|
|
2197
|
+
*
|
|
2198
|
+
* @group String
|
|
2199
|
+
*/
|
|
2200
|
+
export const pascalCaseToConstantCase = (value) => snakeCaseToConstantCase(pascalCaseToSnakeCase(value));
|
|
2201
|
+
/**
|
|
2202
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2203
|
+
*
|
|
2204
|
+
* Converts `http2_port` to `http2Port`. Use {@link camelCaseToSnakeCase} to
|
|
2205
|
+
* recover the original spelling.
|
|
2206
|
+
*
|
|
2207
|
+
* @group String
|
|
2208
|
+
*/
|
|
2209
|
+
export const snakeCaseToCamelCase = (value) => value.replaceAll(/_[a-z]/gu, (word) => word.charAt(1).toUpperCase());
|
|
2210
|
+
/**
|
|
2211
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2212
|
+
*
|
|
2213
|
+
* Converts `http2_port` to `Http2Port`. Use {@link pascalCaseToSnakeCase} to
|
|
2214
|
+
* recover the original spelling.
|
|
2215
|
+
*
|
|
2216
|
+
* @group String
|
|
2217
|
+
*/
|
|
2218
|
+
export const snakeCaseToPascalCase = (value) => camelCaseToPascalCase(snakeCaseToCamelCase(value));
|
|
2219
|
+
/**
|
|
2220
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2221
|
+
*
|
|
2222
|
+
* Converts `http2_port` to `http2-port`. Use {@link kebabCaseToSnakeCase} to
|
|
2223
|
+
* recover the original spelling.
|
|
2224
|
+
*
|
|
2225
|
+
* @group String
|
|
2226
|
+
*/
|
|
2227
|
+
export const snakeCaseToKebabCase = (value) => value.replaceAll("_", "-");
|
|
2228
|
+
/**
|
|
2229
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2230
|
+
*
|
|
2231
|
+
* Converts `http2_port` to `HTTP2_PORT`. Use {@link constantCaseToSnakeCase} to
|
|
2232
|
+
* recover the original spelling.
|
|
2233
|
+
*
|
|
2234
|
+
* @group String
|
|
2235
|
+
*/
|
|
2236
|
+
export const snakeCaseToConstantCase = (value) => value.toUpperCase();
|
|
2237
|
+
/**
|
|
2238
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2239
|
+
*
|
|
2240
|
+
* Converts `http2-port` to `http2Port`. Use {@link camelCaseToKebabCase} to
|
|
2241
|
+
* recover the original spelling.
|
|
2242
|
+
*
|
|
2243
|
+
* @group String
|
|
2244
|
+
*/
|
|
2245
|
+
export const kebabCaseToCamelCase = (value) => snakeCaseToCamelCase(kebabCaseToSnakeCase(value));
|
|
2246
|
+
/**
|
|
2247
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2248
|
+
*
|
|
2249
|
+
* Converts `http2-port` to `Http2Port`. Use {@link pascalCaseToKebabCase} to
|
|
2250
|
+
* recover the original spelling.
|
|
2251
|
+
*
|
|
2252
|
+
* @group String
|
|
2253
|
+
*/
|
|
2254
|
+
export const kebabCaseToPascalCase = (value) => camelCaseToPascalCase(kebabCaseToCamelCase(value));
|
|
2255
|
+
/**
|
|
2256
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2257
|
+
*
|
|
2258
|
+
* Converts `http2-port` to `http2_port`. Use {@link snakeCaseToKebabCase} to
|
|
2259
|
+
* recover the original spelling.
|
|
2260
|
+
*
|
|
2261
|
+
* @group String
|
|
2262
|
+
*/
|
|
2263
|
+
export const kebabCaseToSnakeCase = (value) => value.replaceAll("-", "_");
|
|
2264
|
+
/**
|
|
2265
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
2266
|
+
*
|
|
2267
|
+
* Converts `http2-port` to `HTTP2_PORT`. Use {@link constantCaseToKebabCase} to
|
|
2268
|
+
* recover the original spelling.
|
|
2269
|
+
*
|
|
2270
|
+
* @group String
|
|
2271
|
+
*/
|
|
2272
|
+
export const kebabCaseToConstantCase = (value) => snakeCaseToConstantCase(kebabCaseToSnakeCase(value));
|
|
2273
|
+
/**
|
|
2274
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
2275
|
+
*
|
|
2276
|
+
* Converts `HTTP2_PORT` to `http2Port`. Use {@link camelCaseToConstantCase} to
|
|
2277
|
+
* recover the original spelling.
|
|
2278
|
+
*
|
|
2279
|
+
* @group String
|
|
2280
|
+
*/
|
|
2281
|
+
export const constantCaseToCamelCase = (value) => snakeCaseToCamelCase(constantCaseToSnakeCase(value));
|
|
2282
|
+
/**
|
|
2283
|
+
* Decodes a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier} and
|
|
2284
|
+
* restores the original spelling when encoding.
|
|
2285
|
+
*
|
|
2286
|
+
* ### Example
|
|
2287
|
+
*
|
|
2288
|
+
* ```ts
|
|
2289
|
+
* import {
|
|
2290
|
+
* assertEqual,
|
|
2291
|
+
* assertOk,
|
|
2292
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
2293
|
+
* } from "@evolu/common";
|
|
2294
|
+
*
|
|
2295
|
+
* const Key = CamelCaseIdentifierFromConstantCaseIdentifier;
|
|
2296
|
+
* const result = Key.fromUnknown("HTTP2_PORT");
|
|
2297
|
+
* assertOk(result, "http2Port");
|
|
2298
|
+
* assertEqual(Key.to(result.value), "HTTP2_PORT");
|
|
2299
|
+
* ```
|
|
2300
|
+
*
|
|
2301
|
+
* @group String
|
|
2302
|
+
*/
|
|
2303
|
+
export const CamelCaseIdentifierFromConstantCaseIdentifier =
|
|
2304
|
+
/*#__PURE__*/ transform("CamelCaseIdentifierFromConstantCaseIdentifier", ConstantCaseIdentifier, CamelCaseIdentifier, {
|
|
2305
|
+
from: (value) => ok(constantCaseToCamelCase(value)),
|
|
2306
|
+
to: camelCaseToConstantCase,
|
|
2307
|
+
});
|
|
2308
|
+
/**
|
|
2309
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
2310
|
+
*
|
|
2311
|
+
* Converts `HTTP2_PORT` to `Http2Port`. Use {@link pascalCaseToConstantCase} to
|
|
2312
|
+
* recover the original spelling.
|
|
2313
|
+
*
|
|
2314
|
+
* @group String
|
|
2315
|
+
*/
|
|
2316
|
+
export const constantCaseToPascalCase = (value) => camelCaseToPascalCase(constantCaseToCamelCase(value));
|
|
2317
|
+
/**
|
|
2318
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
2319
|
+
*
|
|
2320
|
+
* Converts `HTTP2_PORT` to `http2_port`. Use {@link snakeCaseToConstantCase} to
|
|
2321
|
+
* recover the original spelling.
|
|
2322
|
+
*
|
|
2323
|
+
* @group String
|
|
2324
|
+
*/
|
|
2325
|
+
export const constantCaseToSnakeCase = (value) => value.toLowerCase();
|
|
2326
|
+
/**
|
|
2327
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
2328
|
+
*
|
|
2329
|
+
* Converts `HTTP2_PORT` to `http2-port`. Use {@link kebabCaseToConstantCase} to
|
|
2330
|
+
* recover the original spelling.
|
|
2331
|
+
*
|
|
2332
|
+
* @group String
|
|
2333
|
+
*/
|
|
2334
|
+
export const constantCaseToKebabCase = (value) => snakeCaseToKebabCase(constantCaseToSnakeCase(value));
|
|
2335
|
+
/**
|
|
2336
|
+
* Adds capitalized text validation to an existing string Type.
|
|
2337
|
+
*
|
|
2338
|
+
* Narrows the output to TypeScript's `Capitalize<string>` while preserving the
|
|
2339
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2340
|
+
* {@link capitalize} to change its casing.
|
|
2341
|
+
*
|
|
2342
|
+
* ### Example
|
|
2343
|
+
*
|
|
2344
|
+
* ```ts
|
|
2345
|
+
* import {
|
|
2346
|
+
* assertErr,
|
|
2347
|
+
* assertOk,
|
|
2348
|
+
* capitalized,
|
|
2349
|
+
* maxLength,
|
|
2350
|
+
* String,
|
|
2351
|
+
* } from "@evolu/common";
|
|
2352
|
+
*
|
|
2353
|
+
* const Label = capitalized(maxLength(50)(String));
|
|
2354
|
+
* assertOk(Label.fromUnknown("Hello world"), "Hello world");
|
|
2355
|
+
* assertErr(Label.fromUnknown("hello world"));
|
|
2356
|
+
* ```
|
|
2357
|
+
*
|
|
2358
|
+
* @group String
|
|
2359
|
+
*/
|
|
2360
|
+
export const capitalized = (parent) => createType("Capitalized", parent, (value) => value === capitalize(value)
|
|
2361
|
+
? ok(value)
|
|
2362
|
+
: err({ type: "Capitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
|
|
2363
|
+
/**
|
|
2364
|
+
* Validates capitalized text as TypeScript's `Capitalize<string>`.
|
|
2365
|
+
*
|
|
2366
|
+
* The rest of the text can use any casing. Empty strings and text starting with
|
|
2367
|
+
* an uncased character, such as a digit or emoji, are valid. Use
|
|
2368
|
+
* {@link capitalize} to produce a capitalized value from any string.
|
|
2369
|
+
*
|
|
2370
|
+
* Capitalization applies to general text, including spaces and punctuation.
|
|
2371
|
+
* Both `hello` and `Hello` become `Hello`, so the original initial casing
|
|
2372
|
+
* cannot be recovered. Capitalization does not identify words or turn text into
|
|
2373
|
+
* an identifier.
|
|
2374
|
+
*
|
|
2375
|
+
* ### Example
|
|
2376
|
+
*
|
|
2377
|
+
* ```ts
|
|
2378
|
+
* import { assertErr, assertOk, CapitalizedString } from "@evolu/common";
|
|
2379
|
+
*
|
|
2380
|
+
* const text: CapitalizedString = "Hello world";
|
|
2381
|
+
* assertOk(CapitalizedString.fromUnknown(text), text);
|
|
2382
|
+
* assertErr(CapitalizedString.fromUnknown("hello world"));
|
|
2383
|
+
* assertOk(CapitalizedString.fromUnknown(""), "");
|
|
2384
|
+
* ```
|
|
2385
|
+
*
|
|
2386
|
+
* @group String
|
|
2387
|
+
*/
|
|
2388
|
+
export const CapitalizedString = /*#__PURE__*/ capitalized(String);
|
|
2389
|
+
/**
|
|
2390
|
+
* Uppercases the first Unicode code point and returns a
|
|
2391
|
+
* {@link CapitalizedString}.
|
|
2392
|
+
*
|
|
2393
|
+
* Preserves the remainder of the string and leaves an empty string unchanged.
|
|
2394
|
+
* Uses JavaScript's default Unicode casing without locale-specific rules.
|
|
2395
|
+
* Changing case can change the length, so input brands are not retained.
|
|
2396
|
+
*
|
|
2397
|
+
* ### Example
|
|
2398
|
+
*
|
|
2399
|
+
* ```ts
|
|
2400
|
+
* import { assertEqual, assertType, capitalize } from "@evolu/common";
|
|
2401
|
+
*
|
|
2402
|
+
* const text = capitalize("hello world");
|
|
2403
|
+
* assertEqual(text, "Hello world");
|
|
2404
|
+
* assertType<typeof text, "Hello world">();
|
|
2405
|
+
* assertEqual(capitalize(text), text);
|
|
2406
|
+
* ```
|
|
2407
|
+
*
|
|
2408
|
+
* @group String
|
|
2409
|
+
*/
|
|
2410
|
+
export const capitalize = (value) => {
|
|
2411
|
+
const [first = ""] = value;
|
|
2412
|
+
return (first.toUpperCase() + value.slice(first.length));
|
|
2413
|
+
};
|
|
2414
|
+
/**
|
|
2415
|
+
* Adds uncapitalized text validation to an existing string Type.
|
|
2416
|
+
*
|
|
2417
|
+
* Narrows the output to TypeScript's `Uncapitalize<string>` while preserving
|
|
2418
|
+
* the parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2419
|
+
* {@link uncapitalize} to change its casing.
|
|
2420
|
+
*
|
|
2421
|
+
* ### Example
|
|
2422
|
+
*
|
|
2423
|
+
* ```ts
|
|
2424
|
+
* import {
|
|
2425
|
+
* assertErr,
|
|
2426
|
+
* assertOk,
|
|
2427
|
+
* uncapitalized,
|
|
2428
|
+
* maxLength,
|
|
2429
|
+
* String,
|
|
2430
|
+
* } from "@evolu/common";
|
|
2431
|
+
*
|
|
2432
|
+
* const Label = uncapitalized(maxLength(50)(String));
|
|
2433
|
+
* assertOk(Label.fromUnknown("hello WORLD"), "hello WORLD");
|
|
2434
|
+
* assertErr(Label.fromUnknown("Hello WORLD"));
|
|
2435
|
+
* ```
|
|
2436
|
+
*
|
|
2437
|
+
* @group String
|
|
2438
|
+
*/
|
|
2439
|
+
export const uncapitalized = (parent) => createType("Uncapitalized", parent, (value) => value === uncapitalize(value)
|
|
2440
|
+
? ok(value)
|
|
2441
|
+
: err({ type: "Uncapitalized", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must not start with an uppercase letter.`);
|
|
2442
|
+
/**
|
|
2443
|
+
* Validates uncapitalized text as TypeScript's `Uncapitalize<string>`.
|
|
2444
|
+
*
|
|
2445
|
+
* The rest of the text can use any casing. Empty strings and text starting with
|
|
2446
|
+
* an uncased character, such as a digit or emoji, are valid. Use
|
|
2447
|
+
* {@link uncapitalize} to produce an uncapitalized value from any string.
|
|
2448
|
+
*
|
|
2449
|
+
* ### Example
|
|
2450
|
+
*
|
|
2451
|
+
* ```ts
|
|
2452
|
+
* import { assertErr, assertOk, UncapitalizedString } from "@evolu/common";
|
|
2453
|
+
*
|
|
2454
|
+
* const text: UncapitalizedString = "hello WORLD";
|
|
2455
|
+
* assertOk(UncapitalizedString.fromUnknown(text), text);
|
|
2456
|
+
* assertErr(UncapitalizedString.fromUnknown("Hello WORLD"));
|
|
2457
|
+
* assertOk(UncapitalizedString.fromUnknown(""), "");
|
|
2458
|
+
* ```
|
|
2459
|
+
*
|
|
2460
|
+
* @group String
|
|
2461
|
+
*/
|
|
2462
|
+
export const UncapitalizedString = /*#__PURE__*/ uncapitalized(String);
|
|
2463
|
+
/**
|
|
2464
|
+
* Lowercases the first Unicode code point and returns a
|
|
2465
|
+
* {@link UncapitalizedString}.
|
|
2466
|
+
*
|
|
2467
|
+
* Preserves the remainder of the string and leaves an empty string unchanged.
|
|
2468
|
+
* Uses JavaScript's default Unicode casing without locale-specific rules.
|
|
2469
|
+
* Changing case can change the length, so input brands are not retained.
|
|
2470
|
+
*
|
|
2471
|
+
* ### Example
|
|
2472
|
+
*
|
|
2473
|
+
* ```ts
|
|
2474
|
+
* import { assertEqual, assertType, uncapitalize } from "@evolu/common";
|
|
2475
|
+
*
|
|
2476
|
+
* const text = uncapitalize("Hello WORLD");
|
|
2477
|
+
* assertEqual(text, "hello WORLD");
|
|
2478
|
+
* assertType<typeof text, "hello WORLD">();
|
|
2479
|
+
* assertEqual(uncapitalize(text), text);
|
|
2480
|
+
* ```
|
|
2481
|
+
*
|
|
2482
|
+
* @group String
|
|
2483
|
+
*/
|
|
2484
|
+
export const uncapitalize = (value) => {
|
|
2485
|
+
const [first = ""] = value;
|
|
2486
|
+
return (first.toLowerCase() + value.slice(first.length));
|
|
2487
|
+
};
|
|
2488
|
+
/**
|
|
2489
|
+
* Adds uppercased text validation to an existing string Type.
|
|
2490
|
+
*
|
|
2491
|
+
* Narrows the output to TypeScript's `Uppercase<string>` while preserving the
|
|
2492
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2493
|
+
* {@link uppercase} to change its casing.
|
|
2494
|
+
*
|
|
2495
|
+
* ### Example
|
|
2496
|
+
*
|
|
2497
|
+
* ```ts
|
|
2498
|
+
* import {
|
|
1779
2499
|
* assertErr,
|
|
1780
2500
|
* assertOk,
|
|
1781
|
-
*
|
|
1782
|
-
*
|
|
1783
|
-
*
|
|
2501
|
+
* uppercased,
|
|
2502
|
+
* maxLength,
|
|
2503
|
+
* String,
|
|
1784
2504
|
* } from "@evolu/common";
|
|
1785
2505
|
*
|
|
1786
|
-
* const
|
|
1787
|
-
* assertOk(
|
|
1788
|
-
*
|
|
1789
|
-
* assertErr(invalid);
|
|
1790
|
-
* assertType(Data, invalid.error);
|
|
1791
|
-
* assertEqual(invalid.error, {
|
|
1792
|
-
* type: "DateIso",
|
|
1793
|
-
* value: "2023-01-01",
|
|
1794
|
-
* });
|
|
2506
|
+
* const Label = uppercased(maxLength(50)(String));
|
|
2507
|
+
* assertOk(Label.fromUnknown("HELLO WORLD"), "HELLO WORLD");
|
|
2508
|
+
* assertErr(Label.fromUnknown("Hello world"));
|
|
1795
2509
|
* ```
|
|
1796
2510
|
*
|
|
1797
2511
|
* @group String
|
|
1798
2512
|
*/
|
|
1799
|
-
export const
|
|
1800
|
-
? ok()
|
|
1801
|
-
: err({ type: "
|
|
2513
|
+
export const uppercased = (parent) => createType("Uppercased", parent, (value) => value === uppercase(value)
|
|
2514
|
+
? ok(value)
|
|
2515
|
+
: err({ type: "Uppercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be uppercased.`);
|
|
1802
2516
|
/**
|
|
1803
|
-
*
|
|
2517
|
+
* Validates uppercased text as TypeScript's `Uppercase<string>`.
|
|
2518
|
+
*
|
|
2519
|
+
* Checks the whole string using JavaScript's Unicode uppercase mapping. Empty
|
|
2520
|
+
* strings and uncased characters, such as digits and emoji, are valid. Use
|
|
2521
|
+
* {@link uppercase} to produce an uppercased value from any string.
|
|
1804
2522
|
*
|
|
1805
2523
|
* ### Example
|
|
1806
2524
|
*
|
|
1807
2525
|
* ```ts
|
|
1808
|
-
* import {
|
|
1809
|
-
*
|
|
1810
|
-
* const date = new Date("2025-01-01T12:00:00.000Z");
|
|
1811
|
-
* const result = DateIsoFromDate.fromUnknown(date);
|
|
2526
|
+
* import { assertErr, assertOk, UppercasedString } from "@evolu/common";
|
|
1812
2527
|
*
|
|
1813
|
-
*
|
|
1814
|
-
*
|
|
2528
|
+
* const text: UppercasedString = "HELLO WORLD";
|
|
2529
|
+
* assertOk(UppercasedString.fromUnknown(text), text);
|
|
2530
|
+
* assertErr(UppercasedString.fromUnknown("Hello world"));
|
|
2531
|
+
* assertOk(UppercasedString.fromUnknown(""), "");
|
|
1815
2532
|
* ```
|
|
1816
2533
|
*
|
|
1817
2534
|
* @group String
|
|
1818
2535
|
*/
|
|
1819
|
-
export const
|
|
1820
|
-
from: (value) => trySync(() => globalThis.Date.prototype.toISOString.call(value), () => ({ type: "DateIsoFromDate", value })),
|
|
1821
|
-
to: (value) => new globalThis.Date(value),
|
|
1822
|
-
}, () => "The Date cannot be represented as DateIso.");
|
|
2536
|
+
export const UppercasedString = /*#__PURE__*/ uppercased(String);
|
|
1823
2537
|
/**
|
|
1824
|
-
*
|
|
2538
|
+
* Uppercases the whole string and returns a {@link UppercasedString}.
|
|
1825
2539
|
*
|
|
1826
|
-
*
|
|
1827
|
-
|
|
1828
|
-
|
|
1829
|
-
? ok()
|
|
1830
|
-
: err({ type: "Int64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid signed 64-bit integer (Int64).`);
|
|
1831
|
-
/**
|
|
1832
|
-
* Unsigned 64-bit {@link BigInt}.
|
|
2540
|
+
* Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
|
|
2541
|
+
* without locale-specific rules. Changing case can change the length, so input
|
|
2542
|
+
* brands are not retained.
|
|
1833
2543
|
*
|
|
1834
|
-
*
|
|
2544
|
+
* ### Example
|
|
2545
|
+
*
|
|
2546
|
+
* ```ts
|
|
2547
|
+
* import { assertEqual, assertType, uppercase } from "@evolu/common";
|
|
2548
|
+
*
|
|
2549
|
+
* const text = uppercase("Hello world");
|
|
2550
|
+
* assertEqual(text, "HELLO WORLD");
|
|
2551
|
+
* assertType<typeof text, "HELLO WORLD">();
|
|
2552
|
+
* assertEqual(uppercase(text), text);
|
|
2553
|
+
* ```
|
|
2554
|
+
*
|
|
2555
|
+
* @group String
|
|
1835
2556
|
*/
|
|
1836
|
-
export const
|
|
1837
|
-
? ok()
|
|
1838
|
-
: err({ type: "UInt64", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid unsigned 64-bit integer (UInt64).`);
|
|
2557
|
+
export const uppercase = (value) => value.toUpperCase();
|
|
1839
2558
|
/**
|
|
1840
|
-
*
|
|
2559
|
+
* Adds lowercased text validation to an existing string Type.
|
|
1841
2560
|
*
|
|
1842
|
-
*
|
|
2561
|
+
* Narrows the output to TypeScript's `Lowercase<string>` while preserving the
|
|
2562
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
2563
|
+
* {@link lowercase} to change its casing.
|
|
1843
2564
|
*
|
|
1844
2565
|
* ### Example
|
|
1845
2566
|
*
|
|
1846
2567
|
* ```ts
|
|
1847
2568
|
* import {
|
|
1848
|
-
* assertEqual,
|
|
1849
2569
|
* assertErr,
|
|
1850
2570
|
* assertOk,
|
|
1851
|
-
*
|
|
1852
|
-
*
|
|
2571
|
+
* lowercased,
|
|
2572
|
+
* maxLength,
|
|
1853
2573
|
* String,
|
|
1854
|
-
* capitalized,
|
|
1855
|
-
* type Brand,
|
|
1856
2574
|
* } from "@evolu/common";
|
|
1857
2575
|
*
|
|
1858
|
-
* const
|
|
1859
|
-
*
|
|
2576
|
+
* const Label = lowercased(maxLength(50)(String));
|
|
2577
|
+
* assertOk(Label.fromUnknown("hello world"), "hello world");
|
|
2578
|
+
* assertErr(Label.fromUnknown("Hello WORLD"));
|
|
2579
|
+
* ```
|
|
1860
2580
|
*
|
|
1861
|
-
*
|
|
2581
|
+
* @group String
|
|
2582
|
+
*/
|
|
2583
|
+
export const lowercased = (parent) => createType("Lowercased", parent, (value) => value === lowercase(value)
|
|
2584
|
+
? ok(value)
|
|
2585
|
+
: err({ type: "Lowercased", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be lowercased.`);
|
|
2586
|
+
/**
|
|
2587
|
+
* Validates lowercased text as TypeScript's `Lowercase<string>`.
|
|
1862
2588
|
*
|
|
1863
|
-
*
|
|
1864
|
-
*
|
|
1865
|
-
*
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
* }
|
|
2589
|
+
* Checks the whole string using JavaScript's Unicode lowercase mapping. Empty
|
|
2590
|
+
* strings and uncased characters, such as digits and emoji, are valid. Use
|
|
2591
|
+
* {@link lowercase} to produce a lowercased value from any string.
|
|
2592
|
+
*
|
|
2593
|
+
* ### Example
|
|
2594
|
+
*
|
|
2595
|
+
* ```ts
|
|
2596
|
+
* import { assertErr, assertOk, LowercasedString } from "@evolu/common";
|
|
2597
|
+
*
|
|
2598
|
+
* const text: LowercasedString = "hello world";
|
|
2599
|
+
* assertOk(LowercasedString.fromUnknown(text), text);
|
|
2600
|
+
* assertErr(LowercasedString.fromUnknown("Hello WORLD"));
|
|
2601
|
+
* assertOk(LowercasedString.fromUnknown(""), "");
|
|
1871
2602
|
* ```
|
|
1872
2603
|
*
|
|
1873
2604
|
* @group String
|
|
1874
2605
|
*/
|
|
1875
|
-
export const
|
|
1876
|
-
const [first = ""] = value;
|
|
1877
|
-
return value === first.toUpperCase() + value.slice(first.length)
|
|
1878
|
-
? ok()
|
|
1879
|
-
: err({ type: "Capitalized", value });
|
|
1880
|
-
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`);
|
|
2606
|
+
export const LowercasedString = /*#__PURE__*/ lowercased(String);
|
|
1881
2607
|
/**
|
|
1882
|
-
*
|
|
2608
|
+
* Lowercases the whole string and returns a {@link LowercasedString}.
|
|
2609
|
+
*
|
|
2610
|
+
* Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
|
|
2611
|
+
* without locale-specific rules. Changing case can change the length, so input
|
|
2612
|
+
* brands are not retained.
|
|
2613
|
+
*
|
|
2614
|
+
* ### Example
|
|
2615
|
+
*
|
|
2616
|
+
* ```ts
|
|
2617
|
+
* import { assertEqual, assertType, lowercase } from "@evolu/common";
|
|
2618
|
+
*
|
|
2619
|
+
* const text = lowercase("Hello WORLD");
|
|
2620
|
+
* assertEqual(text, "hello world");
|
|
2621
|
+
* assertType<typeof text, "hello world">();
|
|
2622
|
+
* assertEqual(lowercase(text), text);
|
|
2623
|
+
* ```
|
|
1883
2624
|
*
|
|
1884
2625
|
* @group String
|
|
1885
2626
|
*/
|
|
1886
|
-
export const
|
|
2627
|
+
export const lowercase = (value) => value.toLowerCase();
|
|
1887
2628
|
/**
|
|
1888
2629
|
* String {@link Brand} without surrounding whitespace.
|
|
1889
2630
|
*
|
|
@@ -1928,6 +2669,113 @@ export const TrimmedString = /*#__PURE__*/ trimmed(String);
|
|
|
1928
2669
|
* @group String
|
|
1929
2670
|
*/
|
|
1930
2671
|
export const trim = (value) => value.trim();
|
|
2672
|
+
/**
|
|
2673
|
+
* String {@link Brand} requiring an exact, case-sensitive prefix.
|
|
2674
|
+
*
|
|
2675
|
+
* Validation preserves the complete string, including the prefix. An empty
|
|
2676
|
+
* prefix accepts every string allowed by the parent Type. The prefix must be
|
|
2677
|
+
* one concrete string literal so different prefixes have distinct brands.
|
|
2678
|
+
*
|
|
2679
|
+
* ### Example
|
|
2680
|
+
*
|
|
2681
|
+
* ```ts
|
|
2682
|
+
* import {
|
|
2683
|
+
* assertEqual,
|
|
2684
|
+
* assertErr,
|
|
2685
|
+
* assertOk,
|
|
2686
|
+
* assertType,
|
|
2687
|
+
* maxLength,
|
|
2688
|
+
* startsWith,
|
|
2689
|
+
* String,
|
|
2690
|
+
* type Brand,
|
|
2691
|
+
* } from "@evolu/common";
|
|
2692
|
+
*
|
|
2693
|
+
* const EnvName = startsWith("APP_")(maxLength(64)(String));
|
|
2694
|
+
*
|
|
2695
|
+
* const name = EnvName.fromUnknown("APP_PORT");
|
|
2696
|
+
* assertOk(name, "APP_PORT");
|
|
2697
|
+
* assertType<
|
|
2698
|
+
* typeof name.value,
|
|
2699
|
+
* string & Brand<"MaxLength64"> & Brand<"StartsWithAPP_">
|
|
2700
|
+
* >();
|
|
2701
|
+
*
|
|
2702
|
+
* assertEqual(EnvName.to(name.value), "APP_PORT");
|
|
2703
|
+
*
|
|
2704
|
+
* assertErr(EnvName.fromUnknown("app_PORT"));
|
|
2705
|
+
* assertErr(EnvName.fromUnknown("APP_" + "X".repeat(61)));
|
|
2706
|
+
* ```
|
|
2707
|
+
*
|
|
2708
|
+
* @group String
|
|
2709
|
+
*/
|
|
2710
|
+
export const startsWith = (prefix) => {
|
|
2711
|
+
const name = `StartsWith${prefix}`;
|
|
2712
|
+
return (parent) => brand(name, parent, (value) => value.startsWith(prefix)
|
|
2713
|
+
? ok()
|
|
2714
|
+
: err({
|
|
2715
|
+
type: name,
|
|
2716
|
+
value,
|
|
2717
|
+
prefix,
|
|
2718
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must start with ${safelyStringifyUnknownValue(error.prefix)}.`);
|
|
2719
|
+
};
|
|
2720
|
+
/**
|
|
2721
|
+
* Decodes a prefixed string with another {@link Type} and restores the prefix
|
|
2722
|
+
* when encoding.
|
|
2723
|
+
*
|
|
2724
|
+
* Uses {@link startsWith} to validate an exact, case-sensitive prefix before
|
|
2725
|
+
* removing one occurrence. The wrapped Type validates and decodes the suffix;
|
|
2726
|
+
* its constraints apply to the suffix, and its Output is preserved. Encoding
|
|
2727
|
+
* prepends the prefix to the wrapped Type's canonical string representation. An
|
|
2728
|
+
* empty prefix leaves that representation unchanged.
|
|
2729
|
+
*
|
|
2730
|
+
* The prefix must be one concrete string literal. The wrapped Type must accept
|
|
2731
|
+
* a string Input and encode to strings; its Output can have another type, as
|
|
2732
|
+
* with {@link PortFromString}.
|
|
2733
|
+
*
|
|
2734
|
+
* ### Example
|
|
2735
|
+
*
|
|
2736
|
+
* ```ts
|
|
2737
|
+
* import {
|
|
2738
|
+
* assertEqual,
|
|
2739
|
+
* assertErr,
|
|
2740
|
+
* assertOk,
|
|
2741
|
+
* assertType,
|
|
2742
|
+
* ConstantCaseIdentifier,
|
|
2743
|
+
* prefixed,
|
|
2744
|
+
* PortFromString,
|
|
2745
|
+
* type Port,
|
|
2746
|
+
* } from "@evolu/common";
|
|
2747
|
+
*
|
|
2748
|
+
* const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
|
|
2749
|
+
*
|
|
2750
|
+
* const name = EnvName.fromUnknown("APP_PORT");
|
|
2751
|
+
* assertOk(name, "PORT");
|
|
2752
|
+
* assertType<typeof name.value, ConstantCaseIdentifier>();
|
|
2753
|
+
*
|
|
2754
|
+
* assertEqual(EnvName.to(name.value), "APP_PORT");
|
|
2755
|
+
*
|
|
2756
|
+
* assertErr(EnvName.fromUnknown("OTHER_PORT"));
|
|
2757
|
+
* assertErr(EnvName.fromUnknown("APP_port"));
|
|
2758
|
+
*
|
|
2759
|
+
* const PortSetting = prefixed("port:")(PortFromString);
|
|
2760
|
+
*
|
|
2761
|
+
* const port = PortSetting.fromUnknown("port:04000");
|
|
2762
|
+
* assertOk(port, 4000);
|
|
2763
|
+
* assertType<typeof port.value, Port>();
|
|
2764
|
+
*
|
|
2765
|
+
* assertEqual(PortSetting.to(port.value), "port:4000");
|
|
2766
|
+
* ```
|
|
2767
|
+
*
|
|
2768
|
+
* @group String
|
|
2769
|
+
*/
|
|
2770
|
+
export const prefixed = (prefix) => (type) => {
|
|
2771
|
+
// A literal prefix makes the generated names concrete and distinct.
|
|
2772
|
+
const source = startsWith(prefix)(String);
|
|
2773
|
+
return transform(`Prefixed${prefix}`, source, type, {
|
|
2774
|
+
// The Input guard permits any string; concatenation establishes the prefix brand.
|
|
2775
|
+
from: (value) => ok(value.slice(prefix.length)),
|
|
2776
|
+
to: (value) => `${prefix}${value}`,
|
|
2777
|
+
});
|
|
2778
|
+
};
|
|
1931
2779
|
/**
|
|
1932
2780
|
* Minimum-length {@link Brand} for values whose `length` is at least `min`.
|
|
1933
2781
|
*
|
|
@@ -2055,6 +2903,7 @@ export const length = (exact) => (parent) => {
|
|
|
2055
2903
|
* assertType<UrlSafeString, string & Brand<"UrlSafeString">>();
|
|
2056
2904
|
*
|
|
2057
2905
|
* assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
|
|
2906
|
+
*
|
|
2058
2907
|
* const invalid = UrlSafeString.fromUnknown("not safe");
|
|
2059
2908
|
* assertErr(invalid);
|
|
2060
2909
|
* assertType(Data, invalid.error);
|
|
@@ -2183,7 +3032,13 @@ export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(byte
|
|
|
2183
3032
|
*/
|
|
2184
3033
|
export const base64UrlToUint8Array = (value) => base64UrlStringToUint8Array(value);
|
|
2185
3034
|
/**
|
|
2186
|
-
* A non-empty URL-safe
|
|
3035
|
+
* A non-empty file-system-safe and URL-safe token of at most 64 UTF-16 code
|
|
3036
|
+
* units.
|
|
3037
|
+
*
|
|
3038
|
+
* Evolu uses it for database file names, storage pool names, and log prefixes.
|
|
3039
|
+
* It accepts the {@link UrlSafeString} alphabet in any order, so it may start
|
|
3040
|
+
* with a digit, `-`, or `_`. It is not a language identifier; for
|
|
3041
|
+
* word-structured names, use {@link identifier}.
|
|
2187
3042
|
*
|
|
2188
3043
|
* @group String
|
|
2189
3044
|
*/
|
|
@@ -2643,6 +3498,7 @@ export const PositiveFiniteNumber = /*#__PURE__*/ positive(NonNegativeFiniteNumb
|
|
|
2643
3498
|
* assertType<Int, number & Brand<"Int">>();
|
|
2644
3499
|
*
|
|
2645
3500
|
* assertOk(Int.fromUnknown(42), 42);
|
|
3501
|
+
*
|
|
2646
3502
|
* const invalid = Int.fromUnknown(1.5);
|
|
2647
3503
|
* assertErr(invalid);
|
|
2648
3504
|
* assertType(Data, invalid.error);
|
|
@@ -2682,6 +3538,49 @@ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
|
|
|
2682
3538
|
* @group Number
|
|
2683
3539
|
*/
|
|
2684
3540
|
export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
|
|
3541
|
+
/**
|
|
3542
|
+
* Transforms a decimal integer string into an {@link Int}.
|
|
3543
|
+
*
|
|
3544
|
+
* This is useful for inputs that carry numbers as text, such as environment
|
|
3545
|
+
* variables, URL query parameters, and form fields. The string must consist of
|
|
3546
|
+
* an optional minus sign and digits; the {@link Int} constraint then rejects
|
|
3547
|
+
* values outside the safe integer range.
|
|
3548
|
+
*
|
|
3549
|
+
* ### Example
|
|
3550
|
+
*
|
|
3551
|
+
* ```ts
|
|
3552
|
+
* import {
|
|
3553
|
+
* assertEqual,
|
|
3554
|
+
* assertErr,
|
|
3555
|
+
* assertOk,
|
|
3556
|
+
* assertSame,
|
|
3557
|
+
* IntFromString,
|
|
3558
|
+
* } from "@evolu/common";
|
|
3559
|
+
*
|
|
3560
|
+
* assertOk(IntFromString.fromUnknown("4000"), 4000);
|
|
3561
|
+
* assertOk(IntFromString.fromUnknown("-1"), -1);
|
|
3562
|
+
* assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
|
|
3563
|
+
*
|
|
3564
|
+
* const negativeZero = IntFromString.orThrow("-0");
|
|
3565
|
+
* assertSame(negativeZero, -0);
|
|
3566
|
+
* assertEqual(IntFromString.to(negativeZero), "-0");
|
|
3567
|
+
*
|
|
3568
|
+
* const invalid = IntFromString.fromUnknown("4000.5");
|
|
3569
|
+
* assertErr(invalid, { type: "IntFromString", value: "4000.5" });
|
|
3570
|
+
* assertEqual(
|
|
3571
|
+
* IntFromString.formatError(invalid.error),
|
|
3572
|
+
* 'The value "4000.5" is not a decimal integer.',
|
|
3573
|
+
* );
|
|
3574
|
+
* ```
|
|
3575
|
+
*
|
|
3576
|
+
* @group Number
|
|
3577
|
+
*/
|
|
3578
|
+
export const IntFromString = /*#__PURE__*/ transform("IntFromString", String, Int, {
|
|
3579
|
+
from: (value) => /^-?\d+$/u.test(value)
|
|
3580
|
+
? ok(globalThis.Number(value))
|
|
3581
|
+
: err({ type: "IntFromString", value }),
|
|
3582
|
+
to: (value) => globalThis.Object.is(value, -0) ? "-0" : globalThis.String(value),
|
|
3583
|
+
}, (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a decimal integer.`);
|
|
2685
3584
|
/**
|
|
2686
3585
|
* Minimum {@link PositiveInt} value.
|
|
2687
3586
|
*
|
|
@@ -2805,6 +3704,55 @@ export const lessThanOrEqualTo = (max) => (parent) => {
|
|
|
2805
3704
|
? ok()
|
|
2806
3705
|
: err({ type: name, value, max }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be less than or equal to ${error.max}.`);
|
|
2807
3706
|
};
|
|
3707
|
+
/**
|
|
3708
|
+
* A TCP or UDP port as an integer from zero through 65535, inclusive.
|
|
3709
|
+
*
|
|
3710
|
+
* When binding a server, zero requests an automatically assigned port. Use
|
|
3711
|
+
* {@link PortFromString} for configuration values supplied as text.
|
|
3712
|
+
*
|
|
3713
|
+
* ### Example
|
|
3714
|
+
*
|
|
3715
|
+
* ```ts
|
|
3716
|
+
* import { assertErr, assertOk, Port } from "@evolu/common";
|
|
3717
|
+
*
|
|
3718
|
+
* assertOk(Port.fromUnknown(0), 0);
|
|
3719
|
+
* assertOk(Port.fromUnknown(4000), 4000);
|
|
3720
|
+
* assertOk(Port.fromUnknown(65535), 65535);
|
|
3721
|
+
* assertErr(Port.fromUnknown(-1));
|
|
3722
|
+
* assertErr(Port.fromUnknown(65536));
|
|
3723
|
+
* assertErr(Port.fromUnknown(4000.5));
|
|
3724
|
+
* ```
|
|
3725
|
+
*
|
|
3726
|
+
* @group Number
|
|
3727
|
+
*/
|
|
3728
|
+
export const Port = /*#__PURE__*/ brand("Port",
|
|
3729
|
+
/*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt));
|
|
3730
|
+
/**
|
|
3731
|
+
* Parses a decimal integer string and validates it as a {@link Port}.
|
|
3732
|
+
*
|
|
3733
|
+
* Uses {@link IntFromString} for decimal parsing, including its rejection of
|
|
3734
|
+
* whitespace, plus signs, fractions, and exponent notation.
|
|
3735
|
+
*
|
|
3736
|
+
* ### Example
|
|
3737
|
+
*
|
|
3738
|
+
* ```ts
|
|
3739
|
+
* import {
|
|
3740
|
+
* assertEqual,
|
|
3741
|
+
* assertErr,
|
|
3742
|
+
* assertOk,
|
|
3743
|
+
* PortFromString,
|
|
3744
|
+
* } from "@evolu/common";
|
|
3745
|
+
*
|
|
3746
|
+
* assertOk(PortFromString.fromUnknown("0"), 0);
|
|
3747
|
+
* assertOk(PortFromString.fromUnknown("4000"), 4000);
|
|
3748
|
+
* assertErr(PortFromString.fromUnknown("65536"));
|
|
3749
|
+
* assertErr(PortFromString.fromUnknown("http"));
|
|
3750
|
+
* assertEqual(PortFromString.to(PortFromString.orThrow("04000")), "4000");
|
|
3751
|
+
* ```
|
|
3752
|
+
*
|
|
3753
|
+
* @group Number
|
|
3754
|
+
*/
|
|
3755
|
+
export const PortFromString = /*#__PURE__*/ transform("PortFromString", IntFromString, Port, { from: ok, to: identity });
|
|
2808
3756
|
/**
|
|
2809
3757
|
* Finite {@link Number} from zero to one, inclusive.
|
|
2810
3758
|
*
|
|
@@ -3033,6 +3981,7 @@ export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPosi
|
|
|
3033
3981
|
* >();
|
|
3034
3982
|
*
|
|
3035
3983
|
* assertOk(Tenths.fromUnknown(0.3), 0.3);
|
|
3984
|
+
*
|
|
3036
3985
|
* const invalid = Tenths.fromUnknown(0.31);
|
|
3037
3986
|
* assertErr(invalid);
|
|
3038
3987
|
* assertType(Data, invalid.error);
|
|
@@ -3152,6 +4101,7 @@ export const between = (min, max) => (parent) => {
|
|
|
3152
4101
|
*
|
|
3153
4102
|
* const UserId = brand("UserId", String);
|
|
3154
4103
|
* const UserIds = array(UserId);
|
|
4104
|
+
*
|
|
3155
4105
|
* const result = UserIds.from.parent(["ada", "grace"]);
|
|
3156
4106
|
*
|
|
3157
4107
|
* assertType<
|
|
@@ -3160,6 +4110,7 @@ export const between = (min, max) => (parent) => {
|
|
|
3160
4110
|
* >();
|
|
3161
4111
|
* assertOk(result, ["ada", "grace"]);
|
|
3162
4112
|
* assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
|
|
4113
|
+
*
|
|
3163
4114
|
* const invalid = UserIds.fromUnknown("ada");
|
|
3164
4115
|
* assertErr(invalid);
|
|
3165
4116
|
* assertType(Data, invalid.error);
|
|
@@ -3731,25 +4682,25 @@ const createTupleType = (typeElements) => {
|
|
|
3731
4682
|
};
|
|
3732
4683
|
const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
|
|
3733
4684
|
/**
|
|
3734
|
-
* Decimal
|
|
4685
|
+
* Decimal integer string from `"0"` to `"9"`.
|
|
3735
4686
|
*
|
|
3736
4687
|
* @group String
|
|
3737
4688
|
*/
|
|
3738
4689
|
export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
3739
4690
|
/**
|
|
3740
|
-
* Decimal
|
|
4691
|
+
* Decimal integer string from `"1"` to `"9"`.
|
|
3741
4692
|
*
|
|
3742
4693
|
* @group String
|
|
3743
4694
|
*/
|
|
3744
4695
|
export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
3745
4696
|
/**
|
|
3746
|
-
* Decimal string from `"1"` to `"6"`.
|
|
4697
|
+
* Decimal integer string from `"1"` to `"6"`.
|
|
3747
4698
|
*
|
|
3748
4699
|
* @group String
|
|
3749
4700
|
*/
|
|
3750
4701
|
export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
|
|
3751
4702
|
/**
|
|
3752
|
-
* Decimal string from `"1"` to `"23"
|
|
4703
|
+
* Decimal integer string from `"1"` to `"23"`, without a leading zero.
|
|
3753
4704
|
*
|
|
3754
4705
|
* @group String
|
|
3755
4706
|
*/
|
|
@@ -3757,7 +4708,7 @@ export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
|
|
|
3757
4708
|
/*#__PURE__*/ templateLiteral("1", Digit),
|
|
3758
4709
|
/*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")));
|
|
3759
4710
|
/**
|
|
3760
|
-
* Decimal string from `"1"` to `"51"
|
|
4711
|
+
* Decimal integer string from `"1"` to `"51"`, without a leading zero.
|
|
3761
4712
|
*
|
|
3762
4713
|
* @group String
|
|
3763
4714
|
*/
|
|
@@ -3765,14 +4716,14 @@ export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
|
|
|
3765
4716
|
/*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
3766
4717
|
/*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
|
|
3767
4718
|
/**
|
|
3768
|
-
* Decimal string from `"1"` to `"99"
|
|
4719
|
+
* Decimal integer string from `"1"` to `"99"`, without a leading zero.
|
|
3769
4720
|
*
|
|
3770
4721
|
* @group String
|
|
3771
4722
|
*/
|
|
3772
4723
|
export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
|
|
3773
4724
|
/*#__PURE__*/ templateLiteral(Digit1To9, Digit));
|
|
3774
4725
|
/**
|
|
3775
|
-
* Decimal string from `"1"` to `"59"
|
|
4726
|
+
* Decimal integer string from `"1"` to `"59"`, without a leading zero.
|
|
3776
4727
|
*
|
|
3777
4728
|
* @group String
|
|
3778
4729
|
*/
|
|
@@ -4026,6 +4977,7 @@ export { _Object as Object };
|
|
|
4026
4977
|
* const valueType = typeof value;
|
|
4027
4978
|
*
|
|
4028
4979
|
* assertEqual(valueType, "function");
|
|
4980
|
+
*
|
|
4029
4981
|
* const called = trySync(
|
|
4030
4982
|
* () => {
|
|
4031
4983
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -4061,6 +5013,7 @@ export { _Object as Object };
|
|
|
4061
5013
|
* const valueType = typeof value;
|
|
4062
5014
|
*
|
|
4063
5015
|
* assertEqual(valueType, "function");
|
|
5016
|
+
*
|
|
4064
5017
|
* const called = trySync(
|
|
4065
5018
|
* () => {
|
|
4066
5019
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -4279,16 +5232,116 @@ const validateRecordEntries = (input, validateKey, validateValue, options) => {
|
|
|
4279
5232
|
* @group Objects
|
|
4280
5233
|
*/
|
|
4281
5234
|
export const optional = (type) => createOptionalProperty(type);
|
|
4282
|
-
const optionalPropertySymbol =
|
|
5235
|
+
const optionalPropertySymbol =
|
|
5236
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
4283
5237
|
const createOptionalProperty = (type) => ({
|
|
4284
5238
|
type,
|
|
4285
5239
|
[optionalPropertySymbol]: true,
|
|
4286
5240
|
});
|
|
5241
|
+
export function withDefault(property, value, { strategy } = {}) {
|
|
5242
|
+
assert(strategy === undefined || strategy === "preserve", 'withDefault strategy must be omitted or "preserve".');
|
|
5243
|
+
const optionalInput = optionalPropertySymbol in property;
|
|
5244
|
+
const type = (optionalInput ? property.type : property);
|
|
5245
|
+
assert(type.is(value), `withDefault value must be an Output of ${type.name}.`);
|
|
5246
|
+
const originals = new Set([
|
|
5247
|
+
...(optionalInput ? ["missing"] : []),
|
|
5248
|
+
...(type.is(null) ? ["null"] : []),
|
|
5249
|
+
...(type.is(undefined) ? ["undefined"] : []),
|
|
5250
|
+
]);
|
|
5251
|
+
const supplied = createRootType("DefaultSupplied", (candidate) => (strategy === undefined && globalThis.Object.is(candidate, value)) ||
|
|
5252
|
+
(candidate !== null && candidate !== undefined && type.is(candidate))
|
|
5253
|
+
? ok(candidate)
|
|
5254
|
+
: err({ type: "DefaultValue" }), () => "The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.");
|
|
5255
|
+
const defaultValue = createRootType("DefaultValue", (candidate) => type.is(candidate) &&
|
|
5256
|
+
(globalThis.Object.is(candidate, value) ||
|
|
5257
|
+
(Data.is(value) && Data.is(candidate) && eqData(candidate, value)))
|
|
5258
|
+
? ok(candidate)
|
|
5259
|
+
: err({ type: "DefaultValue" }), () => "A preserved default must equal the configured default value.");
|
|
5260
|
+
const output = (strategy === undefined
|
|
5261
|
+
? supplied
|
|
5262
|
+
: discriminatedUnion("defaultUsed", object({ value: supplied, defaultUsed: literal(false) }), object({
|
|
5263
|
+
value: defaultValue,
|
|
5264
|
+
defaultUsed: literal(true),
|
|
5265
|
+
original: createRootType("DefaultOriginal", (candidate) => typeof candidate === "string" && originals.has(candidate)
|
|
5266
|
+
? ok(candidate)
|
|
5267
|
+
: err({ type: "DefaultOriginal" }), () => "The original absence must be handled by this default declaration."),
|
|
5268
|
+
})));
|
|
5269
|
+
const ownGetTypeIssues = output[getRuntimeTypeIssuesSymbol];
|
|
5270
|
+
const outputWithIssues = {
|
|
5271
|
+
...output,
|
|
5272
|
+
[outputValidationSymbol]: (value, options) => {
|
|
5273
|
+
const result = output[outputValidationSymbol](value, options);
|
|
5274
|
+
return result.ok
|
|
5275
|
+
? result
|
|
5276
|
+
: err({ type: "WithDefault", outputError: result.error });
|
|
5277
|
+
},
|
|
5278
|
+
[getRuntimeTypeIssuesSymbol]: (error, mode) => error.type === "WithDefault" && "outputError" in error
|
|
5279
|
+
? ownGetTypeIssues(error.outputError, mode)
|
|
5280
|
+
: type[getRuntimeTypeIssuesSymbol](error, mode),
|
|
5281
|
+
};
|
|
5282
|
+
const operations = {
|
|
5283
|
+
output: outputWithIssues,
|
|
5284
|
+
createObject: createDefaultObjectType,
|
|
5285
|
+
partial: (type) => createOptionalProperty(strategy === "preserve"
|
|
5286
|
+
? withDefault(type, value, {
|
|
5287
|
+
strategy,
|
|
5288
|
+
})
|
|
5289
|
+
: withDefault(type, value)),
|
|
5290
|
+
decode: (candidate) => {
|
|
5291
|
+
const original = candidate === missingDefaultValue
|
|
5292
|
+
? "missing"
|
|
5293
|
+
: candidate === null
|
|
5294
|
+
? "null"
|
|
5295
|
+
: candidate === undefined
|
|
5296
|
+
? "undefined"
|
|
5297
|
+
: undefined;
|
|
5298
|
+
const effective = original === undefined ? candidate : value;
|
|
5299
|
+
return strategy === undefined
|
|
5300
|
+
? effective
|
|
5301
|
+
: original === undefined
|
|
5302
|
+
? { value: effective, defaultUsed: false }
|
|
5303
|
+
: { value: effective, defaultUsed: true, original };
|
|
5304
|
+
},
|
|
5305
|
+
encode: (candidate) => {
|
|
5306
|
+
if (strategy === undefined)
|
|
5307
|
+
return candidate;
|
|
5308
|
+
const preserved = candidate;
|
|
5309
|
+
if (!preserved.defaultUsed)
|
|
5310
|
+
return preserved.value;
|
|
5311
|
+
switch (preserved.original) {
|
|
5312
|
+
case "missing":
|
|
5313
|
+
return missingDefaultValue;
|
|
5314
|
+
case "null":
|
|
5315
|
+
return null;
|
|
5316
|
+
case "undefined":
|
|
5317
|
+
return undefined;
|
|
5318
|
+
}
|
|
5319
|
+
},
|
|
5320
|
+
};
|
|
5321
|
+
if (optionalInput) {
|
|
5322
|
+
return {
|
|
5323
|
+
type,
|
|
5324
|
+
value,
|
|
5325
|
+
strategy: strategy ?? "replace",
|
|
5326
|
+
[defaultPropertySymbol]: operations,
|
|
5327
|
+
};
|
|
5328
|
+
}
|
|
5329
|
+
const fromOwn = (value) => ok(operations.decode(value));
|
|
5330
|
+
const fromParent = mapRuntimeOperations(type[fromSymbol], (operation) => (value, options) => flatMapResult(operation(value, options), fromOwn));
|
|
5331
|
+
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]);
|
|
5332
|
+
}
|
|
5333
|
+
const defaultPropertySymbol =
|
|
5334
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
5335
|
+
const missingDefaultValue = /*#__PURE__*/ globalThis.Symbol();
|
|
4287
5336
|
export function object(props, recordType) {
|
|
4288
5337
|
return createObjectType(snapshotObjectProps(props), recordType);
|
|
4289
5338
|
}
|
|
4290
5339
|
const createObjectType = (props, recordType) => {
|
|
4291
5340
|
const runtimeProps = props;
|
|
5341
|
+
const defaultProperty = globalThis.Object.values(runtimeProps).find(isDefaultProperty);
|
|
5342
|
+
if (defaultProperty) {
|
|
5343
|
+
return defaultProperty[defaultPropertySymbol].createObject(props, recordType);
|
|
5344
|
+
}
|
|
4292
5345
|
const keys = globalThis.Object.keys(runtimeProps);
|
|
4293
5346
|
const validate = (value, options, exactOutput) => {
|
|
4294
5347
|
if (value === null || typeof value !== "object") {
|
|
@@ -4598,6 +5651,57 @@ const createObjectType = (props, recordType) => {
|
|
|
4598
5651
|
}
|
|
4599
5652
|
: { props: runtimeProps });
|
|
4600
5653
|
};
|
|
5654
|
+
const createDefaultObjectType = (props, recordType) => {
|
|
5655
|
+
const inputProps = createMutableRecord();
|
|
5656
|
+
const outputProps = createMutableRecord();
|
|
5657
|
+
const defaults = new Map();
|
|
5658
|
+
for (const [key, property] of globalThis.Object.entries(props)) {
|
|
5659
|
+
if (isDefaultProperty(property)) {
|
|
5660
|
+
const defaultProperty = property;
|
|
5661
|
+
inputProps[key] = createOptionalProperty(defaultProperty.type);
|
|
5662
|
+
outputProps[key] = defaultProperty[defaultPropertySymbol].output;
|
|
5663
|
+
defaults.set(key, defaultProperty[defaultPropertySymbol]);
|
|
5664
|
+
}
|
|
5665
|
+
else {
|
|
5666
|
+
inputProps[key] = property;
|
|
5667
|
+
const type = objectPropertyToType(property);
|
|
5668
|
+
const output = createRootType(type.name, type[outputValidationSymbol], type.formatError, type[getRuntimeTypeIssuesSymbol]);
|
|
5669
|
+
outputProps[key] =
|
|
5670
|
+
optionalPropertySymbol in property
|
|
5671
|
+
? createOptionalProperty(output)
|
|
5672
|
+
: output;
|
|
5673
|
+
}
|
|
5674
|
+
}
|
|
5675
|
+
const source = createObjectType(inputProps, recordType);
|
|
5676
|
+
// The object parent validates only its input structure and root value Types.
|
|
5677
|
+
const parent = getTerminalRuntimeNode(source);
|
|
5678
|
+
const output = createObjectType(outputProps, recordType);
|
|
5679
|
+
const finish = (value) => {
|
|
5680
|
+
const result = createMutableRecord(value);
|
|
5681
|
+
for (const [key, operations] of defaults) {
|
|
5682
|
+
result[key] = operations.decode(globalThis.Object.hasOwn(result, key)
|
|
5683
|
+
? result[key]
|
|
5684
|
+
: missingDefaultValue);
|
|
5685
|
+
}
|
|
5686
|
+
return ok(result);
|
|
5687
|
+
};
|
|
5688
|
+
const fromInput = getTerminalRuntimeNode(source[fromSymbol]);
|
|
5689
|
+
const fromParent = (value, options) => flatMapResult(fromInput(value, options), finish);
|
|
5690
|
+
const to = (value) => {
|
|
5691
|
+
const result = createMutableRecord(value);
|
|
5692
|
+
for (const [key, operations] of defaults) {
|
|
5693
|
+
const original = operations.encode(result[key]);
|
|
5694
|
+
if (original === missingDefaultValue)
|
|
5695
|
+
delete result[key];
|
|
5696
|
+
else
|
|
5697
|
+
result[key] = original;
|
|
5698
|
+
}
|
|
5699
|
+
return source[encoderSymbol](result);
|
|
5700
|
+
};
|
|
5701
|
+
return createTypeNode("Object", parent, (value, options) => flatMapResult(source.fromUnknown(value, options), finish), output.is, output[outputValidationSymbol], createFromOperation(fromParent), to, createObjectRuntimeTypeIssues(source.formatError, props, recordType), recordType
|
|
5702
|
+
? { props, record: recordType }
|
|
5703
|
+
: { props });
|
|
5704
|
+
};
|
|
4601
5705
|
// Read descriptors instead of spreading so accessors are not invoked,
|
|
4602
5706
|
// non-enumerable declarations are retained, and later mutations are isolated.
|
|
4603
5707
|
const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
|
|
@@ -4613,7 +5717,12 @@ const snapshotObjectProps = (props, runtimeProps = createMutableRecord()) => {
|
|
|
4613
5717
|
return runtimeProps;
|
|
4614
5718
|
};
|
|
4615
5719
|
const isOptionalProperty = (property) => optionalPropertySymbol in property;
|
|
4616
|
-
const
|
|
5720
|
+
const isDefaultProperty = (property) => defaultPropertySymbol in property;
|
|
5721
|
+
const objectPropertyToType = (property) => isDefaultProperty(property)
|
|
5722
|
+
? property[defaultPropertySymbol].output
|
|
5723
|
+
: isOptionalProperty(property)
|
|
5724
|
+
? property.type
|
|
5725
|
+
: property;
|
|
4617
5726
|
const createRecordPropertyError = (issue) => ({
|
|
4618
5727
|
type: "Record",
|
|
4619
5728
|
reason: { kind: "Entries", issues: [issue] },
|
|
@@ -4622,7 +5731,8 @@ const createRecordPropertyError = (issue) => ({
|
|
|
4622
5731
|
* Object {@link Type} with every property optional.
|
|
4623
5732
|
*
|
|
4624
5733
|
* No property is required, but every present property must still satisfy its
|
|
4625
|
-
* Type.
|
|
5734
|
+
* Type. For {@link withDefault} properties, this disables the missing-property
|
|
5735
|
+
* default; defaults for present `null` or `undefined` values still apply.
|
|
4626
5736
|
*
|
|
4627
5737
|
* ### Example
|
|
4628
5738
|
*
|
|
@@ -4650,9 +5760,11 @@ export const partial = (props, ..._validation) => {
|
|
|
4650
5760
|
const partialProps = createMutableRecord();
|
|
4651
5761
|
for (const key of globalThis.Object.keys(source)) {
|
|
4652
5762
|
const property = source[key];
|
|
4653
|
-
partialProps[key] =
|
|
4654
|
-
? property
|
|
4655
|
-
:
|
|
5763
|
+
partialProps[key] = isDefaultProperty(property)
|
|
5764
|
+
? property[defaultPropertySymbol].partial(property.type)
|
|
5765
|
+
: isOptionalProperty(property)
|
|
5766
|
+
? property
|
|
5767
|
+
: createOptionalProperty(property);
|
|
4656
5768
|
}
|
|
4657
5769
|
return createObjectType(partialProps);
|
|
4658
5770
|
};
|
|
@@ -4693,7 +5805,7 @@ export const nullableToOptional = (props, ..._validation) => {
|
|
|
4693
5805
|
const optionalProps = createMutableRecord();
|
|
4694
5806
|
for (const key of globalThis.Object.keys(source)) {
|
|
4695
5807
|
const property = source[key];
|
|
4696
|
-
if (isOptionalProperty(property)) {
|
|
5808
|
+
if (isOptionalProperty(property) || isDefaultProperty(property)) {
|
|
4697
5809
|
optionalProps[key] = property;
|
|
4698
5810
|
continue;
|
|
4699
5811
|
}
|
|
@@ -4732,6 +5844,101 @@ export const omit = (objectType, ...keys) => {
|
|
|
4732
5844
|
}
|
|
4733
5845
|
return createObjectType(props, runtimeObjectType.record);
|
|
4734
5846
|
};
|
|
5847
|
+
/**
|
|
5848
|
+
* Renames a strict {@link object} Type's encoded keys using a string codec.
|
|
5849
|
+
*
|
|
5850
|
+
* The object declaration uses semantic keys. Each key must be a valid Output of
|
|
5851
|
+
* the key Type. Its canonical encoding becomes the external property name.
|
|
5852
|
+
* Decoding accepts those exact names; aliases and unknown properties are
|
|
5853
|
+
* rejected. Values, optionality, and semantic Output stay with the object Type.
|
|
5854
|
+
* Only the outer keys change. Nested field codecs keep their own behavior.
|
|
5855
|
+
*
|
|
5856
|
+
* Construction rejects invalid schema keys, duplicate encodings, and key codecs
|
|
5857
|
+
* that do not decode their encodings back to the declared keys. These are
|
|
5858
|
+
* schema mistakes; invalid external values return normal Type errors. Error
|
|
5859
|
+
* paths use the external names, including missing required properties.
|
|
5860
|
+
*
|
|
5861
|
+
* ### Example
|
|
5862
|
+
*
|
|
5863
|
+
* ```ts
|
|
5864
|
+
* import {
|
|
5865
|
+
* assertEqual,
|
|
5866
|
+
* assertErr,
|
|
5867
|
+
* assertOk,
|
|
5868
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
5869
|
+
* object,
|
|
5870
|
+
* objectKeys,
|
|
5871
|
+
* optional,
|
|
5872
|
+
* PortFromString,
|
|
5873
|
+
* prefixed,
|
|
5874
|
+
* } from "@evolu/common";
|
|
5875
|
+
*
|
|
5876
|
+
* const Key = prefixed("APP_")(
|
|
5877
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
5878
|
+
* );
|
|
5879
|
+
* const Settings = objectKeys(Key)(
|
|
5880
|
+
* object({ port: optional(PortFromString) }),
|
|
5881
|
+
* );
|
|
5882
|
+
*
|
|
5883
|
+
* const result = Settings.fromUnknown({ APP_PORT: "04000" });
|
|
5884
|
+
* assertOk(result, { port: 4000 });
|
|
5885
|
+
*
|
|
5886
|
+
* assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
|
|
5887
|
+
*
|
|
5888
|
+
* assertErr(Settings.fromUnknown({ APP_POTR: "4000" }));
|
|
5889
|
+
* ```
|
|
5890
|
+
*
|
|
5891
|
+
* @group Objects
|
|
5892
|
+
*/
|
|
5893
|
+
export const objectKeys = (key) => (type) => {
|
|
5894
|
+
const runtimeKey = key;
|
|
5895
|
+
const runtimeOutput = type;
|
|
5896
|
+
const props = createMutableRecord();
|
|
5897
|
+
const outputKeyByInputKey = new Map();
|
|
5898
|
+
const inputKeyByOutputKey = new Map();
|
|
5899
|
+
for (const outputKey of globalThis.Object.keys(type.props)) {
|
|
5900
|
+
assert(runtimeKey.is(outputKey), `Invalid object schema key ${safelyStringifyUnknownValue(outputKey)} for ${key.name}.`);
|
|
5901
|
+
const inputKey = runtimeKey.to(outputKey);
|
|
5902
|
+
assert(!outputKeyByInputKey.has(inputKey), `Duplicate encoded object key ${safelyStringifyUnknownValue(inputKey)}.`);
|
|
5903
|
+
const decoded = runtimeKey.fromUnknown(inputKey, firstValidationOptions);
|
|
5904
|
+
assert(decoded.ok && decoded.value === outputKey, "An object key codec must decode its encoding to the declared key.");
|
|
5905
|
+
props[inputKey] = type.props[outputKey];
|
|
5906
|
+
outputKeyByInputKey.set(inputKey, outputKey);
|
|
5907
|
+
inputKeyByOutputKey.set(outputKey, inputKey);
|
|
5908
|
+
}
|
|
5909
|
+
const input = createObjectType(props);
|
|
5910
|
+
const fromUnknown = (value, options = firstValidationOptions) => {
|
|
5911
|
+
const result = input.fromUnknown(value, options);
|
|
5912
|
+
if (!result.ok)
|
|
5913
|
+
return err({ type: "ObjectKeys", error: result.error });
|
|
5914
|
+
const output = createMutableRecord();
|
|
5915
|
+
for (const [inputKey, value] of globalThis.Object.entries(result.value)) {
|
|
5916
|
+
output[outputKeyByInputKey.get(inputKey)] = value;
|
|
5917
|
+
}
|
|
5918
|
+
return ok(output);
|
|
5919
|
+
};
|
|
5920
|
+
const to = (value) => {
|
|
5921
|
+
const encoded = runtimeOutput[encoderSymbol](value);
|
|
5922
|
+
const input = createMutableRecord();
|
|
5923
|
+
for (const [key, value] of globalThis.Object.entries(encoded)) {
|
|
5924
|
+
input[inputKeyByOutputKey.get(key)] = value;
|
|
5925
|
+
}
|
|
5926
|
+
return input;
|
|
5927
|
+
};
|
|
5928
|
+
const validateOutput = (value, options) => {
|
|
5929
|
+
const result = runtimeOutput[outputValidationSymbol](value, options);
|
|
5930
|
+
return result.ok
|
|
5931
|
+
? result
|
|
5932
|
+
: err({ type: "ObjectKeys", outputError: result.error });
|
|
5933
|
+
};
|
|
5934
|
+
const getTypeIssues = (error, mode) => {
|
|
5935
|
+
if ("outputError" in error) {
|
|
5936
|
+
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
|
|
5937
|
+
}
|
|
5938
|
+
return input[getRuntimeTypeIssuesSymbol](error.error, mode);
|
|
5939
|
+
};
|
|
5940
|
+
return createTypeNode("ObjectKeys", Unknown, fromUnknown, runtimeOutput.is, validateOutput, createFromOperation(fromUnknown), to, getTypeIssues, { key, output: type });
|
|
5941
|
+
};
|
|
4735
5942
|
export function result(okType, errorType) {
|
|
4736
5943
|
return discriminatedUnion("ok", createObjectType({
|
|
4737
5944
|
ok: literal(true),
|
|
@@ -5746,7 +6953,7 @@ export const json = (type, name, ..._validation) => {
|
|
|
5746
6953
|
const BrandedJson = {
|
|
5747
6954
|
...typeNode,
|
|
5748
6955
|
from,
|
|
5749
|
-
orThrow:
|
|
6956
|
+
orThrow: createRuntimeOrThrow(fromInput, typeNode.formatError),
|
|
5750
6957
|
orNull: mapRuntimeResult(fromInput, getOrNull),
|
|
5751
6958
|
};
|
|
5752
6959
|
return [
|