@evolu/common 8.0.0 → 8.2.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/Number.d.ts +66 -6
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +11 -3
- package/dist/src/Schedule.d.ts +2 -2
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/Task.d.ts +2 -1
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +3 -3
- package/dist/src/Time.d.ts +70 -34
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +77 -1
- package/dist/src/Type.d.ts +488 -72
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +634 -67
- package/dist/src/Types.d.ts +0 -24
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/intl/cs.d.ts +11 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +11 -1
- package/package.json +1 -1
- package/src/Number.ts +85 -14
- package/src/Schedule.ts +1 -1
- package/src/Task.ts +3 -4
- package/src/Time.ts +108 -62
- package/src/Type.ts +1267 -130
- package/src/Types.ts +0 -49
- package/src/intl/cs.ts +36 -1
package/dist/src/Type.js
CHANGED
|
@@ -90,7 +90,7 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
90
90
|
throw new Error(`Expected ${name}.`, { cause: error });
|
|
91
91
|
};
|
|
92
92
|
/**
|
|
93
|
-
*
|
|
93
|
+
* Localized copies of selected {@link Type} declarations for every locale.
|
|
94
94
|
*
|
|
95
95
|
* Each locale supplies one formatter for every Type that can own a formatted
|
|
96
96
|
* error. Structural Types use their own formatter for structural failures and
|
|
@@ -341,6 +341,8 @@ const encoderSymbol =
|
|
|
341
341
|
/*#__PURE__*/ globalThis.Symbol();
|
|
342
342
|
const fromSymbol =
|
|
343
343
|
/*#__PURE__*/ globalThis.Symbol();
|
|
344
|
+
const templateLiteralSyntaxSymbol =
|
|
345
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
344
346
|
const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
|
|
345
347
|
// `map` must return a fresh operation because this function can attach `.parent`.
|
|
346
348
|
const mapRuntimeOperations = (operation, map) => {
|
|
@@ -600,7 +602,7 @@ export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
|
|
|
600
602
|
*/
|
|
601
603
|
export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
|
|
602
604
|
/**
|
|
603
|
-
*
|
|
605
|
+
* Instance {@link Type} for one constructor.
|
|
604
606
|
*
|
|
605
607
|
* Membership uses the intrinsic prototype chain, so subclasses are accepted,
|
|
606
608
|
* equivalent constructors from other realms are rejected, and custom
|
|
@@ -644,6 +646,26 @@ export const instanceOf = (constructor) => {
|
|
|
644
646
|
* primitive through `from.parent`. The expected value must have one exact
|
|
645
647
|
* literal type. Validation uses `===`, so `-0` matches `0`.
|
|
646
648
|
*
|
|
649
|
+
* In {@link templateLiteralParser}, use a string Literal Type when the literal
|
|
650
|
+
* should be decoded into the Output Tuple. Use a raw string when it should only
|
|
651
|
+
* frame the canonical string.
|
|
652
|
+
*
|
|
653
|
+
* ### Example
|
|
654
|
+
*
|
|
655
|
+
* ```ts
|
|
656
|
+
* import { literal } from "@evolu/common";
|
|
657
|
+
*
|
|
658
|
+
* const Ready = literal("ready");
|
|
659
|
+
*
|
|
660
|
+
* expectTypeOf<typeof Ready.Output>().toEqualTypeOf<"ready">();
|
|
661
|
+
* expectOk(Ready.fromUnknown("ready"), "ready");
|
|
662
|
+
* expectErr(Ready.fromUnknown("pending"), {
|
|
663
|
+
* type: "Literal",
|
|
664
|
+
* expected: "ready",
|
|
665
|
+
* value: "pending",
|
|
666
|
+
* });
|
|
667
|
+
* ```
|
|
668
|
+
*
|
|
647
669
|
* @group Unions
|
|
648
670
|
*/
|
|
649
671
|
export const literal = (expected) => {
|
|
@@ -663,7 +685,10 @@ export const literal = (expected) => {
|
|
|
663
685
|
const formatError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`;
|
|
664
686
|
return globalThis.Object.assign(parent
|
|
665
687
|
? createChildType("Literal", parent, validate, formatError)
|
|
666
|
-
: createRootType("Literal", validate, formatError), {
|
|
688
|
+
: createRootType("Literal", validate, formatError), {
|
|
689
|
+
expected: literalExpected,
|
|
690
|
+
[templateLiteralSyntaxSymbol]: true,
|
|
691
|
+
});
|
|
667
692
|
};
|
|
668
693
|
/** @group Unions */
|
|
669
694
|
export const Undefined = /*#__PURE__*/ literal(undefined);
|
|
@@ -692,7 +717,7 @@ export function union(...typesOrLiterals) {
|
|
|
692
717
|
assertNonNullable(member);
|
|
693
718
|
return member[encoderSymbol](value);
|
|
694
719
|
};
|
|
695
|
-
return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members });
|
|
720
|
+
return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
|
|
696
721
|
}
|
|
697
722
|
const formatUnionError = () => "A value does not match any union member.";
|
|
698
723
|
const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
|
|
@@ -714,7 +739,7 @@ const createUnionValidation = (members, validateMember) => (value, options = fir
|
|
|
714
739
|
});
|
|
715
740
|
};
|
|
716
741
|
/**
|
|
717
|
-
*
|
|
742
|
+
* Union {@link Type} containing the supplied Type and `undefined`.
|
|
718
743
|
*
|
|
719
744
|
* This does not make an object property optional. It changes only the values
|
|
720
745
|
* accepted when the property is present.
|
|
@@ -723,13 +748,13 @@ const createUnionValidation = (members, validateMember) => (value, options = fir
|
|
|
723
748
|
*/
|
|
724
749
|
export const undefinedOr = (type) => union(type, Undefined);
|
|
725
750
|
/**
|
|
726
|
-
*
|
|
751
|
+
* Union {@link Type} containing the supplied Type and `null`.
|
|
727
752
|
*
|
|
728
753
|
* @group Unions
|
|
729
754
|
*/
|
|
730
755
|
export const nullOr = (type) => union(type, Null);
|
|
731
756
|
/**
|
|
732
|
-
*
|
|
757
|
+
* Union {@link Type} containing the supplied Type, `null`, and `undefined`.
|
|
733
758
|
*
|
|
734
759
|
* @group Unions
|
|
735
760
|
*/
|
|
@@ -737,6 +762,407 @@ export const nullishOr = (type) => union(type, Null, Undefined);
|
|
|
737
762
|
const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
|
|
738
763
|
"members" in type &&
|
|
739
764
|
globalThis.Array.isArray(type.members);
|
|
765
|
+
/**
|
|
766
|
+
* Template literal {@link Type} for validation and parsing.
|
|
767
|
+
*
|
|
768
|
+
* Parses and creates structured strings.
|
|
769
|
+
*
|
|
770
|
+
* Accepts the same template parts as {@link templateLiteral}: fixed string
|
|
771
|
+
* literals and Types canonically encoded as strings. Instead of keeping Output
|
|
772
|
+
* as a string, fixed literals define the framing and Output is a readonly Tuple
|
|
773
|
+
* of the decoded Type parts. `to` encodes that Tuple back into the canonical
|
|
774
|
+
* string represented by the parent Type. At least one Type part is required.
|
|
775
|
+
*
|
|
776
|
+
* When every capture uses identity encoding, the parent Output is the exact
|
|
777
|
+
* TypeScript template literal type. A transforming capture makes it nominal;
|
|
778
|
+
* create such strings with `to` or validate them with the parent Type.
|
|
779
|
+
*
|
|
780
|
+
* Deterministic framing is a core correctness guarantee. It preserves
|
|
781
|
+
* reversibility and keeps capture boundaries unambiguous. Different capture
|
|
782
|
+
* Tuples must never encode to the same string. The parser provides predictable
|
|
783
|
+
* parsing without pathological backtracking and decodes each capture once, so
|
|
784
|
+
* adversarial input cannot trigger exponential parser work. Fixed-width captures
|
|
785
|
+
* may be adjacent, but only one variable-width capture is allowed. Declarations
|
|
786
|
+
* that could join UTF-16 surrogate halves across parts are rejected during
|
|
787
|
+
* construction.
|
|
788
|
+
*
|
|
789
|
+
* Keep capture unions reasonably small to avoid excessive compiler work.
|
|
790
|
+
*
|
|
791
|
+
* TypeScript template literal types can describe a fixed number of digit
|
|
792
|
+
* positions, but not an arbitrarily long sequence of digits. Such grammars use
|
|
793
|
+
* branded Types such as {@link DecimalString}; `templateLiteralParser` preserves
|
|
794
|
+
* that exactness by requiring a validated branded capture when encoding.
|
|
795
|
+
*
|
|
796
|
+
* ### Example
|
|
797
|
+
*
|
|
798
|
+
* A template literal Type defines both a canonical string representation and
|
|
799
|
+
* the structured data decoded from it:
|
|
800
|
+
*
|
|
801
|
+
* ```ts
|
|
802
|
+
* import { templateLiteralParser, union } from "@evolu/common";
|
|
803
|
+
*
|
|
804
|
+
* const Language = union("en", "cs");
|
|
805
|
+
* const Region = union("US", "CZ");
|
|
806
|
+
*
|
|
807
|
+
* // Define a Type for "en-US" | "en-CZ" | "cs-US" | "cs-CZ".
|
|
808
|
+
* const SupportedLocale = templateLiteralParser(Language, "-", Region);
|
|
809
|
+
*
|
|
810
|
+
* // Output is the decoded language and region.
|
|
811
|
+
* type SupportedLocale = typeof SupportedLocale.Output;
|
|
812
|
+
* expectTypeOf<SupportedLocale>().toEqualTypeOf<
|
|
813
|
+
* readonly ["en" | "cs", "US" | "CZ"]
|
|
814
|
+
* >();
|
|
815
|
+
*
|
|
816
|
+
* // The parent Output is the canonical locale string.
|
|
817
|
+
* type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
|
|
818
|
+
* expectTypeOf<SupportedLocaleLiteral>().toEqualTypeOf<
|
|
819
|
+
* "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
|
|
820
|
+
* >();
|
|
821
|
+
*
|
|
822
|
+
* // Parse an unknown string into structured data.
|
|
823
|
+
* const result = SupportedLocale.fromUnknown("cs-CZ");
|
|
824
|
+
* assert(result.ok);
|
|
825
|
+
* const locale = result.value;
|
|
826
|
+
* expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
|
|
827
|
+
* expect(locale).toEqual(["cs", "CZ"]);
|
|
828
|
+
* expectErr(SupportedLocale.fromUnknown("cs/CZ"), {
|
|
829
|
+
* type: "TemplateLiteral",
|
|
830
|
+
* value: "cs/CZ",
|
|
831
|
+
* });
|
|
832
|
+
*
|
|
833
|
+
* // Encode structured data into its canonical string.
|
|
834
|
+
* const localeLiteral = SupportedLocale.to(locale);
|
|
835
|
+
* expectTypeOf(localeLiteral).toEqualTypeOf<SupportedLocaleLiteral>();
|
|
836
|
+
* expect(localeLiteral).toBe("cs-CZ");
|
|
837
|
+
*
|
|
838
|
+
* // Validate a string configuration value.
|
|
839
|
+
* const configValue: unknown = "cs-CZ";
|
|
840
|
+
* assert(SupportedLocale.parent.is(configValue));
|
|
841
|
+
* expectTypeOf(configValue).toEqualTypeOf<SupportedLocaleLiteral>();
|
|
842
|
+
* expect(SupportedLocale.parent.is("fr-CZ")).toBe(false);
|
|
843
|
+
* ```
|
|
844
|
+
*
|
|
845
|
+
* `SupportedLocale` is structured data for application code.
|
|
846
|
+
* `SupportedLocaleLiteral` is its canonical representation for configuration
|
|
847
|
+
* and other APIs that require a string, such as URL parameters, environment
|
|
848
|
+
* variables, and storage keys.
|
|
849
|
+
*
|
|
850
|
+
* Use branded captures for strings that TypeScript template literal types
|
|
851
|
+
* cannot express exactly, such as arbitrary-length canonical decimals:
|
|
852
|
+
*
|
|
853
|
+
* ```ts
|
|
854
|
+
* import {
|
|
855
|
+
* NonNegativeDecimalString,
|
|
856
|
+
* templateLiteralParser,
|
|
857
|
+
* } from "@evolu/common";
|
|
858
|
+
*
|
|
859
|
+
* const DecimalText = templateLiteralParser(
|
|
860
|
+
* "decimal:",
|
|
861
|
+
* NonNegativeDecimalString,
|
|
862
|
+
* );
|
|
863
|
+
*
|
|
864
|
+
* // DecimalText.to requires a validated NonNegativeDecimalString.
|
|
865
|
+
* const zero = NonNegativeDecimalString.orThrow("0");
|
|
866
|
+
*
|
|
867
|
+
* expectOk(DecimalText.fromUnknown("decimal:0"), [zero]);
|
|
868
|
+
* expect(DecimalText.to([zero])).toBe("decimal:0");
|
|
869
|
+
* ```
|
|
870
|
+
*
|
|
871
|
+
* Capture Types (the Type arguments passed to `templateLiteralParser`) can use
|
|
872
|
+
* transformations to decode substrings into non-string data:
|
|
873
|
+
*
|
|
874
|
+
* ```ts
|
|
875
|
+
* import {
|
|
876
|
+
* Int64FromInt64String,
|
|
877
|
+
* templateLiteralParser,
|
|
878
|
+
* } from "@evolu/common";
|
|
879
|
+
*
|
|
880
|
+
* const ItemId = templateLiteralParser("item-", Int64FromInt64String);
|
|
881
|
+
* type ItemId = typeof ItemId.Output;
|
|
882
|
+
* type ItemIdLiteral = typeof ItemId.parent.Output;
|
|
883
|
+
*
|
|
884
|
+
* // Decode the string into structured data.
|
|
885
|
+
* const result = ItemId.fromUnknown("item-42");
|
|
886
|
+
* assert(result.ok);
|
|
887
|
+
* const itemId = result.value;
|
|
888
|
+
* expectTypeOf(itemId).toEqualTypeOf<ItemId>();
|
|
889
|
+
* expect(itemId).toEqual([42n]);
|
|
890
|
+
*
|
|
891
|
+
* // Encode the structured data into its canonical string.
|
|
892
|
+
* const itemIdLiteral = ItemId.to(itemId);
|
|
893
|
+
* expectTypeOf(itemIdLiteral).toEqualTypeOf<ItemIdLiteral>();
|
|
894
|
+
* expect(itemIdLiteral).toBe("item-42");
|
|
895
|
+
*
|
|
896
|
+
* // TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
|
|
897
|
+
* // @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
|
|
898
|
+
* const invalidItemIdLiteral: ItemIdLiteral = "item-42";
|
|
899
|
+
* ```
|
|
900
|
+
*
|
|
901
|
+
* Fixed-width captures can be adjacent:
|
|
902
|
+
*
|
|
903
|
+
* ```ts
|
|
904
|
+
* import { templateLiteralParser, union } from "@evolu/common";
|
|
905
|
+
*
|
|
906
|
+
* const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
907
|
+
* const TwoDigits = templateLiteralParser(Digit, Digit);
|
|
908
|
+
* type TwoDigits = typeof TwoDigits.Output;
|
|
909
|
+
* type TwoDigitsLiteral = typeof TwoDigits.parent.Output;
|
|
910
|
+
*
|
|
911
|
+
* const twoDigits: TwoDigits = ["4", "2"];
|
|
912
|
+
* const twoDigitsLiteral: TwoDigitsLiteral = "42";
|
|
913
|
+
* // @ts-expect-error TwoDigitsLiteral requires exactly two digits.
|
|
914
|
+
* const threeDigitsLiteral: TwoDigitsLiteral = "123";
|
|
915
|
+
*
|
|
916
|
+
* expectOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
|
|
917
|
+
* expect(TwoDigits.to(twoDigits)).toBe(twoDigitsLiteral);
|
|
918
|
+
* ```
|
|
919
|
+
*
|
|
920
|
+
* TypeScript rejects multiple variable-width captures because their encoded
|
|
921
|
+
* boundaries would be ambiguous:
|
|
922
|
+
*
|
|
923
|
+
* ```ts
|
|
924
|
+
* import { String, templateLiteralParser } from "@evolu/common";
|
|
925
|
+
*
|
|
926
|
+
* // @ts-expect-error At most one Type capture can have a variable-width string representation.
|
|
927
|
+
* templateLiteralParser(String, ":", String);
|
|
928
|
+
* ```
|
|
929
|
+
*
|
|
930
|
+
* This restriction keeps encoding reversible: different capture Tuples must
|
|
931
|
+
* never produce the same string. A delimiter alone is not enough because it can
|
|
932
|
+
* also occur inside a capture. Some formats could provide stronger guarantees,
|
|
933
|
+
* such as captures that exclude a delimiter; support for those can be added
|
|
934
|
+
* when concrete use cases justify the additional framing rules.
|
|
935
|
+
*
|
|
936
|
+
* @group Template literals
|
|
937
|
+
*/
|
|
938
|
+
export const templateLiteralParser = (...parts) => createTemplateLiteralParserType(parts);
|
|
939
|
+
const createTemplateLiteralParserType = (templateParts) => {
|
|
940
|
+
const captureTypes = templateParts.filter((part) => typeof part !== "string");
|
|
941
|
+
const runtimeCaptureTypes = captureTypes;
|
|
942
|
+
const output = tuple(...captureTypes);
|
|
943
|
+
const runtimeOutput = output;
|
|
944
|
+
const reflection = {
|
|
945
|
+
output,
|
|
946
|
+
parts: templateParts,
|
|
947
|
+
[templateLiteralSyntaxSymbol]: true,
|
|
948
|
+
};
|
|
949
|
+
const parse = compileTemplateLiteralParser(templateParts);
|
|
950
|
+
const decodeString = (value, options = firstValidationOptions) => {
|
|
951
|
+
const parseResult = parse(value);
|
|
952
|
+
if (!parseResult.ok)
|
|
953
|
+
return parseResult;
|
|
954
|
+
const outputResult = validateTupleItems(parseResult.value, runtimeCaptureTypes, (capture, value, captureOptions) => capture.fromUnknown(value, captureOptions), options, false);
|
|
955
|
+
return (outputResult.ok
|
|
956
|
+
? outputResult
|
|
957
|
+
: err({
|
|
958
|
+
type: "TemplateLiteral",
|
|
959
|
+
outputError: outputResult.error,
|
|
960
|
+
}));
|
|
961
|
+
};
|
|
962
|
+
const encodeCaptures = (captures) => {
|
|
963
|
+
const encodedCaptures = runtimeOutput[encoderSymbol](captures);
|
|
964
|
+
let value = "";
|
|
965
|
+
let captureIndex = 0;
|
|
966
|
+
for (const part of templateParts) {
|
|
967
|
+
value +=
|
|
968
|
+
typeof part === "string" ? part : encodedCaptures[captureIndex++];
|
|
969
|
+
}
|
|
970
|
+
return value;
|
|
971
|
+
};
|
|
972
|
+
const canonicalizeString = (value, options) => {
|
|
973
|
+
const result = decodeString(value, options);
|
|
974
|
+
return result.ok ? ok(encodeCaptures(result.value)) : result;
|
|
975
|
+
};
|
|
976
|
+
const validateCanonicalString = (value, options = firstValidationOptions) => {
|
|
977
|
+
const stringResult = String.fromUnknown(value, options);
|
|
978
|
+
if (!stringResult.ok)
|
|
979
|
+
return stringResult;
|
|
980
|
+
const result = canonicalizeString(stringResult.value, options);
|
|
981
|
+
if (!result.ok || result.value === stringResult.value)
|
|
982
|
+
return result;
|
|
983
|
+
return err({ type: "TemplateLiteral", value: stringResult.value });
|
|
984
|
+
};
|
|
985
|
+
const getTypeIssues = (error, mode) => {
|
|
986
|
+
if (error.type !== "TemplateLiteral") {
|
|
987
|
+
return String[getRuntimeTypeIssuesSymbol](error, mode);
|
|
988
|
+
}
|
|
989
|
+
if ("outputError" in error) {
|
|
990
|
+
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
|
|
991
|
+
}
|
|
992
|
+
return singleRuntimeTypeIssue("TemplateLiteral", error, formatTemplateLiteralError);
|
|
993
|
+
};
|
|
994
|
+
const canonicalStringFromUnknown = (value, options = firstValidationOptions) => {
|
|
995
|
+
const stringResult = String.fromUnknown(value, options);
|
|
996
|
+
return stringResult.ok
|
|
997
|
+
? canonicalizeString(stringResult.value, options)
|
|
998
|
+
: stringResult;
|
|
999
|
+
};
|
|
1000
|
+
const canonicalStringFrom = createFromOperation((value, options = firstValidationOptions) => canonicalizeString(value, options));
|
|
1001
|
+
const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, reflection);
|
|
1002
|
+
const fromUnknown = (value, options = firstValidationOptions) => {
|
|
1003
|
+
const stringResult = String.fromUnknown(value, options);
|
|
1004
|
+
if (!stringResult.ok)
|
|
1005
|
+
return stringResult;
|
|
1006
|
+
// The internal parser has one broad signature, but a statically frameless
|
|
1007
|
+
// declaration cannot return its framing error.
|
|
1008
|
+
return decodeString(stringResult.value, options);
|
|
1009
|
+
};
|
|
1010
|
+
const fromCanonicalString = (value, options = firstValidationOptions) => {
|
|
1011
|
+
String.from(value);
|
|
1012
|
+
const result = decodeString(value, options);
|
|
1013
|
+
if (!result.ok || encodeCaptures(result.value) !== value) {
|
|
1014
|
+
throw new Error("Expected TemplateLiteral.", {
|
|
1015
|
+
cause: result.ok
|
|
1016
|
+
? {
|
|
1017
|
+
type: "TemplateLiteral",
|
|
1018
|
+
value,
|
|
1019
|
+
}
|
|
1020
|
+
: result.error,
|
|
1021
|
+
});
|
|
1022
|
+
}
|
|
1023
|
+
return result;
|
|
1024
|
+
};
|
|
1025
|
+
const fromString = (value, options = firstValidationOptions) => {
|
|
1026
|
+
String.from(value);
|
|
1027
|
+
return decodeString(value, options);
|
|
1028
|
+
};
|
|
1029
|
+
fromCanonicalString.parent = fromString;
|
|
1030
|
+
const from = createFromOperation(fromCanonicalString);
|
|
1031
|
+
const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, reflection);
|
|
1032
|
+
type.from.parent =
|
|
1033
|
+
fromCanonicalString;
|
|
1034
|
+
return type;
|
|
1035
|
+
};
|
|
1036
|
+
/**
|
|
1037
|
+
* Template literal {@link Type} for validation.
|
|
1038
|
+
*
|
|
1039
|
+
* Creates a canonical string Type from fixed strings and string-encoded Types.
|
|
1040
|
+
*
|
|
1041
|
+
* Use this factory when Output should remain a string. Switch to
|
|
1042
|
+
* {@link templateLiteralParser} when the individual Type parts should be
|
|
1043
|
+
* decoded into a Tuple.
|
|
1044
|
+
*
|
|
1045
|
+
* ### Example
|
|
1046
|
+
*
|
|
1047
|
+
* ```ts
|
|
1048
|
+
* import { templateLiteral, union } from "@evolu/common";
|
|
1049
|
+
*
|
|
1050
|
+
* const Language = union("en", "cs");
|
|
1051
|
+
* const Region = union("US", "CZ");
|
|
1052
|
+
* const Locale = templateLiteral(Language, "-", Region);
|
|
1053
|
+
*
|
|
1054
|
+
* expectTypeOf<typeof Locale.Output>().toEqualTypeOf<
|
|
1055
|
+
* "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
|
|
1056
|
+
* >();
|
|
1057
|
+
* expectOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
|
|
1058
|
+
* expect(Locale.is("fr-CZ")).toBe(false);
|
|
1059
|
+
* ```
|
|
1060
|
+
*
|
|
1061
|
+
* @group Template literals
|
|
1062
|
+
*/
|
|
1063
|
+
export const templateLiteral = (...parts) => createTemplateLiteralParserType(parts).parent;
|
|
1064
|
+
const formatTemplateLiteralError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`;
|
|
1065
|
+
const compileTemplateLiteralParser = (parts) => {
|
|
1066
|
+
let framing = emptyTemplateLiteralFraming;
|
|
1067
|
+
let fixedPartsWidth = 0;
|
|
1068
|
+
const compiledParts = parts.map((part) => {
|
|
1069
|
+
const partFraming = getTemplateLiteralPartFraming(part);
|
|
1070
|
+
framing = concatenateTemplateLiteralFraming(framing, partFraming);
|
|
1071
|
+
fixedPartsWidth += partFraming.width ?? 0;
|
|
1072
|
+
return [part, partFraming.width];
|
|
1073
|
+
});
|
|
1074
|
+
return (input) => {
|
|
1075
|
+
const inputCodePoints = globalThis.Array.from(input);
|
|
1076
|
+
const variableWidth = inputCodePoints.length - fixedPartsWidth;
|
|
1077
|
+
if (variableWidth < 0) {
|
|
1078
|
+
return err({ type: "TemplateLiteral", value: input });
|
|
1079
|
+
}
|
|
1080
|
+
const captures = [];
|
|
1081
|
+
let position = 0;
|
|
1082
|
+
for (const [part, width] of compiledParts) {
|
|
1083
|
+
const partWidth = width ?? variableWidth;
|
|
1084
|
+
const value = inputCodePoints
|
|
1085
|
+
.slice(position, position + partWidth)
|
|
1086
|
+
.join("");
|
|
1087
|
+
if (typeof part === "string") {
|
|
1088
|
+
if (value !== part) {
|
|
1089
|
+
return err({ type: "TemplateLiteral", value: input });
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
else {
|
|
1093
|
+
captures.push(value);
|
|
1094
|
+
}
|
|
1095
|
+
position += partWidth;
|
|
1096
|
+
}
|
|
1097
|
+
return position === inputCodePoints.length
|
|
1098
|
+
? ok(captures)
|
|
1099
|
+
: err({ type: "TemplateLiteral", value: input });
|
|
1100
|
+
};
|
|
1101
|
+
};
|
|
1102
|
+
const emptyTemplateLiteralFraming = {
|
|
1103
|
+
width: 0,
|
|
1104
|
+
canBeEmpty: true,
|
|
1105
|
+
canStartWithLowSurrogate: false,
|
|
1106
|
+
canEndWithHighSurrogate: false,
|
|
1107
|
+
};
|
|
1108
|
+
const unknownTemplateLiteralFraming = {
|
|
1109
|
+
width: null,
|
|
1110
|
+
canBeEmpty: true,
|
|
1111
|
+
canStartWithLowSurrogate: true,
|
|
1112
|
+
canEndWithHighSurrogate: true,
|
|
1113
|
+
};
|
|
1114
|
+
const concatenateTemplateLiteralFraming = (left, right) => {
|
|
1115
|
+
assert(!(left.canEndWithHighSurrogate && right.canStartWithLowSurrogate), "A TemplateLiteral cannot form a Unicode surrogate pair across part boundaries.");
|
|
1116
|
+
return {
|
|
1117
|
+
width: left.width !== null && right.width !== null
|
|
1118
|
+
? left.width + right.width
|
|
1119
|
+
: null,
|
|
1120
|
+
canBeEmpty: left.canBeEmpty && right.canBeEmpty,
|
|
1121
|
+
canStartWithLowSurrogate: left.canStartWithLowSurrogate ||
|
|
1122
|
+
(left.canBeEmpty && right.canStartWithLowSurrogate),
|
|
1123
|
+
canEndWithHighSurrogate: right.canEndWithHighSurrogate ||
|
|
1124
|
+
(right.canBeEmpty && left.canEndWithHighSurrogate),
|
|
1125
|
+
};
|
|
1126
|
+
};
|
|
1127
|
+
const getStringTemplateLiteralFraming = (value) => {
|
|
1128
|
+
const firstCodeUnit = value.charCodeAt(0);
|
|
1129
|
+
const lastCodeUnit = value.charCodeAt(value.length - 1);
|
|
1130
|
+
return {
|
|
1131
|
+
width: globalThis.Array.from(value).length,
|
|
1132
|
+
canBeEmpty: value.length === 0,
|
|
1133
|
+
canStartWithLowSurrogate: firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff,
|
|
1134
|
+
canEndWithHighSurrogate: lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff,
|
|
1135
|
+
};
|
|
1136
|
+
};
|
|
1137
|
+
const getTemplateLiteralPartFraming = (part) => {
|
|
1138
|
+
if (typeof part === "string")
|
|
1139
|
+
return getStringTemplateLiteralFraming(part);
|
|
1140
|
+
const type = part;
|
|
1141
|
+
if (type[templateLiteralSyntaxSymbol] === true) {
|
|
1142
|
+
if (type.name === "Literal") {
|
|
1143
|
+
return getStringTemplateLiteralFraming(type.expected);
|
|
1144
|
+
}
|
|
1145
|
+
if (type.name === "Union") {
|
|
1146
|
+
const memberFramings = type.members.map(getTemplateLiteralPartFraming);
|
|
1147
|
+
const width = memberFramings[0].width;
|
|
1148
|
+
return {
|
|
1149
|
+
width: width !== null &&
|
|
1150
|
+
memberFramings.every((framing) => framing.width === width)
|
|
1151
|
+
? width
|
|
1152
|
+
: null,
|
|
1153
|
+
canBeEmpty: memberFramings.some((framing) => framing.canBeEmpty),
|
|
1154
|
+
canStartWithLowSurrogate: memberFramings.some((framing) => framing.canStartWithLowSurrogate),
|
|
1155
|
+
canEndWithHighSurrogate: memberFramings.some((framing) => framing.canEndWithHighSurrogate),
|
|
1156
|
+
};
|
|
1157
|
+
}
|
|
1158
|
+
if (type.name === "TemplateLiteral") {
|
|
1159
|
+
return type.parts.reduce((framing, part) => concatenateTemplateLiteralFraming(framing, getTemplateLiteralPartFraming(part)), emptyTemplateLiteralFraming);
|
|
1160
|
+
}
|
|
1161
|
+
}
|
|
1162
|
+
if (type.parent === null)
|
|
1163
|
+
return unknownTemplateLiteralFraming;
|
|
1164
|
+
return getTemplateLiteralPartFraming(type.parent);
|
|
1165
|
+
};
|
|
740
1166
|
export function brand(name, parent, validate, formatError) {
|
|
741
1167
|
return createChildType(name, parent, validate
|
|
742
1168
|
? (value) => flatMapResult(validate(value), () => ok(value))
|
|
@@ -837,7 +1263,7 @@ export const capitalized = (parent) => brand("Capitalized", parent, (value) => {
|
|
|
837
1263
|
*/
|
|
838
1264
|
export const CapitalizedString = /*#__PURE__*/ capitalized(String);
|
|
839
1265
|
/**
|
|
840
|
-
*
|
|
1266
|
+
* String {@link Brand} without surrounding whitespace.
|
|
841
1267
|
*
|
|
842
1268
|
* @group String
|
|
843
1269
|
*/
|
|
@@ -863,7 +1289,7 @@ export const TrimmedString = /*#__PURE__*/ trimmed(String);
|
|
|
863
1289
|
*/
|
|
864
1290
|
export const trim = (value) => value.trim();
|
|
865
1291
|
/**
|
|
866
|
-
*
|
|
1292
|
+
* Minimum-length {@link Brand} requiring a value to have at least `min` items.
|
|
867
1293
|
*
|
|
868
1294
|
* @group String
|
|
869
1295
|
* @group Collection
|
|
@@ -890,7 +1316,7 @@ export const minLength = (min) => (parent) => {
|
|
|
890
1316
|
*/
|
|
891
1317
|
export const NonEmptyTrimmedString = /*#__PURE__*/ minLength(1)(TrimmedString);
|
|
892
1318
|
/**
|
|
893
|
-
*
|
|
1319
|
+
* Maximum-length {@link Brand} requiring a value to have at most `max` items.
|
|
894
1320
|
*
|
|
895
1321
|
* @group String
|
|
896
1322
|
* @group Collection
|
|
@@ -914,7 +1340,7 @@ export const NonEmptyTrimmedString100 = /*#__PURE__*/ maxLength(100)(NonEmptyTri
|
|
|
914
1340
|
*/
|
|
915
1341
|
export const NonEmptyTrimmedString1000 = /*#__PURE__*/ maxLength(1000)(NonEmptyTrimmedString);
|
|
916
1342
|
/**
|
|
917
|
-
*
|
|
1343
|
+
* Exact-length {@link Brand} requiring a value to have exactly `exact` items.
|
|
918
1344
|
*
|
|
919
1345
|
* @group String
|
|
920
1346
|
* @group Collection
|
|
@@ -926,7 +1352,7 @@ export const length = (exact) => (parent) => {
|
|
|
926
1352
|
: err({ type: name, value, exact }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not have the required length of ${error.exact}.`);
|
|
927
1353
|
};
|
|
928
1354
|
/**
|
|
929
|
-
*
|
|
1355
|
+
* String {@link Brand} constrained by a regular expression.
|
|
930
1356
|
*
|
|
931
1357
|
* ### Example
|
|
932
1358
|
*
|
|
@@ -1133,7 +1559,7 @@ export const createIdAsUuidv7 = (deps, ..._validation) => {
|
|
|
1133
1559
|
return uint8ArrayToBase64Url(bytes);
|
|
1134
1560
|
};
|
|
1135
1561
|
/**
|
|
1136
|
-
*
|
|
1562
|
+
* Table-specific {@link Id} Type.
|
|
1137
1563
|
*
|
|
1138
1564
|
* @group String
|
|
1139
1565
|
*/
|
|
@@ -1187,7 +1613,7 @@ export const Int64FromInt64String = /*#__PURE__*/ transform("Int64FromInt64Strin
|
|
|
1187
1613
|
to: (value) => globalThis.String(value),
|
|
1188
1614
|
});
|
|
1189
1615
|
/**
|
|
1190
|
-
*
|
|
1616
|
+
* Number {@link Brand} requiring a value greater than or equal to zero.
|
|
1191
1617
|
*
|
|
1192
1618
|
* @group Number
|
|
1193
1619
|
*/
|
|
@@ -1199,7 +1625,7 @@ export const nonNegative = (parent) => brand("NonNegative", parent, (value) => v
|
|
|
1199
1625
|
*/
|
|
1200
1626
|
export const NonNegativeNumber = /*#__PURE__*/ nonNegative(Number);
|
|
1201
1627
|
/**
|
|
1202
|
-
*
|
|
1628
|
+
* Number {@link Brand} requiring a value greater than zero.
|
|
1203
1629
|
*
|
|
1204
1630
|
* @group Number
|
|
1205
1631
|
*/
|
|
@@ -1214,7 +1640,7 @@ export const positive = (parent) => brand("Positive", parent, (value) => value >
|
|
|
1214
1640
|
*/
|
|
1215
1641
|
export const PositiveNumber = /*#__PURE__*/ positive(NonNegativeNumber);
|
|
1216
1642
|
/**
|
|
1217
|
-
*
|
|
1643
|
+
* Number {@link Brand} requiring a value less than or equal to zero.
|
|
1218
1644
|
*
|
|
1219
1645
|
* @group Number
|
|
1220
1646
|
*/
|
|
@@ -1226,7 +1652,7 @@ export const nonPositive = (parent) => brand("NonPositive", parent, (value) => v
|
|
|
1226
1652
|
*/
|
|
1227
1653
|
export const NonPositiveNumber = /*#__PURE__*/ nonPositive(Number);
|
|
1228
1654
|
/**
|
|
1229
|
-
*
|
|
1655
|
+
* Number {@link Brand} requiring a value less than zero.
|
|
1230
1656
|
*
|
|
1231
1657
|
* @group Number
|
|
1232
1658
|
*/
|
|
@@ -1241,7 +1667,7 @@ export const negative = (parent) => brand("Negative", parent, (value) => value <
|
|
|
1241
1667
|
*/
|
|
1242
1668
|
export const NegativeNumber = /*#__PURE__*/ negative(NonPositiveNumber);
|
|
1243
1669
|
/**
|
|
1244
|
-
*
|
|
1670
|
+
* Number {@link Brand} requiring a value other than `NaN`.
|
|
1245
1671
|
*
|
|
1246
1672
|
* @group Number
|
|
1247
1673
|
*/
|
|
@@ -1260,7 +1686,7 @@ export const nonNaN = (parent) => brand("NonNaN", parent, (value) => globalThis.
|
|
|
1260
1686
|
*/
|
|
1261
1687
|
export const NonNaNNumber = /*#__PURE__*/ nonNaN(Number);
|
|
1262
1688
|
/**
|
|
1263
|
-
*
|
|
1689
|
+
* Number {@link Brand} requiring a finite value.
|
|
1264
1690
|
*
|
|
1265
1691
|
* @group Number
|
|
1266
1692
|
*/
|
|
@@ -1279,6 +1705,12 @@ export const FiniteNumber = /*#__PURE__*/ finite(NonNaNNumber);
|
|
|
1279
1705
|
* @group Number
|
|
1280
1706
|
*/
|
|
1281
1707
|
export const NonNegativeFiniteNumber = /*#__PURE__*/ nonNegative(FiniteNumber);
|
|
1708
|
+
/**
|
|
1709
|
+
* Positive {@link FiniteNumber}.
|
|
1710
|
+
*
|
|
1711
|
+
* @group Number
|
|
1712
|
+
*/
|
|
1713
|
+
export const PositiveFiniteNumber = /*#__PURE__*/ positive(NonNegativeFiniteNumber);
|
|
1282
1714
|
/**
|
|
1283
1715
|
* Safe integer {@link Brand}.
|
|
1284
1716
|
*
|
|
@@ -1360,7 +1792,7 @@ export const NonPositiveInt = /*#__PURE__*/ nonPositive(Int);
|
|
|
1360
1792
|
*/
|
|
1361
1793
|
export const NegativeInt = /*#__PURE__*/ negative(NonPositiveInt);
|
|
1362
1794
|
/**
|
|
1363
|
-
*
|
|
1795
|
+
* Number {@link Brand} requiring a value greater than `min`.
|
|
1364
1796
|
*
|
|
1365
1797
|
* @group Number
|
|
1366
1798
|
*/
|
|
@@ -1371,7 +1803,7 @@ export const greaterThan = (min) => (parent) => {
|
|
|
1371
1803
|
: err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be greater than ${error.min}.`);
|
|
1372
1804
|
};
|
|
1373
1805
|
/**
|
|
1374
|
-
*
|
|
1806
|
+
* Number {@link Brand} requiring a value greater than or equal to `min`.
|
|
1375
1807
|
*
|
|
1376
1808
|
* @group Number
|
|
1377
1809
|
*/
|
|
@@ -1386,7 +1818,7 @@ export const greaterThanOrEqualTo = (min) => (parent) => {
|
|
|
1386
1818
|
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be greater than or equal to ${error.min}.`);
|
|
1387
1819
|
};
|
|
1388
1820
|
/**
|
|
1389
|
-
*
|
|
1821
|
+
* Number {@link Brand} requiring a value less than `max`.
|
|
1390
1822
|
*
|
|
1391
1823
|
* @group Number
|
|
1392
1824
|
*/
|
|
@@ -1404,7 +1836,7 @@ export const lessThan = (max) => (parent) => {
|
|
|
1404
1836
|
export const Age = /*#__PURE__*/ brand("Age",
|
|
1405
1837
|
/*#__PURE__*/ lessThan(200)(NonNegativeInt));
|
|
1406
1838
|
/**
|
|
1407
|
-
*
|
|
1839
|
+
* Number {@link Brand} requiring a value less than or equal to `max`.
|
|
1408
1840
|
*
|
|
1409
1841
|
* @group Number
|
|
1410
1842
|
*/
|
|
@@ -1424,51 +1856,147 @@ export const lessThanOrEqualTo = (max) => (parent) => {
|
|
|
1424
1856
|
export const Ratio = /*#__PURE__*/ brand("Ratio",
|
|
1425
1857
|
/*#__PURE__*/ lessThanOrEqualTo(1)(NonNegativeFiniteNumber));
|
|
1426
1858
|
/**
|
|
1427
|
-
* Canonical string representation of a
|
|
1859
|
+
* Canonical string representation of a signed base-10 decimal value.
|
|
1860
|
+
*
|
|
1861
|
+
* Use this Type when a decimal value must remain exact instead of being
|
|
1862
|
+
* converted to an IEEE-754 number. Equivalent values have one accepted
|
|
1863
|
+
* representation, so leading zeroes, trailing fractional zeroes, `-0`, plus
|
|
1864
|
+
* signs, and exponent notation are rejected.
|
|
1865
|
+
*
|
|
1866
|
+
* The decoded value remains a string. Arithmetic requires an explicit decimal
|
|
1867
|
+
* or fixed-point representation.
|
|
1428
1868
|
*
|
|
1429
|
-
*
|
|
1430
|
-
*
|
|
1431
|
-
*
|
|
1869
|
+
* TypeScript template literal types can describe a fixed number of digit
|
|
1870
|
+
* positions, but not the arbitrarily long integer and fractional parts accepted
|
|
1871
|
+
* here. `DecimalString` therefore uses a {@link Brand} so its TypeScript type
|
|
1872
|
+
* does not accept strings that have not been validated.
|
|
1873
|
+
*
|
|
1874
|
+
* Use these predefined Types or their corresponding factories to add sign
|
|
1875
|
+
* constraints to compatible decimal string Types:
|
|
1876
|
+
*
|
|
1877
|
+
* - {@link NonNegativeDecimalString} / {@link nonNegativeDecimalString}
|
|
1878
|
+
* - {@link PositiveDecimalString} / {@link positiveDecimalString}
|
|
1879
|
+
* - {@link NonPositiveDecimalString} / {@link nonPositiveDecimalString}
|
|
1880
|
+
* - {@link NegativeDecimalString} / {@link negativeDecimalString}
|
|
1432
1881
|
*
|
|
1433
1882
|
* ### Example
|
|
1434
1883
|
*
|
|
1435
1884
|
* ```ts
|
|
1436
|
-
* import {
|
|
1885
|
+
* import { DecimalString } from "@evolu/common";
|
|
1437
1886
|
*
|
|
1438
|
-
* expectOk(
|
|
1439
|
-
* expectOk(
|
|
1887
|
+
* expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
|
|
1888
|
+
* expectOk(DecimalString.fromUnknown("0"), "0");
|
|
1889
|
+
* expectOk(DecimalString.fromUnknown("10.25"), "10.25");
|
|
1440
1890
|
*
|
|
1441
|
-
* expectErr(
|
|
1442
|
-
* type: "
|
|
1443
|
-
* value: "
|
|
1891
|
+
* expectErr(DecimalString.fromUnknown("10.250"), {
|
|
1892
|
+
* type: "DecimalString",
|
|
1893
|
+
* value: "10.250",
|
|
1444
1894
|
* });
|
|
1445
1895
|
* ```
|
|
1446
1896
|
*
|
|
1447
1897
|
* @group Number
|
|
1448
1898
|
*/
|
|
1449
|
-
export const
|
|
1899
|
+
export const DecimalString = /*#__PURE__*/ brand("DecimalString", String, (value) => /^(?:0|-?(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9]))$/.test(value)
|
|
1900
|
+
? ok()
|
|
1901
|
+
: err({ type: "DecimalString", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical decimal string.`);
|
|
1902
|
+
/**
|
|
1903
|
+
* {@link DecimalString} Brand requiring a value greater than or equal to zero.
|
|
1904
|
+
*
|
|
1905
|
+
* @group Number
|
|
1906
|
+
*/
|
|
1907
|
+
export const nonNegativeDecimalString = (parent) => brand("NonNegativeDecimalString", parent, (value) => value[0] !== "-"
|
|
1908
|
+
? ok()
|
|
1909
|
+
: err({
|
|
1910
|
+
type: "NonNegativeDecimalString",
|
|
1911
|
+
value,
|
|
1912
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a non-negative decimal string.`);
|
|
1913
|
+
/**
|
|
1914
|
+
* Non-negative {@link DecimalString}.
|
|
1915
|
+
*
|
|
1916
|
+
* @group Number
|
|
1917
|
+
*/
|
|
1918
|
+
export const NonNegativeDecimalString =
|
|
1919
|
+
/*#__PURE__*/ nonNegativeDecimalString(DecimalString);
|
|
1920
|
+
/**
|
|
1921
|
+
* {@link DecimalString} Brand requiring a value greater than zero.
|
|
1922
|
+
*
|
|
1923
|
+
* @group Number
|
|
1924
|
+
*/
|
|
1925
|
+
export const positiveDecimalString = (parent) => brand("PositiveDecimalString", parent, (value) => value !== "0" && value[0] !== "-"
|
|
1450
1926
|
? ok()
|
|
1451
1927
|
: err({
|
|
1452
1928
|
type: "PositiveDecimalString",
|
|
1453
1929
|
value,
|
|
1454
|
-
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a
|
|
1930
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a positive decimal string.`);
|
|
1455
1931
|
/**
|
|
1456
|
-
*
|
|
1457
|
-
* `divisor`.
|
|
1932
|
+
* Positive {@link DecimalString}.
|
|
1458
1933
|
*
|
|
1459
|
-
*
|
|
1460
|
-
*
|
|
1461
|
-
*
|
|
1462
|
-
*
|
|
1463
|
-
|
|
1934
|
+
* Also satisfies {@link NonNegativeDecimalString}, so it can be used wherever a
|
|
1935
|
+
* non-negative decimal string is required.
|
|
1936
|
+
*
|
|
1937
|
+
* @group Number
|
|
1938
|
+
*/
|
|
1939
|
+
export const PositiveDecimalString = /*#__PURE__*/ positiveDecimalString(NonNegativeDecimalString);
|
|
1940
|
+
/**
|
|
1941
|
+
* {@link DecimalString} Brand requiring a value less than or equal to zero.
|
|
1942
|
+
*
|
|
1943
|
+
* @group Number
|
|
1944
|
+
*/
|
|
1945
|
+
export const nonPositiveDecimalString = (parent) => brand("NonPositiveDecimalString", parent, (value) => value === "0" || value[0] === "-"
|
|
1946
|
+
? ok()
|
|
1947
|
+
: err({
|
|
1948
|
+
type: "NonPositiveDecimalString",
|
|
1949
|
+
value,
|
|
1950
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a non-positive decimal string.`);
|
|
1951
|
+
/**
|
|
1952
|
+
* Non-positive {@link DecimalString}.
|
|
1953
|
+
*
|
|
1954
|
+
* @group Number
|
|
1955
|
+
*/
|
|
1956
|
+
export const NonPositiveDecimalString =
|
|
1957
|
+
/*#__PURE__*/ nonPositiveDecimalString(DecimalString);
|
|
1958
|
+
/**
|
|
1959
|
+
* {@link DecimalString} Brand requiring a value less than zero.
|
|
1960
|
+
*
|
|
1961
|
+
* @group Number
|
|
1962
|
+
*/
|
|
1963
|
+
export const negativeDecimalString = (parent) => brand("NegativeDecimalString", parent, (value) => value[0] === "-"
|
|
1964
|
+
? ok()
|
|
1965
|
+
: err({
|
|
1966
|
+
type: "NegativeDecimalString",
|
|
1967
|
+
value,
|
|
1968
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a negative decimal string.`);
|
|
1969
|
+
/**
|
|
1970
|
+
* Negative {@link DecimalString}.
|
|
1971
|
+
*
|
|
1972
|
+
* Also satisfies {@link NonPositiveDecimalString}, so it can be used wherever a
|
|
1973
|
+
* non-positive decimal string is required.
|
|
1974
|
+
*
|
|
1975
|
+
* @group Number
|
|
1976
|
+
*/
|
|
1977
|
+
export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPositiveDecimalString);
|
|
1978
|
+
/**
|
|
1979
|
+
* Number {@link Brand} requiring an exact decimal multiple of `divisor`.
|
|
1980
|
+
*
|
|
1981
|
+
* The divisor must be one canonical positive decimal string literal because its
|
|
1982
|
+
* exact value is encoded in the resulting Brand name. The declaration is
|
|
1983
|
+
* validated at compile time and is never converted to an IEEE-754 number.
|
|
1984
|
+
*
|
|
1985
|
+
* Validation treats each finite Number as its canonical decimal representation
|
|
1986
|
+
* and checks exact base-10 divisibility. It does not use floating-point
|
|
1987
|
+
* remainder, apply a tolerance, or round to the divisor's precision.
|
|
1464
1988
|
*
|
|
1465
1989
|
* ### Example
|
|
1466
1990
|
*
|
|
1467
1991
|
* ```ts
|
|
1468
|
-
* import { FiniteNumber, multipleOf } from "@evolu/common";
|
|
1992
|
+
* import { FiniteNumber, multipleOf, type Brand } from "@evolu/common";
|
|
1469
1993
|
*
|
|
1470
1994
|
* const Tenths = multipleOf("0.1")(FiniteNumber);
|
|
1471
1995
|
*
|
|
1996
|
+
* expectTypeOf<typeof Tenths.Output>().toEqualTypeOf<
|
|
1997
|
+
* FiniteNumber & Brand<"MultipleOf0.1">
|
|
1998
|
+
* >();
|
|
1999
|
+
*
|
|
1472
2000
|
* expectOk(Tenths.fromUnknown(0.3), 0.3);
|
|
1473
2001
|
* expectErr(Tenths.fromUnknown(0.31), {
|
|
1474
2002
|
* type: "MultipleOf0.1",
|
|
@@ -1480,31 +2008,18 @@ export const PositiveDecimalString = /*#__PURE__*/ brand("PositiveDecimalString"
|
|
|
1480
2008
|
* @group Number
|
|
1481
2009
|
*/
|
|
1482
2010
|
export const multipleOf = (divisor) => {
|
|
1483
|
-
// TODO: Define the positive-decimal literal domain with templateLiteral so
|
|
1484
|
-
// its TypeScript type is inferred from runtime Type code, then require that
|
|
1485
|
-
// literal Type here instead of accepting a string guarded by assertType.
|
|
1486
|
-
assertType(PositiveDecimalString, divisor);
|
|
1487
2011
|
const name = `MultipleOf${divisor}`;
|
|
1488
2012
|
const decimalDivisor = decimalStringToParts(divisor);
|
|
1489
2013
|
return (parent) => brand(name, parent, (value) => {
|
|
1490
2014
|
let isMultiple = false;
|
|
1491
2015
|
if (globalThis.Number.isFinite(value)) {
|
|
1492
2016
|
const decimalValue = decimalStringToParts(value.toString());
|
|
1493
|
-
const
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
0n;
|
|
1500
|
-
}
|
|
1501
|
-
else {
|
|
1502
|
-
isMultiple =
|
|
1503
|
-
decimalValue.coefficient %
|
|
1504
|
-
(decimalDivisor.coefficient *
|
|
1505
|
-
10n ** globalThis.BigInt(-exponentDifference)) ===
|
|
1506
|
-
0n;
|
|
1507
|
-
}
|
|
2017
|
+
const exponent = Math.min(decimalValue.exponent, decimalDivisor.exponent);
|
|
2018
|
+
const valueCoefficient = decimalValue.coefficient *
|
|
2019
|
+
10n ** globalThis.BigInt(decimalValue.exponent - exponent);
|
|
2020
|
+
const divisorCoefficient = decimalDivisor.coefficient *
|
|
2021
|
+
10n ** globalThis.BigInt(decimalDivisor.exponent - exponent);
|
|
2022
|
+
isMultiple = valueCoefficient % divisorCoefficient === 0n;
|
|
1508
2023
|
}
|
|
1509
2024
|
return isMultiple
|
|
1510
2025
|
? ok()
|
|
@@ -1534,7 +2049,7 @@ const decimalStringToParts = (value) => {
|
|
|
1534
2049
|
return { coefficient, exponent };
|
|
1535
2050
|
};
|
|
1536
2051
|
/**
|
|
1537
|
-
*
|
|
2052
|
+
* Number {@link Brand} requiring a value within an inclusive range.
|
|
1538
2053
|
*
|
|
1539
2054
|
* @group Number
|
|
1540
2055
|
*/
|
|
@@ -1966,6 +2481,57 @@ const createTupleType = (typeElements) => {
|
|
|
1966
2481
|
return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { elements: typeElements });
|
|
1967
2482
|
};
|
|
1968
2483
|
const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
|
|
2484
|
+
/**
|
|
2485
|
+
* Decimal digit from `"0"` to `"9"`.
|
|
2486
|
+
*
|
|
2487
|
+
* @group String
|
|
2488
|
+
*/
|
|
2489
|
+
export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
2490
|
+
/**
|
|
2491
|
+
* Decimal digit from `"1"` to `"9"`.
|
|
2492
|
+
*
|
|
2493
|
+
* @group String
|
|
2494
|
+
*/
|
|
2495
|
+
export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
2496
|
+
/**
|
|
2497
|
+
* Decimal string from `"1"` to `"6"`.
|
|
2498
|
+
*
|
|
2499
|
+
* @group String
|
|
2500
|
+
*/
|
|
2501
|
+
export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
|
|
2502
|
+
/**
|
|
2503
|
+
* Decimal string from `"1"` to `"23"`.
|
|
2504
|
+
*
|
|
2505
|
+
* @group String
|
|
2506
|
+
*/
|
|
2507
|
+
export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
|
|
2508
|
+
/*#__PURE__*/ templateLiteral("1", Digit),
|
|
2509
|
+
/*#__PURE__*/ templateLiteral("2",
|
|
2510
|
+
/*#__PURE__*/ union("0", "1", "2", "3")));
|
|
2511
|
+
/**
|
|
2512
|
+
* Decimal string from `"1"` to `"51"`.
|
|
2513
|
+
*
|
|
2514
|
+
* @group String
|
|
2515
|
+
*/
|
|
2516
|
+
export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
|
|
2517
|
+
/*#__PURE__*/ templateLiteral(
|
|
2518
|
+
/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
2519
|
+
/*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
|
|
2520
|
+
/**
|
|
2521
|
+
* Decimal string from `"1"` to `"99"`.
|
|
2522
|
+
*
|
|
2523
|
+
* @group String
|
|
2524
|
+
*/
|
|
2525
|
+
export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
|
|
2526
|
+
/*#__PURE__*/ templateLiteral(Digit1To9, Digit));
|
|
2527
|
+
/**
|
|
2528
|
+
* Decimal string from `"1"` to `"59"`.
|
|
2529
|
+
*
|
|
2530
|
+
* @group String
|
|
2531
|
+
*/
|
|
2532
|
+
export const Digit1To59 = /*#__PURE__*/ union(Digit1To9,
|
|
2533
|
+
/*#__PURE__*/ templateLiteral(
|
|
2534
|
+
/*#__PURE__*/ union("1", "2", "3", "4", "5"), Digit));
|
|
1969
2535
|
const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (error, mode) => {
|
|
1970
2536
|
const objectError = error;
|
|
1971
2537
|
if (objectError.reason.kind !== "Properties") {
|
|
@@ -2738,7 +3304,7 @@ const createRecordPropertyError = (issue) => ({
|
|
|
2738
3304
|
reason: { kind: "Entries", issues: [issue] },
|
|
2739
3305
|
});
|
|
2740
3306
|
/**
|
|
2741
|
-
*
|
|
3307
|
+
* Object {@link Type} with every property optional.
|
|
2742
3308
|
*
|
|
2743
3309
|
* No property is required, but every present property must still satisfy its
|
|
2744
3310
|
* Type.
|
|
@@ -2769,7 +3335,8 @@ export const partial = (props, ..._validation) => {
|
|
|
2769
3335
|
return createObjectType(partialProps);
|
|
2770
3336
|
};
|
|
2771
3337
|
/**
|
|
2772
|
-
*
|
|
3338
|
+
* Object {@link Type} making every property whose Union Type includes
|
|
3339
|
+
* {@link Null} optional.
|
|
2773
3340
|
*
|
|
2774
3341
|
* The property retains its original Union Type, so consumers may omit it, set
|
|
2775
3342
|
* it to `null`, or provide any other member of that Union. Properties without
|
|
@@ -2814,7 +3381,7 @@ export const nullableToOptional = (props, ..._validation) => {
|
|
|
2814
3381
|
return createObjectType(optionalProps);
|
|
2815
3382
|
};
|
|
2816
3383
|
/**
|
|
2817
|
-
*
|
|
3384
|
+
* Object {@link Type} without the selected declared properties.
|
|
2818
3385
|
*
|
|
2819
3386
|
* @group Objects
|
|
2820
3387
|
*/
|
|
@@ -3425,7 +3992,7 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
|
|
|
3425
3992
|
to: stringifyJsonValue,
|
|
3426
3993
|
});
|
|
3427
3994
|
/**
|
|
3428
|
-
*
|
|
3995
|
+
* Branded {@link Json} Type and total conversions for another Type.
|
|
3429
3996
|
*
|
|
3430
3997
|
* Use this factory when a domain value must be stored as JSON text while its
|
|
3431
3998
|
* exact Type remains visible to TypeScript, such as a JSON column in an Evolu
|