@evolu/common 8.8.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/Assert.d.ts +21 -1
- package/dist/src/Assert.d.ts.map +1 -1
- package/dist/src/Assert.js +31 -27
- 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/Polyfills.d.ts +6 -0
- package/dist/src/Polyfills.d.ts.map +1 -1
- package/dist/src/Polyfills.js +6 -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/Assert.test.ts +51 -0
- package/src/Assert.ts +56 -4
- 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/Polyfills.ts +6 -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/src/Type.ts
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
|
|
@@ -461,17 +461,23 @@ import {
|
|
|
461
461
|
import { assert, assertNonNullable } from "./Assert.ts";
|
|
462
462
|
import type { Brand } from "./Brand.ts";
|
|
463
463
|
import type { RandomBytesDep } from "./Crypto.ts";
|
|
464
|
+
import { eqData } from "./Eq.ts";
|
|
464
465
|
import { identity, type Thunk } from "./Function.ts";
|
|
465
|
-
import {
|
|
466
|
+
import {
|
|
467
|
+
createMutableRecord,
|
|
468
|
+
getObjectKind,
|
|
469
|
+
isPlainObject,
|
|
470
|
+
type ReadonlyRecord,
|
|
471
|
+
} from "./Object.ts";
|
|
466
472
|
import { hasNodeBuffer } from "./Platform.ts";
|
|
467
473
|
import {
|
|
468
474
|
err,
|
|
469
475
|
flatMapResult,
|
|
470
476
|
getOk,
|
|
471
477
|
getOrNull,
|
|
472
|
-
getOrThrow,
|
|
473
478
|
ok,
|
|
474
479
|
trySync,
|
|
480
|
+
type getOrThrow,
|
|
475
481
|
type Result,
|
|
476
482
|
} from "./Result.ts";
|
|
477
483
|
import { safelyStringifyUnknownValue } from "./String.ts";
|
|
@@ -767,15 +773,22 @@ export interface Type<
|
|
|
767
773
|
: (value: Output) => CanonicalInput;
|
|
768
774
|
|
|
769
775
|
/**
|
|
770
|
-
*
|
|
771
|
-
*
|
|
776
|
+
* Runs the deepest `from` operation, which accepts this Type's `Input`, and
|
|
777
|
+
* returns its decoded value or throws an Error.
|
|
778
|
+
*
|
|
779
|
+
* Returned validation errors use this Type's {@link Type.formatError} as the
|
|
780
|
+
* Error message and preserve the original validation error as `cause`. With
|
|
781
|
+
* `{ errors: "all" }`, the cause retains all collected errors; the message
|
|
782
|
+
* still follows `formatError`, which describes the first issue.
|
|
772
783
|
*
|
|
773
784
|
* The typed `Input` boundary is asserted before the remaining pipeline runs.
|
|
774
|
-
* A boundary violation is a bug, so it throws directly
|
|
775
|
-
*
|
|
785
|
+
* A boundary violation is a bug, so it throws directly with its assertion
|
|
786
|
+
* message instead of formatting a returned validation error.
|
|
776
787
|
*
|
|
777
788
|
* `Type.orThrow.parent(value)` does not exist. To throw after starting from a
|
|
778
|
-
* typed boundary, call
|
|
789
|
+
* typed boundary, call {@link getOrThrow} with the corresponding `from`
|
|
790
|
+
* operation. Its generic Error message differs from this operation's
|
|
791
|
+
* message.
|
|
779
792
|
*
|
|
780
793
|
* Use `orThrow` for startup and configuration, module constants, test
|
|
781
794
|
* fixtures, and internal invariants where failure must stop the current flow.
|
|
@@ -787,20 +800,31 @@ export interface Type<
|
|
|
787
800
|
* ```ts
|
|
788
801
|
* import {
|
|
789
802
|
* assertEqual,
|
|
790
|
-
*
|
|
803
|
+
* assertInstanceOf,
|
|
804
|
+
* assertErr,
|
|
791
805
|
* minLength,
|
|
792
806
|
* String,
|
|
807
|
+
* trySync,
|
|
793
808
|
* } from "@evolu/common";
|
|
794
809
|
*
|
|
795
810
|
* const NonEmptyString = minLength(1)(String);
|
|
796
811
|
*
|
|
797
812
|
* const value = NonEmptyString.orThrow("Evolu");
|
|
798
813
|
*
|
|
799
|
-
* // Equivalent because `from.parent` is this Type's deepest `from` operation:
|
|
800
|
-
* const sameValue = getOrThrow(NonEmptyString.from.parent("Evolu"));
|
|
801
|
-
*
|
|
802
814
|
* assertEqual(value, "Evolu");
|
|
803
|
-
*
|
|
815
|
+
*
|
|
816
|
+
* const failed = trySync(() => NonEmptyString.orThrow(""));
|
|
817
|
+
* assertErr(failed);
|
|
818
|
+
* assertInstanceOf(failed.error, Error);
|
|
819
|
+
* assertEqual(
|
|
820
|
+
* failed.error.message,
|
|
821
|
+
* 'The value "" does not meet the minimum length of 1.',
|
|
822
|
+
* );
|
|
823
|
+
* assertEqual(failed.error.cause, {
|
|
824
|
+
* type: "MinLength1",
|
|
825
|
+
* value: "",
|
|
826
|
+
* min: 1,
|
|
827
|
+
* });
|
|
804
828
|
* ```
|
|
805
829
|
*/
|
|
806
830
|
readonly orThrow: TypeOperationFn<"orThrow", Input, Output, never>;
|
|
@@ -900,6 +924,61 @@ export type TypeErrorFormatter<Error extends TypeError> = (
|
|
|
900
924
|
error: Error,
|
|
901
925
|
) => string;
|
|
902
926
|
|
|
927
|
+
/**
|
|
928
|
+
* A formatted validation issue located by its path from the root value.
|
|
929
|
+
*
|
|
930
|
+
* @group Core
|
|
931
|
+
*/
|
|
932
|
+
export interface TypeIssue {
|
|
933
|
+
readonly path: ReadonlyArray<PropertyKey>;
|
|
934
|
+
readonly message: string;
|
|
935
|
+
}
|
|
936
|
+
|
|
937
|
+
/**
|
|
938
|
+
* Converts an error from a {@link Type} into formatted issues with paths.
|
|
939
|
+
*
|
|
940
|
+
* Pass the Type that produced the error. This uses its nested and localized
|
|
941
|
+
* formatters without validating the input again. To retain every issue, decode
|
|
942
|
+
* with `{ errors: "all" }`; this function cannot recover errors omitted during
|
|
943
|
+
* validation. A root issue has an empty path.
|
|
944
|
+
*
|
|
945
|
+
* ### Example
|
|
946
|
+
*
|
|
947
|
+
* ```ts
|
|
948
|
+
* import {
|
|
949
|
+
* assertEqual,
|
|
950
|
+
* assertErr,
|
|
951
|
+
* IntFromString,
|
|
952
|
+
* object,
|
|
953
|
+
* typeErrorToIssues,
|
|
954
|
+
* } from "@evolu/common";
|
|
955
|
+
*
|
|
956
|
+
* const Settings = object({ port: IntFromString });
|
|
957
|
+
*
|
|
958
|
+
* const result = Settings.fromUnknown({ port: "http" }, { errors: "all" });
|
|
959
|
+
* assertErr(result);
|
|
960
|
+
*
|
|
961
|
+
* assertEqual(typeErrorToIssues(Settings, result.error), [
|
|
962
|
+
* {
|
|
963
|
+
* path: ["port"],
|
|
964
|
+
* message: 'The value "http" is not a decimal integer.',
|
|
965
|
+
* },
|
|
966
|
+
* ]);
|
|
967
|
+
* ```
|
|
968
|
+
*
|
|
969
|
+
* @group Core
|
|
970
|
+
*/
|
|
971
|
+
export const typeErrorToIssues = <T extends TypeNode>(
|
|
972
|
+
type: T,
|
|
973
|
+
error: InferErrors<NoInfer<T>>,
|
|
974
|
+
): NonEmptyReadonlyArray<TypeIssue> => {
|
|
975
|
+
const runtimeType = type as unknown as RuntimeTypeNode;
|
|
976
|
+
return runtimeType[getRuntimeTypeIssuesSymbol](error, "all").map((issue) => ({
|
|
977
|
+
path: issue.path,
|
|
978
|
+
message: formatRuntimeTypeIssue(issue),
|
|
979
|
+
})) as unknown as NonEmptyReadonlyArray<TypeIssue>;
|
|
980
|
+
};
|
|
981
|
+
|
|
903
982
|
/**
|
|
904
983
|
* The common structural shape of every {@link Type}, with its specific type
|
|
905
984
|
* parameters erased.
|
|
@@ -950,6 +1029,11 @@ interface RuntimeTypeIssue {
|
|
|
950
1029
|
readonly error: TypeError;
|
|
951
1030
|
readonly path: ReadonlyArray<PropertyKey>;
|
|
952
1031
|
readonly formatError: TypeErrorFormatter<TypeError>;
|
|
1032
|
+
readonly alternatives?: ReadonlyArray<{
|
|
1033
|
+
readonly index: number;
|
|
1034
|
+
readonly name: TypeName;
|
|
1035
|
+
readonly issues: NonEmptyReadonlyArray<RuntimeTypeIssue>;
|
|
1036
|
+
}>;
|
|
953
1037
|
}
|
|
954
1038
|
|
|
955
1039
|
type RuntimeGetTypeIssues = (
|
|
@@ -1033,6 +1117,34 @@ const createCollectionRuntimeTypeIssues =
|
|
|
1033
1117
|
const formatDefaultRuntimeTypeIssue: RuntimeFormatTypeIssue = (issue) =>
|
|
1034
1118
|
issue.formatError(issue.error);
|
|
1035
1119
|
|
|
1120
|
+
const formatRuntimeTypeIssue = (
|
|
1121
|
+
issue: RuntimeTypeIssue,
|
|
1122
|
+
formatIssue: RuntimeFormatTypeIssue = formatDefaultRuntimeTypeIssue,
|
|
1123
|
+
): string => {
|
|
1124
|
+
const summary = formatIssue(issue);
|
|
1125
|
+
if (issue.alternatives === undefined) return summary;
|
|
1126
|
+
|
|
1127
|
+
return [
|
|
1128
|
+
summary,
|
|
1129
|
+
...issue.alternatives.flatMap(({ index, name, issues }) =>
|
|
1130
|
+
issues.map((issue) => {
|
|
1131
|
+
const path = issue.path
|
|
1132
|
+
.map((key) =>
|
|
1133
|
+
typeof key === "string"
|
|
1134
|
+
? `[${JSON.stringify(key)}]`
|
|
1135
|
+
: `[${globalThis.String(key)}]`,
|
|
1136
|
+
)
|
|
1137
|
+
.join("");
|
|
1138
|
+
const message = formatRuntimeTypeIssue(issue, formatIssue).replaceAll(
|
|
1139
|
+
"\n",
|
|
1140
|
+
"\n ",
|
|
1141
|
+
);
|
|
1142
|
+
return `- ${index}: ${name}${path}: ${message}`;
|
|
1143
|
+
}),
|
|
1144
|
+
),
|
|
1145
|
+
].join("\n");
|
|
1146
|
+
};
|
|
1147
|
+
|
|
1036
1148
|
/**
|
|
1037
1149
|
* Asserts type equality or validates a value with a {@link Type}.
|
|
1038
1150
|
*
|
|
@@ -1134,6 +1246,11 @@ const assertTypeOutput = <Error extends TypeError>(
|
|
|
1134
1246
|
* that produced them. Different localized Type sets can coexist in separate
|
|
1135
1247
|
* application or dependency-injection scopes.
|
|
1136
1248
|
*
|
|
1249
|
+
* A Union formatter supplies the summary of the failure. Retained member
|
|
1250
|
+
* failures are appended using their own localized formatters, with member
|
|
1251
|
+
* indexes, Type names, and paths identifying each alternative. The Union still
|
|
1252
|
+
* produces one issue at its enclosing path.
|
|
1253
|
+
*
|
|
1137
1254
|
* Localization is scoped to the selected Types instead of a package-wide
|
|
1138
1255
|
* translation registry. Static imports give bundlers an explicit dependency
|
|
1139
1256
|
* graph, so unrelated Types, locales, and formatters can be removed. Bundling
|
|
@@ -1319,11 +1436,24 @@ const withFormatError = (
|
|
|
1319
1436
|
localizedTypeBySource,
|
|
1320
1437
|
)
|
|
1321
1438
|
: null;
|
|
1439
|
+
const localizeIssue = (issue: RuntimeTypeIssue): RuntimeTypeIssue => ({
|
|
1440
|
+
...issue,
|
|
1441
|
+
formatError: () => formatIssue(issue),
|
|
1442
|
+
...(issue.alternatives === undefined
|
|
1443
|
+
? {}
|
|
1444
|
+
: {
|
|
1445
|
+
alternatives: issue.alternatives.map((alternative) => ({
|
|
1446
|
+
...alternative,
|
|
1447
|
+
issues: alternative.issues.map(
|
|
1448
|
+
localizeIssue,
|
|
1449
|
+
) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>,
|
|
1450
|
+
})),
|
|
1451
|
+
}),
|
|
1452
|
+
});
|
|
1322
1453
|
const getTypeIssues: RuntimeGetTypeIssues = (error, mode) =>
|
|
1323
|
-
source[getRuntimeTypeIssuesSymbol](error, mode).map(
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
})) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
|
|
1454
|
+
source[getRuntimeTypeIssuesSymbol](error, mode).map(
|
|
1455
|
+
localizeIssue,
|
|
1456
|
+
) as unknown as NonEmptyReadonlyArray<RuntimeTypeIssue>;
|
|
1327
1457
|
const derived = createTypeNode<RuntimeTypeNode>(
|
|
1328
1458
|
source.name,
|
|
1329
1459
|
parent,
|
|
@@ -1342,7 +1472,10 @@ const withFormatError = (
|
|
|
1342
1472
|
// boundary. Private operation chains on derived still support composition.
|
|
1343
1473
|
globalThis.Object.assign(derived, {
|
|
1344
1474
|
from: source.from,
|
|
1345
|
-
orThrow:
|
|
1475
|
+
orThrow: createRuntimeOrThrow(
|
|
1476
|
+
getTerminalRuntimeNode(source.from),
|
|
1477
|
+
derived.formatError,
|
|
1478
|
+
),
|
|
1346
1479
|
orNull: source.orNull,
|
|
1347
1480
|
});
|
|
1348
1481
|
|
|
@@ -1396,11 +1529,12 @@ const localizeTypeReflection = (
|
|
|
1396
1529
|
) as Record<string | symbol, unknown>;
|
|
1397
1530
|
|
|
1398
1531
|
for (const key of Reflect.ownKeys(value)) {
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1532
|
+
const property = (value as Record<string | symbol, unknown>)[key];
|
|
1533
|
+
// Configured defaults are opaque data whose identity must be preserved.
|
|
1534
|
+
localized[key] =
|
|
1535
|
+
key === "value" && globalThis.Object.hasOwn(value, defaultPropertySymbol)
|
|
1536
|
+
? property
|
|
1537
|
+
: localizeTypeReflection(property, formatIssue, localizedTypeBySource);
|
|
1404
1538
|
}
|
|
1405
1539
|
|
|
1406
1540
|
return localized;
|
|
@@ -1678,7 +1812,8 @@ type ReflectedTypes<T extends TypeNode> =
|
|
|
1678
1812
|
readonly props: infer Props extends ObjectProps;
|
|
1679
1813
|
readonly record?: infer Rest;
|
|
1680
1814
|
}
|
|
1681
|
-
?
|
|
1815
|
+
? | DefaultableObjectPropertyType<Props[keyof Props]>
|
|
1816
|
+
| Extract<Rest, TypeNode>
|
|
1682
1817
|
: never;
|
|
1683
1818
|
|
|
1684
1819
|
type RootType<T extends TypeNode> = T extends {
|
|
@@ -1845,7 +1980,42 @@ type ValidateParent<T extends ConcreteTypeNode> =
|
|
|
1845
1980
|
"Parent must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes."
|
|
1846
1981
|
>;
|
|
1847
1982
|
|
|
1848
|
-
|
|
1983
|
+
/**
|
|
1984
|
+
* Requires one concrete output {@link Type} node in a factory parameter.
|
|
1985
|
+
*
|
|
1986
|
+
* Rejects TypeScript unions of Type nodes. A {@link union} Type is one concrete
|
|
1987
|
+
* node and remains valid. Use an intersection with the inferred parameter type
|
|
1988
|
+
* to preserve its specific Type. This guard performs no runtime validation.
|
|
1989
|
+
*
|
|
1990
|
+
* ### Example
|
|
1991
|
+
*
|
|
1992
|
+
* ```ts
|
|
1993
|
+
* import {
|
|
1994
|
+
* assertSame,
|
|
1995
|
+
* Number,
|
|
1996
|
+
* String,
|
|
1997
|
+
* union,
|
|
1998
|
+
* type AnyType,
|
|
1999
|
+
* type ValidateOutput,
|
|
2000
|
+
* } from "@evolu/common";
|
|
2001
|
+
*
|
|
2002
|
+
* const defineOutput = <T extends AnyType>(
|
|
2003
|
+
* type: T & ValidateOutput<T>,
|
|
2004
|
+
* ): T => type;
|
|
2005
|
+
*
|
|
2006
|
+
* assertSame(defineOutput(String), String);
|
|
2007
|
+
*
|
|
2008
|
+
* const Value = union(String, Number);
|
|
2009
|
+
* assertSame(defineOutput(Value), Value);
|
|
2010
|
+
*
|
|
2011
|
+
* const uncertain = String as typeof String | typeof Number;
|
|
2012
|
+
* // @ts-expect-error Output Type must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
2013
|
+
* defineOutput(uncertain);
|
|
2014
|
+
* ```
|
|
2015
|
+
*
|
|
2016
|
+
* @group Construction
|
|
2017
|
+
*/
|
|
2018
|
+
export type ValidateOutput<T extends AnyType> =
|
|
1849
2019
|
IsUnion<T> extends false
|
|
1850
2020
|
? T
|
|
1851
2021
|
: CompileTimeError<
|
|
@@ -1927,6 +2097,8 @@ type ConcreteChildTypeNameError = CompileTimeError<
|
|
|
1927
2097
|
* its error's `type` must equal that name and must not duplicate an inherited
|
|
1928
2098
|
* error type. An infallible child has no own error to format.
|
|
1929
2099
|
*
|
|
2100
|
+
* Use {@link createTypeWithError} to wrap an existing validator's errors.
|
|
2101
|
+
*
|
|
1930
2102
|
* ### Example
|
|
1931
2103
|
*
|
|
1932
2104
|
* A root Type for a custom external value category:
|
|
@@ -1957,6 +2129,7 @@ type ConcreteChildTypeNameError = CompileTimeError<
|
|
|
1957
2129
|
* );
|
|
1958
2130
|
*
|
|
1959
2131
|
* assertOk(Text.fromUnknown("Evolu"), "Evolu");
|
|
2132
|
+
*
|
|
1960
2133
|
* const invalid = Text.fromUnknown(42);
|
|
1961
2134
|
* assertErr(invalid);
|
|
1962
2135
|
* assertType(Data, invalid.error);
|
|
@@ -2051,10 +2224,8 @@ export function createType(
|
|
|
2051
2224
|
}
|
|
2052
2225
|
|
|
2053
2226
|
const assertRefinementIdentity =
|
|
2054
|
-
(
|
|
2055
|
-
|
|
2056
|
-
): ((value: unknown) => Result<unknown, TypeError>) =>
|
|
2057
|
-
(value) => {
|
|
2227
|
+
(refinement: (value: unknown) => Result<unknown, TypeError>) =>
|
|
2228
|
+
(value: unknown): Result<unknown, TypeError> => {
|
|
2058
2229
|
const result = refinement(value);
|
|
2059
2230
|
if (result.ok) {
|
|
2060
2231
|
assert(
|
|
@@ -2065,6 +2236,70 @@ const assertRefinementIdentity =
|
|
|
2065
2236
|
return result;
|
|
2066
2237
|
};
|
|
2067
2238
|
|
|
2239
|
+
/**
|
|
2240
|
+
* Creates a root {@link Type} with a custom error for an existing validator.
|
|
2241
|
+
*
|
|
2242
|
+
* The source must use identity encoding. The new Type accepts its Output as
|
|
2243
|
+
* Input and hides its parent boundaries. Validation and error collection are
|
|
2244
|
+
* delegated automatically; the mapper receives the failure and original value.
|
|
2245
|
+
* The formatter presents the mapped error as one issue.
|
|
2246
|
+
*
|
|
2247
|
+
* ### Example
|
|
2248
|
+
*
|
|
2249
|
+
* ```ts
|
|
2250
|
+
* import {
|
|
2251
|
+
* assertEqual,
|
|
2252
|
+
* assertErr,
|
|
2253
|
+
* createTypeWithError,
|
|
2254
|
+
* Number,
|
|
2255
|
+
* String,
|
|
2256
|
+
* union,
|
|
2257
|
+
* type TypeError,
|
|
2258
|
+
* type UnionError,
|
|
2259
|
+
* } from "@evolu/common";
|
|
2260
|
+
*
|
|
2261
|
+
* interface ValueError extends TypeError<"Value"> {
|
|
2262
|
+
* readonly cause: UnionError;
|
|
2263
|
+
* }
|
|
2264
|
+
*
|
|
2265
|
+
* const Value = createTypeWithError(
|
|
2266
|
+
* "Value",
|
|
2267
|
+
* union(String, Number),
|
|
2268
|
+
* (cause): ValueError => ({ type: "Value", cause }),
|
|
2269
|
+
* () => "Enter text or a number.",
|
|
2270
|
+
* );
|
|
2271
|
+
*
|
|
2272
|
+
* const result = Value.fromUnknown(false, { errors: "all" });
|
|
2273
|
+
* assertErr(result);
|
|
2274
|
+
* assertEqual(result.error.cause.errors.length, 2);
|
|
2275
|
+
* assertEqual(Value.formatError(result.error), "Enter text or a number.");
|
|
2276
|
+
* ```
|
|
2277
|
+
*
|
|
2278
|
+
* @group Construction
|
|
2279
|
+
*/
|
|
2280
|
+
export const createTypeWithError = <
|
|
2281
|
+
Name extends TypeName,
|
|
2282
|
+
T extends ConcreteTypeNode,
|
|
2283
|
+
Error extends TypeError<Name>,
|
|
2284
|
+
>(
|
|
2285
|
+
name: Name & ValidateConcreteTypeName<Name>,
|
|
2286
|
+
type: T &
|
|
2287
|
+
ValidateOutput<T> &
|
|
2288
|
+
(IdentityEncodingOf<T> extends true
|
|
2289
|
+
? unknown
|
|
2290
|
+
: CompileTimeError<"Type", "Source Type must use identity encoding.">),
|
|
2291
|
+
mapError: (error: InferErrors<T>, value: unknown) => Error,
|
|
2292
|
+
formatError: TypeErrorFormatter<NoInfer<Error>>,
|
|
2293
|
+
): Type<Name, T["Output"], T["Output"], Error> =>
|
|
2294
|
+
createRootType<Name, T["Output"], Error>(
|
|
2295
|
+
name,
|
|
2296
|
+
(value, options) => {
|
|
2297
|
+
const result = type.fromUnknown(value, options);
|
|
2298
|
+
return result.ok ? result : err(mapError(result.error, value));
|
|
2299
|
+
},
|
|
2300
|
+
formatError,
|
|
2301
|
+
);
|
|
2302
|
+
|
|
2068
2303
|
const createRootType = <Name extends TypeName, Output, Error extends TypeError>(
|
|
2069
2304
|
name: Name,
|
|
2070
2305
|
fromUnknown: (
|
|
@@ -2581,6 +2816,16 @@ const mapRuntimeResult =
|
|
|
2581
2816
|
(value: never, options = firstValidationOptions) =>
|
|
2582
2817
|
map(operation(value, options), options);
|
|
2583
2818
|
|
|
2819
|
+
const createRuntimeOrThrow = (
|
|
2820
|
+
fromInput: RuntimeOperation<Result<unknown, TypeError>>,
|
|
2821
|
+
formatError: TypeErrorFormatter<TypeError>,
|
|
2822
|
+
): RuntimeOperation<unknown> =>
|
|
2823
|
+
mapRuntimeResult(fromInput, (result) => {
|
|
2824
|
+
if (result.ok) return result.value;
|
|
2825
|
+
|
|
2826
|
+
throw new Error(formatError(result.error), { cause: result.error });
|
|
2827
|
+
});
|
|
2828
|
+
|
|
2584
2829
|
// `map` must return a fresh operation because this function can attach `.parent`.
|
|
2585
2830
|
const mapRuntimeOperations = <Input, Output>(
|
|
2586
2831
|
operation: RuntimeOperation<Input>,
|
|
@@ -2666,7 +2911,8 @@ const createTypeNode = <Node extends TypeNode = TypeNode>(
|
|
|
2666
2911
|
const runtimeFormatError: TypeErrorFormatter<TypeError> =
|
|
2667
2912
|
runtimeParent?.[getRuntimeTypeIssuesSymbol] === getTypeIssues
|
|
2668
2913
|
? runtimeParent.formatError
|
|
2669
|
-
: (error) =>
|
|
2914
|
+
: (error) =>
|
|
2915
|
+
formatRuntimeTypeIssue(getTypeIssues(error, "first")[0], formatIssue);
|
|
2670
2916
|
const typedFrom = addRuntimeAssertions(
|
|
2671
2917
|
name,
|
|
2672
2918
|
is,
|
|
@@ -2690,7 +2936,7 @@ const createTypeNode = <Node extends TypeNode = TypeNode>(
|
|
|
2690
2936
|
is,
|
|
2691
2937
|
from: typedFrom,
|
|
2692
2938
|
to: typedTo,
|
|
2693
|
-
orThrow:
|
|
2939
|
+
orThrow: createRuntimeOrThrow(fromInput, runtimeFormatError),
|
|
2694
2940
|
orNull: mapRuntimeResult(fromInput, getOrNull),
|
|
2695
2941
|
"~standard": createStandardSchemaProps(
|
|
2696
2942
|
fromUnknown,
|
|
@@ -2724,7 +2970,7 @@ const createStandardSchemaProps = (
|
|
|
2724
2970
|
? { value: result.value }
|
|
2725
2971
|
: {
|
|
2726
2972
|
issues: getTypeIssues(result.error, "all").map((issue) => ({
|
|
2727
|
-
message:
|
|
2973
|
+
message: formatRuntimeTypeIssue(issue, formatIssue),
|
|
2728
2974
|
path: issue.path,
|
|
2729
2975
|
})),
|
|
2730
2976
|
};
|
|
@@ -2940,6 +3186,7 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
|
|
|
2940
3186
|
* >();
|
|
2941
3187
|
*
|
|
2942
3188
|
* assertOk(Age.fromUnknown(122), 122);
|
|
3189
|
+
*
|
|
2943
3190
|
* const invalid = Age.fromUnknown(200);
|
|
2944
3191
|
* assertErr(invalid);
|
|
2945
3192
|
* assertType(Data, invalid.error);
|
|
@@ -2968,6 +3215,64 @@ export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
|
|
|
2968
3215
|
*/
|
|
2969
3216
|
export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
|
|
2970
3217
|
|
|
3218
|
+
/**
|
|
3219
|
+
* Error returned when a string is neither `true` nor `false`.
|
|
3220
|
+
*
|
|
3221
|
+
* @group Base
|
|
3222
|
+
*/
|
|
3223
|
+
export interface BooleanFromStringError extends TypeError<"BooleanFromString"> {
|
|
3224
|
+
readonly value: string;
|
|
3225
|
+
}
|
|
3226
|
+
|
|
3227
|
+
/**
|
|
3228
|
+
* Transforms a boolean spelled as text into a {@link Boolean}.
|
|
3229
|
+
*
|
|
3230
|
+
* This is useful for inputs that carry booleans as text, such as environment
|
|
3231
|
+
* variables, URL query parameters, and form fields. Exactly `true` and `false`
|
|
3232
|
+
* are accepted, the spellings JSON and JavaScript use, so a boolean has one
|
|
3233
|
+
* representation in every source.
|
|
3234
|
+
*
|
|
3235
|
+
* ### Example
|
|
3236
|
+
*
|
|
3237
|
+
* ```ts
|
|
3238
|
+
* import {
|
|
3239
|
+
* assertEqual,
|
|
3240
|
+
* assertErr,
|
|
3241
|
+
* assertOk,
|
|
3242
|
+
* BooleanFromString,
|
|
3243
|
+
* } from "@evolu/common";
|
|
3244
|
+
*
|
|
3245
|
+
* assertOk(BooleanFromString.fromUnknown("true"), true);
|
|
3246
|
+
* assertOk(BooleanFromString.fromUnknown("false"), false);
|
|
3247
|
+
* assertEqual(BooleanFromString.to(true), "true");
|
|
3248
|
+
*
|
|
3249
|
+
* const invalid = BooleanFromString.fromUnknown("yes");
|
|
3250
|
+
* assertErr(invalid, { type: "BooleanFromString", value: "yes" });
|
|
3251
|
+
* assertEqual(
|
|
3252
|
+
* BooleanFromString.formatError(invalid.error),
|
|
3253
|
+
* 'The value "yes" is not a boolean. Use true or false.',
|
|
3254
|
+
* );
|
|
3255
|
+
* ```
|
|
3256
|
+
*
|
|
3257
|
+
* @group Base
|
|
3258
|
+
*/
|
|
3259
|
+
export const BooleanFromString = /*#__PURE__*/ transform(
|
|
3260
|
+
"BooleanFromString",
|
|
3261
|
+
String,
|
|
3262
|
+
Boolean,
|
|
3263
|
+
{
|
|
3264
|
+
from: (value): Result<boolean, BooleanFromStringError> =>
|
|
3265
|
+
value === "true"
|
|
3266
|
+
? ok(true)
|
|
3267
|
+
: value === "false"
|
|
3268
|
+
? ok(false)
|
|
3269
|
+
: err({ type: "BooleanFromString", value }),
|
|
3270
|
+
to: (value) => (value ? "true" : "false"),
|
|
3271
|
+
},
|
|
3272
|
+
(error) =>
|
|
3273
|
+
`The value ${safelyStringifyUnknownValue(error.value)} is not a boolean. Use true or false.`,
|
|
3274
|
+
);
|
|
3275
|
+
|
|
2971
3276
|
/**
|
|
2972
3277
|
* A JavaScript symbol {@link Type}.
|
|
2973
3278
|
*
|
|
@@ -3142,6 +3447,7 @@ export function objectTag<Name extends keyof ObjectTagOutputByName>(
|
|
|
3142
3447
|
* "TaggedValue",
|
|
3143
3448
|
* instanceOf(TaggedValue),
|
|
3144
3449
|
* );
|
|
3450
|
+
*
|
|
3145
3451
|
* const value = new TaggedValue();
|
|
3146
3452
|
* const result = TaggedValueType.fromUnknown(value);
|
|
3147
3453
|
*
|
|
@@ -3344,6 +3650,40 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
|
|
|
3344
3650
|
"Constructor must preserve one concrete constructor. Create a Union Type from separate Instance Types instead of passing a union or erased constructor."
|
|
3345
3651
|
>;
|
|
3346
3652
|
|
|
3653
|
+
/**
|
|
3654
|
+
* Requires one concrete literal type in a factory parameter.
|
|
3655
|
+
*
|
|
3656
|
+
* Preserves an exact literal and produces a compile-time error for widened,
|
|
3657
|
+
* union, branded, or open template literal types. Use an intersection with the
|
|
3658
|
+
* inferred parameter type, as {@link prefixed} does. This guard performs no
|
|
3659
|
+
* runtime validation.
|
|
3660
|
+
*
|
|
3661
|
+
* ### Example
|
|
3662
|
+
*
|
|
3663
|
+
* ```ts
|
|
3664
|
+
* import { assertType, type ValidateLiteral } from "@evolu/common";
|
|
3665
|
+
*
|
|
3666
|
+
* const definePrefix = <Prefix extends string>(
|
|
3667
|
+
* prefix: Prefix & ValidateLiteral<Prefix>,
|
|
3668
|
+
* ): Prefix => prefix;
|
|
3669
|
+
*
|
|
3670
|
+
* const prefix = definePrefix("APP_");
|
|
3671
|
+
* assertType<typeof prefix, "APP_">();
|
|
3672
|
+
*
|
|
3673
|
+
* const widened: string = "APP_";
|
|
3674
|
+
* // @ts-expect-error Expected must be one concrete literal value.
|
|
3675
|
+
* definePrefix(widened);
|
|
3676
|
+
* ```
|
|
3677
|
+
*
|
|
3678
|
+
* @group Unions
|
|
3679
|
+
*/
|
|
3680
|
+
export type ValidateLiteral<Expected extends Literal> =
|
|
3681
|
+
IsUnion<Expected> extends false
|
|
3682
|
+
? {} extends Readonly<Record<`${Expected}`, never>>
|
|
3683
|
+
? LiteralCompileTimeError
|
|
3684
|
+
: Expected
|
|
3685
|
+
: LiteralCompileTimeError;
|
|
3686
|
+
|
|
3347
3687
|
/**
|
|
3348
3688
|
* Literal {@link Type}.
|
|
3349
3689
|
*
|
|
@@ -3372,6 +3712,7 @@ type InstanceConstructorCompileTimeError = CompileTimeError<
|
|
|
3372
3712
|
*
|
|
3373
3713
|
* assertType<typeof Ready.Output, "ready">();
|
|
3374
3714
|
* assertOk(Ready.fromUnknown("ready"), "ready");
|
|
3715
|
+
*
|
|
3375
3716
|
* const invalid = Ready.fromUnknown("pending");
|
|
3376
3717
|
* assertErr(invalid);
|
|
3377
3718
|
* assertType(Data, invalid.error);
|
|
@@ -3463,13 +3804,6 @@ type LiteralParentErrors<Expected extends Literal> =
|
|
|
3463
3804
|
? InferErrors<Parent>
|
|
3464
3805
|
: never;
|
|
3465
3806
|
|
|
3466
|
-
type ValidateLiteral<Expected extends Literal> =
|
|
3467
|
-
IsUnion<Expected> extends false
|
|
3468
|
-
? {} extends Readonly<Record<`${Expected}`, never>>
|
|
3469
|
-
? LiteralCompileTimeError
|
|
3470
|
-
: Expected
|
|
3471
|
-
: LiteralCompileTimeError;
|
|
3472
|
-
|
|
3473
3807
|
type LiteralCompileTimeError = CompileTimeError<
|
|
3474
3808
|
"Type",
|
|
3475
3809
|
"Expected must be one concrete literal value."
|
|
@@ -3523,6 +3857,11 @@ export const Null = /*#__PURE__*/ literal(null);
|
|
|
3523
3857
|
* errors: "all" }` to retain every member failure and collect nested errors
|
|
3524
3858
|
* within each member.
|
|
3525
3859
|
*
|
|
3860
|
+
* Formatting keeps one Union issue at the enclosing path and appends the
|
|
3861
|
+
* retained failures below its summary. Member indexes identify alternatives,
|
|
3862
|
+
* not positions in the input. Nested member paths appear in the message.
|
|
3863
|
+
* Formatting never retries validation or recovers discarded failures.
|
|
3864
|
+
*
|
|
3526
3865
|
* Member order matters when multiple members accept the same value: validation
|
|
3527
3866
|
* and encoding use the first matching member. When member Inputs overlap,
|
|
3528
3867
|
* decoding the value emitted by the first member selected for an Output must
|
|
@@ -3611,10 +3950,7 @@ export function union(
|
|
|
3611
3950
|
inputMembers,
|
|
3612
3951
|
(member, value, options) => member[outputValidationSymbol](value, options),
|
|
3613
3952
|
);
|
|
3614
|
-
const
|
|
3615
|
-
"A value does not match any allowed variant.") as TypeErrorFormatter<TypeError>;
|
|
3616
|
-
const getTypeIssues: RuntimeGetTypeIssues = (error) =>
|
|
3617
|
-
singleRuntimeTypeIssue("Union", error, defaultFormatter);
|
|
3953
|
+
const getTypeIssues = createUnionRuntimeTypeIssues(members);
|
|
3618
3954
|
const input = createTypeNode<
|
|
3619
3955
|
UnionInputType<unknown, UnionErrorValue<TypeError>>
|
|
3620
3956
|
>(
|
|
@@ -3625,7 +3961,7 @@ export function union(
|
|
|
3625
3961
|
inputValidateOutput,
|
|
3626
3962
|
ok,
|
|
3627
3963
|
identity,
|
|
3628
|
-
|
|
3964
|
+
createUnionRuntimeTypeIssues(inputMembers),
|
|
3629
3965
|
);
|
|
3630
3966
|
const fromUnknown = createUnionValidation(members, (member, value, options) =>
|
|
3631
3967
|
member.fromUnknown(value, options),
|
|
@@ -3667,6 +4003,24 @@ export function union(
|
|
|
3667
4003
|
);
|
|
3668
4004
|
}
|
|
3669
4005
|
|
|
4006
|
+
const createUnionRuntimeTypeIssues =
|
|
4007
|
+
(members: ReadonlyArray<RuntimeTypeNode>): RuntimeGetTypeIssues =>
|
|
4008
|
+
(error) => [
|
|
4009
|
+
{
|
|
4010
|
+
name: "Union",
|
|
4011
|
+
error,
|
|
4012
|
+
path: [],
|
|
4013
|
+
formatError: () => "A value does not match any allowed variant.",
|
|
4014
|
+
alternatives: (error as UnionErrorValue<TypeError>).errors.map(
|
|
4015
|
+
({ index, error }) => ({
|
|
4016
|
+
index,
|
|
4017
|
+
name: members[index].name,
|
|
4018
|
+
issues: members[index][getRuntimeTypeIssuesSymbol](error, "all"),
|
|
4019
|
+
}),
|
|
4020
|
+
),
|
|
4021
|
+
},
|
|
4022
|
+
];
|
|
4023
|
+
|
|
3670
4024
|
const createUnionValidation =
|
|
3671
4025
|
(
|
|
3672
4026
|
members: ReadonlyArray<RuntimeTypeNode>,
|
|
@@ -4024,6 +4378,7 @@ interface UnionErrorValue<
|
|
|
4024
4378
|
* assertOk(result, ["cs", "CZ"]);
|
|
4025
4379
|
* const locale = result.value;
|
|
4026
4380
|
* assertType<typeof locale, SupportedLocale>();
|
|
4381
|
+
*
|
|
4027
4382
|
* const invalid = SupportedLocale.fromUnknown("cs/CZ");
|
|
4028
4383
|
* assertErr(invalid);
|
|
4029
4384
|
* assertType(Data, invalid.error);
|
|
@@ -4892,6 +5247,7 @@ const getTemplateLiteralPartFraming = (
|
|
|
4892
5247
|
* }
|
|
4893
5248
|
*
|
|
4894
5249
|
* assertOk(Int64.fromUnknown(42n), 42n);
|
|
5250
|
+
*
|
|
4895
5251
|
* const invalid = Int64.fromUnknown(2n ** 63n);
|
|
4896
5252
|
* assertErr(invalid);
|
|
4897
5253
|
* assertType(Data, invalid.error);
|
|
@@ -4990,6 +5346,7 @@ export interface BrandType<
|
|
|
4990
5346
|
*
|
|
4991
5347
|
* const value = "2023-01-01T12:00:00.000Z";
|
|
4992
5348
|
* assertOk(DateIso.fromUnknown(value), value);
|
|
5349
|
+
*
|
|
4993
5350
|
* const invalid = DateIso.fromUnknown("2023-01-01");
|
|
4994
5351
|
* assertErr(invalid);
|
|
4995
5352
|
* assertType(Data, invalid.error);
|
|
@@ -5164,6 +5521,7 @@ export interface UInt64Error extends TypeError<"UInt64"> {
|
|
|
5164
5521
|
* }
|
|
5165
5522
|
*
|
|
5166
5523
|
* assertOk(TrimmedString.fromUnknown("Evolu"), "Evolu");
|
|
5524
|
+
*
|
|
5167
5525
|
* const invalid = TrimmedString.fromUnknown(" Evolu");
|
|
5168
5526
|
* assertErr(invalid);
|
|
5169
5527
|
* assertType(Data, invalid.error);
|
|
@@ -5260,144 +5618,1147 @@ type BrandFactoryNumberError = CompileTimeError<
|
|
|
5260
5618
|
>;
|
|
5261
5619
|
|
|
5262
5620
|
/**
|
|
5263
|
-
*
|
|
5621
|
+
* Naming conventions accepted by {@link identifier}.
|
|
5622
|
+
*
|
|
5623
|
+
* @group String
|
|
5624
|
+
*/
|
|
5625
|
+
export type IdentifierCasing =
|
|
5626
|
+
"camelCase" | "PascalCase" | "snake_case" | "kebab-case" | "CONSTANT_CASE";
|
|
5627
|
+
|
|
5628
|
+
/**
|
|
5629
|
+
* Brand names of the Types that {@link identifier} creates, by casing.
|
|
5630
|
+
*
|
|
5631
|
+
* @group String
|
|
5632
|
+
*/
|
|
5633
|
+
export interface IdentifierBrandByCasing {
|
|
5634
|
+
readonly camelCase: "CamelCaseIdentifier";
|
|
5635
|
+
readonly PascalCase: "PascalCaseIdentifier";
|
|
5636
|
+
readonly snake_case: "SnakeCaseIdentifier";
|
|
5637
|
+
readonly "kebab-case": "KebabCaseIdentifier";
|
|
5638
|
+
readonly CONSTANT_CASE: "ConstantCaseIdentifier";
|
|
5639
|
+
}
|
|
5640
|
+
|
|
5641
|
+
/**
|
|
5642
|
+
* Adds identifier validation in a naming convention to an existing string Type.
|
|
5643
|
+
*
|
|
5644
|
+
* An identifier is one or more ASCII words, each starting with a letter and
|
|
5645
|
+
* continuing with letters or digits. camelCase and PascalCase start every word
|
|
5646
|
+
* after the first with an uppercase letter, so `httpUrl` has two words and
|
|
5647
|
+
* `httpURL` has four. snake_case, kebab-case, and CONSTANT_CASE put exactly one
|
|
5648
|
+
* separator between words. Validation keeps the spelling and rejects empty
|
|
5649
|
+
* strings, whitespace, punctuation, and non-ASCII characters.
|
|
5264
5650
|
*
|
|
5265
|
-
*
|
|
5651
|
+
* Words never start with a digit. Identifier grammars in most languages forbid
|
|
5652
|
+
* a leading digit, and camelCase cannot mark a word boundary before one, so the
|
|
5653
|
+
* rule applies to every word and keeps every conversion exact. Join an
|
|
5654
|
+
* abbreviation such as `2FA` to the previous word, as in `MAX2FA_ATTEMPTS` and
|
|
5655
|
+
* `max2faAttempts`, or spell the number out, as in `TWO_FACTOR_SECRET`.
|
|
5656
|
+
*
|
|
5657
|
+
* Convert validated identifiers with functions such as
|
|
5658
|
+
* {@link camelCaseToSnakeCase}. Conversions preserve word boundaries, and
|
|
5659
|
+
* converting back restores the original spelling. They return only the
|
|
5660
|
+
* destination brand, dropping unrelated constraints such as input length.
|
|
5266
5661
|
*
|
|
5267
5662
|
* ### Example
|
|
5268
5663
|
*
|
|
5269
5664
|
* ```ts
|
|
5270
5665
|
* import {
|
|
5271
|
-
* assertEqual,
|
|
5272
5666
|
* assertErr,
|
|
5273
5667
|
* assertOk,
|
|
5274
|
-
*
|
|
5275
|
-
*
|
|
5668
|
+
* identifier,
|
|
5669
|
+
* maxLength,
|
|
5276
5670
|
* String,
|
|
5277
|
-
* capitalized,
|
|
5278
|
-
* type Brand,
|
|
5279
5671
|
* } from "@evolu/common";
|
|
5280
5672
|
*
|
|
5281
|
-
* const
|
|
5282
|
-
* type CapitalizedString = typeof CapitalizedString.Output;
|
|
5283
|
-
*
|
|
5284
|
-
* assertType<CapitalizedString, string & Brand<"Capitalized">>();
|
|
5673
|
+
* const EnvName = identifier("CONSTANT_CASE")(maxLength(16)(String));
|
|
5285
5674
|
*
|
|
5286
|
-
* assertOk(
|
|
5287
|
-
*
|
|
5288
|
-
* assertErr(invalid);
|
|
5289
|
-
* assertType(Data, invalid.error);
|
|
5290
|
-
* assertEqual(invalid.error, {
|
|
5291
|
-
* type: "Capitalized",
|
|
5292
|
-
* value: "evolu",
|
|
5293
|
-
* });
|
|
5675
|
+
* assertOk(EnvName.fromUnknown("HTTP2_PORT"), "HTTP2_PORT");
|
|
5676
|
+
* assertErr(EnvName.fromUnknown("HTTP_2_PORT"));
|
|
5294
5677
|
* ```
|
|
5295
5678
|
*
|
|
5296
5679
|
* @group String
|
|
5297
5680
|
*/
|
|
5298
|
-
export const
|
|
5299
|
-
|
|
5300
|
-
|
|
5301
|
-
|
|
5302
|
-
|
|
5303
|
-
|
|
5304
|
-
|
|
5305
|
-
|
|
5306
|
-
|
|
5307
|
-
|
|
5681
|
+
export const identifier =
|
|
5682
|
+
<Casing extends IdentifierCasing>(
|
|
5683
|
+
casing: Casing,
|
|
5684
|
+
): BrandFactory<
|
|
5685
|
+
IdentifierBrandByCasing[Casing],
|
|
5686
|
+
string,
|
|
5687
|
+
IdentifierError<Casing>
|
|
5688
|
+
> =>
|
|
5689
|
+
(parent) => {
|
|
5690
|
+
const name = identifierBrandByCasing[casing];
|
|
5691
|
+
const pattern = identifierPatternByCasing[casing];
|
|
5308
5692
|
|
|
5309
|
-
|
|
5310
|
-
|
|
5311
|
-
|
|
5312
|
-
|
|
5313
|
-
|
|
5314
|
-
|
|
5315
|
-
|
|
5693
|
+
return brand(
|
|
5694
|
+
name,
|
|
5695
|
+
parent,
|
|
5696
|
+
(value) =>
|
|
5697
|
+
pattern.test(value)
|
|
5698
|
+
? ok()
|
|
5699
|
+
: err<IdentifierError<Casing>>({ type: name, value, casing }),
|
|
5700
|
+
(error) =>
|
|
5701
|
+
`The value ${safelyStringifyUnknownValue(error.value)} is not a ${error.casing} identifier.`,
|
|
5702
|
+
);
|
|
5703
|
+
};
|
|
5316
5704
|
|
|
5317
5705
|
/**
|
|
5318
|
-
* Error returned when {@link
|
|
5706
|
+
* Error returned when {@link identifier} rejects a string.
|
|
5319
5707
|
*
|
|
5320
5708
|
* @group String
|
|
5321
5709
|
*/
|
|
5322
|
-
export interface
|
|
5710
|
+
export interface IdentifierError<
|
|
5711
|
+
Casing extends IdentifierCasing = IdentifierCasing,
|
|
5712
|
+
> extends TypeError<IdentifierBrandByCasing[Casing]> {
|
|
5323
5713
|
readonly value: string;
|
|
5714
|
+
readonly casing: Casing;
|
|
5324
5715
|
}
|
|
5325
5716
|
|
|
5717
|
+
const identifierBrandByCasing: IdentifierBrandByCasing = {
|
|
5718
|
+
camelCase: "CamelCaseIdentifier",
|
|
5719
|
+
PascalCase: "PascalCaseIdentifier",
|
|
5720
|
+
snake_case: "SnakeCaseIdentifier",
|
|
5721
|
+
"kebab-case": "KebabCaseIdentifier",
|
|
5722
|
+
CONSTANT_CASE: "ConstantCaseIdentifier",
|
|
5723
|
+
};
|
|
5724
|
+
|
|
5725
|
+
const identifierPatternByCasing: ReadonlyRecord<IdentifierCasing, RegExp> = {
|
|
5726
|
+
camelCase: /^[a-z][a-zA-Z0-9]*$/u,
|
|
5727
|
+
PascalCase: /^[A-Z][a-zA-Z0-9]*$/u,
|
|
5728
|
+
snake_case: /^[a-z][a-z0-9]*(?:_[a-z][a-z0-9]*)*$/u,
|
|
5729
|
+
"kebab-case": /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*$/u,
|
|
5730
|
+
CONSTANT_CASE: /^[A-Z][A-Z0-9]*(?:_[A-Z][A-Z0-9]*)*$/u,
|
|
5731
|
+
};
|
|
5732
|
+
|
|
5326
5733
|
/**
|
|
5327
|
-
*
|
|
5734
|
+
* A validated camelCase identifier, such as `http2Port`.
|
|
5735
|
+
*
|
|
5736
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
5328
5737
|
*
|
|
5329
5738
|
* @group String
|
|
5330
5739
|
*/
|
|
5331
|
-
export const
|
|
5332
|
-
|
|
5740
|
+
export const CamelCaseIdentifier =
|
|
5741
|
+
/*#__PURE__*/ identifier("camelCase")(String);
|
|
5742
|
+
export type CamelCaseIdentifier = typeof CamelCaseIdentifier.Output;
|
|
5333
5743
|
|
|
5334
5744
|
/**
|
|
5335
|
-
*
|
|
5745
|
+
* A validated PascalCase identifier, such as `Http2Port`.
|
|
5336
5746
|
*
|
|
5337
|
-
*
|
|
5747
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
5338
5748
|
*
|
|
5339
|
-
*
|
|
5749
|
+
* @group String
|
|
5750
|
+
*/
|
|
5751
|
+
export const PascalCaseIdentifier =
|
|
5752
|
+
/*#__PURE__*/ identifier("PascalCase")(String);
|
|
5753
|
+
export type PascalCaseIdentifier = typeof PascalCaseIdentifier.Output;
|
|
5754
|
+
|
|
5755
|
+
/**
|
|
5756
|
+
* A validated snake_case identifier, such as `http2_port`.
|
|
5757
|
+
*
|
|
5758
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
5759
|
+
*
|
|
5760
|
+
* @group String
|
|
5761
|
+
*/
|
|
5762
|
+
export const SnakeCaseIdentifier =
|
|
5763
|
+
/*#__PURE__*/ identifier("snake_case")(String);
|
|
5764
|
+
export type SnakeCaseIdentifier = typeof SnakeCaseIdentifier.Output;
|
|
5765
|
+
|
|
5766
|
+
/**
|
|
5767
|
+
* A validated kebab-case identifier, such as `http2-port`.
|
|
5768
|
+
*
|
|
5769
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
5770
|
+
*
|
|
5771
|
+
* @group String
|
|
5772
|
+
*/
|
|
5773
|
+
export const KebabCaseIdentifier =
|
|
5774
|
+
/*#__PURE__*/ identifier("kebab-case")(String);
|
|
5775
|
+
export type KebabCaseIdentifier = typeof KebabCaseIdentifier.Output;
|
|
5776
|
+
|
|
5777
|
+
/**
|
|
5778
|
+
* A validated CONSTANT_CASE identifier, such as `HTTP2_PORT`.
|
|
5779
|
+
*
|
|
5780
|
+
* See {@link identifier} for the grammar and the conversion functions.
|
|
5781
|
+
*
|
|
5782
|
+
* @group String
|
|
5783
|
+
*/
|
|
5784
|
+
export const ConstantCaseIdentifier =
|
|
5785
|
+
/*#__PURE__*/ identifier("CONSTANT_CASE")(String);
|
|
5786
|
+
export type ConstantCaseIdentifier = typeof ConstantCaseIdentifier.Output;
|
|
5787
|
+
|
|
5788
|
+
/**
|
|
5789
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
5790
|
+
*
|
|
5791
|
+
* Converts `http2Port` to `Http2Port`. Use {@link pascalCaseToCamelCase} to
|
|
5792
|
+
* recover the original spelling.
|
|
5793
|
+
*
|
|
5794
|
+
* @group String
|
|
5795
|
+
*/
|
|
5796
|
+
export const camelCaseToPascalCase = (
|
|
5797
|
+
value: CamelCaseIdentifier,
|
|
5798
|
+
): PascalCaseIdentifier =>
|
|
5799
|
+
(value.charAt(0).toUpperCase() + value.slice(1)) as PascalCaseIdentifier;
|
|
5800
|
+
|
|
5801
|
+
/**
|
|
5802
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
5803
|
+
*
|
|
5804
|
+
* Converts `http2Port` to `http2_port`. Use {@link snakeCaseToCamelCase} to
|
|
5805
|
+
* recover the original spelling.
|
|
5806
|
+
*
|
|
5807
|
+
* @group String
|
|
5808
|
+
*/
|
|
5809
|
+
export const camelCaseToSnakeCase = (
|
|
5810
|
+
value: CamelCaseIdentifier,
|
|
5811
|
+
): SnakeCaseIdentifier =>
|
|
5812
|
+
value.replaceAll(
|
|
5813
|
+
/[A-Z]/gu,
|
|
5814
|
+
(letter) => `_${letter.toLowerCase()}`,
|
|
5815
|
+
) as SnakeCaseIdentifier;
|
|
5816
|
+
|
|
5817
|
+
/**
|
|
5818
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
5819
|
+
*
|
|
5820
|
+
* Converts `http2Port` to `http2-port`. Use {@link kebabCaseToCamelCase} to
|
|
5821
|
+
* recover the original spelling.
|
|
5822
|
+
*
|
|
5823
|
+
* @group String
|
|
5824
|
+
*/
|
|
5825
|
+
export const camelCaseToKebabCase = (
|
|
5826
|
+
value: CamelCaseIdentifier,
|
|
5827
|
+
): KebabCaseIdentifier => snakeCaseToKebabCase(camelCaseToSnakeCase(value));
|
|
5828
|
+
|
|
5829
|
+
/**
|
|
5830
|
+
* Converts a {@link CamelCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
5831
|
+
*
|
|
5832
|
+
* Converts `http2Port` to `HTTP2_PORT`. Use {@link constantCaseToCamelCase} to
|
|
5833
|
+
* recover the original spelling.
|
|
5834
|
+
*
|
|
5835
|
+
* @group String
|
|
5836
|
+
*/
|
|
5837
|
+
export const camelCaseToConstantCase = (
|
|
5838
|
+
value: CamelCaseIdentifier,
|
|
5839
|
+
): ConstantCaseIdentifier =>
|
|
5840
|
+
snakeCaseToConstantCase(camelCaseToSnakeCase(value));
|
|
5841
|
+
|
|
5842
|
+
/**
|
|
5843
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
5844
|
+
*
|
|
5845
|
+
* Converts `Http2Port` to `http2Port`. Use {@link camelCaseToPascalCase} to
|
|
5846
|
+
* recover the original spelling.
|
|
5847
|
+
*
|
|
5848
|
+
* @group String
|
|
5849
|
+
*/
|
|
5850
|
+
export const pascalCaseToCamelCase = (
|
|
5851
|
+
value: PascalCaseIdentifier,
|
|
5852
|
+
): CamelCaseIdentifier =>
|
|
5853
|
+
(value.charAt(0).toLowerCase() + value.slice(1)) as CamelCaseIdentifier;
|
|
5854
|
+
|
|
5855
|
+
/**
|
|
5856
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
5857
|
+
*
|
|
5858
|
+
* Converts `Http2Port` to `http2_port`. Use {@link snakeCaseToPascalCase} to
|
|
5859
|
+
* recover the original spelling.
|
|
5860
|
+
*
|
|
5861
|
+
* @group String
|
|
5862
|
+
*/
|
|
5863
|
+
export const pascalCaseToSnakeCase = (
|
|
5864
|
+
value: PascalCaseIdentifier,
|
|
5865
|
+
): SnakeCaseIdentifier => camelCaseToSnakeCase(pascalCaseToCamelCase(value));
|
|
5866
|
+
|
|
5867
|
+
/**
|
|
5868
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
5869
|
+
*
|
|
5870
|
+
* Converts `Http2Port` to `http2-port`. Use {@link kebabCaseToPascalCase} to
|
|
5871
|
+
* recover the original spelling.
|
|
5872
|
+
*
|
|
5873
|
+
* @group String
|
|
5874
|
+
*/
|
|
5875
|
+
export const pascalCaseToKebabCase = (
|
|
5876
|
+
value: PascalCaseIdentifier,
|
|
5877
|
+
): KebabCaseIdentifier => snakeCaseToKebabCase(pascalCaseToSnakeCase(value));
|
|
5878
|
+
|
|
5879
|
+
/**
|
|
5880
|
+
* Converts a {@link PascalCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
5881
|
+
*
|
|
5882
|
+
* Converts `Http2Port` to `HTTP2_PORT`. Use {@link constantCaseToPascalCase} to
|
|
5883
|
+
* recover the original spelling.
|
|
5884
|
+
*
|
|
5885
|
+
* @group String
|
|
5886
|
+
*/
|
|
5887
|
+
export const pascalCaseToConstantCase = (
|
|
5888
|
+
value: PascalCaseIdentifier,
|
|
5889
|
+
): ConstantCaseIdentifier =>
|
|
5890
|
+
snakeCaseToConstantCase(pascalCaseToSnakeCase(value));
|
|
5891
|
+
|
|
5892
|
+
/**
|
|
5893
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
5894
|
+
*
|
|
5895
|
+
* Converts `http2_port` to `http2Port`. Use {@link camelCaseToSnakeCase} to
|
|
5896
|
+
* recover the original spelling.
|
|
5897
|
+
*
|
|
5898
|
+
* @group String
|
|
5899
|
+
*/
|
|
5900
|
+
export const snakeCaseToCamelCase = (
|
|
5901
|
+
value: SnakeCaseIdentifier,
|
|
5902
|
+
): CamelCaseIdentifier =>
|
|
5903
|
+
value.replaceAll(/_[a-z]/gu, (word) =>
|
|
5904
|
+
word.charAt(1).toUpperCase(),
|
|
5905
|
+
) as CamelCaseIdentifier;
|
|
5906
|
+
|
|
5907
|
+
/**
|
|
5908
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
5909
|
+
*
|
|
5910
|
+
* Converts `http2_port` to `Http2Port`. Use {@link pascalCaseToSnakeCase} to
|
|
5911
|
+
* recover the original spelling.
|
|
5912
|
+
*
|
|
5913
|
+
* @group String
|
|
5914
|
+
*/
|
|
5915
|
+
export const snakeCaseToPascalCase = (
|
|
5916
|
+
value: SnakeCaseIdentifier,
|
|
5917
|
+
): PascalCaseIdentifier => camelCaseToPascalCase(snakeCaseToCamelCase(value));
|
|
5918
|
+
|
|
5919
|
+
/**
|
|
5920
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
5921
|
+
*
|
|
5922
|
+
* Converts `http2_port` to `http2-port`. Use {@link kebabCaseToSnakeCase} to
|
|
5923
|
+
* recover the original spelling.
|
|
5924
|
+
*
|
|
5925
|
+
* @group String
|
|
5926
|
+
*/
|
|
5927
|
+
export const snakeCaseToKebabCase = (
|
|
5928
|
+
value: SnakeCaseIdentifier,
|
|
5929
|
+
): KebabCaseIdentifier => value.replaceAll("_", "-") as KebabCaseIdentifier;
|
|
5930
|
+
|
|
5931
|
+
/**
|
|
5932
|
+
* Converts a {@link SnakeCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
5933
|
+
*
|
|
5934
|
+
* Converts `http2_port` to `HTTP2_PORT`. Use {@link constantCaseToSnakeCase} to
|
|
5935
|
+
* recover the original spelling.
|
|
5936
|
+
*
|
|
5937
|
+
* @group String
|
|
5938
|
+
*/
|
|
5939
|
+
export const snakeCaseToConstantCase = (
|
|
5940
|
+
value: SnakeCaseIdentifier,
|
|
5941
|
+
): ConstantCaseIdentifier => value.toUpperCase() as ConstantCaseIdentifier;
|
|
5942
|
+
|
|
5943
|
+
/**
|
|
5944
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
5945
|
+
*
|
|
5946
|
+
* Converts `http2-port` to `http2Port`. Use {@link camelCaseToKebabCase} to
|
|
5947
|
+
* recover the original spelling.
|
|
5948
|
+
*
|
|
5949
|
+
* @group String
|
|
5950
|
+
*/
|
|
5951
|
+
export const kebabCaseToCamelCase = (
|
|
5952
|
+
value: KebabCaseIdentifier,
|
|
5953
|
+
): CamelCaseIdentifier => snakeCaseToCamelCase(kebabCaseToSnakeCase(value));
|
|
5954
|
+
|
|
5955
|
+
/**
|
|
5956
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
5957
|
+
*
|
|
5958
|
+
* Converts `http2-port` to `Http2Port`. Use {@link pascalCaseToKebabCase} to
|
|
5959
|
+
* recover the original spelling.
|
|
5960
|
+
*
|
|
5961
|
+
* @group String
|
|
5962
|
+
*/
|
|
5963
|
+
export const kebabCaseToPascalCase = (
|
|
5964
|
+
value: KebabCaseIdentifier,
|
|
5965
|
+
): PascalCaseIdentifier => camelCaseToPascalCase(kebabCaseToCamelCase(value));
|
|
5966
|
+
|
|
5967
|
+
/**
|
|
5968
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
5969
|
+
*
|
|
5970
|
+
* Converts `http2-port` to `http2_port`. Use {@link snakeCaseToKebabCase} to
|
|
5971
|
+
* recover the original spelling.
|
|
5972
|
+
*
|
|
5973
|
+
* @group String
|
|
5974
|
+
*/
|
|
5975
|
+
export const kebabCaseToSnakeCase = (
|
|
5976
|
+
value: KebabCaseIdentifier,
|
|
5977
|
+
): SnakeCaseIdentifier => value.replaceAll("-", "_") as SnakeCaseIdentifier;
|
|
5978
|
+
|
|
5979
|
+
/**
|
|
5980
|
+
* Converts a {@link KebabCaseIdentifier} to a {@link ConstantCaseIdentifier}.
|
|
5981
|
+
*
|
|
5982
|
+
* Converts `http2-port` to `HTTP2_PORT`. Use {@link constantCaseToKebabCase} to
|
|
5983
|
+
* recover the original spelling.
|
|
5984
|
+
*
|
|
5985
|
+
* @group String
|
|
5986
|
+
*/
|
|
5987
|
+
export const kebabCaseToConstantCase = (
|
|
5988
|
+
value: KebabCaseIdentifier,
|
|
5989
|
+
): ConstantCaseIdentifier =>
|
|
5990
|
+
snakeCaseToConstantCase(kebabCaseToSnakeCase(value));
|
|
5991
|
+
|
|
5992
|
+
/**
|
|
5993
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier}.
|
|
5994
|
+
*
|
|
5995
|
+
* Converts `HTTP2_PORT` to `http2Port`. Use {@link camelCaseToConstantCase} to
|
|
5996
|
+
* recover the original spelling.
|
|
5997
|
+
*
|
|
5998
|
+
* @group String
|
|
5999
|
+
*/
|
|
6000
|
+
export const constantCaseToCamelCase = (
|
|
6001
|
+
value: ConstantCaseIdentifier,
|
|
6002
|
+
): CamelCaseIdentifier => snakeCaseToCamelCase(constantCaseToSnakeCase(value));
|
|
6003
|
+
|
|
6004
|
+
/**
|
|
6005
|
+
* Decodes a {@link ConstantCaseIdentifier} to a {@link CamelCaseIdentifier} and
|
|
6006
|
+
* restores the original spelling when encoding.
|
|
6007
|
+
*
|
|
6008
|
+
* ### Example
|
|
6009
|
+
*
|
|
6010
|
+
* ```ts
|
|
6011
|
+
* import {
|
|
6012
|
+
* assertEqual,
|
|
6013
|
+
* assertOk,
|
|
6014
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
6015
|
+
* } from "@evolu/common";
|
|
6016
|
+
*
|
|
6017
|
+
* const Key = CamelCaseIdentifierFromConstantCaseIdentifier;
|
|
6018
|
+
* const result = Key.fromUnknown("HTTP2_PORT");
|
|
6019
|
+
* assertOk(result, "http2Port");
|
|
6020
|
+
* assertEqual(Key.to(result.value), "HTTP2_PORT");
|
|
6021
|
+
* ```
|
|
6022
|
+
*
|
|
6023
|
+
* @group String
|
|
6024
|
+
*/
|
|
6025
|
+
export const CamelCaseIdentifierFromConstantCaseIdentifier =
|
|
6026
|
+
/*#__PURE__*/ transform(
|
|
6027
|
+
"CamelCaseIdentifierFromConstantCaseIdentifier",
|
|
6028
|
+
ConstantCaseIdentifier,
|
|
6029
|
+
CamelCaseIdentifier,
|
|
6030
|
+
{
|
|
6031
|
+
from: (value) => ok(constantCaseToCamelCase(value)),
|
|
6032
|
+
to: camelCaseToConstantCase,
|
|
6033
|
+
},
|
|
6034
|
+
);
|
|
6035
|
+
|
|
6036
|
+
/**
|
|
6037
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link PascalCaseIdentifier}.
|
|
6038
|
+
*
|
|
6039
|
+
* Converts `HTTP2_PORT` to `Http2Port`. Use {@link pascalCaseToConstantCase} to
|
|
6040
|
+
* recover the original spelling.
|
|
6041
|
+
*
|
|
6042
|
+
* @group String
|
|
6043
|
+
*/
|
|
6044
|
+
export const constantCaseToPascalCase = (
|
|
6045
|
+
value: ConstantCaseIdentifier,
|
|
6046
|
+
): PascalCaseIdentifier =>
|
|
6047
|
+
camelCaseToPascalCase(constantCaseToCamelCase(value));
|
|
6048
|
+
|
|
6049
|
+
/**
|
|
6050
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link SnakeCaseIdentifier}.
|
|
6051
|
+
*
|
|
6052
|
+
* Converts `HTTP2_PORT` to `http2_port`. Use {@link snakeCaseToConstantCase} to
|
|
6053
|
+
* recover the original spelling.
|
|
6054
|
+
*
|
|
6055
|
+
* @group String
|
|
6056
|
+
*/
|
|
6057
|
+
export const constantCaseToSnakeCase = (
|
|
6058
|
+
value: ConstantCaseIdentifier,
|
|
6059
|
+
): SnakeCaseIdentifier => value.toLowerCase() as SnakeCaseIdentifier;
|
|
6060
|
+
|
|
6061
|
+
/**
|
|
6062
|
+
* Converts a {@link ConstantCaseIdentifier} to a {@link KebabCaseIdentifier}.
|
|
6063
|
+
*
|
|
6064
|
+
* Converts `HTTP2_PORT` to `http2-port`. Use {@link kebabCaseToConstantCase} to
|
|
6065
|
+
* recover the original spelling.
|
|
6066
|
+
*
|
|
6067
|
+
* @group String
|
|
6068
|
+
*/
|
|
6069
|
+
export const constantCaseToKebabCase = (
|
|
6070
|
+
value: ConstantCaseIdentifier,
|
|
6071
|
+
): KebabCaseIdentifier => snakeCaseToKebabCase(constantCaseToSnakeCase(value));
|
|
6072
|
+
|
|
6073
|
+
/**
|
|
6074
|
+
* Adds capitalized text validation to an existing string Type.
|
|
6075
|
+
*
|
|
6076
|
+
* Narrows the output to TypeScript's `Capitalize<string>` while preserving the
|
|
6077
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
6078
|
+
* {@link capitalize} to change its casing.
|
|
6079
|
+
*
|
|
6080
|
+
* ### Example
|
|
6081
|
+
*
|
|
6082
|
+
* ```ts
|
|
6083
|
+
* import {
|
|
6084
|
+
* assertErr,
|
|
6085
|
+
* assertOk,
|
|
6086
|
+
* capitalized,
|
|
6087
|
+
* maxLength,
|
|
6088
|
+
* String,
|
|
6089
|
+
* } from "@evolu/common";
|
|
6090
|
+
*
|
|
6091
|
+
* const Label = capitalized(maxLength(50)(String));
|
|
6092
|
+
* assertOk(Label.fromUnknown("Hello world"), "Hello world");
|
|
6093
|
+
* assertErr(Label.fromUnknown("hello world"));
|
|
6094
|
+
* ```
|
|
6095
|
+
*
|
|
6096
|
+
* @group String
|
|
6097
|
+
*/
|
|
6098
|
+
export const capitalized = <
|
|
6099
|
+
ParentType extends ConcreteTypeNode & { readonly Output: string },
|
|
6100
|
+
>(
|
|
6101
|
+
parent: ValidateBrandParent<"Capitalized", ParentType>,
|
|
6102
|
+
): ReturnType<
|
|
6103
|
+
typeof createType<
|
|
6104
|
+
"Capitalized",
|
|
6105
|
+
ParentType,
|
|
6106
|
+
ParentType["Output"] & Capitalize<string>,
|
|
6107
|
+
CapitalizedError
|
|
6108
|
+
>
|
|
6109
|
+
> =>
|
|
6110
|
+
createType<
|
|
6111
|
+
"Capitalized",
|
|
6112
|
+
ParentType,
|
|
6113
|
+
ParentType["Output"] & Capitalize<string>,
|
|
6114
|
+
CapitalizedError
|
|
6115
|
+
>(
|
|
6116
|
+
"Capitalized",
|
|
6117
|
+
parent,
|
|
6118
|
+
(value) =>
|
|
6119
|
+
value === capitalize(value)
|
|
6120
|
+
? ok(value as ParentType["Output"] & Capitalize<string>)
|
|
6121
|
+
: err({ type: "Capitalized", value }),
|
|
6122
|
+
(error) =>
|
|
6123
|
+
`The value ${safelyStringifyUnknownValue(error.value)} must be capitalized.`,
|
|
6124
|
+
);
|
|
6125
|
+
|
|
6126
|
+
/**
|
|
6127
|
+
* Error returned when {@link capitalized} rejects a string.
|
|
6128
|
+
*
|
|
6129
|
+
* @group String
|
|
6130
|
+
*/
|
|
6131
|
+
export interface CapitalizedError extends TypeError<"Capitalized"> {
|
|
6132
|
+
readonly value: string;
|
|
6133
|
+
}
|
|
6134
|
+
|
|
6135
|
+
/**
|
|
6136
|
+
* Validates capitalized text as TypeScript's `Capitalize<string>`.
|
|
6137
|
+
*
|
|
6138
|
+
* The rest of the text can use any casing. Empty strings and text starting with
|
|
6139
|
+
* an uncased character, such as a digit or emoji, are valid. Use
|
|
6140
|
+
* {@link capitalize} to produce a capitalized value from any string.
|
|
6141
|
+
*
|
|
6142
|
+
* Capitalization applies to general text, including spaces and punctuation.
|
|
6143
|
+
* Both `hello` and `Hello` become `Hello`, so the original initial casing
|
|
6144
|
+
* cannot be recovered. Capitalization does not identify words or turn text into
|
|
6145
|
+
* an identifier.
|
|
6146
|
+
*
|
|
6147
|
+
* ### Example
|
|
6148
|
+
*
|
|
6149
|
+
* ```ts
|
|
6150
|
+
* import { assertErr, assertOk, CapitalizedString } from "@evolu/common";
|
|
6151
|
+
*
|
|
6152
|
+
* const text: CapitalizedString = "Hello world";
|
|
6153
|
+
* assertOk(CapitalizedString.fromUnknown(text), text);
|
|
6154
|
+
* assertErr(CapitalizedString.fromUnknown("hello world"));
|
|
6155
|
+
* assertOk(CapitalizedString.fromUnknown(""), "");
|
|
6156
|
+
* ```
|
|
6157
|
+
*
|
|
6158
|
+
* @group String
|
|
6159
|
+
*/
|
|
6160
|
+
export const CapitalizedString = /*#__PURE__*/ capitalized(String);
|
|
6161
|
+
export type CapitalizedString = typeof CapitalizedString.Output;
|
|
6162
|
+
|
|
6163
|
+
/**
|
|
6164
|
+
* Uppercases the first Unicode code point and returns a
|
|
6165
|
+
* {@link CapitalizedString}.
|
|
6166
|
+
*
|
|
6167
|
+
* Preserves the remainder of the string and leaves an empty string unchanged.
|
|
6168
|
+
* Uses JavaScript's default Unicode casing without locale-specific rules.
|
|
6169
|
+
* Changing case can change the length, so input brands are not retained.
|
|
6170
|
+
*
|
|
6171
|
+
* ### Example
|
|
6172
|
+
*
|
|
6173
|
+
* ```ts
|
|
6174
|
+
* import { assertEqual, assertType, capitalize } from "@evolu/common";
|
|
6175
|
+
*
|
|
6176
|
+
* const text = capitalize("hello world");
|
|
6177
|
+
* assertEqual(text, "Hello world");
|
|
6178
|
+
* assertType<typeof text, "Hello world">();
|
|
6179
|
+
* assertEqual(capitalize(text), text);
|
|
6180
|
+
* ```
|
|
6181
|
+
*
|
|
6182
|
+
* @group String
|
|
6183
|
+
*/
|
|
6184
|
+
export const capitalize = <S extends string>(value: S): Capitalize<S> => {
|
|
6185
|
+
const [first = ""] = value;
|
|
6186
|
+
return (first.toUpperCase() + value.slice(first.length)) as Capitalize<S>;
|
|
6187
|
+
};
|
|
6188
|
+
|
|
6189
|
+
/**
|
|
6190
|
+
* Adds uncapitalized text validation to an existing string Type.
|
|
6191
|
+
*
|
|
6192
|
+
* Narrows the output to TypeScript's `Uncapitalize<string>` while preserving
|
|
6193
|
+
* the parent Type's constraints. Validation leaves the text unchanged. Use
|
|
6194
|
+
* {@link uncapitalize} to change its casing.
|
|
6195
|
+
*
|
|
6196
|
+
* ### Example
|
|
6197
|
+
*
|
|
6198
|
+
* ```ts
|
|
6199
|
+
* import {
|
|
6200
|
+
* assertErr,
|
|
6201
|
+
* assertOk,
|
|
6202
|
+
* uncapitalized,
|
|
6203
|
+
* maxLength,
|
|
6204
|
+
* String,
|
|
6205
|
+
* } from "@evolu/common";
|
|
6206
|
+
*
|
|
6207
|
+
* const Label = uncapitalized(maxLength(50)(String));
|
|
6208
|
+
* assertOk(Label.fromUnknown("hello WORLD"), "hello WORLD");
|
|
6209
|
+
* assertErr(Label.fromUnknown("Hello WORLD"));
|
|
6210
|
+
* ```
|
|
6211
|
+
*
|
|
6212
|
+
* @group String
|
|
6213
|
+
*/
|
|
6214
|
+
export const uncapitalized = <
|
|
6215
|
+
ParentType extends ConcreteTypeNode & { readonly Output: string },
|
|
6216
|
+
>(
|
|
6217
|
+
parent: ValidateBrandParent<"Uncapitalized", ParentType>,
|
|
6218
|
+
): ReturnType<
|
|
6219
|
+
typeof createType<
|
|
6220
|
+
"Uncapitalized",
|
|
6221
|
+
ParentType,
|
|
6222
|
+
ParentType["Output"] & Uncapitalize<string>,
|
|
6223
|
+
UncapitalizedError
|
|
6224
|
+
>
|
|
6225
|
+
> =>
|
|
6226
|
+
createType<
|
|
6227
|
+
"Uncapitalized",
|
|
6228
|
+
ParentType,
|
|
6229
|
+
ParentType["Output"] & Uncapitalize<string>,
|
|
6230
|
+
UncapitalizedError
|
|
6231
|
+
>(
|
|
6232
|
+
"Uncapitalized",
|
|
6233
|
+
parent,
|
|
6234
|
+
(value) =>
|
|
6235
|
+
value === uncapitalize(value)
|
|
6236
|
+
? ok(value as ParentType["Output"] & Uncapitalize<string>)
|
|
6237
|
+
: err({ type: "Uncapitalized", value }),
|
|
6238
|
+
(error) =>
|
|
6239
|
+
`The value ${safelyStringifyUnknownValue(error.value)} must not start with an uppercase letter.`,
|
|
6240
|
+
);
|
|
6241
|
+
|
|
6242
|
+
/**
|
|
6243
|
+
* Error returned when {@link uncapitalized} rejects a string.
|
|
6244
|
+
*
|
|
6245
|
+
* @group String
|
|
6246
|
+
*/
|
|
6247
|
+
export interface UncapitalizedError extends TypeError<"Uncapitalized"> {
|
|
6248
|
+
readonly value: string;
|
|
6249
|
+
}
|
|
6250
|
+
|
|
6251
|
+
/**
|
|
6252
|
+
* Validates uncapitalized text as TypeScript's `Uncapitalize<string>`.
|
|
6253
|
+
*
|
|
6254
|
+
* The rest of the text can use any casing. Empty strings and text starting with
|
|
6255
|
+
* an uncased character, such as a digit or emoji, are valid. Use
|
|
6256
|
+
* {@link uncapitalize} to produce an uncapitalized value from any string.
|
|
6257
|
+
*
|
|
6258
|
+
* ### Example
|
|
6259
|
+
*
|
|
6260
|
+
* ```ts
|
|
6261
|
+
* import { assertErr, assertOk, UncapitalizedString } from "@evolu/common";
|
|
6262
|
+
*
|
|
6263
|
+
* const text: UncapitalizedString = "hello WORLD";
|
|
6264
|
+
* assertOk(UncapitalizedString.fromUnknown(text), text);
|
|
6265
|
+
* assertErr(UncapitalizedString.fromUnknown("Hello WORLD"));
|
|
6266
|
+
* assertOk(UncapitalizedString.fromUnknown(""), "");
|
|
6267
|
+
* ```
|
|
6268
|
+
*
|
|
6269
|
+
* @group String
|
|
6270
|
+
*/
|
|
6271
|
+
export const UncapitalizedString = /*#__PURE__*/ uncapitalized(String);
|
|
6272
|
+
export type UncapitalizedString = typeof UncapitalizedString.Output;
|
|
6273
|
+
|
|
6274
|
+
/**
|
|
6275
|
+
* Lowercases the first Unicode code point and returns a
|
|
6276
|
+
* {@link UncapitalizedString}.
|
|
6277
|
+
*
|
|
6278
|
+
* Preserves the remainder of the string and leaves an empty string unchanged.
|
|
6279
|
+
* Uses JavaScript's default Unicode casing without locale-specific rules.
|
|
6280
|
+
* Changing case can change the length, so input brands are not retained.
|
|
6281
|
+
*
|
|
6282
|
+
* ### Example
|
|
6283
|
+
*
|
|
6284
|
+
* ```ts
|
|
6285
|
+
* import { assertEqual, assertType, uncapitalize } from "@evolu/common";
|
|
6286
|
+
*
|
|
6287
|
+
* const text = uncapitalize("Hello WORLD");
|
|
6288
|
+
* assertEqual(text, "hello WORLD");
|
|
6289
|
+
* assertType<typeof text, "hello WORLD">();
|
|
6290
|
+
* assertEqual(uncapitalize(text), text);
|
|
6291
|
+
* ```
|
|
6292
|
+
*
|
|
6293
|
+
* @group String
|
|
6294
|
+
*/
|
|
6295
|
+
export const uncapitalize = <S extends string>(value: S): Uncapitalize<S> => {
|
|
6296
|
+
const [first = ""] = value;
|
|
6297
|
+
return (first.toLowerCase() + value.slice(first.length)) as Uncapitalize<S>;
|
|
6298
|
+
};
|
|
6299
|
+
|
|
6300
|
+
/**
|
|
6301
|
+
* Adds uppercased text validation to an existing string Type.
|
|
6302
|
+
*
|
|
6303
|
+
* Narrows the output to TypeScript's `Uppercase<string>` while preserving the
|
|
6304
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
6305
|
+
* {@link uppercase} to change its casing.
|
|
6306
|
+
*
|
|
6307
|
+
* ### Example
|
|
6308
|
+
*
|
|
6309
|
+
* ```ts
|
|
6310
|
+
* import {
|
|
6311
|
+
* assertErr,
|
|
6312
|
+
* assertOk,
|
|
6313
|
+
* uppercased,
|
|
6314
|
+
* maxLength,
|
|
6315
|
+
* String,
|
|
6316
|
+
* } from "@evolu/common";
|
|
6317
|
+
*
|
|
6318
|
+
* const Label = uppercased(maxLength(50)(String));
|
|
6319
|
+
* assertOk(Label.fromUnknown("HELLO WORLD"), "HELLO WORLD");
|
|
6320
|
+
* assertErr(Label.fromUnknown("Hello world"));
|
|
6321
|
+
* ```
|
|
6322
|
+
*
|
|
6323
|
+
* @group String
|
|
6324
|
+
*/
|
|
6325
|
+
export const uppercased = <
|
|
6326
|
+
ParentType extends ConcreteTypeNode & { readonly Output: string },
|
|
6327
|
+
>(
|
|
6328
|
+
parent: ValidateBrandParent<"Uppercased", ParentType>,
|
|
6329
|
+
): ReturnType<
|
|
6330
|
+
typeof createType<
|
|
6331
|
+
"Uppercased",
|
|
6332
|
+
ParentType,
|
|
6333
|
+
ParentType["Output"] & Uppercase<string>,
|
|
6334
|
+
UppercasedError
|
|
6335
|
+
>
|
|
6336
|
+
> =>
|
|
6337
|
+
createType<
|
|
6338
|
+
"Uppercased",
|
|
6339
|
+
ParentType,
|
|
6340
|
+
ParentType["Output"] & Uppercase<string>,
|
|
6341
|
+
UppercasedError
|
|
6342
|
+
>(
|
|
6343
|
+
"Uppercased",
|
|
6344
|
+
parent,
|
|
6345
|
+
(value) =>
|
|
6346
|
+
value === uppercase(value)
|
|
6347
|
+
? ok(value as ParentType["Output"] & Uppercase<string>)
|
|
6348
|
+
: err({ type: "Uppercased", value }),
|
|
6349
|
+
(error) =>
|
|
6350
|
+
`The value ${safelyStringifyUnknownValue(error.value)} must be uppercased.`,
|
|
6351
|
+
);
|
|
6352
|
+
|
|
6353
|
+
/**
|
|
6354
|
+
* Error returned when {@link uppercased} rejects a string.
|
|
6355
|
+
*
|
|
6356
|
+
* @group String
|
|
6357
|
+
*/
|
|
6358
|
+
export interface UppercasedError extends TypeError<"Uppercased"> {
|
|
6359
|
+
readonly value: string;
|
|
6360
|
+
}
|
|
6361
|
+
|
|
6362
|
+
/**
|
|
6363
|
+
* Validates uppercased text as TypeScript's `Uppercase<string>`.
|
|
6364
|
+
*
|
|
6365
|
+
* Checks the whole string using JavaScript's Unicode uppercase mapping. Empty
|
|
6366
|
+
* strings and uncased characters, such as digits and emoji, are valid. Use
|
|
6367
|
+
* {@link uppercase} to produce an uppercased value from any string.
|
|
6368
|
+
*
|
|
6369
|
+
* ### Example
|
|
6370
|
+
*
|
|
6371
|
+
* ```ts
|
|
6372
|
+
* import { assertErr, assertOk, UppercasedString } from "@evolu/common";
|
|
6373
|
+
*
|
|
6374
|
+
* const text: UppercasedString = "HELLO WORLD";
|
|
6375
|
+
* assertOk(UppercasedString.fromUnknown(text), text);
|
|
6376
|
+
* assertErr(UppercasedString.fromUnknown("Hello world"));
|
|
6377
|
+
* assertOk(UppercasedString.fromUnknown(""), "");
|
|
6378
|
+
* ```
|
|
6379
|
+
*
|
|
6380
|
+
* @group String
|
|
6381
|
+
*/
|
|
6382
|
+
export const UppercasedString = /*#__PURE__*/ uppercased(String);
|
|
6383
|
+
export type UppercasedString = typeof UppercasedString.Output;
|
|
6384
|
+
|
|
6385
|
+
/**
|
|
6386
|
+
* Uppercases the whole string and returns a {@link UppercasedString}.
|
|
6387
|
+
*
|
|
6388
|
+
* Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
|
|
6389
|
+
* without locale-specific rules. Changing case can change the length, so input
|
|
6390
|
+
* brands are not retained.
|
|
6391
|
+
*
|
|
6392
|
+
* ### Example
|
|
6393
|
+
*
|
|
6394
|
+
* ```ts
|
|
6395
|
+
* import { assertEqual, assertType, uppercase } from "@evolu/common";
|
|
6396
|
+
*
|
|
6397
|
+
* const text = uppercase("Hello world");
|
|
6398
|
+
* assertEqual(text, "HELLO WORLD");
|
|
6399
|
+
* assertType<typeof text, "HELLO WORLD">();
|
|
6400
|
+
* assertEqual(uppercase(text), text);
|
|
6401
|
+
* ```
|
|
6402
|
+
*
|
|
6403
|
+
* @group String
|
|
6404
|
+
*/
|
|
6405
|
+
export const uppercase = <S extends string>(value: S): Uppercase<S> =>
|
|
6406
|
+
value.toUpperCase() as Uppercase<S>;
|
|
6407
|
+
|
|
6408
|
+
/**
|
|
6409
|
+
* Adds lowercased text validation to an existing string Type.
|
|
6410
|
+
*
|
|
6411
|
+
* Narrows the output to TypeScript's `Lowercase<string>` while preserving the
|
|
6412
|
+
* parent Type's constraints. Validation leaves the text unchanged. Use
|
|
6413
|
+
* {@link lowercase} to change its casing.
|
|
6414
|
+
*
|
|
6415
|
+
* ### Example
|
|
6416
|
+
*
|
|
6417
|
+
* ```ts
|
|
6418
|
+
* import {
|
|
6419
|
+
* assertErr,
|
|
6420
|
+
* assertOk,
|
|
6421
|
+
* lowercased,
|
|
6422
|
+
* maxLength,
|
|
6423
|
+
* String,
|
|
6424
|
+
* } from "@evolu/common";
|
|
6425
|
+
*
|
|
6426
|
+
* const Label = lowercased(maxLength(50)(String));
|
|
6427
|
+
* assertOk(Label.fromUnknown("hello world"), "hello world");
|
|
6428
|
+
* assertErr(Label.fromUnknown("Hello WORLD"));
|
|
6429
|
+
* ```
|
|
6430
|
+
*
|
|
6431
|
+
* @group String
|
|
6432
|
+
*/
|
|
6433
|
+
export const lowercased = <
|
|
6434
|
+
ParentType extends ConcreteTypeNode & { readonly Output: string },
|
|
6435
|
+
>(
|
|
6436
|
+
parent: ValidateBrandParent<"Lowercased", ParentType>,
|
|
6437
|
+
): ReturnType<
|
|
6438
|
+
typeof createType<
|
|
6439
|
+
"Lowercased",
|
|
6440
|
+
ParentType,
|
|
6441
|
+
ParentType["Output"] & Lowercase<string>,
|
|
6442
|
+
LowercasedError
|
|
6443
|
+
>
|
|
6444
|
+
> =>
|
|
6445
|
+
createType<
|
|
6446
|
+
"Lowercased",
|
|
6447
|
+
ParentType,
|
|
6448
|
+
ParentType["Output"] & Lowercase<string>,
|
|
6449
|
+
LowercasedError
|
|
6450
|
+
>(
|
|
6451
|
+
"Lowercased",
|
|
6452
|
+
parent,
|
|
6453
|
+
(value) =>
|
|
6454
|
+
value === lowercase(value)
|
|
6455
|
+
? ok(value as ParentType["Output"] & Lowercase<string>)
|
|
6456
|
+
: err({ type: "Lowercased", value }),
|
|
6457
|
+
(error) =>
|
|
6458
|
+
`The value ${safelyStringifyUnknownValue(error.value)} must be lowercased.`,
|
|
6459
|
+
);
|
|
6460
|
+
|
|
6461
|
+
/**
|
|
6462
|
+
* Error returned when {@link lowercased} rejects a string.
|
|
6463
|
+
*
|
|
6464
|
+
* @group String
|
|
6465
|
+
*/
|
|
6466
|
+
export interface LowercasedError extends TypeError<"Lowercased"> {
|
|
6467
|
+
readonly value: string;
|
|
6468
|
+
}
|
|
6469
|
+
|
|
6470
|
+
/**
|
|
6471
|
+
* Validates lowercased text as TypeScript's `Lowercase<string>`.
|
|
6472
|
+
*
|
|
6473
|
+
* Checks the whole string using JavaScript's Unicode lowercase mapping. Empty
|
|
6474
|
+
* strings and uncased characters, such as digits and emoji, are valid. Use
|
|
6475
|
+
* {@link lowercase} to produce a lowercased value from any string.
|
|
6476
|
+
*
|
|
6477
|
+
* ### Example
|
|
6478
|
+
*
|
|
6479
|
+
* ```ts
|
|
6480
|
+
* import { assertErr, assertOk, LowercasedString } from "@evolu/common";
|
|
6481
|
+
*
|
|
6482
|
+
* const text: LowercasedString = "hello world";
|
|
6483
|
+
* assertOk(LowercasedString.fromUnknown(text), text);
|
|
6484
|
+
* assertErr(LowercasedString.fromUnknown("Hello WORLD"));
|
|
6485
|
+
* assertOk(LowercasedString.fromUnknown(""), "");
|
|
6486
|
+
* ```
|
|
6487
|
+
*
|
|
6488
|
+
* @group String
|
|
6489
|
+
*/
|
|
6490
|
+
export const LowercasedString = /*#__PURE__*/ lowercased(String);
|
|
6491
|
+
export type LowercasedString = typeof LowercasedString.Output;
|
|
6492
|
+
|
|
6493
|
+
/**
|
|
6494
|
+
* Lowercases the whole string and returns a {@link LowercasedString}.
|
|
6495
|
+
*
|
|
6496
|
+
* Leaves an empty string unchanged. Uses JavaScript's default Unicode casing
|
|
6497
|
+
* without locale-specific rules. Changing case can change the length, so input
|
|
6498
|
+
* brands are not retained.
|
|
6499
|
+
*
|
|
6500
|
+
* ### Example
|
|
6501
|
+
*
|
|
6502
|
+
* ```ts
|
|
6503
|
+
* import { assertEqual, assertType, lowercase } from "@evolu/common";
|
|
6504
|
+
*
|
|
6505
|
+
* const text = lowercase("Hello WORLD");
|
|
6506
|
+
* assertEqual(text, "hello world");
|
|
6507
|
+
* assertType<typeof text, "hello world">();
|
|
6508
|
+
* assertEqual(lowercase(text), text);
|
|
6509
|
+
* ```
|
|
6510
|
+
*
|
|
6511
|
+
* @group String
|
|
6512
|
+
*/
|
|
6513
|
+
export const lowercase = <S extends string>(value: S): Lowercase<S> =>
|
|
6514
|
+
value.toLowerCase() as Lowercase<S>;
|
|
6515
|
+
|
|
6516
|
+
/**
|
|
6517
|
+
* String {@link Brand} without surrounding whitespace.
|
|
6518
|
+
*
|
|
6519
|
+
* ### Example
|
|
6520
|
+
*
|
|
6521
|
+
* ```ts
|
|
5340
6522
|
* import { assertOk, String, trimmed } from "@evolu/common";
|
|
5341
6523
|
*
|
|
5342
|
-
* const Trimmed = trimmed(String);
|
|
6524
|
+
* const Trimmed = trimmed(String);
|
|
6525
|
+
*
|
|
6526
|
+
* assertOk(Trimmed.fromUnknown("Evolu"), "Evolu");
|
|
6527
|
+
* ```
|
|
6528
|
+
*
|
|
6529
|
+
* @group String
|
|
6530
|
+
*/
|
|
6531
|
+
export const trimmed: BrandFactory<"Trimmed", string, TrimmedError> = (
|
|
6532
|
+
parent,
|
|
6533
|
+
) =>
|
|
6534
|
+
brand(
|
|
6535
|
+
"Trimmed",
|
|
6536
|
+
parent,
|
|
6537
|
+
(value) =>
|
|
6538
|
+
value === value.trim()
|
|
6539
|
+
? ok()
|
|
6540
|
+
: err<TrimmedError>({ type: "Trimmed", value }),
|
|
6541
|
+
(error) =>
|
|
6542
|
+
`The value ${safelyStringifyUnknownValue(error.value)} must be trimmed.`,
|
|
6543
|
+
);
|
|
6544
|
+
|
|
6545
|
+
/**
|
|
6546
|
+
* Error returned when {@link trimmed} rejects a string.
|
|
6547
|
+
*
|
|
6548
|
+
* @group String
|
|
6549
|
+
*/
|
|
6550
|
+
export interface TrimmedError extends TypeError<"Trimmed"> {
|
|
6551
|
+
readonly value: string;
|
|
6552
|
+
}
|
|
6553
|
+
|
|
6554
|
+
/**
|
|
6555
|
+
* A {@link String} without surrounding whitespace.
|
|
6556
|
+
*
|
|
6557
|
+
* This Type validates that a string is already trimmed; it does not modify the
|
|
6558
|
+
* value. Use {@link trim} to normalize a string. Because an empty string is
|
|
6559
|
+
* valid, this Type is useful as an intermediate boundary for input controls
|
|
6560
|
+
* that trim their values before domain validation. If an empty string is
|
|
6561
|
+
* invalid, use {@link NonEmptyTrimmedString}.
|
|
6562
|
+
*
|
|
6563
|
+
* @group String
|
|
6564
|
+
*/
|
|
6565
|
+
export const TrimmedString = /*#__PURE__*/ trimmed(String);
|
|
6566
|
+
export type TrimmedString = typeof TrimmedString.Output;
|
|
6567
|
+
|
|
6568
|
+
/**
|
|
6569
|
+
* Trims a string and returns a {@link TrimmedString}.
|
|
6570
|
+
*
|
|
6571
|
+
* ### Example
|
|
6572
|
+
*
|
|
6573
|
+
* ```ts
|
|
6574
|
+
* import { assertEqual, trim } from "@evolu/common";
|
|
6575
|
+
*
|
|
6576
|
+
* assertEqual(trim(" Evolu "), "Evolu");
|
|
6577
|
+
* ```
|
|
6578
|
+
*
|
|
6579
|
+
* @group String
|
|
6580
|
+
*/
|
|
6581
|
+
export const trim = (value: string): TrimmedString =>
|
|
6582
|
+
value.trim() as TrimmedString;
|
|
6583
|
+
|
|
6584
|
+
/**
|
|
6585
|
+
* String {@link Brand} requiring an exact, case-sensitive prefix.
|
|
6586
|
+
*
|
|
6587
|
+
* Validation preserves the complete string, including the prefix. An empty
|
|
6588
|
+
* prefix accepts every string allowed by the parent Type. The prefix must be
|
|
6589
|
+
* one concrete string literal so different prefixes have distinct brands.
|
|
6590
|
+
*
|
|
6591
|
+
* ### Example
|
|
6592
|
+
*
|
|
6593
|
+
* ```ts
|
|
6594
|
+
* import {
|
|
6595
|
+
* assertEqual,
|
|
6596
|
+
* assertErr,
|
|
6597
|
+
* assertOk,
|
|
6598
|
+
* assertType,
|
|
6599
|
+
* maxLength,
|
|
6600
|
+
* startsWith,
|
|
6601
|
+
* String,
|
|
6602
|
+
* type Brand,
|
|
6603
|
+
* } from "@evolu/common";
|
|
6604
|
+
*
|
|
6605
|
+
* const EnvName = startsWith("APP_")(maxLength(64)(String));
|
|
5343
6606
|
*
|
|
5344
|
-
*
|
|
6607
|
+
* const name = EnvName.fromUnknown("APP_PORT");
|
|
6608
|
+
* assertOk(name, "APP_PORT");
|
|
6609
|
+
* assertType<
|
|
6610
|
+
* typeof name.value,
|
|
6611
|
+
* string & Brand<"MaxLength64"> & Brand<"StartsWithAPP_">
|
|
6612
|
+
* >();
|
|
6613
|
+
*
|
|
6614
|
+
* assertEqual(EnvName.to(name.value), "APP_PORT");
|
|
6615
|
+
*
|
|
6616
|
+
* assertErr(EnvName.fromUnknown("app_PORT"));
|
|
6617
|
+
* assertErr(EnvName.fromUnknown("APP_" + "X".repeat(61)));
|
|
5345
6618
|
* ```
|
|
5346
6619
|
*
|
|
5347
6620
|
* @group String
|
|
5348
6621
|
*/
|
|
5349
|
-
export const
|
|
5350
|
-
|
|
5351
|
-
) =>
|
|
5352
|
-
|
|
5353
|
-
|
|
5354
|
-
|
|
5355
|
-
(
|
|
5356
|
-
|
|
5357
|
-
|
|
5358
|
-
|
|
5359
|
-
|
|
5360
|
-
|
|
5361
|
-
|
|
6622
|
+
export const startsWith = <Prefix extends string>(
|
|
6623
|
+
prefix: Prefix & ValidateLiteral<Prefix>,
|
|
6624
|
+
): BrandFactory<`StartsWith${Prefix}`, string, StartsWithError<Prefix>> => {
|
|
6625
|
+
const name = `StartsWith${prefix}` as const;
|
|
6626
|
+
|
|
6627
|
+
return (parent) =>
|
|
6628
|
+
brand(
|
|
6629
|
+
name,
|
|
6630
|
+
parent,
|
|
6631
|
+
(value) =>
|
|
6632
|
+
value.startsWith(prefix)
|
|
6633
|
+
? ok()
|
|
6634
|
+
: err<StartsWithError<Prefix>>({
|
|
6635
|
+
type: name,
|
|
6636
|
+
value,
|
|
6637
|
+
prefix,
|
|
6638
|
+
}),
|
|
6639
|
+
(error) =>
|
|
6640
|
+
`The value ${safelyStringifyUnknownValue(error.value)} must start with ${safelyStringifyUnknownValue(error.prefix)}.`,
|
|
6641
|
+
);
|
|
6642
|
+
};
|
|
5362
6643
|
|
|
5363
6644
|
/**
|
|
5364
|
-
* Error returned when {@link
|
|
6645
|
+
* Error returned when {@link startsWith} rejects a string.
|
|
5365
6646
|
*
|
|
5366
6647
|
* @group String
|
|
5367
6648
|
*/
|
|
5368
|
-
export interface
|
|
6649
|
+
export interface StartsWithError<
|
|
6650
|
+
Prefix extends string = string,
|
|
6651
|
+
> extends TypeError<`StartsWith${Prefix}`> {
|
|
5369
6652
|
readonly value: string;
|
|
6653
|
+
readonly prefix: Prefix;
|
|
5370
6654
|
}
|
|
5371
6655
|
|
|
5372
6656
|
/**
|
|
5373
|
-
*
|
|
6657
|
+
* Decodes a prefixed string with another {@link Type} and restores the prefix
|
|
6658
|
+
* when encoding.
|
|
5374
6659
|
*
|
|
5375
|
-
*
|
|
5376
|
-
*
|
|
5377
|
-
*
|
|
5378
|
-
*
|
|
5379
|
-
*
|
|
6660
|
+
* Uses {@link startsWith} to validate an exact, case-sensitive prefix before
|
|
6661
|
+
* removing one occurrence. The wrapped Type validates and decodes the suffix;
|
|
6662
|
+
* its constraints apply to the suffix, and its Output is preserved. Encoding
|
|
6663
|
+
* prepends the prefix to the wrapped Type's canonical string representation. An
|
|
6664
|
+
* empty prefix leaves that representation unchanged.
|
|
5380
6665
|
*
|
|
5381
|
-
*
|
|
5382
|
-
|
|
5383
|
-
|
|
5384
|
-
export type TrimmedString = typeof TrimmedString.Output;
|
|
5385
|
-
|
|
5386
|
-
/**
|
|
5387
|
-
* Trims a string and returns a {@link TrimmedString}.
|
|
6666
|
+
* The prefix must be one concrete string literal. The wrapped Type must accept
|
|
6667
|
+
* a string Input and encode to strings; its Output can have another type, as
|
|
6668
|
+
* with {@link PortFromString}.
|
|
5388
6669
|
*
|
|
5389
6670
|
* ### Example
|
|
5390
6671
|
*
|
|
5391
6672
|
* ```ts
|
|
5392
|
-
* import {
|
|
6673
|
+
* import {
|
|
6674
|
+
* assertEqual,
|
|
6675
|
+
* assertErr,
|
|
6676
|
+
* assertOk,
|
|
6677
|
+
* assertType,
|
|
6678
|
+
* ConstantCaseIdentifier,
|
|
6679
|
+
* prefixed,
|
|
6680
|
+
* PortFromString,
|
|
6681
|
+
* type Port,
|
|
6682
|
+
* } from "@evolu/common";
|
|
5393
6683
|
*
|
|
5394
|
-
*
|
|
6684
|
+
* const EnvName = prefixed("APP_")(ConstantCaseIdentifier);
|
|
6685
|
+
*
|
|
6686
|
+
* const name = EnvName.fromUnknown("APP_PORT");
|
|
6687
|
+
* assertOk(name, "PORT");
|
|
6688
|
+
* assertType<typeof name.value, ConstantCaseIdentifier>();
|
|
6689
|
+
*
|
|
6690
|
+
* assertEqual(EnvName.to(name.value), "APP_PORT");
|
|
6691
|
+
*
|
|
6692
|
+
* assertErr(EnvName.fromUnknown("OTHER_PORT"));
|
|
6693
|
+
* assertErr(EnvName.fromUnknown("APP_port"));
|
|
6694
|
+
*
|
|
6695
|
+
* const PortSetting = prefixed("port:")(PortFromString);
|
|
6696
|
+
*
|
|
6697
|
+
* const port = PortSetting.fromUnknown("port:04000");
|
|
6698
|
+
* assertOk(port, 4000);
|
|
6699
|
+
* assertType<typeof port.value, Port>();
|
|
6700
|
+
*
|
|
6701
|
+
* assertEqual(PortSetting.to(port.value), "port:4000");
|
|
5395
6702
|
* ```
|
|
5396
6703
|
*
|
|
5397
6704
|
* @group String
|
|
5398
6705
|
*/
|
|
5399
|
-
export const
|
|
5400
|
-
|
|
6706
|
+
export const prefixed =
|
|
6707
|
+
<Prefix extends string>(prefix: Prefix & ValidateLiteral<Prefix>) =>
|
|
6708
|
+
<
|
|
6709
|
+
T extends ConcreteTypeNode & {
|
|
6710
|
+
readonly Input: string;
|
|
6711
|
+
readonly CanonicalInput: string;
|
|
6712
|
+
},
|
|
6713
|
+
>(
|
|
6714
|
+
type: T &
|
|
6715
|
+
ValidateOutput<T> &
|
|
6716
|
+
(string extends T["Input"] ? unknown : PrefixedInputTypeError),
|
|
6717
|
+
): PrefixedType<Prefix, T> => {
|
|
6718
|
+
// A literal prefix makes the generated names concrete and distinct.
|
|
6719
|
+
const source = startsWith<Prefix>(prefix)(
|
|
6720
|
+
String as ValidateBrandParent<`StartsWith${Prefix}`, typeof String>,
|
|
6721
|
+
);
|
|
6722
|
+
|
|
6723
|
+
return transform<
|
|
6724
|
+
`Prefixed${Prefix}`,
|
|
6725
|
+
typeof source,
|
|
6726
|
+
T,
|
|
6727
|
+
typeof source.Output
|
|
6728
|
+
>(
|
|
6729
|
+
`Prefixed${prefix}` as ValidateChildTypeName<
|
|
6730
|
+
`Prefixed${Prefix}`,
|
|
6731
|
+
typeof source
|
|
6732
|
+
>,
|
|
6733
|
+
source,
|
|
6734
|
+
type,
|
|
6735
|
+
{
|
|
6736
|
+
// The Input guard permits any string; concatenation establishes the prefix brand.
|
|
6737
|
+
from: (value) => ok(value.slice(prefix.length) as T["Input"]),
|
|
6738
|
+
to: (value) => `${prefix}${value}` as typeof source.Output,
|
|
6739
|
+
},
|
|
6740
|
+
);
|
|
6741
|
+
};
|
|
6742
|
+
|
|
6743
|
+
/**
|
|
6744
|
+
* The {@link Type} returned by {@link prefixed}.
|
|
6745
|
+
*
|
|
6746
|
+
* @group String
|
|
6747
|
+
*/
|
|
6748
|
+
export interface PrefixedType<
|
|
6749
|
+
Prefix extends string,
|
|
6750
|
+
T extends TypeNode,
|
|
6751
|
+
> extends TransformType<
|
|
6752
|
+
BrandType<typeof String, `StartsWith${Prefix}`, StartsWithError<Prefix>>,
|
|
6753
|
+
T,
|
|
6754
|
+
`Prefixed${Prefix}`,
|
|
6755
|
+
never
|
|
6756
|
+
> {}
|
|
6757
|
+
|
|
6758
|
+
type PrefixedInputTypeError = CompileTimeError<
|
|
6759
|
+
"Type",
|
|
6760
|
+
"Prefixed Type Input must accept every string."
|
|
6761
|
+
>;
|
|
5401
6762
|
|
|
5402
6763
|
/**
|
|
5403
6764
|
* Minimum-length {@link Brand} for values whose `length` is at least `min`.
|
|
@@ -5611,6 +6972,7 @@ export interface LengthError<
|
|
|
5611
6972
|
* assertType<UrlSafeString, string & Brand<"UrlSafeString">>();
|
|
5612
6973
|
*
|
|
5613
6974
|
* assertOk(UrlSafeString.fromUnknown("abc-123_DEF"), "abc-123_DEF");
|
|
6975
|
+
*
|
|
5614
6976
|
* const invalid = UrlSafeString.fromUnknown("not safe");
|
|
5615
6977
|
* assertErr(invalid);
|
|
5616
6978
|
* assertType(Data, invalid.error);
|
|
@@ -5803,7 +7165,13 @@ export const base64UrlToUint8Array = (value: Base64Url): Uint8Array =>
|
|
|
5803
7165
|
base64UrlStringToUint8Array(value);
|
|
5804
7166
|
|
|
5805
7167
|
/**
|
|
5806
|
-
* A non-empty URL-safe
|
|
7168
|
+
* A non-empty file-system-safe and URL-safe token of at most 64 UTF-16 code
|
|
7169
|
+
* units.
|
|
7170
|
+
*
|
|
7171
|
+
* Evolu uses it for database file names, storage pool names, and log prefixes.
|
|
7172
|
+
* It accepts the {@link UrlSafeString} alphabet in any order, so it may start
|
|
7173
|
+
* with a digit, `-`, or `_`. It is not a language identifier; for
|
|
7174
|
+
* word-structured names, use {@link identifier}.
|
|
5807
7175
|
*
|
|
5808
7176
|
* @group String
|
|
5809
7177
|
*/
|
|
@@ -6593,6 +7961,7 @@ export type PositiveFiniteNumber = typeof PositiveFiniteNumber.Output;
|
|
|
6593
7961
|
* assertType<Int, number & Brand<"Int">>();
|
|
6594
7962
|
*
|
|
6595
7963
|
* assertOk(Int.fromUnknown(42), 42);
|
|
7964
|
+
*
|
|
6596
7965
|
* const invalid = Int.fromUnknown(1.5);
|
|
6597
7966
|
* assertErr(invalid);
|
|
6598
7967
|
* assertType(Data, invalid.error);
|
|
@@ -6657,6 +8026,68 @@ export const zeroNonNegativeInt = /*#__PURE__*/ NonNegativeInt.orThrow(0);
|
|
|
6657
8026
|
export const PositiveInt = /*#__PURE__*/ positive(NonNegativeInt);
|
|
6658
8027
|
export type PositiveInt = typeof PositiveInt.Output;
|
|
6659
8028
|
|
|
8029
|
+
/**
|
|
8030
|
+
* Error returned when a string is not a decimal integer.
|
|
8031
|
+
*
|
|
8032
|
+
* @group Number
|
|
8033
|
+
*/
|
|
8034
|
+
export interface IntFromStringError extends TypeError<"IntFromString"> {
|
|
8035
|
+
readonly value: string;
|
|
8036
|
+
}
|
|
8037
|
+
|
|
8038
|
+
/**
|
|
8039
|
+
* Transforms a decimal integer string into an {@link Int}.
|
|
8040
|
+
*
|
|
8041
|
+
* This is useful for inputs that carry numbers as text, such as environment
|
|
8042
|
+
* variables, URL query parameters, and form fields. The string must consist of
|
|
8043
|
+
* an optional minus sign and digits; the {@link Int} constraint then rejects
|
|
8044
|
+
* values outside the safe integer range.
|
|
8045
|
+
*
|
|
8046
|
+
* ### Example
|
|
8047
|
+
*
|
|
8048
|
+
* ```ts
|
|
8049
|
+
* import {
|
|
8050
|
+
* assertEqual,
|
|
8051
|
+
* assertErr,
|
|
8052
|
+
* assertOk,
|
|
8053
|
+
* assertSame,
|
|
8054
|
+
* IntFromString,
|
|
8055
|
+
* } from "@evolu/common";
|
|
8056
|
+
*
|
|
8057
|
+
* assertOk(IntFromString.fromUnknown("4000"), 4000);
|
|
8058
|
+
* assertOk(IntFromString.fromUnknown("-1"), -1);
|
|
8059
|
+
* assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
|
|
8060
|
+
*
|
|
8061
|
+
* const negativeZero = IntFromString.orThrow("-0");
|
|
8062
|
+
* assertSame(negativeZero, -0);
|
|
8063
|
+
* assertEqual(IntFromString.to(negativeZero), "-0");
|
|
8064
|
+
*
|
|
8065
|
+
* const invalid = IntFromString.fromUnknown("4000.5");
|
|
8066
|
+
* assertErr(invalid, { type: "IntFromString", value: "4000.5" });
|
|
8067
|
+
* assertEqual(
|
|
8068
|
+
* IntFromString.formatError(invalid.error),
|
|
8069
|
+
* 'The value "4000.5" is not a decimal integer.',
|
|
8070
|
+
* );
|
|
8071
|
+
* ```
|
|
8072
|
+
*
|
|
8073
|
+
* @group Number
|
|
8074
|
+
*/
|
|
8075
|
+
export const IntFromString = /*#__PURE__*/ transform(
|
|
8076
|
+
"IntFromString",
|
|
8077
|
+
String,
|
|
8078
|
+
Int,
|
|
8079
|
+
{
|
|
8080
|
+
from: (value): Result<number, IntFromStringError> =>
|
|
8081
|
+
/^-?\d+$/u.test(value)
|
|
8082
|
+
? ok(globalThis.Number(value))
|
|
8083
|
+
: err({ type: "IntFromString", value }),
|
|
8084
|
+
to: (value) =>
|
|
8085
|
+
globalThis.Object.is(value, -0) ? "-0" : globalThis.String(value),
|
|
8086
|
+
},
|
|
8087
|
+
(error) =>
|
|
8088
|
+
`The value ${safelyStringifyUnknownValue(error.value)} is not a decimal integer.`,
|
|
8089
|
+
);
|
|
8090
|
+
|
|
6660
8091
|
/**
|
|
6661
8092
|
* Minimum {@link PositiveInt} value.
|
|
6662
8093
|
*
|
|
@@ -6898,6 +8329,65 @@ export interface LessThanOrEqualToError<
|
|
|
6898
8329
|
readonly max: Max;
|
|
6899
8330
|
}
|
|
6900
8331
|
|
|
8332
|
+
/**
|
|
8333
|
+
* A TCP or UDP port as an integer from zero through 65535, inclusive.
|
|
8334
|
+
*
|
|
8335
|
+
* When binding a server, zero requests an automatically assigned port. Use
|
|
8336
|
+
* {@link PortFromString} for configuration values supplied as text.
|
|
8337
|
+
*
|
|
8338
|
+
* ### Example
|
|
8339
|
+
*
|
|
8340
|
+
* ```ts
|
|
8341
|
+
* import { assertErr, assertOk, Port } from "@evolu/common";
|
|
8342
|
+
*
|
|
8343
|
+
* assertOk(Port.fromUnknown(0), 0);
|
|
8344
|
+
* assertOk(Port.fromUnknown(4000), 4000);
|
|
8345
|
+
* assertOk(Port.fromUnknown(65535), 65535);
|
|
8346
|
+
* assertErr(Port.fromUnknown(-1));
|
|
8347
|
+
* assertErr(Port.fromUnknown(65536));
|
|
8348
|
+
* assertErr(Port.fromUnknown(4000.5));
|
|
8349
|
+
* ```
|
|
8350
|
+
*
|
|
8351
|
+
* @group Number
|
|
8352
|
+
*/
|
|
8353
|
+
export const Port = /*#__PURE__*/ brand(
|
|
8354
|
+
"Port",
|
|
8355
|
+
/*#__PURE__*/ lessThanOrEqualTo(65535)(NonNegativeInt),
|
|
8356
|
+
);
|
|
8357
|
+
export type Port = typeof Port.Output;
|
|
8358
|
+
|
|
8359
|
+
/**
|
|
8360
|
+
* Parses a decimal integer string and validates it as a {@link Port}.
|
|
8361
|
+
*
|
|
8362
|
+
* Uses {@link IntFromString} for decimal parsing, including its rejection of
|
|
8363
|
+
* whitespace, plus signs, fractions, and exponent notation.
|
|
8364
|
+
*
|
|
8365
|
+
* ### Example
|
|
8366
|
+
*
|
|
8367
|
+
* ```ts
|
|
8368
|
+
* import {
|
|
8369
|
+
* assertEqual,
|
|
8370
|
+
* assertErr,
|
|
8371
|
+
* assertOk,
|
|
8372
|
+
* PortFromString,
|
|
8373
|
+
* } from "@evolu/common";
|
|
8374
|
+
*
|
|
8375
|
+
* assertOk(PortFromString.fromUnknown("0"), 0);
|
|
8376
|
+
* assertOk(PortFromString.fromUnknown("4000"), 4000);
|
|
8377
|
+
* assertErr(PortFromString.fromUnknown("65536"));
|
|
8378
|
+
* assertErr(PortFromString.fromUnknown("http"));
|
|
8379
|
+
* assertEqual(PortFromString.to(PortFromString.orThrow("04000")), "4000");
|
|
8380
|
+
* ```
|
|
8381
|
+
*
|
|
8382
|
+
* @group Number
|
|
8383
|
+
*/
|
|
8384
|
+
export const PortFromString = /*#__PURE__*/ transform(
|
|
8385
|
+
"PortFromString",
|
|
8386
|
+
IntFromString,
|
|
8387
|
+
Port,
|
|
8388
|
+
{ from: ok, to: identity },
|
|
8389
|
+
);
|
|
8390
|
+
|
|
6901
8391
|
/**
|
|
6902
8392
|
* Finite {@link Number} from zero to one, inclusive.
|
|
6903
8393
|
*
|
|
@@ -7248,6 +8738,7 @@ export type NegativeDecimalString = typeof NegativeDecimalString.Output;
|
|
|
7248
8738
|
* >();
|
|
7249
8739
|
*
|
|
7250
8740
|
* assertOk(Tenths.fromUnknown(0.3), 0.3);
|
|
8741
|
+
*
|
|
7251
8742
|
* const invalid = Tenths.fromUnknown(0.31);
|
|
7252
8743
|
* assertErr(invalid);
|
|
7253
8744
|
* assertType(Data, invalid.error);
|
|
@@ -7476,6 +8967,7 @@ export interface BetweenError<
|
|
|
7476
8967
|
*
|
|
7477
8968
|
* const UserId = brand("UserId", String);
|
|
7478
8969
|
* const UserIds = array(UserId);
|
|
8970
|
+
*
|
|
7479
8971
|
* const result = UserIds.from.parent(["ada", "grace"]);
|
|
7480
8972
|
*
|
|
7481
8973
|
* assertType<
|
|
@@ -7484,6 +8976,7 @@ export interface BetweenError<
|
|
|
7484
8976
|
* >();
|
|
7485
8977
|
* assertOk(result, ["ada", "grace"]);
|
|
7486
8978
|
* assertOk(UserIds.fromUnknown(["ada", "grace"]), ["ada", "grace"]);
|
|
8979
|
+
*
|
|
7487
8980
|
* const invalid = UserIds.fromUnknown("ada");
|
|
7488
8981
|
* assertErr(invalid);
|
|
7489
8982
|
* assertType(Data, invalid.error);
|
|
@@ -9238,7 +10731,7 @@ const validateTupleItems = (
|
|
|
9238
10731
|
);
|
|
9239
10732
|
|
|
9240
10733
|
/**
|
|
9241
|
-
* Decimal
|
|
10734
|
+
* Decimal integer string from `"0"` to `"9"`.
|
|
9242
10735
|
*
|
|
9243
10736
|
* @group String
|
|
9244
10737
|
*/
|
|
@@ -9257,7 +10750,7 @@ export const Digit = /*#__PURE__*/ union(
|
|
|
9257
10750
|
export type Digit = typeof Digit.Output;
|
|
9258
10751
|
|
|
9259
10752
|
/**
|
|
9260
|
-
* Decimal
|
|
10753
|
+
* Decimal integer string from `"1"` to `"9"`.
|
|
9261
10754
|
*
|
|
9262
10755
|
* @group String
|
|
9263
10756
|
*/
|
|
@@ -9275,7 +10768,7 @@ export const Digit1To9 = /*#__PURE__*/ union(
|
|
|
9275
10768
|
export type Digit1To9 = typeof Digit1To9.Output;
|
|
9276
10769
|
|
|
9277
10770
|
/**
|
|
9278
|
-
* Decimal string from `"1"` to `"6"`.
|
|
10771
|
+
* Decimal integer string from `"1"` to `"6"`.
|
|
9279
10772
|
*
|
|
9280
10773
|
* @group String
|
|
9281
10774
|
*/
|
|
@@ -9283,11 +10776,29 @@ export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
|
|
|
9283
10776
|
export type Digit1To6 = typeof Digit1To6.Output;
|
|
9284
10777
|
|
|
9285
10778
|
/**
|
|
9286
|
-
* Decimal string from `"1"` to `"23"
|
|
10779
|
+
* Decimal integer string from `"1"` to `"23"`, without a leading zero.
|
|
9287
10780
|
*
|
|
9288
10781
|
* @group String
|
|
9289
10782
|
*/
|
|
9290
|
-
export const Digit1To23
|
|
10783
|
+
export const Digit1To23: UnionType<
|
|
10784
|
+
readonly [
|
|
10785
|
+
typeof Digit1To9,
|
|
10786
|
+
TemplateLiteralType<readonly ["1", typeof Digit]>,
|
|
10787
|
+
TemplateLiteralType<
|
|
10788
|
+
readonly [
|
|
10789
|
+
"2",
|
|
10790
|
+
UnionType<
|
|
10791
|
+
readonly [
|
|
10792
|
+
LiteralType<"0">,
|
|
10793
|
+
LiteralType<"1">,
|
|
10794
|
+
LiteralType<"2">,
|
|
10795
|
+
LiteralType<"3">,
|
|
10796
|
+
]
|
|
10797
|
+
>,
|
|
10798
|
+
]
|
|
10799
|
+
>,
|
|
10800
|
+
]
|
|
10801
|
+
> = /*#__PURE__*/ union(
|
|
9291
10802
|
Digit1To9,
|
|
9292
10803
|
/*#__PURE__*/ templateLiteral("1", Digit),
|
|
9293
10804
|
/*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")),
|
|
@@ -9295,11 +10806,31 @@ export const Digit1To23 = /*#__PURE__*/ union(
|
|
|
9295
10806
|
export type Digit1To23 = typeof Digit1To23.Output;
|
|
9296
10807
|
|
|
9297
10808
|
/**
|
|
9298
|
-
* Decimal string from `"1"` to `"51"
|
|
10809
|
+
* Decimal integer string from `"1"` to `"51"`, without a leading zero.
|
|
9299
10810
|
*
|
|
9300
10811
|
* @group String
|
|
9301
10812
|
*/
|
|
9302
|
-
export const Digit1To51
|
|
10813
|
+
export const Digit1To51: UnionType<
|
|
10814
|
+
readonly [
|
|
10815
|
+
typeof Digit1To9,
|
|
10816
|
+
TemplateLiteralType<
|
|
10817
|
+
readonly [
|
|
10818
|
+
UnionType<
|
|
10819
|
+
readonly [
|
|
10820
|
+
LiteralType<"1">,
|
|
10821
|
+
LiteralType<"2">,
|
|
10822
|
+
LiteralType<"3">,
|
|
10823
|
+
LiteralType<"4">,
|
|
10824
|
+
]
|
|
10825
|
+
>,
|
|
10826
|
+
typeof Digit,
|
|
10827
|
+
]
|
|
10828
|
+
>,
|
|
10829
|
+
TemplateLiteralType<
|
|
10830
|
+
readonly ["5", UnionType<readonly [LiteralType<"0">, LiteralType<"1">]>]
|
|
10831
|
+
>,
|
|
10832
|
+
]
|
|
10833
|
+
> = /*#__PURE__*/ union(
|
|
9303
10834
|
Digit1To9,
|
|
9304
10835
|
/*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
9305
10836
|
/*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")),
|
|
@@ -9307,22 +10838,45 @@ export const Digit1To51 = /*#__PURE__*/ union(
|
|
|
9307
10838
|
export type Digit1To51 = typeof Digit1To51.Output;
|
|
9308
10839
|
|
|
9309
10840
|
/**
|
|
9310
|
-
* Decimal string from `"1"` to `"99"
|
|
10841
|
+
* Decimal integer string from `"1"` to `"99"`, without a leading zero.
|
|
9311
10842
|
*
|
|
9312
10843
|
* @group String
|
|
9313
10844
|
*/
|
|
9314
|
-
export const Digit1To99
|
|
10845
|
+
export const Digit1To99: UnionType<
|
|
10846
|
+
readonly [
|
|
10847
|
+
typeof Digit1To9,
|
|
10848
|
+
TemplateLiteralType<readonly [typeof Digit1To9, typeof Digit]>,
|
|
10849
|
+
]
|
|
10850
|
+
> = /*#__PURE__*/ union(
|
|
9315
10851
|
Digit1To9,
|
|
9316
10852
|
/*#__PURE__*/ templateLiteral(Digit1To9, Digit),
|
|
9317
10853
|
);
|
|
9318
10854
|
export type Digit1To99 = typeof Digit1To99.Output;
|
|
9319
10855
|
|
|
9320
10856
|
/**
|
|
9321
|
-
* Decimal string from `"1"` to `"59"
|
|
10857
|
+
* Decimal integer string from `"1"` to `"59"`, without a leading zero.
|
|
9322
10858
|
*
|
|
9323
10859
|
* @group String
|
|
9324
10860
|
*/
|
|
9325
|
-
export const Digit1To59
|
|
10861
|
+
export const Digit1To59: UnionType<
|
|
10862
|
+
readonly [
|
|
10863
|
+
typeof Digit1To9,
|
|
10864
|
+
TemplateLiteralType<
|
|
10865
|
+
readonly [
|
|
10866
|
+
UnionType<
|
|
10867
|
+
readonly [
|
|
10868
|
+
LiteralType<"1">,
|
|
10869
|
+
LiteralType<"2">,
|
|
10870
|
+
LiteralType<"3">,
|
|
10871
|
+
LiteralType<"4">,
|
|
10872
|
+
LiteralType<"5">,
|
|
10873
|
+
]
|
|
10874
|
+
>,
|
|
10875
|
+
typeof Digit,
|
|
10876
|
+
]
|
|
10877
|
+
>,
|
|
10878
|
+
]
|
|
10879
|
+
> = /*#__PURE__*/ union(
|
|
9326
10880
|
Digit1To9,
|
|
9327
10881
|
/*#__PURE__*/ templateLiteral(
|
|
9328
10882
|
/*#__PURE__*/ union("1", "2", "3", "4", "5"),
|
|
@@ -9645,6 +11199,7 @@ export { _Object as Object };
|
|
|
9645
11199
|
* const valueType = typeof value;
|
|
9646
11200
|
*
|
|
9647
11201
|
* assertEqual(valueType, "function");
|
|
11202
|
+
*
|
|
9648
11203
|
* const called = trySync(
|
|
9649
11204
|
* () => {
|
|
9650
11205
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -9680,6 +11235,7 @@ export { _Object as Object };
|
|
|
9680
11235
|
* const valueType = typeof value;
|
|
9681
11236
|
*
|
|
9682
11237
|
* assertEqual(valueType, "function");
|
|
11238
|
+
*
|
|
9683
11239
|
* const called = trySync(
|
|
9684
11240
|
* () => {
|
|
9685
11241
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -10290,28 +11846,389 @@ export interface OptionalProperty<T extends TypeNode> {
|
|
|
10290
11846
|
* });
|
|
10291
11847
|
* ```
|
|
10292
11848
|
*
|
|
10293
|
-
* @group Objects
|
|
11849
|
+
* @group Objects
|
|
11850
|
+
*/
|
|
11851
|
+
export const optional = <T extends TypeNode>(
|
|
11852
|
+
type: ValidateOptionalPropertyType<T>,
|
|
11853
|
+
): OptionalProperty<T> => createOptionalProperty(type as T);
|
|
11854
|
+
|
|
11855
|
+
declare const optionalPropertySymbolType: unique symbol;
|
|
11856
|
+
const optionalPropertySymbol: typeof optionalPropertySymbolType =
|
|
11857
|
+
/*#__PURE__*/ globalThis.Symbol() as typeof optionalPropertySymbolType;
|
|
11858
|
+
|
|
11859
|
+
const createOptionalProperty = <T extends TypeNode>(
|
|
11860
|
+
type: T,
|
|
11861
|
+
): OptionalProperty<T> =>
|
|
11862
|
+
({
|
|
11863
|
+
type,
|
|
11864
|
+
[optionalPropertySymbol]: true,
|
|
11865
|
+
}) as OptionalProperty<T>;
|
|
11866
|
+
|
|
11867
|
+
type ValidateOptionalPropertyType<T extends TypeNode> =
|
|
11868
|
+
IsUnion<T> extends false
|
|
11869
|
+
? T extends ConcreteTypeNode
|
|
11870
|
+
? T
|
|
11871
|
+
: ObjectPropertyTypeError
|
|
11872
|
+
: ObjectPropertyTypeError;
|
|
11873
|
+
|
|
11874
|
+
/**
|
|
11875
|
+
* A value decoded by {@link withDefault} with the `preserve` strategy.
|
|
11876
|
+
*
|
|
11877
|
+
* `defaultUsed` distinguishes supplied values from defaults, even when their
|
|
11878
|
+
* values are equal. `original` records the decoded absence, not the raw input
|
|
11879
|
+
* before other transformations.
|
|
11880
|
+
*
|
|
11881
|
+
* @group Construction
|
|
11882
|
+
*/
|
|
11883
|
+
export type Defaulted<
|
|
11884
|
+
Value,
|
|
11885
|
+
Default = Value,
|
|
11886
|
+
Original extends "missing" | "null" | "undefined" =
|
|
11887
|
+
"missing" | "null" | "undefined",
|
|
11888
|
+
> =
|
|
11889
|
+
| { readonly value: Value; readonly defaultUsed: false }
|
|
11890
|
+
| {
|
|
11891
|
+
readonly value: Default;
|
|
11892
|
+
readonly defaultUsed: true;
|
|
11893
|
+
readonly original: Original;
|
|
11894
|
+
};
|
|
11895
|
+
|
|
11896
|
+
/**
|
|
11897
|
+
* The value Type returned by {@link withDefault}.
|
|
11898
|
+
*
|
|
11899
|
+
* @group Construction
|
|
11900
|
+
*/
|
|
11901
|
+
export interface WithDefaultType<
|
|
11902
|
+
T extends TypeNode,
|
|
11903
|
+
Value,
|
|
11904
|
+
Strategy extends "replace" | "preserve",
|
|
11905
|
+
> extends Type<
|
|
11906
|
+
"WithDefault",
|
|
11907
|
+
T["Input"],
|
|
11908
|
+
WithDefaultOutput<T, Value, Strategy>,
|
|
11909
|
+
never,
|
|
11910
|
+
T,
|
|
11911
|
+
InferErrors<T>,
|
|
11912
|
+
ChildCustomFrom<T, WithDefaultOutput<T, Value, Strategy>, never>,
|
|
11913
|
+
T["CanonicalInput"],
|
|
11914
|
+
false
|
|
11915
|
+
> {}
|
|
11916
|
+
|
|
11917
|
+
/**
|
|
11918
|
+
* An optional input property made required in the Output by {@link withDefault}.
|
|
11919
|
+
*
|
|
11920
|
+
* @group Objects
|
|
11921
|
+
*/
|
|
11922
|
+
export interface WithDefaultProperty<
|
|
11923
|
+
T extends TypeNode,
|
|
11924
|
+
Value,
|
|
11925
|
+
Strategy extends "replace" | "preserve",
|
|
11926
|
+
> {
|
|
11927
|
+
readonly type: T;
|
|
11928
|
+
readonly value: Value;
|
|
11929
|
+
readonly strategy: Strategy;
|
|
11930
|
+
/** @ignore */
|
|
11931
|
+
readonly [errorsSymbol]: InferErrors<T>;
|
|
11932
|
+
/** @ignore */
|
|
11933
|
+
readonly [defaultPropertySymbol]: RuntimeDefaultOperations;
|
|
11934
|
+
}
|
|
11935
|
+
|
|
11936
|
+
/**
|
|
11937
|
+
* Supplies a decoded default for absence accepted by another {@link Type}.
|
|
11938
|
+
*
|
|
11939
|
+
* You might not need this: use `??` where a value is consumed if the default
|
|
11940
|
+
* does not need to become part of the decoded data. By default, replacement
|
|
11941
|
+
* loses the distinction between absence and an explicitly supplied value. Set
|
|
11942
|
+
* `strategy: "preserve"` to retain it.
|
|
11943
|
+
*
|
|
11944
|
+
* Defaults apply after decoding, to `null` and `undefined` accepted by the
|
|
11945
|
+
* wrapped Type, and to missing properties when wrapping {@link optional}. Types
|
|
11946
|
+
* whose Output excludes both `null` and `undefined` require `optional`;
|
|
11947
|
+
* otherwise there is no accepted absence to default. Invalid supplied values
|
|
11948
|
+
* still fail. An optional property does not accept an explicit `undefined`
|
|
11949
|
+
* unless its Type does. The default must be an Output of the wrapped Type; it
|
|
11950
|
+
* is checked when constructing the declaration. It may itself be `null` or
|
|
11951
|
+
* `undefined` when the wrapped Output accepts that value.
|
|
11952
|
+
*
|
|
11953
|
+
* Omitting `strategy` returns the effective value and encodes it as supplied
|
|
11954
|
+
* data. `strategy: "preserve"` returns {@link Defaulted} and restores absence
|
|
11955
|
+
* when encoding. A supplied value equal to the default still has `defaultUsed:
|
|
11956
|
+
* false`. Preserved defaults must equal the configured value: {@link Data}
|
|
11957
|
+
* values use structural equality, and other values use reference identity.
|
|
11958
|
+
* Every decoded absence reuses the configured default by reference; defaults
|
|
11959
|
+
* are not cloned. Treat default values as immutable, like other Type
|
|
11960
|
+
* declarations.
|
|
11961
|
+
*
|
|
11962
|
+
* Pass the default value as the second argument. Add `{ strategy: "preserve" }`
|
|
11963
|
+
* as the third argument to retain supplied-input evidence. Explicit `strategy:
|
|
11964
|
+
* "replace"` is not accepted.
|
|
11965
|
+
*
|
|
11966
|
+
* ### Example
|
|
11967
|
+
*
|
|
11968
|
+
* ```ts
|
|
11969
|
+
* import {
|
|
11970
|
+
* assertEqual,
|
|
11971
|
+
* assertErr,
|
|
11972
|
+
* assertOk,
|
|
11973
|
+
* Boolean,
|
|
11974
|
+
* nullOr,
|
|
11975
|
+
* object,
|
|
11976
|
+
* optional,
|
|
11977
|
+
* withDefault,
|
|
11978
|
+
* } from "@evolu/common";
|
|
11979
|
+
*
|
|
11980
|
+
* const Settings = object({
|
|
11981
|
+
* enabled: withDefault(optional(Boolean), true, {
|
|
11982
|
+
* strategy: "preserve",
|
|
11983
|
+
* }),
|
|
11984
|
+
* });
|
|
11985
|
+
*
|
|
11986
|
+
* const missing = Settings.fromUnknown({});
|
|
11987
|
+
* assertOk(missing, {
|
|
11988
|
+
* enabled: { value: true, defaultUsed: true, original: "missing" },
|
|
11989
|
+
* });
|
|
11990
|
+
* assertEqual(Settings.to(missing.value), {});
|
|
11991
|
+
*
|
|
11992
|
+
* assertOk(Settings.fromUnknown({ enabled: true }), {
|
|
11993
|
+
* enabled: { value: true, defaultUsed: false },
|
|
11994
|
+
* });
|
|
11995
|
+
* assertErr(Settings.fromUnknown({ enabled: undefined }));
|
|
11996
|
+
*
|
|
11997
|
+
* const Enabled = withDefault(nullOr(Boolean), true);
|
|
11998
|
+
*
|
|
11999
|
+
* assertOk(Enabled.fromUnknown(null), true);
|
|
12000
|
+
* assertEqual(Enabled.to(true), true);
|
|
12001
|
+
* ```
|
|
12002
|
+
*
|
|
12003
|
+
* @group Construction
|
|
10294
12004
|
*/
|
|
10295
|
-
export
|
|
10296
|
-
|
|
10297
|
-
|
|
12005
|
+
export function withDefault<
|
|
12006
|
+
T extends TypeNode,
|
|
12007
|
+
const Value extends NoInfer<T["Output"]>,
|
|
12008
|
+
>(
|
|
12009
|
+
property: OptionalProperty<T> & {
|
|
12010
|
+
readonly type: ValidateDefaultType<T, true>;
|
|
12011
|
+
},
|
|
12012
|
+
value: Value,
|
|
12013
|
+
): WithDefaultProperty<T, Value, "replace">;
|
|
12014
|
+
export function withDefault<
|
|
12015
|
+
T extends TypeNode,
|
|
12016
|
+
const Value extends NoInfer<T["Output"]>,
|
|
12017
|
+
>(
|
|
12018
|
+
property: OptionalProperty<T> & {
|
|
12019
|
+
readonly type: ValidateDefaultType<T, true>;
|
|
12020
|
+
},
|
|
12021
|
+
value: Value,
|
|
12022
|
+
options: { readonly strategy: "preserve" },
|
|
12023
|
+
): WithDefaultProperty<T, Value, "preserve">;
|
|
12024
|
+
export function withDefault<
|
|
12025
|
+
T extends TypeNode,
|
|
12026
|
+
const Value extends NoInfer<T["Output"]>,
|
|
12027
|
+
>(
|
|
12028
|
+
type: T & ValidateDefaultType<T>,
|
|
12029
|
+
value: Value,
|
|
12030
|
+
): WithDefaultType<T, Value, "replace">;
|
|
12031
|
+
export function withDefault<
|
|
12032
|
+
T extends TypeNode,
|
|
12033
|
+
const Value extends NoInfer<T["Output"]>,
|
|
12034
|
+
>(
|
|
12035
|
+
type: T & ValidateDefaultType<T>,
|
|
12036
|
+
value: Value,
|
|
12037
|
+
options: { readonly strategy: "preserve" },
|
|
12038
|
+
): WithDefaultType<T, Value, "preserve">;
|
|
12039
|
+
export function withDefault(
|
|
12040
|
+
property: TypeNode | OptionalProperty<TypeNode>,
|
|
12041
|
+
value: unknown,
|
|
12042
|
+
{ strategy }: { readonly strategy?: string } = {},
|
|
12043
|
+
): TypeNode | WithDefaultProperty<TypeNode, unknown, "replace" | "preserve"> {
|
|
12044
|
+
assert(
|
|
12045
|
+
strategy === undefined || strategy === "preserve",
|
|
12046
|
+
'withDefault strategy must be omitted or "preserve".',
|
|
12047
|
+
);
|
|
12048
|
+
const optionalInput = optionalPropertySymbol in property;
|
|
12049
|
+
const type = (optionalInput ? property.type : property) as RuntimeTypeNode;
|
|
12050
|
+
assert(
|
|
12051
|
+
type.is(value),
|
|
12052
|
+
`withDefault value must be an Output of ${type.name}.`,
|
|
12053
|
+
);
|
|
12054
|
+
const originals = new Set([
|
|
12055
|
+
...(optionalInput ? ["missing"] : []),
|
|
12056
|
+
...(type.is(null) ? ["null"] : []),
|
|
12057
|
+
...(type.is(undefined) ? ["undefined"] : []),
|
|
12058
|
+
]);
|
|
12059
|
+
const supplied = createRootType(
|
|
12060
|
+
"DefaultSupplied",
|
|
12061
|
+
(candidate): Result<unknown, TypeError<"DefaultValue">> =>
|
|
12062
|
+
(strategy === undefined && globalThis.Object.is(candidate, value)) ||
|
|
12063
|
+
(candidate !== null && candidate !== undefined && type.is(candidate))
|
|
12064
|
+
? ok(candidate)
|
|
12065
|
+
: err({ type: "DefaultValue" }),
|
|
12066
|
+
() =>
|
|
12067
|
+
"The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.",
|
|
12068
|
+
);
|
|
12069
|
+
const defaultValue = createRootType(
|
|
12070
|
+
"DefaultValue",
|
|
12071
|
+
(candidate): Result<unknown, TypeError<"DefaultValue">> =>
|
|
12072
|
+
type.is(candidate) &&
|
|
12073
|
+
(globalThis.Object.is(candidate, value) ||
|
|
12074
|
+
(Data.is(value) && Data.is(candidate) && eqData(candidate, value)))
|
|
12075
|
+
? ok(candidate)
|
|
12076
|
+
: err({ type: "DefaultValue" }),
|
|
12077
|
+
() => "A preserved default must equal the configured default value.",
|
|
12078
|
+
);
|
|
12079
|
+
const output = (strategy === undefined
|
|
12080
|
+
? supplied
|
|
12081
|
+
: discriminatedUnion(
|
|
12082
|
+
"defaultUsed",
|
|
12083
|
+
object({ value: supplied, defaultUsed: literal(false) }),
|
|
12084
|
+
object({
|
|
12085
|
+
value: defaultValue,
|
|
12086
|
+
defaultUsed: literal(true),
|
|
12087
|
+
original: createRootType(
|
|
12088
|
+
"DefaultOriginal",
|
|
12089
|
+
(candidate): Result<string, TypeError<"DefaultOriginal">> =>
|
|
12090
|
+
typeof candidate === "string" && originals.has(candidate)
|
|
12091
|
+
? ok(candidate)
|
|
12092
|
+
: err({ type: "DefaultOriginal" }),
|
|
12093
|
+
() =>
|
|
12094
|
+
"The original absence must be handled by this default declaration.",
|
|
12095
|
+
),
|
|
12096
|
+
}),
|
|
12097
|
+
)) as unknown as RuntimeTypeNode;
|
|
12098
|
+
const ownGetTypeIssues = output[getRuntimeTypeIssuesSymbol];
|
|
12099
|
+
const outputWithIssues: RuntimeTypeNode = {
|
|
12100
|
+
...output,
|
|
12101
|
+
[outputValidationSymbol]: (value: unknown, options?: ValidationOptions) => {
|
|
12102
|
+
const result = output[outputValidationSymbol](value, options);
|
|
12103
|
+
return result.ok
|
|
12104
|
+
? result
|
|
12105
|
+
: err({ type: "WithDefault", outputError: result.error });
|
|
12106
|
+
},
|
|
12107
|
+
[getRuntimeTypeIssuesSymbol]: (error, mode) =>
|
|
12108
|
+
error.type === "WithDefault" && "outputError" in error
|
|
12109
|
+
? ownGetTypeIssues(error.outputError as TypeError, mode)
|
|
12110
|
+
: type[getRuntimeTypeIssuesSymbol](error, mode),
|
|
12111
|
+
};
|
|
12112
|
+
|
|
12113
|
+
const operations: RuntimeDefaultOperations = {
|
|
12114
|
+
output: outputWithIssues,
|
|
12115
|
+
createObject: createDefaultObjectType,
|
|
12116
|
+
partial: (type) =>
|
|
12117
|
+
createOptionalProperty(
|
|
12118
|
+
strategy === "preserve"
|
|
12119
|
+
? withDefault(type as unknown as typeof Unknown, value, {
|
|
12120
|
+
strategy,
|
|
12121
|
+
})
|
|
12122
|
+
: withDefault(type as unknown as typeof Unknown, value),
|
|
12123
|
+
),
|
|
12124
|
+
decode: (candidate) => {
|
|
12125
|
+
const original =
|
|
12126
|
+
candidate === missingDefaultValue
|
|
12127
|
+
? "missing"
|
|
12128
|
+
: candidate === null
|
|
12129
|
+
? "null"
|
|
12130
|
+
: candidate === undefined
|
|
12131
|
+
? "undefined"
|
|
12132
|
+
: undefined;
|
|
12133
|
+
const effective = original === undefined ? candidate : value;
|
|
12134
|
+
return strategy === undefined
|
|
12135
|
+
? effective
|
|
12136
|
+
: original === undefined
|
|
12137
|
+
? { value: effective, defaultUsed: false }
|
|
12138
|
+
: { value: effective, defaultUsed: true, original };
|
|
12139
|
+
},
|
|
12140
|
+
encode: (candidate: unknown) => {
|
|
12141
|
+
if (strategy === undefined) return candidate;
|
|
12142
|
+
const preserved = candidate as Defaulted<unknown>;
|
|
12143
|
+
if (!preserved.defaultUsed) return preserved.value;
|
|
12144
|
+
switch (preserved.original) {
|
|
12145
|
+
case "missing":
|
|
12146
|
+
return missingDefaultValue;
|
|
12147
|
+
case "null":
|
|
12148
|
+
return null;
|
|
12149
|
+
case "undefined":
|
|
12150
|
+
return undefined;
|
|
12151
|
+
}
|
|
12152
|
+
},
|
|
12153
|
+
};
|
|
10298
12154
|
|
|
10299
|
-
|
|
12155
|
+
if (optionalInput) {
|
|
12156
|
+
return {
|
|
12157
|
+
type,
|
|
12158
|
+
value,
|
|
12159
|
+
strategy: strategy ?? "replace",
|
|
12160
|
+
[defaultPropertySymbol]: operations,
|
|
12161
|
+
} as unknown as WithDefaultProperty<
|
|
12162
|
+
TypeNode,
|
|
12163
|
+
unknown,
|
|
12164
|
+
"replace" | "preserve"
|
|
12165
|
+
>;
|
|
12166
|
+
}
|
|
10300
12167
|
|
|
10301
|
-
const
|
|
10302
|
-
|
|
10303
|
-
|
|
10304
|
-
|
|
12168
|
+
const fromOwn = (value: unknown) => ok(operations.decode(value));
|
|
12169
|
+
const fromParent = mapRuntimeOperations(
|
|
12170
|
+
type[fromSymbol],
|
|
12171
|
+
(operation) => (value: never, options) =>
|
|
12172
|
+
flatMapResult(operation(value, options), fromOwn),
|
|
12173
|
+
);
|
|
12174
|
+
|
|
12175
|
+
return createTypeNode(
|
|
12176
|
+
"WithDefault",
|
|
10305
12177
|
type,
|
|
10306
|
-
|
|
10307
|
-
|
|
12178
|
+
(value, options) =>
|
|
12179
|
+
flatMapResult(type.fromUnknown(value, options), fromOwn),
|
|
12180
|
+
operations.output.is,
|
|
12181
|
+
operations.output[outputValidationSymbol],
|
|
12182
|
+
createFromOperation(fromParent),
|
|
12183
|
+
operations.encode,
|
|
12184
|
+
operations.output[getRuntimeTypeIssuesSymbol],
|
|
12185
|
+
);
|
|
12186
|
+
}
|
|
10308
12187
|
|
|
10309
|
-
type
|
|
10310
|
-
|
|
10311
|
-
|
|
10312
|
-
|
|
10313
|
-
|
|
10314
|
-
|
|
12188
|
+
type ValidateDefaultType<
|
|
12189
|
+
T extends TypeNode,
|
|
12190
|
+
Optional extends boolean = false,
|
|
12191
|
+
> = ValidateOptionalPropertyType<T> &
|
|
12192
|
+
([ChildTypeNameValidationError<"WithDefault", T>] extends [never]
|
|
12193
|
+
? unknown
|
|
12194
|
+
: ChildTypeNameValidationError<"WithDefault", T>) &
|
|
12195
|
+
(Optional extends true
|
|
12196
|
+
? unknown
|
|
12197
|
+
: [DefaultOriginal<T>] extends [never]
|
|
12198
|
+
? CompileTimeError<
|
|
12199
|
+
"Type",
|
|
12200
|
+
"withDefault requires an optional property or a Type whose Output includes null or undefined."
|
|
12201
|
+
>
|
|
12202
|
+
: unknown);
|
|
12203
|
+
|
|
12204
|
+
type DefaultOriginal<T extends TypeNode> =
|
|
12205
|
+
| (null extends T["Output"] ? "null" : never)
|
|
12206
|
+
| (undefined extends T["Output"] ? "undefined" : never);
|
|
12207
|
+
|
|
12208
|
+
type WithDefaultOutput<
|
|
12209
|
+
T extends TypeNode,
|
|
12210
|
+
Value,
|
|
12211
|
+
Strategy extends "replace" | "preserve",
|
|
12212
|
+
Original extends "missing" | "null" | "undefined" = DefaultOriginal<T>,
|
|
12213
|
+
> = Strategy extends "replace"
|
|
12214
|
+
? NonNullable<T["Output"]> | Value
|
|
12215
|
+
: Defaulted<NonNullable<T["Output"]>, Value, Original>;
|
|
12216
|
+
|
|
12217
|
+
declare const defaultPropertySymbolType: unique symbol;
|
|
12218
|
+
const defaultPropertySymbol: typeof defaultPropertySymbolType =
|
|
12219
|
+
/*#__PURE__*/ globalThis.Symbol() as typeof defaultPropertySymbolType;
|
|
12220
|
+
const missingDefaultValue = /*#__PURE__*/ globalThis.Symbol();
|
|
12221
|
+
|
|
12222
|
+
interface RuntimeDefaultOperations {
|
|
12223
|
+
readonly output: RuntimeTypeNode;
|
|
12224
|
+
readonly decode: (value: unknown) => unknown;
|
|
12225
|
+
readonly encode: (value: never) => unknown;
|
|
12226
|
+
readonly createObject: (
|
|
12227
|
+
props: ObjectProps,
|
|
12228
|
+
record: RuntimeRecordTypeNode | undefined,
|
|
12229
|
+
) => ObjectTypeNode;
|
|
12230
|
+
readonly partial: (type: RuntimeTypeNode) => OptionalProperty<TypeNode>;
|
|
12231
|
+
}
|
|
10315
12232
|
|
|
10316
12233
|
/**
|
|
10317
12234
|
* Properties used to construct an {@link object} Type.
|
|
@@ -10319,6 +12236,15 @@ type ValidateOptionalPropertyType<T extends TypeNode> =
|
|
|
10319
12236
|
* @group Objects
|
|
10320
12237
|
*/
|
|
10321
12238
|
export type ObjectProps = Readonly<
|
|
12239
|
+
Record<
|
|
12240
|
+
string,
|
|
12241
|
+
| TypeNode
|
|
12242
|
+
| OptionalProperty<TypeNode>
|
|
12243
|
+
| WithDefaultProperty<TypeNode, unknown, "replace" | "preserve">
|
|
12244
|
+
>
|
|
12245
|
+
>;
|
|
12246
|
+
|
|
12247
|
+
type ObjectValueProps = Readonly<
|
|
10322
12248
|
Record<string, TypeNode | OptionalProperty<TypeNode>>
|
|
10323
12249
|
>;
|
|
10324
12250
|
|
|
@@ -10330,7 +12256,10 @@ type ObjectProperty = ObjectProps[string];
|
|
|
10330
12256
|
* Use `object(props)` for objects with fixed property names. Properties are
|
|
10331
12257
|
* required unless wrapped with {@link optional}. An optional property may be
|
|
10332
12258
|
* absent, but a present value is still validated and does not implicitly accept
|
|
10333
|
-
* `undefined`.
|
|
12259
|
+
* `undefined`. Wrapping an optional property with {@link withDefault} keeps the
|
|
12260
|
+
* input optional and makes the decoded output required. A default for a
|
|
12261
|
+
* nullable or undefined-accepting Type alone does not make the property
|
|
12262
|
+
* optional.
|
|
10334
12263
|
*
|
|
10335
12264
|
* Without a second argument, `fromUnknown` rejects additional properties. Pass
|
|
10336
12265
|
* a {@link record} with the predefined {@link String} key Type to validate and
|
|
@@ -10418,6 +12347,7 @@ type ObjectProperty = ObjectProps[string];
|
|
|
10418
12347
|
* const valueType = typeof value;
|
|
10419
12348
|
*
|
|
10420
12349
|
* assertEqual(valueType, "function");
|
|
12350
|
+
*
|
|
10421
12351
|
* const called = trySync(
|
|
10422
12352
|
* () => {
|
|
10423
12353
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -10453,6 +12383,7 @@ type ObjectProperty = ObjectProps[string];
|
|
|
10453
12383
|
* const valueType = typeof value;
|
|
10454
12384
|
*
|
|
10455
12385
|
* assertEqual(valueType, "function");
|
|
12386
|
+
*
|
|
10456
12387
|
* const called = trySync(
|
|
10457
12388
|
* () => {
|
|
10458
12389
|
* if (value !== undefined) value.toFixed(0);
|
|
@@ -10468,7 +12399,7 @@ type ObjectProperty = ObjectProps[string];
|
|
|
10468
12399
|
*
|
|
10469
12400
|
* @group Objects
|
|
10470
12401
|
*/
|
|
10471
|
-
export function object<const Props extends
|
|
12402
|
+
export function object<const Props extends ObjectValueProps>(
|
|
10472
12403
|
props: Props,
|
|
10473
12404
|
...validation: [ObjectValidationError<Props>] extends [never]
|
|
10474
12405
|
? []
|
|
@@ -10493,6 +12424,7 @@ export function object<const Props extends ObjectProps>(
|
|
|
10493
12424
|
* { authorization: String },
|
|
10494
12425
|
* record(String, String),
|
|
10495
12426
|
* );
|
|
12427
|
+
*
|
|
10496
12428
|
* const result = RequestHeaders.fromUnknown({
|
|
10497
12429
|
* authorization: "Bearer token",
|
|
10498
12430
|
* "x-request-id": "request-1",
|
|
@@ -10506,7 +12438,7 @@ export function object<const Props extends ObjectProps>(
|
|
|
10506
12438
|
* ```
|
|
10507
12439
|
*/
|
|
10508
12440
|
export function object<
|
|
10509
|
-
const Props extends
|
|
12441
|
+
const Props extends ObjectValueProps,
|
|
10510
12442
|
const Rest extends RecordTypeNode & ConcreteTypeNode,
|
|
10511
12443
|
>(
|
|
10512
12444
|
props: Props,
|
|
@@ -10525,6 +12457,34 @@ export function object<
|
|
|
10525
12457
|
Props,
|
|
10526
12458
|
Rest extends ObjectRecordTypeNode ? Rest : never
|
|
10527
12459
|
>;
|
|
12460
|
+
/** Creates an Object Type with explicit defaults for optional inputs. */
|
|
12461
|
+
export function object<const Props extends ObjectProps>(
|
|
12462
|
+
props: Props,
|
|
12463
|
+
...validation: [ObjectValidationError<Props>] extends [never]
|
|
12464
|
+
? []
|
|
12465
|
+
: [ValidationFailure<ObjectValidationError<Props>>]
|
|
12466
|
+
): ObjectType<Props>;
|
|
12467
|
+
/**
|
|
12468
|
+
* Creates an Object Type with explicit defaults and additional record
|
|
12469
|
+
* properties.
|
|
12470
|
+
*/
|
|
12471
|
+
export function object<
|
|
12472
|
+
const Props extends ObjectProps,
|
|
12473
|
+
const Rest extends ObjectRecordTypeNode & ConcreteTypeNode,
|
|
12474
|
+
>(
|
|
12475
|
+
props: Props,
|
|
12476
|
+
record: Rest,
|
|
12477
|
+
...validation: [
|
|
12478
|
+
ObjectValidationError<Props> | ObjectRecordValidationError<Props, Rest>,
|
|
12479
|
+
] extends [never]
|
|
12480
|
+
? []
|
|
12481
|
+
: [
|
|
12482
|
+
ValidationFailure<
|
|
12483
|
+
| ObjectValidationError<Props>
|
|
12484
|
+
| ObjectRecordValidationError<Props, Rest>
|
|
12485
|
+
>,
|
|
12486
|
+
]
|
|
12487
|
+
): ObjectType<Props, Rest>;
|
|
10528
12488
|
export function object(props: ObjectProps, recordType?: unknown): TypeNode {
|
|
10529
12489
|
return createObjectType(
|
|
10530
12490
|
snapshotObjectProps(props),
|
|
@@ -10537,6 +12497,14 @@ const createObjectType = (
|
|
|
10537
12497
|
recordType?: RuntimeRecordTypeNode,
|
|
10538
12498
|
): ObjectTypeNode => {
|
|
10539
12499
|
const runtimeProps = props as Readonly<Record<string, RuntimeObjectProperty>>;
|
|
12500
|
+
const defaultProperty =
|
|
12501
|
+
globalThis.Object.values(runtimeProps).find(isDefaultProperty);
|
|
12502
|
+
if (defaultProperty) {
|
|
12503
|
+
return defaultProperty[defaultPropertySymbol].createObject(
|
|
12504
|
+
props,
|
|
12505
|
+
recordType,
|
|
12506
|
+
);
|
|
12507
|
+
}
|
|
10540
12508
|
const keys = globalThis.Object.keys(runtimeProps);
|
|
10541
12509
|
|
|
10542
12510
|
const validate = (
|
|
@@ -10933,6 +12901,100 @@ const createObjectType = (
|
|
|
10933
12901
|
);
|
|
10934
12902
|
};
|
|
10935
12903
|
|
|
12904
|
+
const createDefaultObjectType = (
|
|
12905
|
+
props: ObjectProps,
|
|
12906
|
+
recordType: RuntimeRecordTypeNode | undefined,
|
|
12907
|
+
): ObjectTypeNode => {
|
|
12908
|
+
const inputProps = createMutableRecord<string, ObjectProperty>();
|
|
12909
|
+
const outputProps = createMutableRecord<string, ObjectProperty>();
|
|
12910
|
+
const defaults = new Map<string, RuntimeDefaultOperations>();
|
|
12911
|
+
|
|
12912
|
+
for (const [key, property] of globalThis.Object.entries(props)) {
|
|
12913
|
+
if (isDefaultProperty(property as RuntimeObjectProperty)) {
|
|
12914
|
+
const defaultProperty = property as WithDefaultProperty<
|
|
12915
|
+
TypeNode,
|
|
12916
|
+
unknown,
|
|
12917
|
+
"replace" | "preserve"
|
|
12918
|
+
>;
|
|
12919
|
+
inputProps[key] = createOptionalProperty(defaultProperty.type);
|
|
12920
|
+
outputProps[key] = defaultProperty[defaultPropertySymbol].output;
|
|
12921
|
+
defaults.set(key, defaultProperty[defaultPropertySymbol]);
|
|
12922
|
+
} else {
|
|
12923
|
+
inputProps[key] = property;
|
|
12924
|
+
const type = objectPropertyToType(property as RuntimeObjectProperty);
|
|
12925
|
+
const output = createRootType(
|
|
12926
|
+
type.name,
|
|
12927
|
+
type[outputValidationSymbol],
|
|
12928
|
+
type.formatError,
|
|
12929
|
+
type[getRuntimeTypeIssuesSymbol],
|
|
12930
|
+
);
|
|
12931
|
+
outputProps[key] =
|
|
12932
|
+
optionalPropertySymbol in property
|
|
12933
|
+
? createOptionalProperty(output)
|
|
12934
|
+
: output;
|
|
12935
|
+
}
|
|
12936
|
+
}
|
|
12937
|
+
|
|
12938
|
+
const source = createObjectType(
|
|
12939
|
+
inputProps,
|
|
12940
|
+
recordType,
|
|
12941
|
+
) as unknown as RuntimeTypeNode;
|
|
12942
|
+
// The object parent validates only its input structure and root value Types.
|
|
12943
|
+
const parent = getTerminalRuntimeNode(source);
|
|
12944
|
+
const output = createObjectType(
|
|
12945
|
+
outputProps,
|
|
12946
|
+
recordType,
|
|
12947
|
+
) as unknown as RuntimeTypeNode;
|
|
12948
|
+
const finish = (value: unknown): Result<unknown> => {
|
|
12949
|
+
const result = createMutableRecord(
|
|
12950
|
+
value as Readonly<Record<string, unknown>>,
|
|
12951
|
+
);
|
|
12952
|
+
for (const [key, operations] of defaults) {
|
|
12953
|
+
result[key] = operations.decode(
|
|
12954
|
+
globalThis.Object.hasOwn(result, key)
|
|
12955
|
+
? result[key]
|
|
12956
|
+
: missingDefaultValue,
|
|
12957
|
+
);
|
|
12958
|
+
}
|
|
12959
|
+
return ok(result);
|
|
12960
|
+
};
|
|
12961
|
+
const fromInput = getTerminalRuntimeNode(source[fromSymbol]);
|
|
12962
|
+
const fromParent: RuntimeOperation<Result<unknown, TypeError>> = (
|
|
12963
|
+
value: never,
|
|
12964
|
+
options,
|
|
12965
|
+
) => flatMapResult(fromInput(value, options), finish);
|
|
12966
|
+
const to = (value: never): unknown => {
|
|
12967
|
+
const result = createMutableRecord(
|
|
12968
|
+
value as Readonly<Record<string, unknown>>,
|
|
12969
|
+
);
|
|
12970
|
+
for (const [key, operations] of defaults) {
|
|
12971
|
+
const original = operations.encode(result[key] as never);
|
|
12972
|
+
if (original === missingDefaultValue) delete result[key];
|
|
12973
|
+
else result[key] = original;
|
|
12974
|
+
}
|
|
12975
|
+
return source[encoderSymbol](result as never);
|
|
12976
|
+
};
|
|
12977
|
+
|
|
12978
|
+
return createTypeNode<ObjectTypeNode>(
|
|
12979
|
+
"Object",
|
|
12980
|
+
parent,
|
|
12981
|
+
(value, options) =>
|
|
12982
|
+
flatMapResult(source.fromUnknown(value, options), finish),
|
|
12983
|
+
output.is,
|
|
12984
|
+
output[outputValidationSymbol],
|
|
12985
|
+
createFromOperation(fromParent),
|
|
12986
|
+
to,
|
|
12987
|
+
createObjectRuntimeTypeIssues(
|
|
12988
|
+
source.formatError,
|
|
12989
|
+
props as Readonly<Record<string, RuntimeObjectProperty>>,
|
|
12990
|
+
recordType,
|
|
12991
|
+
),
|
|
12992
|
+
recordType
|
|
12993
|
+
? { props, record: recordType as unknown as RecordTypeNode }
|
|
12994
|
+
: { props },
|
|
12995
|
+
);
|
|
12996
|
+
};
|
|
12997
|
+
|
|
10936
12998
|
// Read descriptors instead of spreading so accessors are not invoked,
|
|
10937
12999
|
// non-enumerable declarations are retained, and later mutations are isolated.
|
|
10938
13000
|
const snapshotObjectProps = (
|
|
@@ -10992,9 +13054,9 @@ type ObjectRecordPropertyValidationError<
|
|
|
10992
13054
|
Props extends ObjectProps,
|
|
10993
13055
|
ValueType extends TypeNode,
|
|
10994
13056
|
> = [
|
|
10995
|
-
|
|
10996
|
-
|
|
10997
|
-
|
|
13057
|
+
DefaultableObjectPropertyType<Props[keyof Props]>["Input"],
|
|
13058
|
+
DefaultableObjectPropertyType<Props[keyof Props]>["Output"],
|
|
13059
|
+
DefaultableObjectPropertyType<Props[keyof Props]>["CanonicalInput"],
|
|
10998
13060
|
] extends [ValueType["Input"], ValueType["Output"], ValueType["CanonicalInput"]]
|
|
10999
13061
|
? never
|
|
11000
13062
|
: ObjectRecordPropertyTypeError<Props, ValueType>;
|
|
@@ -11002,10 +13064,10 @@ type ObjectRecordPropertyValidationError<
|
|
|
11002
13064
|
type ObjectRecordPropertyTypeError<
|
|
11003
13065
|
Props extends ObjectProps,
|
|
11004
13066
|
ValueType extends TypeNode,
|
|
11005
|
-
> = [
|
|
13067
|
+
> = [DefaultableObjectPropertyType<Props[keyof Props]>["Input"]] extends [
|
|
11006
13068
|
ValueType["Input"],
|
|
11007
13069
|
]
|
|
11008
|
-
? [
|
|
13070
|
+
? [DefaultableObjectPropertyType<Props[keyof Props]>["Output"]] extends [
|
|
11009
13071
|
ValueType["Output"],
|
|
11010
13072
|
]
|
|
11011
13073
|
? ObjectRecordCanonicalInputTypeError
|
|
@@ -11014,10 +13076,10 @@ type ObjectRecordPropertyTypeError<
|
|
|
11014
13076
|
|
|
11015
13077
|
type ObjectPropertyValidationError<Property extends ObjectProperty> =
|
|
11016
13078
|
IsUnion<Property> extends false
|
|
11017
|
-
? Property extends
|
|
11018
|
-
? ObjectPropertyTypeValidationError<
|
|
11019
|
-
: Property extends TypeNode
|
|
11020
|
-
? ObjectPropertyTypeValidationError<
|
|
13079
|
+
? Property extends TypeNode
|
|
13080
|
+
? ObjectPropertyTypeValidationError<Property>
|
|
13081
|
+
: Property extends { readonly type: infer T extends TypeNode }
|
|
13082
|
+
? ObjectPropertyTypeValidationError<T>
|
|
11021
13083
|
: ObjectPropertyTypeError
|
|
11022
13084
|
: ObjectPropertyTypeError;
|
|
11023
13085
|
|
|
@@ -11109,15 +13171,83 @@ type ObjectRecordCanonicalInputTypeError = CompileTimeError<
|
|
|
11109
13171
|
export type ObjectType<
|
|
11110
13172
|
Props extends ObjectProps,
|
|
11111
13173
|
Rest extends ObjectRecordTypeNode | undefined = undefined,
|
|
13174
|
+
> = Props extends ObjectValueProps
|
|
13175
|
+
? Rest extends ObjectRecordTypeNode
|
|
13176
|
+
? ObjectWithRecordType<Props, Rest>
|
|
13177
|
+
: StrictObjectType<Props>
|
|
13178
|
+
: DefaultObjectType<Props, Rest>;
|
|
13179
|
+
|
|
13180
|
+
type ObjectValueType<
|
|
13181
|
+
Props extends ObjectValueProps,
|
|
13182
|
+
Rest extends ObjectRecordTypeNode | undefined,
|
|
11112
13183
|
> = Rest extends ObjectRecordTypeNode
|
|
11113
13184
|
? ObjectWithRecordType<Props, Rest>
|
|
11114
13185
|
: StrictObjectType<Props>;
|
|
11115
13186
|
|
|
13187
|
+
type DefaultableObjectPropertyType<Property extends ObjectProperty> =
|
|
13188
|
+
Property extends WithDefaultProperty<infer T, infer Value, infer Strategy>
|
|
13189
|
+
? Type<
|
|
13190
|
+
"WithDefault",
|
|
13191
|
+
T["Input"],
|
|
13192
|
+
WithDefaultOutput<T, Value, Strategy, "missing" | DefaultOriginal<T>>,
|
|
13193
|
+
never,
|
|
13194
|
+
T,
|
|
13195
|
+
InferErrors<T>,
|
|
13196
|
+
never,
|
|
13197
|
+
T["CanonicalInput"],
|
|
13198
|
+
false
|
|
13199
|
+
>
|
|
13200
|
+
: Property extends TypeNode | OptionalProperty<TypeNode>
|
|
13201
|
+
? ObjectPropertyType<Property>
|
|
13202
|
+
: never;
|
|
13203
|
+
|
|
13204
|
+
type DefaultInputProps<Props extends ObjectProps> = {
|
|
13205
|
+
readonly [Key in keyof Props]: Props[Key] extends WithDefaultProperty<
|
|
13206
|
+
infer T,
|
|
13207
|
+
unknown,
|
|
13208
|
+
"replace" | "preserve"
|
|
13209
|
+
>
|
|
13210
|
+
? OptionalProperty<T>
|
|
13211
|
+
: Extract<Props[Key], TypeNode | OptionalProperty<TypeNode>>;
|
|
13212
|
+
};
|
|
13213
|
+
|
|
13214
|
+
type DefaultOutputProps<Props extends ObjectProps> = {
|
|
13215
|
+
readonly [Key in keyof Props]: Props[Key] extends WithDefaultProperty<
|
|
13216
|
+
infer T,
|
|
13217
|
+
infer Value,
|
|
13218
|
+
infer Strategy
|
|
13219
|
+
>
|
|
13220
|
+
? Type<
|
|
13221
|
+
"DefaultValue",
|
|
13222
|
+
WithDefaultOutput<T, Value, Strategy, "missing" | DefaultOriginal<T>>,
|
|
13223
|
+
WithDefaultOutput<T, Value, Strategy, "missing" | DefaultOriginal<T>>,
|
|
13224
|
+
never
|
|
13225
|
+
>
|
|
13226
|
+
: Extract<Props[Key], TypeNode | OptionalProperty<TypeNode>>;
|
|
13227
|
+
};
|
|
13228
|
+
|
|
13229
|
+
type DefaultObjectType<
|
|
13230
|
+
Props extends ObjectProps,
|
|
13231
|
+
Rest extends ObjectRecordTypeNode | undefined,
|
|
13232
|
+
> = Type<
|
|
13233
|
+
"Object",
|
|
13234
|
+
ObjectValueType<DefaultInputProps<Props>, Rest>["Input"],
|
|
13235
|
+
ObjectValueType<DefaultOutputProps<Props>, Rest>["Output"],
|
|
13236
|
+
TypeFromError<ObjectValueType<DefaultInputProps<Props>, Rest>>,
|
|
13237
|
+
RootType<ObjectValueType<DefaultInputProps<Props>, Rest>>,
|
|
13238
|
+
InferErrors<ObjectValueType<DefaultInputProps<Props>, Rest>>,
|
|
13239
|
+
never,
|
|
13240
|
+
ObjectValueType<DefaultInputProps<Props>, Rest>["CanonicalInput"],
|
|
13241
|
+
false
|
|
13242
|
+
> & { readonly props: Readonly<Props> } & (Rest extends ObjectRecordTypeNode
|
|
13243
|
+
? { readonly record: Rest }
|
|
13244
|
+
: {});
|
|
13245
|
+
|
|
11116
13246
|
// `fromUnknown` can report Object root errors plus declared-property child,
|
|
11117
13247
|
// missing, access, and excess/Record-rest errors. When Object has a parent, its
|
|
11118
13248
|
// own `Error` contains only the child and rest errors remaining after that
|
|
11119
13249
|
// parent validated the Object structure.
|
|
11120
|
-
type StrictObjectType<Props extends
|
|
13250
|
+
type StrictObjectType<Props extends ObjectValueProps> = Type<
|
|
11121
13251
|
"Object",
|
|
11122
13252
|
StrictObjectShape<Props, "Input">,
|
|
11123
13253
|
StrictObjectShape<Props, "Output">,
|
|
@@ -11136,7 +13266,7 @@ type StrictObjectType<Props extends ObjectProps> = Type<
|
|
|
11136
13266
|
};
|
|
11137
13267
|
|
|
11138
13268
|
type ObjectWithRecordType<
|
|
11139
|
-
Props extends
|
|
13269
|
+
Props extends ObjectValueProps,
|
|
11140
13270
|
Rest extends ObjectRecordTypeNode,
|
|
11141
13271
|
> = Type<
|
|
11142
13272
|
"Object",
|
|
@@ -11159,7 +13289,7 @@ type ObjectWithRecordType<
|
|
|
11159
13289
|
ObjectWithRecordReflection<Props, Rest>;
|
|
11160
13290
|
|
|
11161
13291
|
type StrictObjectShape<
|
|
11162
|
-
Props extends
|
|
13292
|
+
Props extends ObjectValueProps,
|
|
11163
13293
|
Field extends "Input" | "Output" | "CanonicalInput",
|
|
11164
13294
|
> = keyof Props extends never
|
|
11165
13295
|
? Readonly<Record<string, never>>
|
|
@@ -11176,7 +13306,7 @@ type StrictObjectShape<
|
|
|
11176
13306
|
};
|
|
11177
13307
|
|
|
11178
13308
|
type ObjectWithRecordShape<
|
|
11179
|
-
Props extends
|
|
13309
|
+
Props extends ObjectValueProps,
|
|
11180
13310
|
Rest extends ObjectRecordTypeNode,
|
|
11181
13311
|
Field extends "Input" | "Output" | "CanonicalInput",
|
|
11182
13312
|
> = keyof Props extends never
|
|
@@ -11184,7 +13314,7 @@ type ObjectWithRecordShape<
|
|
|
11184
13314
|
: ObjectDeclaredShape<Props, Field> & ObjectRestShape<Rest, Field>;
|
|
11185
13315
|
|
|
11186
13316
|
type ObjectDeclaredShape<
|
|
11187
|
-
Props extends
|
|
13317
|
+
Props extends ObjectValueProps,
|
|
11188
13318
|
Field extends "Input" | "Output" | "CanonicalInput",
|
|
11189
13319
|
> = Simplify<
|
|
11190
13320
|
{
|
|
@@ -11210,25 +13340,25 @@ type ObjectTypeField<
|
|
|
11210
13340
|
Field extends "Input" | "Output" | "CanonicalInput",
|
|
11211
13341
|
> = Field extends "CanonicalInput" ? CanonicalInputOf<T> : T[Field];
|
|
11212
13342
|
|
|
11213
|
-
type RequiredObjectKeys<Props extends
|
|
13343
|
+
type RequiredObjectKeys<Props extends ObjectValueProps> = Exclude<
|
|
11214
13344
|
keyof Props,
|
|
11215
13345
|
OptionalObjectKeys<Props>
|
|
11216
13346
|
>;
|
|
11217
13347
|
|
|
11218
|
-
type OptionalObjectKeys<Props extends
|
|
13348
|
+
type OptionalObjectKeys<Props extends ObjectValueProps> = {
|
|
11219
13349
|
readonly [Key in keyof Props]: Props[Key] extends OptionalProperty<TypeNode>
|
|
11220
13350
|
? Key
|
|
11221
13351
|
: never;
|
|
11222
13352
|
}[keyof Props];
|
|
11223
13353
|
|
|
11224
|
-
type ObjectPropertyType<Property extends
|
|
13354
|
+
type ObjectPropertyType<Property extends ObjectValueProps[string]> =
|
|
11225
13355
|
Property extends OptionalProperty<infer T> ? T : Property;
|
|
11226
13356
|
|
|
11227
|
-
type ObjectFromParentPropertyErrors<Props extends
|
|
13357
|
+
type ObjectFromParentPropertyErrors<Props extends ObjectValueProps> = {
|
|
11228
13358
|
readonly [Key in keyof Props]: TypeFromError<ObjectPropertyType<Props[Key]>>;
|
|
11229
13359
|
};
|
|
11230
13360
|
|
|
11231
|
-
type StrictRootObjectType<Props extends
|
|
13361
|
+
type StrictRootObjectType<Props extends ObjectValueProps> = Type<
|
|
11232
13362
|
"Object",
|
|
11233
13363
|
StrictObjectShape<Props, "Input">,
|
|
11234
13364
|
StrictObjectShape<Props, "Output">,
|
|
@@ -11243,7 +13373,7 @@ type StrictRootObjectType<Props extends ObjectProps> = Type<
|
|
|
11243
13373
|
};
|
|
11244
13374
|
|
|
11245
13375
|
type RootObjectWithRecordType<
|
|
11246
|
-
Props extends
|
|
13376
|
+
Props extends ObjectValueProps,
|
|
11247
13377
|
Rest extends ObjectRecordTypeNode,
|
|
11248
13378
|
> = Type<
|
|
11249
13379
|
"Object",
|
|
@@ -11258,16 +13388,16 @@ type RootObjectWithRecordType<
|
|
|
11258
13388
|
> &
|
|
11259
13389
|
ObjectWithRecordReflection<Props, Rest>;
|
|
11260
13390
|
|
|
11261
|
-
type StrictObjectParents<Props extends
|
|
13391
|
+
type StrictObjectParents<Props extends ObjectValueProps> = ObjectPropertyType<
|
|
11262
13392
|
Props[keyof Props]
|
|
11263
13393
|
>["parent"];
|
|
11264
13394
|
|
|
11265
13395
|
type ObjectWithRecordParents<
|
|
11266
|
-
Props extends
|
|
13396
|
+
Props extends ObjectValueProps,
|
|
11267
13397
|
Rest extends ObjectRecordTypeNode,
|
|
11268
13398
|
> = ObjectPropertyType<Props[keyof Props]>["parent"] | Rest["parent"];
|
|
11269
13399
|
|
|
11270
|
-
type RootObjectProps<Props extends
|
|
13400
|
+
type RootObjectProps<Props extends ObjectValueProps> = {
|
|
11271
13401
|
readonly [Key in keyof Props]: Props[Key] extends OptionalProperty<infer T>
|
|
11272
13402
|
? OptionalProperty<RootType<T>>
|
|
11273
13403
|
: Props[Key] extends TypeNode
|
|
@@ -11281,20 +13411,20 @@ type RootObjectRecord<Rest extends ObjectRecordTypeNode> = RecordType<
|
|
|
11281
13411
|
>;
|
|
11282
13412
|
|
|
11283
13413
|
interface ObjectWithRecordReflection<
|
|
11284
|
-
Props extends
|
|
13414
|
+
Props extends ObjectValueProps,
|
|
11285
13415
|
Rest extends ObjectRecordTypeNode,
|
|
11286
13416
|
> {
|
|
11287
13417
|
readonly props: Readonly<Props>;
|
|
11288
13418
|
readonly record: Rest;
|
|
11289
13419
|
}
|
|
11290
13420
|
|
|
11291
|
-
type ObjectDeclaredErrors<Props extends
|
|
13421
|
+
type ObjectDeclaredErrors<Props extends ObjectValueProps> = {
|
|
11292
13422
|
readonly [Key in RequiredObjectKeys<Props>]: Props[Key][typeof errorsSymbol];
|
|
11293
13423
|
} & {
|
|
11294
13424
|
readonly [Key in OptionalObjectKeys<Props>]?: Props[Key][typeof errorsSymbol];
|
|
11295
13425
|
};
|
|
11296
13426
|
|
|
11297
|
-
type StrictObjectFromUnknownPropertyErrors<Props extends
|
|
13427
|
+
type StrictObjectFromUnknownPropertyErrors<Props extends ObjectValueProps> = {
|
|
11298
13428
|
readonly [Key in RequiredObjectKeys<Props>]:
|
|
11299
13429
|
| Props[Key][typeof errorsSymbol]
|
|
11300
13430
|
| ObjectMissingPropertyError
|
|
@@ -11474,16 +13604,31 @@ type ObjectRestFromParentError<Rest extends ObjectRecordTypeNode | undefined> =
|
|
|
11474
13604
|
: never;
|
|
11475
13605
|
|
|
11476
13606
|
type RuntimeObjectProperty =
|
|
11477
|
-
|
|
13607
|
+
| RuntimeTypeNode
|
|
13608
|
+
| OptionalProperty<RuntimeTypeNode>
|
|
13609
|
+
| WithDefaultProperty<RuntimeTypeNode, unknown, "replace" | "preserve">;
|
|
11478
13610
|
|
|
11479
13611
|
const isOptionalProperty = (
|
|
11480
13612
|
property: RuntimeObjectProperty,
|
|
11481
13613
|
): property is OptionalProperty<RuntimeTypeNode> =>
|
|
11482
13614
|
optionalPropertySymbol in property;
|
|
11483
13615
|
|
|
13616
|
+
const isDefaultProperty = (
|
|
13617
|
+
property: RuntimeObjectProperty,
|
|
13618
|
+
): property is WithDefaultProperty<
|
|
13619
|
+
RuntimeTypeNode,
|
|
13620
|
+
unknown,
|
|
13621
|
+
"replace" | "preserve"
|
|
13622
|
+
> => defaultPropertySymbol in property;
|
|
13623
|
+
|
|
11484
13624
|
const objectPropertyToType = (
|
|
11485
13625
|
property: RuntimeObjectProperty,
|
|
11486
|
-
): RuntimeTypeNode =>
|
|
13626
|
+
): RuntimeTypeNode =>
|
|
13627
|
+
isDefaultProperty(property)
|
|
13628
|
+
? property[defaultPropertySymbol].output
|
|
13629
|
+
: isOptionalProperty(property)
|
|
13630
|
+
? property.type
|
|
13631
|
+
: property;
|
|
11487
13632
|
|
|
11488
13633
|
interface RuntimeObjectPropertyErrors {
|
|
11489
13634
|
[key: string]: TypeError;
|
|
@@ -11501,7 +13646,8 @@ const createRecordPropertyError = <Error extends TypeError>(
|
|
|
11501
13646
|
* Object {@link Type} with every property optional.
|
|
11502
13647
|
*
|
|
11503
13648
|
* No property is required, but every present property must still satisfy its
|
|
11504
|
-
* Type.
|
|
13649
|
+
* Type. For {@link withDefault} properties, this disables the missing-property
|
|
13650
|
+
* default; defaults for present `null` or `undefined` values still apply.
|
|
11505
13651
|
*
|
|
11506
13652
|
* ### Example
|
|
11507
13653
|
*
|
|
@@ -11535,9 +13681,11 @@ export const partial = <const Props extends ObjectProps>(
|
|
|
11535
13681
|
|
|
11536
13682
|
for (const key of globalThis.Object.keys(source)) {
|
|
11537
13683
|
const property = source[key] as RuntimeObjectProperty;
|
|
11538
|
-
partialProps[key] =
|
|
11539
|
-
? property
|
|
11540
|
-
:
|
|
13684
|
+
partialProps[key] = isDefaultProperty(property)
|
|
13685
|
+
? property[defaultPropertySymbol].partial(property.type)
|
|
13686
|
+
: isOptionalProperty(property)
|
|
13687
|
+
? property
|
|
13688
|
+
: createOptionalProperty(property);
|
|
11541
13689
|
}
|
|
11542
13690
|
|
|
11543
13691
|
return createObjectType(partialProps) as unknown as ObjectType<
|
|
@@ -11551,11 +13699,17 @@ export const partial = <const Props extends ObjectProps>(
|
|
|
11551
13699
|
* @group Objects
|
|
11552
13700
|
*/
|
|
11553
13701
|
export type PartialObjectProps<Props extends ObjectProps> = {
|
|
11554
|
-
readonly [Key in keyof Props]: Props[Key] extends
|
|
11555
|
-
|
|
11556
|
-
|
|
11557
|
-
|
|
11558
|
-
|
|
13702
|
+
readonly [Key in keyof Props]: Props[Key] extends WithDefaultProperty<
|
|
13703
|
+
infer T,
|
|
13704
|
+
infer Value,
|
|
13705
|
+
infer Strategy
|
|
13706
|
+
>
|
|
13707
|
+
? OptionalProperty<WithDefaultType<T, Value, Strategy>>
|
|
13708
|
+
: Props[Key] extends OptionalProperty<TypeNode>
|
|
13709
|
+
? Props[Key]
|
|
13710
|
+
: Props[Key] extends TypeNode
|
|
13711
|
+
? OptionalProperty<Props[Key]>
|
|
13712
|
+
: never;
|
|
11559
13713
|
};
|
|
11560
13714
|
|
|
11561
13715
|
/**
|
|
@@ -11602,7 +13756,7 @@ export const nullableToOptional = <const Props extends ObjectProps>(
|
|
|
11602
13756
|
for (const key of globalThis.Object.keys(source)) {
|
|
11603
13757
|
const property = source[key] as RuntimeObjectProperty;
|
|
11604
13758
|
|
|
11605
|
-
if (isOptionalProperty(property)) {
|
|
13759
|
+
if (isOptionalProperty(property) || isDefaultProperty(property)) {
|
|
11606
13760
|
optionalProps[key] = property;
|
|
11607
13761
|
continue;
|
|
11608
13762
|
}
|
|
@@ -11661,7 +13815,10 @@ export const omit = <
|
|
|
11661
13815
|
const Keys extends ReadonlyArray<keyof Props>,
|
|
11662
13816
|
Rest extends ObjectRecordTypeNode | undefined = undefined,
|
|
11663
13817
|
>(
|
|
11664
|
-
objectType:
|
|
13818
|
+
objectType: {
|
|
13819
|
+
readonly props: Props;
|
|
13820
|
+
readonly record?: Rest;
|
|
13821
|
+
} & NoInfer<ObjectType<Props, Rest>>,
|
|
11665
13822
|
...keys: Keys &
|
|
11666
13823
|
([ValidateOmitKeys<Keys>] extends [never]
|
|
11667
13824
|
? unknown
|
|
@@ -11705,6 +13862,198 @@ type OmitKeyConcreteTypeError = CompileTimeError<
|
|
|
11705
13862
|
"Each omitted key must be one concrete property key."
|
|
11706
13863
|
>;
|
|
11707
13864
|
|
|
13865
|
+
/**
|
|
13866
|
+
* The {@link Type} returned by {@link objectKeys}.
|
|
13867
|
+
*
|
|
13868
|
+
* @group Objects
|
|
13869
|
+
*/
|
|
13870
|
+
export interface ObjectKeysType<
|
|
13871
|
+
Key extends TypeNode,
|
|
13872
|
+
T extends TypeNode & { readonly props: ObjectProps },
|
|
13873
|
+
> extends Type<
|
|
13874
|
+
"ObjectKeys",
|
|
13875
|
+
unknown,
|
|
13876
|
+
T["Output"],
|
|
13877
|
+
ObjectKeysError<T> | TransformOutputError<"ObjectKeys", InferErrors<T>>,
|
|
13878
|
+
typeof Unknown,
|
|
13879
|
+
ObjectKeysError<T> | TransformOutputError<"ObjectKeys", InferErrors<T>>,
|
|
13880
|
+
never,
|
|
13881
|
+
Readonly<Record<string, T["CanonicalInput"][keyof T["CanonicalInput"]]>>,
|
|
13882
|
+
false
|
|
13883
|
+
> {
|
|
13884
|
+
readonly key: Key;
|
|
13885
|
+
readonly output: T;
|
|
13886
|
+
readonly [reflectedTypesSymbol]?: Key | T;
|
|
13887
|
+
}
|
|
13888
|
+
|
|
13889
|
+
/**
|
|
13890
|
+
* Renames a strict {@link object} Type's encoded keys using a string codec.
|
|
13891
|
+
*
|
|
13892
|
+
* The object declaration uses semantic keys. Each key must be a valid Output of
|
|
13893
|
+
* the key Type. Its canonical encoding becomes the external property name.
|
|
13894
|
+
* Decoding accepts those exact names; aliases and unknown properties are
|
|
13895
|
+
* rejected. Values, optionality, and semantic Output stay with the object Type.
|
|
13896
|
+
* Only the outer keys change. Nested field codecs keep their own behavior.
|
|
13897
|
+
*
|
|
13898
|
+
* Construction rejects invalid schema keys, duplicate encodings, and key codecs
|
|
13899
|
+
* that do not decode their encodings back to the declared keys. These are
|
|
13900
|
+
* schema mistakes; invalid external values return normal Type errors. Error
|
|
13901
|
+
* paths use the external names, including missing required properties.
|
|
13902
|
+
*
|
|
13903
|
+
* ### Example
|
|
13904
|
+
*
|
|
13905
|
+
* ```ts
|
|
13906
|
+
* import {
|
|
13907
|
+
* assertEqual,
|
|
13908
|
+
* assertErr,
|
|
13909
|
+
* assertOk,
|
|
13910
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
13911
|
+
* object,
|
|
13912
|
+
* objectKeys,
|
|
13913
|
+
* optional,
|
|
13914
|
+
* PortFromString,
|
|
13915
|
+
* prefixed,
|
|
13916
|
+
* } from "@evolu/common";
|
|
13917
|
+
*
|
|
13918
|
+
* const Key = prefixed("APP_")(
|
|
13919
|
+
* CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
13920
|
+
* );
|
|
13921
|
+
* const Settings = objectKeys(Key)(
|
|
13922
|
+
* object({ port: optional(PortFromString) }),
|
|
13923
|
+
* );
|
|
13924
|
+
*
|
|
13925
|
+
* const result = Settings.fromUnknown({ APP_PORT: "04000" });
|
|
13926
|
+
* assertOk(result, { port: 4000 });
|
|
13927
|
+
*
|
|
13928
|
+
* assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
|
|
13929
|
+
*
|
|
13930
|
+
* assertErr(Settings.fromUnknown({ APP_POTR: "4000" }));
|
|
13931
|
+
* ```
|
|
13932
|
+
*
|
|
13933
|
+
* @group Objects
|
|
13934
|
+
*/
|
|
13935
|
+
export const objectKeys =
|
|
13936
|
+
<
|
|
13937
|
+
Key extends ConcreteTypeNode & {
|
|
13938
|
+
readonly Output: string;
|
|
13939
|
+
readonly CanonicalInput: string;
|
|
13940
|
+
},
|
|
13941
|
+
>(
|
|
13942
|
+
key: Key & ValidateOutput<Key>,
|
|
13943
|
+
) =>
|
|
13944
|
+
<
|
|
13945
|
+
T extends ConcreteTypeNode & {
|
|
13946
|
+
readonly name: "Object";
|
|
13947
|
+
readonly props: ObjectProps;
|
|
13948
|
+
readonly record?: never;
|
|
13949
|
+
},
|
|
13950
|
+
>(
|
|
13951
|
+
type: T & ValidateOutput<T>,
|
|
13952
|
+
): ObjectKeysType<Key, T> => {
|
|
13953
|
+
const runtimeKey = key as unknown as RuntimeTypeNode;
|
|
13954
|
+
const runtimeOutput = type as unknown as RuntimeTypeNode;
|
|
13955
|
+
const props = createMutableRecord<string, ObjectProperty>();
|
|
13956
|
+
const outputKeyByInputKey = new Map<string, string>();
|
|
13957
|
+
const inputKeyByOutputKey = new Map<string, string>();
|
|
13958
|
+
|
|
13959
|
+
for (const outputKey of globalThis.Object.keys(type.props)) {
|
|
13960
|
+
assert(
|
|
13961
|
+
runtimeKey.is(outputKey),
|
|
13962
|
+
`Invalid object schema key ${safelyStringifyUnknownValue(outputKey)} for ${key.name}.`,
|
|
13963
|
+
);
|
|
13964
|
+
const inputKey = runtimeKey.to(outputKey as never) as string;
|
|
13965
|
+
assert(
|
|
13966
|
+
!outputKeyByInputKey.has(inputKey),
|
|
13967
|
+
`Duplicate encoded object key ${safelyStringifyUnknownValue(inputKey)}.`,
|
|
13968
|
+
);
|
|
13969
|
+
const decoded = runtimeKey.fromUnknown(inputKey, firstValidationOptions);
|
|
13970
|
+
assert(
|
|
13971
|
+
decoded.ok && decoded.value === outputKey,
|
|
13972
|
+
"An object key codec must decode its encoding to the declared key.",
|
|
13973
|
+
);
|
|
13974
|
+
props[inputKey] = type.props[outputKey];
|
|
13975
|
+
outputKeyByInputKey.set(inputKey, outputKey);
|
|
13976
|
+
inputKeyByOutputKey.set(outputKey, inputKey);
|
|
13977
|
+
}
|
|
13978
|
+
|
|
13979
|
+
const input = createObjectType(props) as unknown as RuntimeTypeNode;
|
|
13980
|
+
const fromUnknown = (
|
|
13981
|
+
value: unknown,
|
|
13982
|
+
options = firstValidationOptions,
|
|
13983
|
+
): Result<unknown, TypeError> => {
|
|
13984
|
+
const result = input.fromUnknown(value, options);
|
|
13985
|
+
if (!result.ok) return err({ type: "ObjectKeys", error: result.error });
|
|
13986
|
+
const output = createMutableRecord();
|
|
13987
|
+
for (const [inputKey, value] of globalThis.Object.entries(
|
|
13988
|
+
result.value as Record<string, unknown>,
|
|
13989
|
+
)) {
|
|
13990
|
+
output[outputKeyByInputKey.get(inputKey)!] = value;
|
|
13991
|
+
}
|
|
13992
|
+
return ok(output);
|
|
13993
|
+
};
|
|
13994
|
+
const to: RuntimeEncoder = (value: never) => {
|
|
13995
|
+
const encoded = runtimeOutput[encoderSymbol](value) as Record<
|
|
13996
|
+
string,
|
|
13997
|
+
unknown
|
|
13998
|
+
>;
|
|
13999
|
+
const input = createMutableRecord();
|
|
14000
|
+
for (const [key, value] of globalThis.Object.entries(encoded)) {
|
|
14001
|
+
input[inputKeyByOutputKey.get(key)!] = value;
|
|
14002
|
+
}
|
|
14003
|
+
return input;
|
|
14004
|
+
};
|
|
14005
|
+
const validateOutput: RuntimeOutputValidation = (value, options) => {
|
|
14006
|
+
const result = runtimeOutput[outputValidationSymbol](value, options);
|
|
14007
|
+
return result.ok
|
|
14008
|
+
? result
|
|
14009
|
+
: err({ type: "ObjectKeys", outputError: result.error });
|
|
14010
|
+
};
|
|
14011
|
+
const getTypeIssues: RuntimeGetTypeIssues = (error, mode) => {
|
|
14012
|
+
if ("outputError" in error) {
|
|
14013
|
+
return runtimeOutput[getRuntimeTypeIssuesSymbol](
|
|
14014
|
+
error.outputError as TypeError,
|
|
14015
|
+
mode,
|
|
14016
|
+
);
|
|
14017
|
+
}
|
|
14018
|
+
return input[getRuntimeTypeIssuesSymbol](
|
|
14019
|
+
(error as ObjectKeysError<T>).error,
|
|
14020
|
+
mode,
|
|
14021
|
+
);
|
|
14022
|
+
};
|
|
14023
|
+
return createTypeNode<ObjectKeysType<Key, T>>(
|
|
14024
|
+
"ObjectKeys",
|
|
14025
|
+
Unknown,
|
|
14026
|
+
fromUnknown,
|
|
14027
|
+
runtimeOutput.is,
|
|
14028
|
+
validateOutput,
|
|
14029
|
+
createFromOperation(fromUnknown),
|
|
14030
|
+
to,
|
|
14031
|
+
getTypeIssues,
|
|
14032
|
+
{ key, output: type },
|
|
14033
|
+
);
|
|
14034
|
+
};
|
|
14035
|
+
|
|
14036
|
+
/**
|
|
14037
|
+
* An error from the externally named object decoded by {@link objectKeys}.
|
|
14038
|
+
*
|
|
14039
|
+
* @group Objects
|
|
14040
|
+
*/
|
|
14041
|
+
export interface ObjectKeysError<
|
|
14042
|
+
T extends TypeNode & { readonly props: ObjectProps },
|
|
14043
|
+
>
|
|
14044
|
+
extends TypeError<"ObjectKeys">, TransparentTypeError {
|
|
14045
|
+
readonly error: ObjectError<
|
|
14046
|
+
Readonly<
|
|
14047
|
+
Record<
|
|
14048
|
+
string,
|
|
14049
|
+
| T["props"][keyof T["props"]][typeof errorsSymbol]
|
|
14050
|
+
| ObjectMissingPropertyError
|
|
14051
|
+
| ObjectExcessPropertyError
|
|
14052
|
+
>
|
|
14053
|
+
>
|
|
14054
|
+
>;
|
|
14055
|
+
}
|
|
14056
|
+
|
|
11708
14057
|
/**
|
|
11709
14058
|
* Creates a {@link Type} for {@link Result} values.
|
|
11710
14059
|
*
|
|
@@ -11821,7 +14170,9 @@ export type UnknownResult = typeof UnknownResult.Output;
|
|
|
11821
14170
|
* type: "Loaded",
|
|
11822
14171
|
* value: "Evolu",
|
|
11823
14172
|
* });
|
|
14173
|
+
*
|
|
11824
14174
|
* assertFalse(Loading.is({ type: "Loading", progress: 1 }));
|
|
14175
|
+
*
|
|
11825
14176
|
* assertType<
|
|
11826
14177
|
* typeof Loading.Output extends { readonly type: "Loading" }
|
|
11827
14178
|
* ? true
|
|
@@ -11880,6 +14231,7 @@ export function typed<
|
|
|
11880
14231
|
* } from "@evolu/common";
|
|
11881
14232
|
*
|
|
11882
14233
|
* const Open = typed("Open", { label: String }, record(String, String));
|
|
14234
|
+
*
|
|
11883
14235
|
* const result = Open.fromUnknown({
|
|
11884
14236
|
* type: "Open",
|
|
11885
14237
|
* label: "Ready",
|
|
@@ -12088,6 +14440,7 @@ type TypedTypePropertyError = CompileTimeError<
|
|
|
12088
14440
|
* };
|
|
12089
14441
|
*
|
|
12090
14442
|
* assertEqual(describeNext({ ok: true, value: "item" }), "Value: item");
|
|
14443
|
+
*
|
|
12091
14444
|
* assertEqual(
|
|
12092
14445
|
* describeNext({
|
|
12093
14446
|
* ok: false,
|
|
@@ -12095,6 +14448,7 @@ type TypedTypePropertyError = CompileTimeError<
|
|
|
12095
14448
|
* }),
|
|
12096
14449
|
* "Done: complete",
|
|
12097
14450
|
* );
|
|
14451
|
+
*
|
|
12098
14452
|
* assertEqual(
|
|
12099
14453
|
* describeNext({
|
|
12100
14454
|
* ok: false,
|
|
@@ -14480,7 +16834,10 @@ export const json = <T extends ConcreteTypeNode, Name extends TypeName>(
|
|
|
14480
16834
|
const BrandedJson = {
|
|
14481
16835
|
...typeNode,
|
|
14482
16836
|
from,
|
|
14483
|
-
orThrow:
|
|
16837
|
+
orThrow: createRuntimeOrThrow(
|
|
16838
|
+
fromInput,
|
|
16839
|
+
(typeNode as unknown as RuntimeTypeNode).formatError,
|
|
16840
|
+
),
|
|
14484
16841
|
orNull: mapRuntimeResult(fromInput, getOrNull),
|
|
14485
16842
|
} as BrandType<typeof Json, Name, JsonTypeError>;
|
|
14486
16843
|
|