@evolu/common 8.1.0 → 8.3.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/Function.d.ts +1 -0
- package/dist/src/Function.d.ts.map +1 -1
- package/dist/src/Http.d.ts +9 -0
- package/dist/src/Http.d.ts.map +1 -1
- 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/Object.d.ts +1 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/RefCount.d.ts +1 -0
- package/dist/src/RefCount.d.ts.map +1 -1
- package/dist/src/Relation.d.ts +2 -0
- package/dist/src/Relation.d.ts.map +1 -1
- package/dist/src/Resource.d.ts +1 -0
- package/dist/src/Resource.d.ts.map +1 -1
- package/dist/src/Result.d.ts +383 -548
- package/dist/src/Result.d.ts.map +1 -1
- package/dist/src/Result.js +296 -46
- package/dist/src/Schedule.d.ts +2 -2
- package/dist/src/Schedule.d.ts.map +1 -1
- package/dist/src/StackTrace.d.ts +1 -0
- package/dist/src/StackTrace.d.ts.map +1 -1
- package/dist/src/Task.d.ts +978 -1061
- package/dist/src/Task.d.ts.map +1 -1
- package/dist/src/Task.js +1010 -159
- package/dist/src/Time.d.ts +71 -34
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +77 -1
- package/dist/src/Type.d.ts +1740 -795
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1634 -211
- package/dist/src/Types.d.ts +0 -24
- package/dist/src/Types.d.ts.map +1 -1
- package/dist/src/intl/_en.d.ts +126 -0
- package/dist/src/intl/_en.d.ts.map +1 -0
- package/dist/src/intl/_en.js +267 -0
- package/dist/src/intl/ar.d.ts +117 -0
- package/dist/src/intl/ar.d.ts.map +1 -0
- package/dist/src/intl/ar.js +245 -0
- package/dist/src/intl/bn.d.ts +117 -0
- package/dist/src/intl/bn.d.ts.map +1 -0
- package/dist/src/intl/bn.js +258 -0
- package/dist/src/intl/ca.d.ts +117 -0
- package/dist/src/intl/ca.d.ts.map +1 -0
- package/dist/src/intl/ca.js +258 -0
- package/dist/src/intl/cs.d.ts +30 -7
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +65 -28
- package/dist/src/intl/da.d.ts +117 -0
- package/dist/src/intl/da.d.ts.map +1 -0
- package/dist/src/intl/da.js +258 -0
- package/dist/src/intl/de.d.ts +117 -0
- package/dist/src/intl/de.d.ts.map +1 -0
- package/dist/src/intl/de.js +258 -0
- package/dist/src/intl/el.d.ts +117 -0
- package/dist/src/intl/el.d.ts.map +1 -0
- package/dist/src/intl/el.js +265 -0
- package/dist/src/intl/es.d.ts +117 -0
- package/dist/src/intl/es.d.ts.map +1 -0
- package/dist/src/intl/es.js +258 -0
- package/dist/src/intl/fa.d.ts +117 -0
- package/dist/src/intl/fa.d.ts.map +1 -0
- package/dist/src/intl/fa.js +258 -0
- package/dist/src/intl/fi.d.ts +117 -0
- package/dist/src/intl/fi.d.ts.map +1 -0
- package/dist/src/intl/fi.js +258 -0
- package/dist/src/intl/fil.d.ts +117 -0
- package/dist/src/intl/fil.d.ts.map +1 -0
- package/dist/src/intl/fil.js +258 -0
- package/dist/src/intl/fr.d.ts +117 -0
- package/dist/src/intl/fr.d.ts.map +1 -0
- package/dist/src/intl/fr.js +258 -0
- package/dist/src/intl/he.d.ts +117 -0
- package/dist/src/intl/he.d.ts.map +1 -0
- package/dist/src/intl/he.js +258 -0
- package/dist/src/intl/hi.d.ts +117 -0
- package/dist/src/intl/hi.d.ts.map +1 -0
- package/dist/src/intl/hi.js +258 -0
- package/dist/src/intl/hr.d.ts +117 -0
- package/dist/src/intl/hr.d.ts.map +1 -0
- package/dist/src/intl/hr.js +265 -0
- package/dist/src/intl/hu.d.ts +63 -0
- package/dist/src/intl/hu.d.ts.map +1 -0
- package/dist/src/intl/hu.js +204 -0
- package/dist/src/intl/id.d.ts +117 -0
- package/dist/src/intl/id.d.ts.map +1 -0
- package/dist/src/intl/id.js +258 -0
- package/dist/src/intl/index.d.ts +42 -0
- package/dist/src/intl/index.d.ts.map +1 -1
- package/dist/src/intl/index.js +42 -0
- package/dist/src/intl/it.d.ts +117 -0
- package/dist/src/intl/it.d.ts.map +1 -0
- package/dist/src/intl/it.js +258 -0
- package/dist/src/intl/ja.d.ts +117 -0
- package/dist/src/intl/ja.d.ts.map +1 -0
- package/dist/src/intl/ja.js +258 -0
- package/dist/src/intl/ko.d.ts +117 -0
- package/dist/src/intl/ko.d.ts.map +1 -0
- package/dist/src/intl/ko.js +258 -0
- package/dist/src/intl/ml.d.ts +117 -0
- package/dist/src/intl/ml.d.ts.map +1 -0
- package/dist/src/intl/ml.js +258 -0
- package/dist/src/intl/mr.d.ts +117 -0
- package/dist/src/intl/mr.d.ts.map +1 -0
- package/dist/src/intl/mr.js +258 -0
- package/dist/src/intl/ms.d.ts +117 -0
- package/dist/src/intl/ms.d.ts.map +1 -0
- package/dist/src/intl/ms.js +245 -0
- package/dist/src/intl/nb.d.ts +63 -0
- package/dist/src/intl/nb.d.ts.map +1 -0
- package/dist/src/intl/nb.js +191 -0
- package/dist/src/intl/nl.d.ts +117 -0
- package/dist/src/intl/nl.d.ts.map +1 -0
- package/dist/src/intl/nl.js +258 -0
- package/dist/src/intl/pa.d.ts +117 -0
- package/dist/src/intl/pa.d.ts.map +1 -0
- package/dist/src/intl/pa.js +258 -0
- package/dist/src/intl/pl.d.ts +117 -0
- package/dist/src/intl/pl.d.ts.map +1 -0
- package/dist/src/intl/pl.js +242 -0
- package/dist/src/intl/pt-BR.d.ts +117 -0
- package/dist/src/intl/pt-BR.d.ts.map +1 -0
- package/dist/src/intl/pt-BR.js +258 -0
- package/dist/src/intl/pt.d.ts +117 -0
- package/dist/src/intl/pt.d.ts.map +1 -0
- package/dist/src/intl/pt.js +246 -0
- package/dist/src/intl/ro.d.ts +117 -0
- package/dist/src/intl/ro.d.ts.map +1 -0
- package/dist/src/intl/ro.js +258 -0
- package/dist/src/intl/sk.d.ts +117 -0
- package/dist/src/intl/sk.d.ts.map +1 -0
- package/dist/src/intl/sk.js +244 -0
- package/dist/src/intl/sl.d.ts +117 -0
- package/dist/src/intl/sl.d.ts.map +1 -0
- package/dist/src/intl/sl.js +258 -0
- package/dist/src/intl/sv.d.ts +117 -0
- package/dist/src/intl/sv.d.ts.map +1 -0
- package/dist/src/intl/sv.js +258 -0
- package/dist/src/intl/sw.d.ts +63 -0
- package/dist/src/intl/sw.d.ts.map +1 -0
- package/dist/src/intl/sw.js +191 -0
- package/dist/src/intl/ta.d.ts +117 -0
- package/dist/src/intl/ta.d.ts.map +1 -0
- package/dist/src/intl/ta.js +258 -0
- package/dist/src/intl/te.d.ts +117 -0
- package/dist/src/intl/te.d.ts.map +1 -0
- package/dist/src/intl/te.js +258 -0
- package/dist/src/intl/th.d.ts +117 -0
- package/dist/src/intl/th.d.ts.map +1 -0
- package/dist/src/intl/th.js +258 -0
- package/dist/src/intl/tr.d.ts +117 -0
- package/dist/src/intl/tr.d.ts.map +1 -0
- package/dist/src/intl/tr.js +255 -0
- package/dist/src/intl/uk.d.ts +117 -0
- package/dist/src/intl/uk.d.ts.map +1 -0
- package/dist/src/intl/uk.js +258 -0
- package/dist/src/intl/ur.d.ts +117 -0
- package/dist/src/intl/ur.d.ts.map +1 -0
- package/dist/src/intl/ur.js +245 -0
- package/dist/src/intl/vi.d.ts +117 -0
- package/dist/src/intl/vi.d.ts.map +1 -0
- package/dist/src/intl/vi.js +255 -0
- package/dist/src/intl/zh-CN.d.ts +117 -0
- package/dist/src/intl/zh-CN.d.ts.map +1 -0
- package/dist/src/intl/zh-CN.js +258 -0
- package/dist/src/intl/zh-TW.d.ts +117 -0
- package/dist/src/intl/zh-TW.d.ts.map +1 -0
- package/dist/src/intl/zh-TW.js +258 -0
- package/dist/src/local-first/Owner.d.ts +156 -62
- package/dist/src/local-first/Owner.d.ts.map +1 -1
- package/dist/src/local-first/Owner.js +119 -20
- package/package.json +1 -1
- package/src/Function.ts +2 -0
- package/src/Http.ts +9 -0
- package/src/Number.ts +85 -14
- package/src/Object.ts +2 -0
- package/src/RefCount.ts +2 -0
- package/src/Relation.ts +4 -0
- package/src/Resource.ts +2 -0
- package/src/Result.ts +397 -560
- package/src/Schedule.ts +1 -1
- package/src/StackTrace.ts +2 -0
- package/src/Task.ts +1145 -1225
- package/src/Time.ts +110 -62
- package/src/Type.ts +2842 -835
- package/src/Types.ts +0 -49
- package/src/intl/_en.ts +497 -0
- package/src/intl/ar.ts +417 -0
- package/src/intl/bn.ts +486 -0
- package/src/intl/ca.ts +488 -0
- package/src/intl/cs.ts +131 -32
- package/src/intl/da.ts +488 -0
- package/src/intl/de.ts +488 -0
- package/src/intl/el.ts +494 -0
- package/src/intl/es.ts +488 -0
- package/src/intl/fa.ts +487 -0
- package/src/intl/fi.ts +487 -0
- package/src/intl/fil.ts +488 -0
- package/src/intl/fr.ts +488 -0
- package/src/intl/he.ts +486 -0
- package/src/intl/hi.ts +487 -0
- package/src/intl/hr.ts +494 -0
- package/src/intl/hu.ts +387 -0
- package/src/intl/id.ts +487 -0
- package/src/intl/index.ts +42 -0
- package/src/intl/it.ts +488 -0
- package/src/intl/ja.ts +488 -0
- package/src/intl/ko.ts +487 -0
- package/src/intl/ml.ts +487 -0
- package/src/intl/mr.ts +487 -0
- package/src/intl/ms.ts +410 -0
- package/src/intl/nb.ts +364 -0
- package/src/intl/nl.ts +488 -0
- package/src/intl/pa.ts +487 -0
- package/src/intl/pl.ts +450 -0
- package/src/intl/pt-BR.ts +487 -0
- package/src/intl/pt.ts +424 -0
- package/src/intl/ro.ts +488 -0
- package/src/intl/sk.ts +412 -0
- package/src/intl/sl.ts +487 -0
- package/src/intl/sv.ts +488 -0
- package/src/intl/sw.ts +324 -0
- package/src/intl/ta.ts +487 -0
- package/src/intl/te.ts +487 -0
- package/src/intl/th.ts +484 -0
- package/src/intl/tr.ts +487 -0
- package/src/intl/uk.ts +488 -0
- package/src/intl/ur.ts +410 -0
- package/src/intl/vi.ts +435 -0
- package/src/intl/zh-CN.ts +482 -0
- package/src/intl/zh-TW.ts +482 -0
- package/src/local-first/Owner.ts +156 -62
package/dist/src/Type.js
CHANGED
|
@@ -1,5 +1,328 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* ## Intro
|
|
3
|
+
*
|
|
4
|
+
* Runtime validation with precise TypeScript types and structured errors.
|
|
5
|
+
*
|
|
6
|
+
* Evolu {@link Type} is a pure, synchronous codec for defining semantic domains.
|
|
7
|
+
* It partially decodes an `Input` into an `Output` and totally encodes every
|
|
8
|
+
* valid `Output` into a `CanonicalInput`. Types can validate, refine,
|
|
9
|
+
* transform, and compose without losing the contracts TypeScript can express.
|
|
10
|
+
*
|
|
11
|
+
* ```ts
|
|
12
|
+
* import {
|
|
13
|
+
* NonEmptyTrimmedString100,
|
|
14
|
+
* PositiveInt,
|
|
15
|
+
* object,
|
|
16
|
+
* type InferType,
|
|
17
|
+
* } from "@evolu/common";
|
|
18
|
+
*
|
|
19
|
+
* const User = object({
|
|
20
|
+
* name: NonEmptyTrimmedString100,
|
|
21
|
+
* age: PositiveInt,
|
|
22
|
+
* });
|
|
23
|
+
* interface User extends InferType<typeof User> {}
|
|
24
|
+
*
|
|
25
|
+
* const value: unknown = { name: "Ada", age: 37 };
|
|
26
|
+
* const user = User.fromUnknown(value);
|
|
27
|
+
*
|
|
28
|
+
* expectOk(user, { name: "Ada", age: 37 });
|
|
29
|
+
* expectTypeOf(user.value).toExtend<User>();
|
|
30
|
+
* ```
|
|
31
|
+
*
|
|
32
|
+
* Decoding failures are explicit {@link Result} values. Error formatters are
|
|
33
|
+
* separate from validation, so structured errors remain exhaustively typed and
|
|
34
|
+
* can be localized without changing the Type.
|
|
35
|
+
*
|
|
36
|
+
* Evolu Type is designed to make correct code the easiest code to write:
|
|
37
|
+
*
|
|
38
|
+
* - {@link Brand} carries every refinement constraint into TypeScript.
|
|
39
|
+
* - Invalid declarations produce readable {@link CompileTimeError} types when the
|
|
40
|
+
* compiler can detect them; runtime assertions enforce construction contracts
|
|
41
|
+
* it cannot prove.
|
|
42
|
+
* - Typed `from` boundaries allow connecting value producers to domain fields
|
|
43
|
+
* through their exact TypeScript types, so incompatible contract changes are
|
|
44
|
+
* compile-time errors rather than runtime validation errors.
|
|
45
|
+
* - Lawful codecs compose without creating unencodable values: every valid
|
|
46
|
+
* Output has a canonical Input representation and round-trips to the same
|
|
47
|
+
* semantic value.
|
|
48
|
+
* - Type-safe localization infers the required error formatters from selected
|
|
49
|
+
* Types, so missing validation messages are compile-time errors.
|
|
50
|
+
*
|
|
51
|
+
* Correctness is especially important for local-first data: application authors
|
|
52
|
+
* cannot inspect or repair a user's data on a server. Type declarations reject
|
|
53
|
+
* invalid data at system boundaries, then preserve those guarantees wherever
|
|
54
|
+
* the data travels.
|
|
55
|
+
*
|
|
56
|
+
* Evolu Type supports [Standard Schema](https://standardschema.dev/) while
|
|
57
|
+
* preserving each Type's exact Input and Output. Its implementation is
|
|
58
|
+
* optimized for small bundles, but keeps actionable assertion messages as a
|
|
59
|
+
* deliberate developer-experience tradeoff.
|
|
60
|
+
*
|
|
61
|
+
* Evolu Type requires TypeScript 7 or newer and `exactOptionalPropertyTypes`.
|
|
62
|
+
* Predefined Types use the names of corresponding JavaScript built-ins. When
|
|
63
|
+
* one shadows a built-in, access the built-in through `globalThis`, such as
|
|
64
|
+
* `globalThis.String` or `globalThis.Date`.
|
|
65
|
+
*
|
|
66
|
+
* ## Boundaries
|
|
67
|
+
*
|
|
68
|
+
* `fromUnknown` validates untyped input through the complete pipeline. `from`
|
|
69
|
+
* and its `.parent` operations assert their declared boundary, then return only
|
|
70
|
+
* errors from the remaining stages. `orThrow` and `orNull` reuse the deepest
|
|
71
|
+
* `from` operation accepting `Input`, while `to` asserts its Output boundary. A
|
|
72
|
+
* failed assertion is a developer error; its cause preserves the exact
|
|
73
|
+
* structured Output validation error.
|
|
74
|
+
*
|
|
75
|
+
* Prefer the most precise typed boundary available. A value is not unknown
|
|
76
|
+
* merely because it originated outside the application: forms, components, and
|
|
77
|
+
* other producers often expose a `string` or a branded value that can connect
|
|
78
|
+
* directly to a matching `from` boundary. Reserve `fromUnknown` for values
|
|
79
|
+
* whose TypeScript type is genuinely `unknown`. `is` means exact membership in
|
|
80
|
+
* the Output domain, not merely that an encoded Input can be decoded.
|
|
81
|
+
*
|
|
82
|
+
* Typed boundaries also connect producer and domain constraints at compile
|
|
83
|
+
* time. Suppose a form and its domain field both use
|
|
84
|
+
* {@link NonEmptyTrimmedString100}. If the form is later relaxed to
|
|
85
|
+
* {@link NonEmptyTrimmedString1000}, the domain Type will still reject longer
|
|
86
|
+
* values, so invalid data cannot be stored. But a boundary accepting only
|
|
87
|
+
* `unknown` or `string` cannot reveal that the producer contract changed. The
|
|
88
|
+
* application still compiles, and users discover the incompatibility only when
|
|
89
|
+
* a valid form value fails to save.
|
|
90
|
+
*
|
|
91
|
+
* Passing the precise branded value to `from` makes that incompatibility a
|
|
92
|
+
* compile-time error. The developer must preserve the original form limit or
|
|
93
|
+
* introduce a new field for the wider domain instead of shipping a broken form.
|
|
94
|
+
*
|
|
95
|
+
* A weaker producer is sometimes intentional. In that case, a matching
|
|
96
|
+
* `.parent` boundary validates only the constraints the producer does not
|
|
97
|
+
* already guarantee, while preserving its existing guarantees in the type.
|
|
98
|
+
*
|
|
99
|
+
* ## FAQ
|
|
100
|
+
*
|
|
101
|
+
* ### What does a Type represent?
|
|
102
|
+
*
|
|
103
|
+
* A Type is a lawful, pure codec for an exact semantic domain:
|
|
104
|
+
*
|
|
105
|
+
* ```text
|
|
106
|
+
* Input ── partial decode ──▶ Output
|
|
107
|
+
* CanonicalInput ◀─── total encode ─── Output
|
|
108
|
+
*
|
|
109
|
+
* CanonicalInput ⊆ Input
|
|
110
|
+
* ```
|
|
111
|
+
*
|
|
112
|
+
* Read each line in the direction of its arrowhead. `Input` is the complete
|
|
113
|
+
* typed decoding boundary, including candidates that validation can reject and
|
|
114
|
+
* noncanonical representations that decoding can normalize. `Output` is the
|
|
115
|
+
* validated semantic value. `CanonicalInput` is the statically known subtype of
|
|
116
|
+
* `Input` returned by the complete `to` operation. It can be wider than the
|
|
117
|
+
* values actually emitted when a refinement follows an arbitrary transformation
|
|
118
|
+
* because TypeScript cannot determine which values its encoder returns for the
|
|
119
|
+
* narrowed Output. `fromUnknown` and the `from` operations decode; `to`
|
|
120
|
+
* encodes.
|
|
121
|
+
*
|
|
122
|
+
* A lawful Type round-trips every Output:
|
|
123
|
+
*
|
|
124
|
+
* ```text
|
|
125
|
+
* fromUnknown(to(output)) ≈ ok(output)
|
|
126
|
+
* ```
|
|
127
|
+
*
|
|
128
|
+
* Encoding can canonicalize a valid Input:
|
|
129
|
+
*
|
|
130
|
+
* ```text
|
|
131
|
+
* "0042" ──decode──▶ 42 ──encode──▶ "42"
|
|
132
|
+
* ```
|
|
133
|
+
*
|
|
134
|
+
* Once canonicalized, repeating the decode-encode cycle must preserve that
|
|
135
|
+
* representation:
|
|
136
|
+
*
|
|
137
|
+
* ```text
|
|
138
|
+
* "42" ──decode──▶ 42 ──encode──▶ "42"
|
|
139
|
+
* ```
|
|
140
|
+
*
|
|
141
|
+
* Here `decode` means running the complete decoding pipeline, as `fromUnknown`
|
|
142
|
+
* does, and `≈` means equality appropriate for the semantic domain. Validation
|
|
143
|
+
* refinements, {@link ArrayType | Array Types}, and
|
|
144
|
+
* {@link ObjectType | Object Types} preserve these laws when their contained
|
|
145
|
+
* Types do. A {@link union} additionally requires compatible dispatch: it
|
|
146
|
+
* encodes through the first member matching the Output and decodes through the
|
|
147
|
+
* first member accepting the Input. Member ordering is lawful only when those
|
|
148
|
+
* choices agree semantically. Encoded representations can overlap even when
|
|
149
|
+
* member Output types are disjoint.
|
|
150
|
+
*
|
|
151
|
+
* When encoding returns a refined value unchanged, the refinement can narrow
|
|
152
|
+
* `CanonicalInput` without changing its JavaScript representation. For example,
|
|
153
|
+
* {@link FiniteNumber} has `number` as its Input, while its Output and
|
|
154
|
+
* CanonicalInput are `FiniteNumber`: decoding can reject non-finite number
|
|
155
|
+
* candidates, and encoding only receives validated finite Outputs. A
|
|
156
|
+
* transformation can change the representation entirely. For
|
|
157
|
+
* {@link Int64FromInt64String}, Input is `string`, Output is `Int64`, and
|
|
158
|
+
* CanonicalInput is `Int64String`. Structural Type factories derive their
|
|
159
|
+
* CanonicalInput recursively from their contained Types.
|
|
160
|
+
*
|
|
161
|
+
* ### Why is to total?
|
|
162
|
+
*
|
|
163
|
+
* Suppose a Type accepts only strings containing decimal digits and decodes
|
|
164
|
+
* them to JavaScript numbers. Parsing `"42"` is possible, but the Type cannot
|
|
165
|
+
* lawfully declare its Output as `number`:
|
|
166
|
+
*
|
|
167
|
+
* ```text
|
|
168
|
+
* digits-only string ──partial decode──▶ number
|
|
169
|
+
* digits-only string ◀─── total encode ── number // impossible
|
|
170
|
+
* ```
|
|
171
|
+
*
|
|
172
|
+
* `number` also contains negative and fractional numbers, `NaN`, positive and
|
|
173
|
+
* negative infinity, and `-0`. None of those values has a digits-only
|
|
174
|
+
* representation, so `to` could not encode every valid Output.
|
|
175
|
+
*
|
|
176
|
+
* One lawful design narrows the Output to the exact representable domain:
|
|
177
|
+
*
|
|
178
|
+
* ```text
|
|
179
|
+
* digits-only string ──partial decode──▶ NonNegativeSafeInteger
|
|
180
|
+
* digits-only string ◀─── total encode ── NonNegativeSafeInteger
|
|
181
|
+
*
|
|
182
|
+
* "0042" ──decode──▶ 42 ──encode──▶ "42"
|
|
183
|
+
* ```
|
|
184
|
+
*
|
|
185
|
+
* Another lawful design keeps `number` as the Output but expands the Input
|
|
186
|
+
* representation to include a canonical string for every number, including
|
|
187
|
+
* `"NaN"`, `"Infinity"`, `"-Infinity"`, and `"-0"`, as well as negative and
|
|
188
|
+
* fractional numbers.
|
|
189
|
+
*
|
|
190
|
+
* The same principle applies when converting between two representations. Give
|
|
191
|
+
* each representation its own Type with the same exact Output. For example, a
|
|
192
|
+
* string representation and a number representation can both decode to the
|
|
193
|
+
* shared `SafeInteger` domain:
|
|
194
|
+
*
|
|
195
|
+
* ```text
|
|
196
|
+
* string ──partial decode──▶ SafeInteger
|
|
197
|
+
* string ◀─── total encode ── SafeInteger
|
|
198
|
+
*
|
|
199
|
+
* number ──partial decode──▶ SafeInteger
|
|
200
|
+
* number ◀─── total encode ── SafeInteger
|
|
201
|
+
* ```
|
|
202
|
+
*
|
|
203
|
+
* Conversion decodes the source representation, then total-encodes the shared
|
|
204
|
+
* Output into the target representation. If no lossless shared domain exists,
|
|
205
|
+
* the operation is a partial conversion, migration, or policy decision and
|
|
206
|
+
* should be an explicit function returning Result, not a Type transformation.
|
|
207
|
+
*
|
|
208
|
+
* ### Why can a typed operation throw?
|
|
209
|
+
*
|
|
210
|
+
* TypeScript proves structural assignability, but it cannot describe every
|
|
211
|
+
* runtime invariant. For example, it cannot express whether an object property
|
|
212
|
+
* is own, enumerable, or a data property. It also permits a wider object with
|
|
213
|
+
* excess properties where a narrower object type is expected.
|
|
214
|
+
*
|
|
215
|
+
* `fromUnknown` treats such invalid external values as input data and returns a
|
|
216
|
+
* typed error. Typed boundaries instead assert the domain promised by their
|
|
217
|
+
* parameter type. If application code claims an accessor-backed object or an
|
|
218
|
+
* object with excess properties is an Object Output, the assertion throws
|
|
219
|
+
* because the application contract is broken. `orThrow` and `orNull` preserve
|
|
220
|
+
* the assertion at their typed `Input` boundary, then apply {@link getOrThrow}
|
|
221
|
+
* or {@link getOrNull} only to validation failures returned by the remaining
|
|
222
|
+
* pipeline.
|
|
223
|
+
*
|
|
224
|
+
* Consequently, structural representation errors such as sparse Arrays,
|
|
225
|
+
* accessors, and excess properties normally do not enter user-facing validation
|
|
226
|
+
* in typed application flows. They violate the producer's declared contract and
|
|
227
|
+
* throw as developer errors. At a genuinely unknown boundary, such as a
|
|
228
|
+
* schema-authoring tool, import, or external protocol, the same issues are
|
|
229
|
+
* legitimate typed validation errors and their formatter messages are useful.
|
|
230
|
+
*
|
|
231
|
+
* This distinction applies to data failures. Any Type operation, including
|
|
232
|
+
* `fromUnknown`, can throw when trusted Type-declaration code, such as a
|
|
233
|
+
* successful transformation callback, violates its declared contract.
|
|
234
|
+
*
|
|
235
|
+
* Materialize accessor values into plain data, remove properties the Type does
|
|
236
|
+
* not represent, or use a different Type. Silently discarding excess data would
|
|
237
|
+
* make the code constructing it dead while appearing to encode it successfully.
|
|
238
|
+
* One exact Object policy also keeps Output membership independent of parsing
|
|
239
|
+
* configuration. Exact structural policies also keep Output membership
|
|
240
|
+
* independent of whether a transformation happens to allocate a new value.
|
|
241
|
+
* Evolu Type therefore does not invoke accessors, discard excess properties, or
|
|
242
|
+
* make `to` fallible. This keeps `to` total for every legitimate Output and
|
|
243
|
+
* lets transformations compose without an encoding-error channel.
|
|
244
|
+
*
|
|
245
|
+
* ### How should values from another realm be handled?
|
|
246
|
+
*
|
|
247
|
+
* Code trust and data validation are separate decisions. Values returned by
|
|
248
|
+
* trusted legacy code or another realm can still be uncertain and should be
|
|
249
|
+
* validated. Realm-neutral Types accept an otherwise legitimate representation
|
|
250
|
+
* without requiring conversion merely because its built-ins belong to another
|
|
251
|
+
* realm.
|
|
252
|
+
*
|
|
253
|
+
* When an application trusts both the producer and its return contract, expose
|
|
254
|
+
* that contract as an accurate TypeScript type and use the typed value directly.
|
|
255
|
+
* If the boundary returns `unknown`, validate it instead of bypassing the
|
|
256
|
+
* boundary with a cast. Use a specialized Type or explicit transformation when
|
|
257
|
+
* the producer uses a different representation that needs adaptation or
|
|
258
|
+
* normalization.
|
|
259
|
+
*
|
|
260
|
+
* All executing JavaScript remains trusted. Deliberately forged built-ins,
|
|
261
|
+
* hostile Proxies, throwing traps, or sabotaged executable behavior can throw;
|
|
262
|
+
* Evolu Type does not selectively contain them or claim to be a security
|
|
263
|
+
* boundary for untrusted code.
|
|
264
|
+
*
|
|
265
|
+
* ### Why doesn't Evolu Type extract data from rich objects?
|
|
266
|
+
*
|
|
267
|
+
* Some validation libraries parse an object's data projection. An imaginary
|
|
268
|
+
* validation library can enumerate own enumerable string properties and decode
|
|
269
|
+
* them into a fresh plain object. That lets a class instance decode as plain
|
|
270
|
+
* data while its prototype and methods are ignored. The same general policy can
|
|
271
|
+
* treat a `Date` or `Map` as an empty Record and can invoke enumerable getters.
|
|
272
|
+
* This is a coherent but intentionally forgiving normalization model.
|
|
273
|
+
*
|
|
274
|
+
* Evolu Type validates exactly the runtime representation defined by each Type;
|
|
275
|
+
* it does not implicitly project one representation into another. The
|
|
276
|
+
* predefined {@link Object} defines an open plain-object representation with
|
|
277
|
+
* unknown values, {@link object} defines a closed plain-object representation,
|
|
278
|
+
* and {@link record} defines a plain-dictionary representation whose complete
|
|
279
|
+
* set of own properties are its entries. Their realm-neutral plain-object rule
|
|
280
|
+
* accepts a `null` prototype or an immediate root prototype whose own prototype
|
|
281
|
+
* is `null`; ordinary class instances and deeper prototype chains are rejected.
|
|
282
|
+
* Every property must be an enumerable data property; inherited members are not
|
|
283
|
+
* entries, while accessors and hidden properties are invalid instead of being
|
|
284
|
+
* invoked or ignored. {@link array} similarly defines a dense sequence whose
|
|
285
|
+
* only own properties are `length` and its indexed data properties;
|
|
286
|
+
* {@link tuple} applies the same representation rules with a fixed length and a
|
|
287
|
+
* distinct Type for each position. Only an explicit {@link transform} changes
|
|
288
|
+
* the representation. Consequently, `is` tests exact Output membership and `to`
|
|
289
|
+
* stays total for valid Outputs.
|
|
290
|
+
*
|
|
291
|
+
* ### Why is JsonValue stricter than JSON.stringify?
|
|
292
|
+
*
|
|
293
|
+
* `JSON.stringify` is a forgiving data projection. It can invoke `toJSON` and
|
|
294
|
+
* accessors, discard object properties, replace unsupported array elements and
|
|
295
|
+
* non-finite numbers with `null`, and normalize `-0` to `0`. Those rules are
|
|
296
|
+
* useful for ordinary serialization, but they do not preserve an exact value.
|
|
297
|
+
*
|
|
298
|
+
* {@link JsonValue} instead defines data that is already represented as data.
|
|
299
|
+
* Invalid runtime behavior and values are rejected rather than interpreted or
|
|
300
|
+
* silently discarded. Its encoder is total and stack-safe for every valid
|
|
301
|
+
* Output, and {@link JsonValueFromJson} preserves the semantic value when it is
|
|
302
|
+
* encoded and decoded, including JavaScript's distinction between `-0` and `0`.
|
|
303
|
+
* Use an explicit transformation before this boundary when a projection or
|
|
304
|
+
* other normalization is desired.
|
|
305
|
+
*
|
|
306
|
+
* ### Why are Types pure and synchronous?
|
|
307
|
+
*
|
|
308
|
+
* A Type describes data meaning, not work. Time, I/O, dependencies, external
|
|
309
|
+
* state, authorization, and other contextual decisions belong in a {@link Task}.
|
|
310
|
+
* Use a Type to decode the data required by that work, then pass the decoded
|
|
311
|
+
* value to a Task. A pure synchronous conversion that can fail can be an
|
|
312
|
+
* ordinary function returning Result.
|
|
313
|
+
*
|
|
314
|
+
* Keeping those responsibilities separate prevents Evolu Type from becoming a
|
|
315
|
+
* hidden application workflow. It also keeps validation deterministic,
|
|
316
|
+
* dependency-free, immediately composable, and straightforward to test.
|
|
317
|
+
*
|
|
318
|
+
* ### What if only decoding is needed?
|
|
319
|
+
*
|
|
320
|
+
* Use `fromUnknown` for unknown data. For typed application data, call `from`
|
|
321
|
+
* at the boundary its input type proves, or use `orThrow` or `orNull` for a
|
|
322
|
+
* flat conversion from `Input`. The canonical `to` encoder still keeps the Type
|
|
323
|
+
* lawful and composable with transformations and structural Types. A genuinely
|
|
324
|
+
* irreversible operation is a separate function or Task, not a Type
|
|
325
|
+
* transformation.
|
|
3
326
|
*
|
|
4
327
|
* @module
|
|
5
328
|
*/
|
|
@@ -50,7 +373,7 @@ const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
|
|
|
50
373
|
* Asserts that a value belongs to a {@link Type} Output domain.
|
|
51
374
|
*
|
|
52
375
|
* Use this for internal invariants, not external input. Validate external input
|
|
53
|
-
* with `Type.fromUnknown` so
|
|
376
|
+
* with `Type.fromUnknown` so validation failures remain typed values. A failed
|
|
54
377
|
* assertion uses the Type name for its message and preserves the exact Output
|
|
55
378
|
* validation error as the thrown Error's cause.
|
|
56
379
|
*
|
|
@@ -70,7 +393,7 @@ const formatDefaultRuntimeTypeIssue = (issue) => issue.formatError(issue.error);
|
|
|
70
393
|
* >();
|
|
71
394
|
* ```
|
|
72
395
|
*
|
|
73
|
-
* @group Core
|
|
396
|
+
* @group Core
|
|
74
397
|
*/
|
|
75
398
|
export const assertType = (type, value) => {
|
|
76
399
|
// TODO: Make assert prepend "Expected " and accept an optional third cause
|
|
@@ -90,34 +413,32 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
90
413
|
throw new Error(`Expected ${name}.`, { cause: error });
|
|
91
414
|
};
|
|
92
415
|
/**
|
|
93
|
-
* Creates localized copies of selected {@link Type
|
|
416
|
+
* Creates localized copies of selected {@link Type} declarations.
|
|
94
417
|
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
*
|
|
100
|
-
* Lazy error interfaces.
|
|
418
|
+
* Pass the Types used together in one localization scope and formatter maps
|
|
419
|
+
* keyed by locale. TypeScript infers every formatter required by the selected
|
|
420
|
+
* Types, including errors from nested structural Types and recursive Lazy
|
|
421
|
+
* Types. Every locale must provide the complete inferred formatter set;
|
|
422
|
+
* missing and unrelated formatters are compile-time errors.
|
|
101
423
|
*
|
|
102
|
-
*
|
|
103
|
-
*
|
|
104
|
-
*
|
|
424
|
+
* The result preserves the locale names, selected Type names, and exact
|
|
425
|
+
* TypeScript types. A localized Type validates exactly like its source Type;
|
|
426
|
+
* only its human-readable `formatError` and Standard Schema messages change.
|
|
427
|
+
* The source Types remain unchanged.
|
|
105
428
|
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
429
|
+
* Parents and reflected child Types are localized with the same formatter set.
|
|
430
|
+
* Structural Types retain error paths and delegate nested messages to the Type
|
|
431
|
+
* that produced them. Different localized Type sets can coexist in separate
|
|
432
|
+
* application or dependency-injection scopes.
|
|
108
433
|
*
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
112
|
-
*
|
|
434
|
+
* Localization is scoped to the selected Types instead of a package-wide
|
|
435
|
+
* translation registry. Static imports give bundlers an explicit dependency
|
|
436
|
+
* graph, so unrelated Types, locales, and formatters can be removed. Bundling
|
|
437
|
+
* every locale an app supports also allows language changes without a network
|
|
438
|
+
* connection.
|
|
113
439
|
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
* dependency graph from those Types to their formatters, so unrelated Type and
|
|
117
|
-
* localization code can be removed. An app supplies all locales it supports in
|
|
118
|
-
* the same self-contained bundle, allowing users to change language without a
|
|
119
|
-
* network connection. Different localized Type sets can coexist on one page or
|
|
120
|
-
* in separate dependency-injection scopes.
|
|
440
|
+
* The selected Type map, locale map, and formatter maps must be plain objects
|
|
441
|
+
* with own enumerable string-keyed data properties.
|
|
121
442
|
*
|
|
122
443
|
* ### Example
|
|
123
444
|
*
|
|
@@ -127,7 +448,7 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
127
448
|
*
|
|
128
449
|
* const Label = minLength(1)(String);
|
|
129
450
|
*
|
|
130
|
-
* const
|
|
451
|
+
* const typesByLocale = localizeTypes(
|
|
131
452
|
* { Label },
|
|
132
453
|
* {
|
|
133
454
|
* cs: {
|
|
@@ -137,11 +458,66 @@ const assertTypeOutput = (name, is, validateOutput, value, options = firstValida
|
|
|
137
458
|
* },
|
|
138
459
|
* );
|
|
139
460
|
*
|
|
140
|
-
* expectTypeOf<typeof
|
|
461
|
+
* expectTypeOf<typeof typesByLocale.cs.Label>().toEqualTypeOf<
|
|
141
462
|
* typeof Label
|
|
142
463
|
* >();
|
|
464
|
+
*
|
|
465
|
+
* const result = typesByLocale.cs.Label.fromUnknown("");
|
|
466
|
+
* expectErr(result, { type: "MinLength1", min: 1, value: "" });
|
|
467
|
+
* expect(typesByLocale.cs.Label.formatError(result.error)).toBe(
|
|
468
|
+
* "Text nesmí být prázdný.",
|
|
469
|
+
* );
|
|
143
470
|
* ```
|
|
144
471
|
*
|
|
472
|
+
* ### Supported locales
|
|
473
|
+
*
|
|
474
|
+
* English is built in; use {@link Type} directly for its default formatters.
|
|
475
|
+
* The following additional locales are available:
|
|
476
|
+
*
|
|
477
|
+
* - Arabic (`ar`)
|
|
478
|
+
* - Bengali (`bn`)
|
|
479
|
+
* - Catalan (`ca`)
|
|
480
|
+
* - Chinese, Simplified (`zhCN`)
|
|
481
|
+
* - Chinese, Traditional (`zhTW`)
|
|
482
|
+
* - Croatian (`hr`)
|
|
483
|
+
* - Czech (`cs`)
|
|
484
|
+
* - Danish (`da`)
|
|
485
|
+
* - Dutch (`nl`)
|
|
486
|
+
* - Filipino (`fil`)
|
|
487
|
+
* - Finnish (`fi`)
|
|
488
|
+
* - French (`fr`)
|
|
489
|
+
* - German (`de`)
|
|
490
|
+
* - Greek (`el`)
|
|
491
|
+
* - Hebrew (`he`)
|
|
492
|
+
* - Hindi (`hi`)
|
|
493
|
+
* - Hungarian (`hu`)
|
|
494
|
+
* - Indonesian (`id`)
|
|
495
|
+
* - Italian (`it`)
|
|
496
|
+
* - Japanese (`ja`)
|
|
497
|
+
* - Korean (`ko`)
|
|
498
|
+
* - Malay (`ms`)
|
|
499
|
+
* - Malayalam (`ml`)
|
|
500
|
+
* - Marathi (`mr`)
|
|
501
|
+
* - Norwegian Bokmål (`nb`)
|
|
502
|
+
* - Persian (`fa`)
|
|
503
|
+
* - Polish (`pl`)
|
|
504
|
+
* - Portuguese (`pt`)
|
|
505
|
+
* - Portuguese, Brazilian (`ptBR`)
|
|
506
|
+
* - Punjabi (`pa`)
|
|
507
|
+
* - Romanian (`ro`)
|
|
508
|
+
* - Slovak (`sk`)
|
|
509
|
+
* - Slovenian (`sl`)
|
|
510
|
+
* - Spanish (`es`)
|
|
511
|
+
* - Swahili (`sw`)
|
|
512
|
+
* - Swedish (`sv`)
|
|
513
|
+
* - Tamil (`ta`)
|
|
514
|
+
* - Telugu (`te`)
|
|
515
|
+
* - Thai (`th`)
|
|
516
|
+
* - Turkish (`tr`)
|
|
517
|
+
* - Ukrainian (`uk`)
|
|
518
|
+
* - Urdu (`ur`)
|
|
519
|
+
* - Vietnamese (`vi`)
|
|
520
|
+
*
|
|
145
521
|
* @group Localization
|
|
146
522
|
*/
|
|
147
523
|
export const localizeTypes = ((typesByName, formatErrorByTypeByLocale) => {
|
|
@@ -341,6 +717,8 @@ const encoderSymbol =
|
|
|
341
717
|
/*#__PURE__*/ globalThis.Symbol();
|
|
342
718
|
const fromSymbol =
|
|
343
719
|
/*#__PURE__*/ globalThis.Symbol();
|
|
720
|
+
const templateLiteralSyntaxSymbol =
|
|
721
|
+
/*#__PURE__*/ globalThis.Symbol();
|
|
344
722
|
const mapRuntimeResult = (operation, map) => (value, options = firstValidationOptions) => map(operation(value, options), options);
|
|
345
723
|
// `map` must return a fresh operation because this function can attach `.parent`.
|
|
346
724
|
const mapRuntimeOperations = (operation, map) => {
|
|
@@ -433,9 +811,17 @@ const addRuntimeAssertions = (name, is, validateOutput, parent, operation) => {
|
|
|
433
811
|
}
|
|
434
812
|
return asserted;
|
|
435
813
|
};
|
|
436
|
-
/**
|
|
814
|
+
/**
|
|
815
|
+
* An infallible {@link Type} accepting every value.
|
|
816
|
+
*
|
|
817
|
+
* @group Base
|
|
818
|
+
*/
|
|
437
819
|
export const Unknown = /*#__PURE__*/ createRootType("Unknown", ok, identity);
|
|
438
|
-
/**
|
|
820
|
+
/**
|
|
821
|
+
* A {@link Type} rejecting every value.
|
|
822
|
+
*
|
|
823
|
+
* @group Base
|
|
824
|
+
*/
|
|
439
825
|
export const Never = /*#__PURE__*/ createRootType("Never", (value) => err({ type: "Never", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not valid for type Never.`);
|
|
440
826
|
const createTypeOfType = (name) => {
|
|
441
827
|
const typeOf = name.toLowerCase();
|
|
@@ -466,6 +852,8 @@ const createTypeOfType = (name) => {
|
|
|
466
852
|
* expectTypeOf<WireValue100>().toEqualTypeOf<
|
|
467
853
|
* string & Brand<"MaxLength100">
|
|
468
854
|
* >();
|
|
855
|
+
* expectOk(WireValue100.fromUnknown(""), "");
|
|
856
|
+
* expectOk(WireValue100.fromUnknown(" value "), " value ");
|
|
469
857
|
* ```
|
|
470
858
|
*
|
|
471
859
|
* @group String
|
|
@@ -540,36 +928,52 @@ export const String = /*#__PURE__*/ createTypeOfType("String");
|
|
|
540
928
|
* @group Number
|
|
541
929
|
*/
|
|
542
930
|
export const Number = /*#__PURE__*/ createTypeOfType("Number");
|
|
543
|
-
/**
|
|
931
|
+
/**
|
|
932
|
+
* A JavaScript bigint {@link Type}.
|
|
933
|
+
*
|
|
934
|
+
* @group Base
|
|
935
|
+
*/
|
|
544
936
|
export const BigInt = /*#__PURE__*/ createTypeOfType("BigInt");
|
|
545
|
-
/**
|
|
937
|
+
/**
|
|
938
|
+
* A JavaScript boolean {@link Type}.
|
|
939
|
+
*
|
|
940
|
+
* @group Base
|
|
941
|
+
*/
|
|
546
942
|
export const Boolean = /*#__PURE__*/ createTypeOfType("Boolean");
|
|
547
|
-
/**
|
|
943
|
+
/**
|
|
944
|
+
* A JavaScript symbol {@link Type}.
|
|
945
|
+
*
|
|
946
|
+
* @group Base
|
|
947
|
+
*/
|
|
548
948
|
export const Symbol = /*#__PURE__*/ createTypeOfType("Symbol");
|
|
549
|
-
/**
|
|
949
|
+
/**
|
|
950
|
+
* A JavaScript function {@link Type}.
|
|
951
|
+
*
|
|
952
|
+
* @group Base
|
|
953
|
+
*/
|
|
550
954
|
export const Function = /*#__PURE__*/ createTypeOfType("Function");
|
|
551
955
|
/**
|
|
552
|
-
*
|
|
956
|
+
* A {@link Type} validating Evolu Type declarations.
|
|
553
957
|
*
|
|
554
958
|
* This is useful when a Type itself crosses an unknown boundary or must be
|
|
555
959
|
* asserted with {@link assertType}.
|
|
556
960
|
*
|
|
557
|
-
* @group Core
|
|
961
|
+
* @group Core
|
|
558
962
|
*/
|
|
559
963
|
export const EvoluType = /*#__PURE__*/ createType("EvoluType", (value) => isInstance("Type")(value)
|
|
560
964
|
? ok(value)
|
|
561
965
|
: err({ type: "EvoluType", value }), (error) => `A value ${safelyStringifyUnknownValue(error.value)} is not an Evolu Type.`);
|
|
562
966
|
export function objectTag(name, outputType) {
|
|
967
|
+
const formatError = (error) => `A value ${safelyStringifyUnknownValue(error.value)} does not have the expected object tag ${safelyStringifyUnknownValue(error.expected)}.`;
|
|
563
968
|
if (outputType === undefined) {
|
|
564
969
|
return createRootType(name, (value) => hasObjectTag(value, name)
|
|
565
970
|
? ok(value)
|
|
566
|
-
: err({ type: "ObjectTag", expected: name, value }),
|
|
971
|
+
: err({ type: "ObjectTag", expected: name, value }), formatError);
|
|
567
972
|
}
|
|
568
973
|
return globalThis.Object.assign(createChildType("ObjectTag", outputType, (value) => hasObjectTag(value, name)
|
|
569
974
|
? ok(value)
|
|
570
|
-
: err({ type: "ObjectTag", expected: name, value }),
|
|
975
|
+
: err({ type: "ObjectTag", expected: name, value }), formatError), { expected: name });
|
|
571
976
|
}
|
|
572
|
-
const formatObjectTagError = (error) => `A value ${safelyStringifyUnknownValue(error.value)} does not have the object tag ${safelyStringifyUnknownValue(error.expected)}.`;
|
|
573
977
|
const hasObjectTag = (value, expected) => value !== null &&
|
|
574
978
|
(typeof value === "object" || typeof value === "function") &&
|
|
575
979
|
globalThis.Object.prototype.toString.call(value) === `[object ${expected}]`;
|
|
@@ -578,7 +982,7 @@ const hasObjectTag = (value, expected) => value !== null &&
|
|
|
578
982
|
*
|
|
579
983
|
* It trusts the reported object tag and does not verify Date internal slots.
|
|
580
984
|
*
|
|
581
|
-
* @group Base
|
|
985
|
+
* @group Base
|
|
582
986
|
*/
|
|
583
987
|
export const Date = /*#__PURE__*/ objectTag("Date");
|
|
584
988
|
/**
|
|
@@ -587,7 +991,7 @@ export const Date = /*#__PURE__*/ objectTag("Date");
|
|
|
587
991
|
* It trusts the reported object tag and does not verify Uint8Array internal
|
|
588
992
|
* slots.
|
|
589
993
|
*
|
|
590
|
-
* @group Base
|
|
994
|
+
* @group Base
|
|
591
995
|
*/
|
|
592
996
|
export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
|
|
593
997
|
/**
|
|
@@ -596,11 +1000,11 @@ export const Uint8Array = /*#__PURE__*/ objectTag("Uint8Array");
|
|
|
596
1000
|
* It trusts the reported object tag and does not verify ArrayBuffer internal
|
|
597
1001
|
* slots.
|
|
598
1002
|
*
|
|
599
|
-
* @group Base
|
|
1003
|
+
* @group Base
|
|
600
1004
|
*/
|
|
601
1005
|
export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
|
|
602
1006
|
/**
|
|
603
|
-
*
|
|
1007
|
+
* Instance {@link Type} for one constructor.
|
|
604
1008
|
*
|
|
605
1009
|
* Membership uses the intrinsic prototype chain, so subclasses are accepted,
|
|
606
1010
|
* equivalent constructors from other realms are rejected, and custom
|
|
@@ -627,7 +1031,7 @@ export const ArrayBuffer = /*#__PURE__*/ objectTag("ArrayBuffer");
|
|
|
627
1031
|
* assert(!UserInstance.is({ name: "Ada" }));
|
|
628
1032
|
* ```
|
|
629
1033
|
*
|
|
630
|
-
* @group Base
|
|
1034
|
+
* @group Base
|
|
631
1035
|
*/
|
|
632
1036
|
export const instanceOf = (constructor) => {
|
|
633
1037
|
const concreteConstructor = constructor;
|
|
@@ -644,6 +1048,26 @@ export const instanceOf = (constructor) => {
|
|
|
644
1048
|
* primitive through `from.parent`. The expected value must have one exact
|
|
645
1049
|
* literal type. Validation uses `===`, so `-0` matches `0`.
|
|
646
1050
|
*
|
|
1051
|
+
* In {@link templateLiteralParser}, use a string Literal Type when the literal
|
|
1052
|
+
* should be decoded into the Output Tuple. Use a raw string when it should only
|
|
1053
|
+
* frame the canonical string.
|
|
1054
|
+
*
|
|
1055
|
+
* ### Example
|
|
1056
|
+
*
|
|
1057
|
+
* ```ts
|
|
1058
|
+
* import { literal } from "@evolu/common";
|
|
1059
|
+
*
|
|
1060
|
+
* const Ready = literal("ready");
|
|
1061
|
+
*
|
|
1062
|
+
* expectTypeOf<typeof Ready.Output>().toEqualTypeOf<"ready">();
|
|
1063
|
+
* expectOk(Ready.fromUnknown("ready"), "ready");
|
|
1064
|
+
* expectErr(Ready.fromUnknown("pending"), {
|
|
1065
|
+
* type: "Literal",
|
|
1066
|
+
* expected: "ready",
|
|
1067
|
+
* value: "pending",
|
|
1068
|
+
* });
|
|
1069
|
+
* ```
|
|
1070
|
+
*
|
|
647
1071
|
* @group Unions
|
|
648
1072
|
*/
|
|
649
1073
|
export const literal = (expected) => {
|
|
@@ -663,11 +1087,22 @@ export const literal = (expected) => {
|
|
|
663
1087
|
const formatError = (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not strictly equal to the expected literal: ${globalThis.String(error.expected)}.`;
|
|
664
1088
|
return globalThis.Object.assign(parent
|
|
665
1089
|
? createChildType("Literal", parent, validate, formatError)
|
|
666
|
-
: createRootType("Literal", validate, formatError), {
|
|
1090
|
+
: createRootType("Literal", validate, formatError), {
|
|
1091
|
+
expected: literalExpected,
|
|
1092
|
+
[templateLiteralSyntaxSymbol]: true,
|
|
1093
|
+
});
|
|
667
1094
|
};
|
|
668
|
-
/**
|
|
1095
|
+
/**
|
|
1096
|
+
* Literal {@link Type} accepting only `undefined`.
|
|
1097
|
+
*
|
|
1098
|
+
* @group Unions
|
|
1099
|
+
*/
|
|
669
1100
|
export const Undefined = /*#__PURE__*/ literal(undefined);
|
|
670
|
-
/**
|
|
1101
|
+
/**
|
|
1102
|
+
* Literal {@link Type} accepting only `null`.
|
|
1103
|
+
*
|
|
1104
|
+
* @group Unions
|
|
1105
|
+
*/
|
|
671
1106
|
export const Null = /*#__PURE__*/ literal(null);
|
|
672
1107
|
export function union(...typesOrLiterals) {
|
|
673
1108
|
const members = typesOrLiterals.map((typeOrLiteral) => typeOrLiteral !== null && typeof typeOrLiteral === "object"
|
|
@@ -676,7 +1111,7 @@ export function union(...typesOrLiterals) {
|
|
|
676
1111
|
const inputMembers = members.map(getTerminalRuntimeNode);
|
|
677
1112
|
const inputFrom = createUnionValidation(inputMembers, (member, value, options) => member.fromUnknown(value, options));
|
|
678
1113
|
const inputValidateOutput = createUnionValidation(inputMembers, (member, value, options) => member[outputValidationSymbol](value, options));
|
|
679
|
-
const defaultFormatter =
|
|
1114
|
+
const defaultFormatter = (() => "A value does not match any allowed variant.");
|
|
680
1115
|
const getTypeIssues = (error) => singleRuntimeTypeIssue("Union", error, defaultFormatter);
|
|
681
1116
|
const input = createTypeNode("Union", null, inputFrom, (value) => inputMembers.some((member) => member.is(value)), inputValidateOutput, ok, identity, getTypeIssues);
|
|
682
1117
|
const fromUnknown = createUnionValidation(members, (member, value, options) => member.fromUnknown(value, options));
|
|
@@ -692,9 +1127,8 @@ export function union(...typesOrLiterals) {
|
|
|
692
1127
|
assertNonNullable(member);
|
|
693
1128
|
return member[encoderSymbol](value);
|
|
694
1129
|
};
|
|
695
|
-
return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members });
|
|
1130
|
+
return createTypeNode("Union", input, fromUnknown, (value) => members.some((member) => member.is(value)), validateOutput, from, to, getTypeIssues, { members, [templateLiteralSyntaxSymbol]: true });
|
|
696
1131
|
}
|
|
697
|
-
const formatUnionError = () => "A value does not match any union member.";
|
|
698
1132
|
const createUnionValidation = (members, validateMember) => (value, options = firstValidationOptions) => {
|
|
699
1133
|
let errors;
|
|
700
1134
|
for (let index = 0; index < members.length; index++) {
|
|
@@ -714,22 +1148,53 @@ const createUnionValidation = (members, validateMember) => (value, options = fir
|
|
|
714
1148
|
});
|
|
715
1149
|
};
|
|
716
1150
|
/**
|
|
717
|
-
*
|
|
1151
|
+
* Union {@link Type} containing the supplied Type and `undefined`.
|
|
718
1152
|
*
|
|
719
1153
|
* This does not make an object property optional. It changes only the values
|
|
720
1154
|
* accepted when the property is present.
|
|
721
1155
|
*
|
|
1156
|
+
* ### Example
|
|
1157
|
+
*
|
|
1158
|
+
* ```ts
|
|
1159
|
+
* import { String, undefinedOr } from "@evolu/common";
|
|
1160
|
+
*
|
|
1161
|
+
* const StringOrUndefined = undefinedOr(String);
|
|
1162
|
+
*
|
|
1163
|
+
* expectOk(StringOrUndefined.fromUnknown(undefined), undefined);
|
|
1164
|
+
* ```
|
|
1165
|
+
*
|
|
722
1166
|
* @group Unions
|
|
723
1167
|
*/
|
|
724
1168
|
export const undefinedOr = (type) => union(type, Undefined);
|
|
725
1169
|
/**
|
|
726
|
-
*
|
|
1170
|
+
* Union {@link Type} containing the supplied Type and `null`.
|
|
1171
|
+
*
|
|
1172
|
+
* ### Example
|
|
1173
|
+
*
|
|
1174
|
+
* ```ts
|
|
1175
|
+
* import { String, nullOr } from "@evolu/common";
|
|
1176
|
+
*
|
|
1177
|
+
* const NullableString = nullOr(String);
|
|
1178
|
+
*
|
|
1179
|
+
* expectOk(NullableString.fromUnknown(null), null);
|
|
1180
|
+
* ```
|
|
727
1181
|
*
|
|
728
1182
|
* @group Unions
|
|
729
1183
|
*/
|
|
730
1184
|
export const nullOr = (type) => union(type, Null);
|
|
731
1185
|
/**
|
|
732
|
-
*
|
|
1186
|
+
* Union {@link Type} containing the supplied Type, `null`, and `undefined`.
|
|
1187
|
+
*
|
|
1188
|
+
* ### Example
|
|
1189
|
+
*
|
|
1190
|
+
* ```ts
|
|
1191
|
+
* import { String, nullishOr } from "@evolu/common";
|
|
1192
|
+
*
|
|
1193
|
+
* const NullishString = nullishOr(String);
|
|
1194
|
+
*
|
|
1195
|
+
* expectOk(NullishString.fromUnknown(undefined), undefined);
|
|
1196
|
+
* expectOk(NullishString.fromUnknown(null), null);
|
|
1197
|
+
* ```
|
|
733
1198
|
*
|
|
734
1199
|
* @group Unions
|
|
735
1200
|
*/
|
|
@@ -737,6 +1202,402 @@ export const nullishOr = (type) => union(type, Null, Undefined);
|
|
|
737
1202
|
const isRuntimeUnionTypeNode = (type) => type.name === "Union" &&
|
|
738
1203
|
"members" in type &&
|
|
739
1204
|
globalThis.Array.isArray(type.members);
|
|
1205
|
+
/**
|
|
1206
|
+
* Template literal {@link Type} that parses canonical strings into Tuples.
|
|
1207
|
+
*
|
|
1208
|
+
* Accepts the same template parts as {@link templateLiteral}: fixed string
|
|
1209
|
+
* literals and Types canonically encoded as strings. Instead of keeping Output
|
|
1210
|
+
* as a string, fixed literals define the framing and Output is a readonly Tuple
|
|
1211
|
+
* of the decoded Type parts. `to` encodes that Tuple back into the canonical
|
|
1212
|
+
* string represented by the parent Type. At least one Type part is required.
|
|
1213
|
+
*
|
|
1214
|
+
* When every capture uses identity encoding, the parent Output is the exact
|
|
1215
|
+
* TypeScript template literal type. A transforming capture makes it nominal;
|
|
1216
|
+
* create such strings with `to` or validate them with the parent Type.
|
|
1217
|
+
*
|
|
1218
|
+
* Deterministic framing is a core correctness guarantee. It preserves
|
|
1219
|
+
* reversibility and keeps capture boundaries unambiguous. Different capture
|
|
1220
|
+
* Tuples must never encode to the same string. The parser provides predictable
|
|
1221
|
+
* parsing without pathological backtracking and decodes each capture once, so
|
|
1222
|
+
* adversarial input cannot trigger exponential parser work. Fixed-width
|
|
1223
|
+
* captures may be adjacent, but only one variable-width capture is allowed.
|
|
1224
|
+
* Declarations that could join UTF-16 surrogate halves across parts are
|
|
1225
|
+
* rejected during construction.
|
|
1226
|
+
*
|
|
1227
|
+
* Keep capture unions reasonably small to avoid excessive compiler work.
|
|
1228
|
+
*
|
|
1229
|
+
* TypeScript template literal types can describe a fixed number of digit
|
|
1230
|
+
* positions, but not an arbitrarily long sequence of digits. Such grammars use
|
|
1231
|
+
* branded Types such as {@link DecimalString}; `templateLiteralParser` preserves
|
|
1232
|
+
* that exactness by requiring a validated branded capture when encoding.
|
|
1233
|
+
*
|
|
1234
|
+
* ### Example
|
|
1235
|
+
*
|
|
1236
|
+
* A template literal Type defines both a canonical string representation and
|
|
1237
|
+
* the structured data decoded from it:
|
|
1238
|
+
*
|
|
1239
|
+
* ```ts
|
|
1240
|
+
* import { templateLiteralParser, union } from "@evolu/common";
|
|
1241
|
+
*
|
|
1242
|
+
* const Language = union("en", "cs");
|
|
1243
|
+
* const Region = union("US", "CZ");
|
|
1244
|
+
*
|
|
1245
|
+
* // Define a Type for "en-US" | "en-CZ" | "cs-US" | "cs-CZ".
|
|
1246
|
+
* const SupportedLocale = templateLiteralParser(Language, "-", Region);
|
|
1247
|
+
*
|
|
1248
|
+
* // Output is the decoded language and region.
|
|
1249
|
+
* type SupportedLocale = typeof SupportedLocale.Output;
|
|
1250
|
+
* expectTypeOf<SupportedLocale>().toEqualTypeOf<
|
|
1251
|
+
* readonly ["en" | "cs", "US" | "CZ"]
|
|
1252
|
+
* >();
|
|
1253
|
+
*
|
|
1254
|
+
* // The parent Output is the canonical locale string.
|
|
1255
|
+
* type SupportedLocaleLiteral = typeof SupportedLocale.parent.Output;
|
|
1256
|
+
* expectTypeOf<SupportedLocaleLiteral>().toEqualTypeOf<
|
|
1257
|
+
* "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
|
|
1258
|
+
* >();
|
|
1259
|
+
*
|
|
1260
|
+
* // Parse an unknown string into structured data.
|
|
1261
|
+
* const result = SupportedLocale.fromUnknown("cs-CZ");
|
|
1262
|
+
* expectOk(result, ["cs", "CZ"]);
|
|
1263
|
+
* const locale = result.value;
|
|
1264
|
+
* expectTypeOf(locale).toEqualTypeOf<SupportedLocale>();
|
|
1265
|
+
* expectErr(SupportedLocale.fromUnknown("cs/CZ"), {
|
|
1266
|
+
* type: "TemplateLiteral",
|
|
1267
|
+
* value: "cs/CZ",
|
|
1268
|
+
* });
|
|
1269
|
+
*
|
|
1270
|
+
* // Encode structured data into its canonical string.
|
|
1271
|
+
* const localeLiteral = SupportedLocale.to(locale);
|
|
1272
|
+
* expectTypeOf(localeLiteral).toEqualTypeOf<SupportedLocaleLiteral>();
|
|
1273
|
+
* expect(localeLiteral).toBe("cs-CZ");
|
|
1274
|
+
*
|
|
1275
|
+
* // Validate a string configuration value.
|
|
1276
|
+
* const configValue: unknown = "cs-CZ";
|
|
1277
|
+
* assert(SupportedLocale.parent.is(configValue));
|
|
1278
|
+
* expectTypeOf(configValue).toEqualTypeOf<SupportedLocaleLiteral>();
|
|
1279
|
+
* expect(SupportedLocale.parent.is("fr-CZ")).toBe(false);
|
|
1280
|
+
* ```
|
|
1281
|
+
*
|
|
1282
|
+
* `SupportedLocale` is structured data for application code.
|
|
1283
|
+
* `SupportedLocaleLiteral` is its canonical representation for configuration
|
|
1284
|
+
* and other APIs that require a string, such as URL parameters, environment
|
|
1285
|
+
* variables, and storage keys.
|
|
1286
|
+
*
|
|
1287
|
+
* Use branded captures for strings that TypeScript template literal types
|
|
1288
|
+
* cannot express exactly, such as arbitrary-length canonical decimals:
|
|
1289
|
+
*
|
|
1290
|
+
* ```ts
|
|
1291
|
+
* import {
|
|
1292
|
+
* NonNegativeDecimalString,
|
|
1293
|
+
* templateLiteralParser,
|
|
1294
|
+
* } from "@evolu/common";
|
|
1295
|
+
*
|
|
1296
|
+
* const DecimalText = templateLiteralParser(
|
|
1297
|
+
* "decimal:",
|
|
1298
|
+
* NonNegativeDecimalString,
|
|
1299
|
+
* );
|
|
1300
|
+
*
|
|
1301
|
+
* // DecimalText.to requires a validated NonNegativeDecimalString.
|
|
1302
|
+
* const zero = NonNegativeDecimalString.orThrow("0");
|
|
1303
|
+
*
|
|
1304
|
+
* expectOk(DecimalText.fromUnknown("decimal:0"), [zero]);
|
|
1305
|
+
* expect(DecimalText.to([zero])).toBe("decimal:0");
|
|
1306
|
+
* ```
|
|
1307
|
+
*
|
|
1308
|
+
* Capture Types (the Type arguments passed to `templateLiteralParser`) can use
|
|
1309
|
+
* transformations to decode substrings into non-string data:
|
|
1310
|
+
*
|
|
1311
|
+
* ```ts
|
|
1312
|
+
* import {
|
|
1313
|
+
* Int64FromInt64String,
|
|
1314
|
+
* templateLiteralParser,
|
|
1315
|
+
* } from "@evolu/common";
|
|
1316
|
+
*
|
|
1317
|
+
* const ItemId = templateLiteralParser("item-", Int64FromInt64String);
|
|
1318
|
+
* type ItemId = typeof ItemId.Output;
|
|
1319
|
+
* type ItemIdLiteral = typeof ItemId.parent.Output;
|
|
1320
|
+
*
|
|
1321
|
+
* // Decode the string into structured data.
|
|
1322
|
+
* const result = ItemId.fromUnknown("item-42");
|
|
1323
|
+
* expectOk(result, [42n]);
|
|
1324
|
+
* const itemId = result.value;
|
|
1325
|
+
* expectTypeOf(itemId).toEqualTypeOf<ItemId>();
|
|
1326
|
+
*
|
|
1327
|
+
* // Encode the structured data into its canonical string.
|
|
1328
|
+
* const itemIdLiteral = ItemId.to(itemId);
|
|
1329
|
+
* expectTypeOf(itemIdLiteral).toEqualTypeOf<ItemIdLiteral>();
|
|
1330
|
+
* expect(itemIdLiteral).toBe("item-42");
|
|
1331
|
+
*
|
|
1332
|
+
* // TypeScript cannot prove from the literal alone that "42" is a valid Int64 encoding.
|
|
1333
|
+
* // @ts-expect-error Validate it with ItemId.parent or create it with ItemId.to.
|
|
1334
|
+
* const invalidItemIdLiteral: ItemIdLiteral = "item-42";
|
|
1335
|
+
* ```
|
|
1336
|
+
*
|
|
1337
|
+
* Fixed-width captures can be adjacent:
|
|
1338
|
+
*
|
|
1339
|
+
* ```ts
|
|
1340
|
+
* import { templateLiteralParser, union } from "@evolu/common";
|
|
1341
|
+
*
|
|
1342
|
+
* const Digit = union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
1343
|
+
* const TwoDigits = templateLiteralParser(Digit, Digit);
|
|
1344
|
+
* type TwoDigits = typeof TwoDigits.Output;
|
|
1345
|
+
* type TwoDigitsLiteral = typeof TwoDigits.parent.Output;
|
|
1346
|
+
*
|
|
1347
|
+
* const twoDigits: TwoDigits = ["4", "2"];
|
|
1348
|
+
* const twoDigitsLiteral: TwoDigitsLiteral = "42";
|
|
1349
|
+
* // @ts-expect-error TwoDigitsLiteral requires exactly two digits.
|
|
1350
|
+
* const threeDigitsLiteral: TwoDigitsLiteral = "123";
|
|
1351
|
+
*
|
|
1352
|
+
* expectOk(TwoDigits.from.parent(twoDigitsLiteral), twoDigits);
|
|
1353
|
+
* expect(TwoDigits.to(twoDigits)).toBe(twoDigitsLiteral);
|
|
1354
|
+
* ```
|
|
1355
|
+
*
|
|
1356
|
+
* TypeScript rejects multiple variable-width captures because their encoded
|
|
1357
|
+
* boundaries would be ambiguous:
|
|
1358
|
+
*
|
|
1359
|
+
* ```ts
|
|
1360
|
+
* import { String, templateLiteralParser } from "@evolu/common";
|
|
1361
|
+
*
|
|
1362
|
+
* // @ts-expect-error At most one Type capture can have a variable-width string representation.
|
|
1363
|
+
* templateLiteralParser(String, ":", String);
|
|
1364
|
+
* ```
|
|
1365
|
+
*
|
|
1366
|
+
* This restriction keeps encoding reversible: different capture Tuples must
|
|
1367
|
+
* never produce the same string. A delimiter alone is not enough because it can
|
|
1368
|
+
* also occur inside a capture. Some formats could provide stronger guarantees,
|
|
1369
|
+
* such as captures that exclude a delimiter; support for those can be added
|
|
1370
|
+
* when concrete use cases justify the additional framing rules.
|
|
1371
|
+
*
|
|
1372
|
+
* @group Template literals
|
|
1373
|
+
*/
|
|
1374
|
+
export const templateLiteralParser = (...parts) => createTemplateLiteralParserType(parts);
|
|
1375
|
+
const createTemplateLiteralParserType = (templateParts) => {
|
|
1376
|
+
const captureTypes = templateParts.filter((part) => typeof part !== "string");
|
|
1377
|
+
const runtimeCaptureTypes = captureTypes;
|
|
1378
|
+
const output = tuple(...captureTypes);
|
|
1379
|
+
const runtimeOutput = output;
|
|
1380
|
+
const reflection = {
|
|
1381
|
+
output,
|
|
1382
|
+
parts: templateParts,
|
|
1383
|
+
[templateLiteralSyntaxSymbol]: true,
|
|
1384
|
+
};
|
|
1385
|
+
const parse = compileTemplateLiteralParser(templateParts);
|
|
1386
|
+
const decodeString = (value, options = firstValidationOptions) => {
|
|
1387
|
+
const parseResult = parse(value);
|
|
1388
|
+
if (!parseResult.ok)
|
|
1389
|
+
return parseResult;
|
|
1390
|
+
const outputResult = validateTupleItems(parseResult.value, runtimeCaptureTypes, (capture, value, captureOptions) => capture.fromUnknown(value, captureOptions), options, false);
|
|
1391
|
+
return (outputResult.ok
|
|
1392
|
+
? outputResult
|
|
1393
|
+
: err({
|
|
1394
|
+
type: "TemplateLiteral",
|
|
1395
|
+
outputError: outputResult.error,
|
|
1396
|
+
}));
|
|
1397
|
+
};
|
|
1398
|
+
const encodeCaptures = (captures) => {
|
|
1399
|
+
const encodedCaptures = runtimeOutput[encoderSymbol](captures);
|
|
1400
|
+
let value = "";
|
|
1401
|
+
let captureIndex = 0;
|
|
1402
|
+
for (const part of templateParts) {
|
|
1403
|
+
value +=
|
|
1404
|
+
typeof part === "string" ? part : encodedCaptures[captureIndex++];
|
|
1405
|
+
}
|
|
1406
|
+
return value;
|
|
1407
|
+
};
|
|
1408
|
+
const canonicalizeString = (value, options) => {
|
|
1409
|
+
const result = decodeString(value, options);
|
|
1410
|
+
return result.ok ? ok(encodeCaptures(result.value)) : result;
|
|
1411
|
+
};
|
|
1412
|
+
const validateCanonicalString = (value, options = firstValidationOptions) => {
|
|
1413
|
+
const stringResult = String.fromUnknown(value, options);
|
|
1414
|
+
if (!stringResult.ok)
|
|
1415
|
+
return stringResult;
|
|
1416
|
+
const result = canonicalizeString(stringResult.value, options);
|
|
1417
|
+
if (!result.ok || result.value === stringResult.value)
|
|
1418
|
+
return result;
|
|
1419
|
+
return err({ type: "TemplateLiteral", value: stringResult.value });
|
|
1420
|
+
};
|
|
1421
|
+
const getTypeIssues = (error, mode) => {
|
|
1422
|
+
if (error.type !== "TemplateLiteral") {
|
|
1423
|
+
return String[getRuntimeTypeIssuesSymbol](error, mode);
|
|
1424
|
+
}
|
|
1425
|
+
if ("outputError" in error) {
|
|
1426
|
+
return runtimeOutput[getRuntimeTypeIssuesSymbol](error.outputError, mode);
|
|
1427
|
+
}
|
|
1428
|
+
return singleRuntimeTypeIssue("TemplateLiteral", error, ((error) => `The value ${safelyStringifyUnknownValue(error.value)} does not match the template literal.`));
|
|
1429
|
+
};
|
|
1430
|
+
const canonicalStringFromUnknown = (value, options = firstValidationOptions) => {
|
|
1431
|
+
const stringResult = String.fromUnknown(value, options);
|
|
1432
|
+
return stringResult.ok
|
|
1433
|
+
? canonicalizeString(stringResult.value, options)
|
|
1434
|
+
: stringResult;
|
|
1435
|
+
};
|
|
1436
|
+
const canonicalStringFrom = createFromOperation((value, options = firstValidationOptions) => canonicalizeString(value, options));
|
|
1437
|
+
const stringType = createTypeNode("TemplateLiteral", String, canonicalStringFromUnknown, (value) => validateCanonicalString(value, firstValidationOptions).ok, validateCanonicalString, canonicalStringFrom, identity, getTypeIssues, reflection);
|
|
1438
|
+
const fromUnknown = (value, options = firstValidationOptions) => {
|
|
1439
|
+
const stringResult = String.fromUnknown(value, options);
|
|
1440
|
+
if (!stringResult.ok)
|
|
1441
|
+
return stringResult;
|
|
1442
|
+
// The internal parser has one broad signature, but a statically frameless
|
|
1443
|
+
// declaration cannot return its framing error.
|
|
1444
|
+
return decodeString(stringResult.value, options);
|
|
1445
|
+
};
|
|
1446
|
+
const fromCanonicalString = (value, options = firstValidationOptions) => {
|
|
1447
|
+
String.from(value);
|
|
1448
|
+
const result = decodeString(value, options);
|
|
1449
|
+
if (!result.ok || encodeCaptures(result.value) !== value) {
|
|
1450
|
+
throw new Error("Expected TemplateLiteral.", {
|
|
1451
|
+
cause: result.ok
|
|
1452
|
+
? {
|
|
1453
|
+
type: "TemplateLiteral",
|
|
1454
|
+
value,
|
|
1455
|
+
}
|
|
1456
|
+
: result.error,
|
|
1457
|
+
});
|
|
1458
|
+
}
|
|
1459
|
+
return result;
|
|
1460
|
+
};
|
|
1461
|
+
const fromString = (value, options = firstValidationOptions) => {
|
|
1462
|
+
String.from(value);
|
|
1463
|
+
return decodeString(value, options);
|
|
1464
|
+
};
|
|
1465
|
+
fromCanonicalString.parent = fromString;
|
|
1466
|
+
const from = createFromOperation(fromCanonicalString);
|
|
1467
|
+
const type = createTypeNode("TemplateLiteral", stringType, fromUnknown, runtimeOutput.is, runtimeOutput[outputValidationSymbol], from, encodeCaptures, getTypeIssues, reflection);
|
|
1468
|
+
type.from.parent =
|
|
1469
|
+
fromCanonicalString;
|
|
1470
|
+
return type;
|
|
1471
|
+
};
|
|
1472
|
+
/**
|
|
1473
|
+
* Template literal {@link Type} for validation.
|
|
1474
|
+
*
|
|
1475
|
+
* Creates a canonical string Type from fixed strings and string-encoded Types.
|
|
1476
|
+
*
|
|
1477
|
+
* Use this factory when Output should remain a string. Switch to
|
|
1478
|
+
* {@link templateLiteralParser} when the individual Type parts should be decoded
|
|
1479
|
+
* into a Tuple.
|
|
1480
|
+
*
|
|
1481
|
+
* ### Example
|
|
1482
|
+
*
|
|
1483
|
+
* ```ts
|
|
1484
|
+
* import { templateLiteral, union } from "@evolu/common";
|
|
1485
|
+
*
|
|
1486
|
+
* const Language = union("en", "cs");
|
|
1487
|
+
* const Region = union("US", "CZ");
|
|
1488
|
+
* const Locale = templateLiteral(Language, "-", Region);
|
|
1489
|
+
*
|
|
1490
|
+
* expectTypeOf<typeof Locale.Output>().toEqualTypeOf<
|
|
1491
|
+
* "en-US" | "en-CZ" | "cs-US" | "cs-CZ"
|
|
1492
|
+
* >();
|
|
1493
|
+
* expectOk(Locale.fromUnknown("cs-CZ"), "cs-CZ");
|
|
1494
|
+
* expect(Locale.is("fr-CZ")).toBe(false);
|
|
1495
|
+
* ```
|
|
1496
|
+
*
|
|
1497
|
+
* @group Template literals
|
|
1498
|
+
*/
|
|
1499
|
+
export const templateLiteral = (...parts) => createTemplateLiteralParserType(parts).parent;
|
|
1500
|
+
const compileTemplateLiteralParser = (parts) => {
|
|
1501
|
+
let framing = emptyTemplateLiteralFraming;
|
|
1502
|
+
let fixedPartsWidth = 0;
|
|
1503
|
+
const compiledParts = parts.map((part) => {
|
|
1504
|
+
const partFraming = getTemplateLiteralPartFraming(part);
|
|
1505
|
+
framing = concatenateTemplateLiteralFraming(framing, partFraming);
|
|
1506
|
+
fixedPartsWidth += partFraming.width ?? 0;
|
|
1507
|
+
return [part, partFraming.width];
|
|
1508
|
+
});
|
|
1509
|
+
return (input) => {
|
|
1510
|
+
const inputCodePoints = globalThis.Array.from(input);
|
|
1511
|
+
const variableWidth = inputCodePoints.length - fixedPartsWidth;
|
|
1512
|
+
if (variableWidth < 0) {
|
|
1513
|
+
return err({ type: "TemplateLiteral", value: input });
|
|
1514
|
+
}
|
|
1515
|
+
const captures = [];
|
|
1516
|
+
let position = 0;
|
|
1517
|
+
for (const [part, width] of compiledParts) {
|
|
1518
|
+
const partWidth = width ?? variableWidth;
|
|
1519
|
+
const value = inputCodePoints
|
|
1520
|
+
.slice(position, position + partWidth)
|
|
1521
|
+
.join("");
|
|
1522
|
+
if (typeof part === "string") {
|
|
1523
|
+
if (value !== part) {
|
|
1524
|
+
return err({ type: "TemplateLiteral", value: input });
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
else {
|
|
1528
|
+
captures.push(value);
|
|
1529
|
+
}
|
|
1530
|
+
position += partWidth;
|
|
1531
|
+
}
|
|
1532
|
+
return position === inputCodePoints.length
|
|
1533
|
+
? ok(captures)
|
|
1534
|
+
: err({ type: "TemplateLiteral", value: input });
|
|
1535
|
+
};
|
|
1536
|
+
};
|
|
1537
|
+
const emptyTemplateLiteralFraming = {
|
|
1538
|
+
width: 0,
|
|
1539
|
+
canBeEmpty: true,
|
|
1540
|
+
canStartWithLowSurrogate: false,
|
|
1541
|
+
canEndWithHighSurrogate: false,
|
|
1542
|
+
};
|
|
1543
|
+
const unknownTemplateLiteralFraming = {
|
|
1544
|
+
width: null,
|
|
1545
|
+
canBeEmpty: true,
|
|
1546
|
+
canStartWithLowSurrogate: true,
|
|
1547
|
+
canEndWithHighSurrogate: true,
|
|
1548
|
+
};
|
|
1549
|
+
const concatenateTemplateLiteralFraming = (left, right) => {
|
|
1550
|
+
assert(!(left.canEndWithHighSurrogate && right.canStartWithLowSurrogate), "A TemplateLiteral cannot form a Unicode surrogate pair across part boundaries.");
|
|
1551
|
+
return {
|
|
1552
|
+
width: left.width !== null && right.width !== null
|
|
1553
|
+
? left.width + right.width
|
|
1554
|
+
: null,
|
|
1555
|
+
canBeEmpty: left.canBeEmpty && right.canBeEmpty,
|
|
1556
|
+
canStartWithLowSurrogate: left.canStartWithLowSurrogate ||
|
|
1557
|
+
(left.canBeEmpty && right.canStartWithLowSurrogate),
|
|
1558
|
+
canEndWithHighSurrogate: right.canEndWithHighSurrogate ||
|
|
1559
|
+
(right.canBeEmpty && left.canEndWithHighSurrogate),
|
|
1560
|
+
};
|
|
1561
|
+
};
|
|
1562
|
+
const getStringTemplateLiteralFraming = (value) => {
|
|
1563
|
+
const firstCodeUnit = value.charCodeAt(0);
|
|
1564
|
+
const lastCodeUnit = value.charCodeAt(value.length - 1);
|
|
1565
|
+
return {
|
|
1566
|
+
width: globalThis.Array.from(value).length,
|
|
1567
|
+
canBeEmpty: value.length === 0,
|
|
1568
|
+
canStartWithLowSurrogate: firstCodeUnit >= 0xdc00 && firstCodeUnit <= 0xdfff,
|
|
1569
|
+
canEndWithHighSurrogate: lastCodeUnit >= 0xd800 && lastCodeUnit <= 0xdbff,
|
|
1570
|
+
};
|
|
1571
|
+
};
|
|
1572
|
+
const getTemplateLiteralPartFraming = (part) => {
|
|
1573
|
+
if (typeof part === "string")
|
|
1574
|
+
return getStringTemplateLiteralFraming(part);
|
|
1575
|
+
const type = part;
|
|
1576
|
+
if (type[templateLiteralSyntaxSymbol] === true) {
|
|
1577
|
+
if (type.name === "Literal") {
|
|
1578
|
+
return getStringTemplateLiteralFraming(type.expected);
|
|
1579
|
+
}
|
|
1580
|
+
if (type.name === "Union") {
|
|
1581
|
+
const memberFramings = type.members.map(getTemplateLiteralPartFraming);
|
|
1582
|
+
const width = memberFramings[0].width;
|
|
1583
|
+
return {
|
|
1584
|
+
width: width !== null &&
|
|
1585
|
+
memberFramings.every((framing) => framing.width === width)
|
|
1586
|
+
? width
|
|
1587
|
+
: null,
|
|
1588
|
+
canBeEmpty: memberFramings.some((framing) => framing.canBeEmpty),
|
|
1589
|
+
canStartWithLowSurrogate: memberFramings.some((framing) => framing.canStartWithLowSurrogate),
|
|
1590
|
+
canEndWithHighSurrogate: memberFramings.some((framing) => framing.canEndWithHighSurrogate),
|
|
1591
|
+
};
|
|
1592
|
+
}
|
|
1593
|
+
if (type.name === "TemplateLiteral") {
|
|
1594
|
+
return type.parts.reduce((framing, part) => concatenateTemplateLiteralFraming(framing, getTemplateLiteralPartFraming(part)), emptyTemplateLiteralFraming);
|
|
1595
|
+
}
|
|
1596
|
+
}
|
|
1597
|
+
if (type.parent === null)
|
|
1598
|
+
return unknownTemplateLiteralFraming;
|
|
1599
|
+
return getTemplateLiteralPartFraming(type.parent);
|
|
1600
|
+
};
|
|
740
1601
|
export function brand(name, parent, validate, formatError) {
|
|
741
1602
|
return createChildType(name, parent, validate
|
|
742
1603
|
? (value) => flatMapResult(validate(value), () => ok(value))
|
|
@@ -776,6 +1637,18 @@ export const DateIso = /*#__PURE__*/ brand("DateIso", String, (value) => value.l
|
|
|
776
1637
|
/**
|
|
777
1638
|
* Safely transforms a {@link Date} into a canonical {@link DateIso}.
|
|
778
1639
|
*
|
|
1640
|
+
* ### Example
|
|
1641
|
+
*
|
|
1642
|
+
* ```ts
|
|
1643
|
+
* import { DateIsoFromDate } from "@evolu/common";
|
|
1644
|
+
*
|
|
1645
|
+
* const date = new globalThis.Date("2025-01-01T12:00:00.000Z");
|
|
1646
|
+
* const result = DateIsoFromDate.fromUnknown(date);
|
|
1647
|
+
*
|
|
1648
|
+
* expectOk(result, "2025-01-01T12:00:00.000Z");
|
|
1649
|
+
* expect(DateIsoFromDate.to(result.value)).toEqual(date);
|
|
1650
|
+
* ```
|
|
1651
|
+
*
|
|
779
1652
|
* @group String
|
|
780
1653
|
*/
|
|
781
1654
|
export const DateIsoFromDate = /*#__PURE__*/ transform("DateIsoFromDate", Date, DateIso, {
|
|
@@ -837,7 +1710,17 @@ export const capitalized = (parent) => brand("Capitalized", parent, (value) => {
|
|
|
837
1710
|
*/
|
|
838
1711
|
export const CapitalizedString = /*#__PURE__*/ capitalized(String);
|
|
839
1712
|
/**
|
|
840
|
-
*
|
|
1713
|
+
* String {@link Brand} without surrounding whitespace.
|
|
1714
|
+
*
|
|
1715
|
+
* ### Example
|
|
1716
|
+
*
|
|
1717
|
+
* ```ts
|
|
1718
|
+
* import { String, trimmed } from "@evolu/common";
|
|
1719
|
+
*
|
|
1720
|
+
* const Trimmed = trimmed(String);
|
|
1721
|
+
*
|
|
1722
|
+
* expectOk(Trimmed.fromUnknown("Evolu"), "Evolu");
|
|
1723
|
+
* ```
|
|
841
1724
|
*
|
|
842
1725
|
* @group String
|
|
843
1726
|
*/
|
|
@@ -859,11 +1742,31 @@ export const TrimmedString = /*#__PURE__*/ trimmed(String);
|
|
|
859
1742
|
/**
|
|
860
1743
|
* Trims a string and returns a {@link TrimmedString}.
|
|
861
1744
|
*
|
|
1745
|
+
* ### Example
|
|
1746
|
+
*
|
|
1747
|
+
* ```ts
|
|
1748
|
+
* import { trim } from "@evolu/common";
|
|
1749
|
+
*
|
|
1750
|
+
* expect(trim(" Evolu ")).toBe("Evolu");
|
|
1751
|
+
* ```
|
|
1752
|
+
*
|
|
862
1753
|
* @group String
|
|
863
1754
|
*/
|
|
864
1755
|
export const trim = (value) => value.trim();
|
|
865
1756
|
/**
|
|
866
|
-
*
|
|
1757
|
+
* Minimum-length {@link Brand} for values whose `length` is at least `min`.
|
|
1758
|
+
*
|
|
1759
|
+
* ### Example
|
|
1760
|
+
*
|
|
1761
|
+
* ```ts
|
|
1762
|
+
* import { String, array, minLength } from "@evolu/common";
|
|
1763
|
+
*
|
|
1764
|
+
* const AtLeastThreeCharacters = minLength(3)(String);
|
|
1765
|
+
* const AtLeastTwoItems = minLength(2)(array(String));
|
|
1766
|
+
*
|
|
1767
|
+
* expectOk(AtLeastThreeCharacters.fromUnknown("abc"), "abc");
|
|
1768
|
+
* expectOk(AtLeastTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
1769
|
+
* ```
|
|
867
1770
|
*
|
|
868
1771
|
* @group String
|
|
869
1772
|
* @group Collection
|
|
@@ -890,7 +1793,19 @@ export const minLength = (min) => (parent) => {
|
|
|
890
1793
|
*/
|
|
891
1794
|
export const NonEmptyTrimmedString = /*#__PURE__*/ minLength(1)(TrimmedString);
|
|
892
1795
|
/**
|
|
893
|
-
*
|
|
1796
|
+
* Maximum-length {@link Brand} for values whose `length` is at most `max`.
|
|
1797
|
+
*
|
|
1798
|
+
* ### Example
|
|
1799
|
+
*
|
|
1800
|
+
* ```ts
|
|
1801
|
+
* import { String, array, maxLength } from "@evolu/common";
|
|
1802
|
+
*
|
|
1803
|
+
* const AtMostThreeCharacters = maxLength(3)(String);
|
|
1804
|
+
* const AtMostTwoItems = maxLength(2)(array(String));
|
|
1805
|
+
*
|
|
1806
|
+
* expectOk(AtMostThreeCharacters.fromUnknown("abc"), "abc");
|
|
1807
|
+
* expectOk(AtMostTwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
1808
|
+
* ```
|
|
894
1809
|
*
|
|
895
1810
|
* @group String
|
|
896
1811
|
* @group Collection
|
|
@@ -914,7 +1829,19 @@ export const NonEmptyTrimmedString100 = /*#__PURE__*/ maxLength(100)(NonEmptyTri
|
|
|
914
1829
|
*/
|
|
915
1830
|
export const NonEmptyTrimmedString1000 = /*#__PURE__*/ maxLength(1000)(NonEmptyTrimmedString);
|
|
916
1831
|
/**
|
|
917
|
-
*
|
|
1832
|
+
* Exact-length {@link Brand} for values whose `length` equals `exact`.
|
|
1833
|
+
*
|
|
1834
|
+
* ### Example
|
|
1835
|
+
*
|
|
1836
|
+
* ```ts
|
|
1837
|
+
* import { String, array, length } from "@evolu/common";
|
|
1838
|
+
*
|
|
1839
|
+
* const ThreeCharacters = length(3)(String);
|
|
1840
|
+
* const TwoItems = length(2)(array(String));
|
|
1841
|
+
*
|
|
1842
|
+
* expectOk(ThreeCharacters.fromUnknown("abc"), "abc");
|
|
1843
|
+
* expectOk(TwoItems.fromUnknown(["a", "b"]), ["a", "b"]);
|
|
1844
|
+
* ```
|
|
918
1845
|
*
|
|
919
1846
|
* @group String
|
|
920
1847
|
* @group Collection
|
|
@@ -926,7 +1853,7 @@ export const length = (exact) => (parent) => {
|
|
|
926
1853
|
: err({ type: name, value, exact }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} does not have the required length of ${error.exact}.`);
|
|
927
1854
|
};
|
|
928
1855
|
/**
|
|
929
|
-
*
|
|
1856
|
+
* String {@link Brand} constrained by a regular expression.
|
|
930
1857
|
*
|
|
931
1858
|
* ### Example
|
|
932
1859
|
*
|
|
@@ -1028,12 +1955,34 @@ export const Base64Url = /*#__PURE__*/ brand("Base64Url", String, (value) => {
|
|
|
1028
1955
|
/**
|
|
1029
1956
|
* Encodes bytes as {@link Base64Url}.
|
|
1030
1957
|
*
|
|
1958
|
+
* ### Example
|
|
1959
|
+
*
|
|
1960
|
+
* ```ts
|
|
1961
|
+
* import { uint8ArrayToBase64Url } from "@evolu/common";
|
|
1962
|
+
*
|
|
1963
|
+
* expect(uint8ArrayToBase64Url(new Uint8Array([0, 1, 2, 255]))).toBe(
|
|
1964
|
+
* "AAEC_w",
|
|
1965
|
+
* );
|
|
1966
|
+
* ```
|
|
1967
|
+
*
|
|
1031
1968
|
* @group String
|
|
1032
1969
|
*/
|
|
1033
1970
|
export const uint8ArrayToBase64Url = (bytes) => uint8ArrayToBase64UrlString(bytes);
|
|
1034
1971
|
/**
|
|
1035
1972
|
* Decodes {@link Base64Url} as bytes.
|
|
1036
1973
|
*
|
|
1974
|
+
* ### Example
|
|
1975
|
+
*
|
|
1976
|
+
* ```ts
|
|
1977
|
+
* import { Base64Url, base64UrlToUint8Array } from "@evolu/common";
|
|
1978
|
+
*
|
|
1979
|
+
* const value = Base64Url.orThrow("AAEC_w");
|
|
1980
|
+
*
|
|
1981
|
+
* expect(base64UrlToUint8Array(value)).toEqual(
|
|
1982
|
+
* new Uint8Array([0, 1, 2, 255]),
|
|
1983
|
+
* );
|
|
1984
|
+
* ```
|
|
1985
|
+
*
|
|
1037
1986
|
* @group String
|
|
1038
1987
|
*/
|
|
1039
1988
|
export const base64UrlToUint8Array = (value) => base64UrlStringToUint8Array(value);
|
|
@@ -1063,7 +2012,7 @@ export const SimplePassword = /*#__PURE__*/ brand("SimplePassword",
|
|
|
1063
2012
|
*/
|
|
1064
2013
|
export const Mnemonic = /*#__PURE__*/ brand("Mnemonic", NonEmptyTrimmedString, (value) => bip39.validateMnemonic(value, wordlist)
|
|
1065
2014
|
? ok()
|
|
1066
|
-
: err({ type: "Mnemonic", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid BIP39 mnemonic.`);
|
|
2015
|
+
: err({ type: "Mnemonic", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} is not a valid English BIP39 mnemonic.`);
|
|
1067
2016
|
/**
|
|
1068
2017
|
* Evolu Id: 16 bytes encoded as a 22-character {@link Base64Url}.
|
|
1069
2018
|
*
|
|
@@ -1083,6 +2032,22 @@ export const Id = /*#__PURE__*/ brand("Id", String, (value) => value.length ===
|
|
|
1083
2032
|
* This is the recommended default because it does not encode creation time.
|
|
1084
2033
|
* Pass a Brand name when the returned Id belongs to one domain entity.
|
|
1085
2034
|
*
|
|
2035
|
+
* ### Example
|
|
2036
|
+
*
|
|
2037
|
+
* ```ts
|
|
2038
|
+
* import {
|
|
2039
|
+
* Id,
|
|
2040
|
+
* createId,
|
|
2041
|
+
* createRandomBytes,
|
|
2042
|
+
* type Brand,
|
|
2043
|
+
* } from "@evolu/common";
|
|
2044
|
+
*
|
|
2045
|
+
* const userId = createId<"User">({ randomBytes: createRandomBytes() });
|
|
2046
|
+
*
|
|
2047
|
+
* expect(Id.is(userId)).toBe(true);
|
|
2048
|
+
* expectTypeOf(userId).toEqualTypeOf<Id & Brand<"User">>();
|
|
2049
|
+
* ```
|
|
2050
|
+
*
|
|
1086
2051
|
* @group String
|
|
1087
2052
|
*/
|
|
1088
2053
|
export const createId = (deps, ..._validation) => uint8ArrayToBase64Url(deps.randomBytes.create(16));
|
|
@@ -1117,6 +2082,26 @@ export const createIdFromString = (value, ..._validation) => idBytesToId(sha256(
|
|
|
1117
2082
|
* logs, URLs, and exports. Prefer {@link createId} unless that tradeoff is
|
|
1118
2083
|
* deliberate.
|
|
1119
2084
|
*
|
|
2085
|
+
* ### Example
|
|
2086
|
+
*
|
|
2087
|
+
* ```ts
|
|
2088
|
+
* import {
|
|
2089
|
+
* createIdAsUuidv7,
|
|
2090
|
+
* createRandomBytes,
|
|
2091
|
+
* createTime,
|
|
2092
|
+
* idToIdBytes,
|
|
2093
|
+
* } from "@evolu/common";
|
|
2094
|
+
*
|
|
2095
|
+
* const value = createIdAsUuidv7({
|
|
2096
|
+
* randomBytes: createRandomBytes(),
|
|
2097
|
+
* time: createTime(),
|
|
2098
|
+
* });
|
|
2099
|
+
* const bytes = idToIdBytes(value);
|
|
2100
|
+
*
|
|
2101
|
+
* expect(bytes[6] >> 4).toBe(0x7);
|
|
2102
|
+
* expect(bytes[8] & 0xc0).toBe(0x80);
|
|
2103
|
+
* ```
|
|
2104
|
+
*
|
|
1120
2105
|
* @group String
|
|
1121
2106
|
*/
|
|
1122
2107
|
export const createIdAsUuidv7 = (deps, ..._validation) => {
|
|
@@ -1133,7 +2118,23 @@ export const createIdAsUuidv7 = (deps, ..._validation) => {
|
|
|
1133
2118
|
return uint8ArrayToBase64Url(bytes);
|
|
1134
2119
|
};
|
|
1135
2120
|
/**
|
|
1136
|
-
*
|
|
2121
|
+
* Table-specific {@link Id} Type.
|
|
2122
|
+
*
|
|
2123
|
+
* ### Example
|
|
2124
|
+
*
|
|
2125
|
+
* ```ts
|
|
2126
|
+
* import {
|
|
2127
|
+
* createIdFromString,
|
|
2128
|
+
* id,
|
|
2129
|
+
* type Brand,
|
|
2130
|
+
* type Id,
|
|
2131
|
+
* } from "@evolu/common";
|
|
2132
|
+
*
|
|
2133
|
+
* const TodoId = id("Todo");
|
|
2134
|
+
* const todoId = TodoId.orThrow(createIdFromString("todo"));
|
|
2135
|
+
*
|
|
2136
|
+
* expectTypeOf(todoId).toEqualTypeOf<Id & Brand<"Todo">>();
|
|
2137
|
+
* ```
|
|
1137
2138
|
*
|
|
1138
2139
|
* @group String
|
|
1139
2140
|
*/
|
|
@@ -1150,11 +2151,47 @@ export const id = (table) => {
|
|
|
1150
2151
|
*/
|
|
1151
2152
|
export const IdBytes = /*#__PURE__*/ brand("IdBytes",
|
|
1152
2153
|
/*#__PURE__*/ length(16)(Uint8Array));
|
|
1153
|
-
/**
|
|
2154
|
+
/**
|
|
2155
|
+
* Byte length of an {@link IdBytes} value.
|
|
2156
|
+
*
|
|
2157
|
+
* @group String
|
|
2158
|
+
*/
|
|
1154
2159
|
export const idBytesTypeValueLength = 16;
|
|
1155
|
-
/**
|
|
2160
|
+
/**
|
|
2161
|
+
* Converts an {@link Id} to {@link IdBytes}.
|
|
2162
|
+
*
|
|
2163
|
+
* ### Example
|
|
2164
|
+
*
|
|
2165
|
+
* ```ts
|
|
2166
|
+
* import { createIdFromString, idToIdBytes } from "@evolu/common";
|
|
2167
|
+
*
|
|
2168
|
+
* const bytes = idToIdBytes(createIdFromString("todo"));
|
|
2169
|
+
*
|
|
2170
|
+
* expect(bytes).toHaveLength(16);
|
|
2171
|
+
* ```
|
|
2172
|
+
*
|
|
2173
|
+
* @group String
|
|
2174
|
+
*/
|
|
1156
2175
|
export const idToIdBytes = (value) => base64UrlToUint8Array(value);
|
|
1157
|
-
/**
|
|
2176
|
+
/**
|
|
2177
|
+
* Converts {@link IdBytes} to an {@link Id}.
|
|
2178
|
+
*
|
|
2179
|
+
* ### Example
|
|
2180
|
+
*
|
|
2181
|
+
* ```ts
|
|
2182
|
+
* import {
|
|
2183
|
+
* createIdFromString,
|
|
2184
|
+
* idBytesToId,
|
|
2185
|
+
* idToIdBytes,
|
|
2186
|
+
* } from "@evolu/common";
|
|
2187
|
+
*
|
|
2188
|
+
* const value = createIdFromString("todo");
|
|
2189
|
+
*
|
|
2190
|
+
* expect(idBytesToId(idToIdBytes(value))).toBe(value);
|
|
2191
|
+
* ```
|
|
2192
|
+
*
|
|
2193
|
+
* @group String
|
|
2194
|
+
*/
|
|
1158
2195
|
export const idBytesToId = (value) => uint8ArrayToBase64Url(value);
|
|
1159
2196
|
/**
|
|
1160
2197
|
* Decimal string representation of a signed {@link Int64}.
|
|
@@ -1180,6 +2217,17 @@ export const Int64String = /*#__PURE__*/ brand("Int64String", NonEmptyTrimmedStr
|
|
|
1180
2217
|
* text, including SQLite queries that cast INTEGER values to TEXT to avoid a
|
|
1181
2218
|
* lossy JavaScript number conversion.
|
|
1182
2219
|
*
|
|
2220
|
+
* ### Example
|
|
2221
|
+
*
|
|
2222
|
+
* ```ts
|
|
2223
|
+
* import { Int64FromInt64String } from "@evolu/common";
|
|
2224
|
+
*
|
|
2225
|
+
* const result = Int64FromInt64String.fromUnknown("9223372036854775807");
|
|
2226
|
+
*
|
|
2227
|
+
* expectOk(result, 9223372036854775807n);
|
|
2228
|
+
* expect(Int64FromInt64String.to(result.value)).toBe("9223372036854775807");
|
|
2229
|
+
* ```
|
|
2230
|
+
*
|
|
1183
2231
|
* @group Number
|
|
1184
2232
|
*/
|
|
1185
2233
|
export const Int64FromInt64String = /*#__PURE__*/ transform("Int64FromInt64String", Int64String, Int64, {
|
|
@@ -1187,7 +2235,17 @@ export const Int64FromInt64String = /*#__PURE__*/ transform("Int64FromInt64Strin
|
|
|
1187
2235
|
to: (value) => globalThis.String(value),
|
|
1188
2236
|
});
|
|
1189
2237
|
/**
|
|
1190
|
-
*
|
|
2238
|
+
* Number {@link Brand} requiring a value greater than or equal to zero.
|
|
2239
|
+
*
|
|
2240
|
+
* ### Example
|
|
2241
|
+
*
|
|
2242
|
+
* ```ts
|
|
2243
|
+
* import { Number, nonNegative } from "@evolu/common";
|
|
2244
|
+
*
|
|
2245
|
+
* const NonNegative = nonNegative(Number);
|
|
2246
|
+
*
|
|
2247
|
+
* expectOk(NonNegative.fromUnknown(0), 0);
|
|
2248
|
+
* ```
|
|
1191
2249
|
*
|
|
1192
2250
|
* @group Number
|
|
1193
2251
|
*/
|
|
@@ -1199,7 +2257,17 @@ export const nonNegative = (parent) => brand("NonNegative", parent, (value) => v
|
|
|
1199
2257
|
*/
|
|
1200
2258
|
export const NonNegativeNumber = /*#__PURE__*/ nonNegative(Number);
|
|
1201
2259
|
/**
|
|
1202
|
-
*
|
|
2260
|
+
* Number {@link Brand} requiring a value greater than zero.
|
|
2261
|
+
*
|
|
2262
|
+
* ### Example
|
|
2263
|
+
*
|
|
2264
|
+
* ```ts
|
|
2265
|
+
* import { Number, positive } from "@evolu/common";
|
|
2266
|
+
*
|
|
2267
|
+
* const Positive = positive(Number);
|
|
2268
|
+
*
|
|
2269
|
+
* expectOk(Positive.fromUnknown(1), 1);
|
|
2270
|
+
* ```
|
|
1203
2271
|
*
|
|
1204
2272
|
* @group Number
|
|
1205
2273
|
*/
|
|
@@ -1214,7 +2282,17 @@ export const positive = (parent) => brand("Positive", parent, (value) => value >
|
|
|
1214
2282
|
*/
|
|
1215
2283
|
export const PositiveNumber = /*#__PURE__*/ positive(NonNegativeNumber);
|
|
1216
2284
|
/**
|
|
1217
|
-
*
|
|
2285
|
+
* Number {@link Brand} requiring a value less than or equal to zero.
|
|
2286
|
+
*
|
|
2287
|
+
* ### Example
|
|
2288
|
+
*
|
|
2289
|
+
* ```ts
|
|
2290
|
+
* import { Number, nonPositive } from "@evolu/common";
|
|
2291
|
+
*
|
|
2292
|
+
* const NonPositive = nonPositive(Number);
|
|
2293
|
+
*
|
|
2294
|
+
* expectOk(NonPositive.fromUnknown(0), 0);
|
|
2295
|
+
* ```
|
|
1218
2296
|
*
|
|
1219
2297
|
* @group Number
|
|
1220
2298
|
*/
|
|
@@ -1226,7 +2304,17 @@ export const nonPositive = (parent) => brand("NonPositive", parent, (value) => v
|
|
|
1226
2304
|
*/
|
|
1227
2305
|
export const NonPositiveNumber = /*#__PURE__*/ nonPositive(Number);
|
|
1228
2306
|
/**
|
|
1229
|
-
*
|
|
2307
|
+
* Number {@link Brand} requiring a value less than zero.
|
|
2308
|
+
*
|
|
2309
|
+
* ### Example
|
|
2310
|
+
*
|
|
2311
|
+
* ```ts
|
|
2312
|
+
* import { Number, negative } from "@evolu/common";
|
|
2313
|
+
*
|
|
2314
|
+
* const Negative = negative(Number);
|
|
2315
|
+
*
|
|
2316
|
+
* expectOk(Negative.fromUnknown(-1), -1);
|
|
2317
|
+
* ```
|
|
1230
2318
|
*
|
|
1231
2319
|
* @group Number
|
|
1232
2320
|
*/
|
|
@@ -1241,7 +2329,17 @@ export const negative = (parent) => brand("Negative", parent, (value) => value <
|
|
|
1241
2329
|
*/
|
|
1242
2330
|
export const NegativeNumber = /*#__PURE__*/ negative(NonPositiveNumber);
|
|
1243
2331
|
/**
|
|
1244
|
-
*
|
|
2332
|
+
* Number {@link Brand} requiring a value other than `NaN`.
|
|
2333
|
+
*
|
|
2334
|
+
* ### Example
|
|
2335
|
+
*
|
|
2336
|
+
* ```ts
|
|
2337
|
+
* import { Number, nonNaN } from "@evolu/common";
|
|
2338
|
+
*
|
|
2339
|
+
* const NonNaN = nonNaN(Number);
|
|
2340
|
+
*
|
|
2341
|
+
* expectOk(NonNaN.fromUnknown(Infinity), Infinity);
|
|
2342
|
+
* ```
|
|
1245
2343
|
*
|
|
1246
2344
|
* @group Number
|
|
1247
2345
|
*/
|
|
@@ -1260,7 +2358,17 @@ export const nonNaN = (parent) => brand("NonNaN", parent, (value) => globalThis.
|
|
|
1260
2358
|
*/
|
|
1261
2359
|
export const NonNaNNumber = /*#__PURE__*/ nonNaN(Number);
|
|
1262
2360
|
/**
|
|
1263
|
-
*
|
|
2361
|
+
* Number {@link Brand} requiring a finite value.
|
|
2362
|
+
*
|
|
2363
|
+
* ### Example
|
|
2364
|
+
*
|
|
2365
|
+
* ```ts
|
|
2366
|
+
* import { Number, finite } from "@evolu/common";
|
|
2367
|
+
*
|
|
2368
|
+
* const Finite = finite(Number);
|
|
2369
|
+
*
|
|
2370
|
+
* expectOk(Finite.fromUnknown(42), 42);
|
|
2371
|
+
* ```
|
|
1264
2372
|
*
|
|
1265
2373
|
* @group Number
|
|
1266
2374
|
*/
|
|
@@ -1366,7 +2474,17 @@ export const NonPositiveInt = /*#__PURE__*/ nonPositive(Int);
|
|
|
1366
2474
|
*/
|
|
1367
2475
|
export const NegativeInt = /*#__PURE__*/ negative(NonPositiveInt);
|
|
1368
2476
|
/**
|
|
1369
|
-
*
|
|
2477
|
+
* Number {@link Brand} requiring a value greater than `min`.
|
|
2478
|
+
*
|
|
2479
|
+
* ### Example
|
|
2480
|
+
*
|
|
2481
|
+
* ```ts
|
|
2482
|
+
* import { Number, greaterThan } from "@evolu/common";
|
|
2483
|
+
*
|
|
2484
|
+
* const GreaterThanTen = greaterThan(10)(Number);
|
|
2485
|
+
*
|
|
2486
|
+
* expectOk(GreaterThanTen.fromUnknown(11), 11);
|
|
2487
|
+
* ```
|
|
1370
2488
|
*
|
|
1371
2489
|
* @group Number
|
|
1372
2490
|
*/
|
|
@@ -1377,7 +2495,17 @@ export const greaterThan = (min) => (parent) => {
|
|
|
1377
2495
|
: err({ type: name, value, min }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be greater than ${error.min}.`);
|
|
1378
2496
|
};
|
|
1379
2497
|
/**
|
|
1380
|
-
*
|
|
2498
|
+
* Number {@link Brand} requiring a value greater than or equal to `min`.
|
|
2499
|
+
*
|
|
2500
|
+
* ### Example
|
|
2501
|
+
*
|
|
2502
|
+
* ```ts
|
|
2503
|
+
* import { Number, greaterThanOrEqualTo } from "@evolu/common";
|
|
2504
|
+
*
|
|
2505
|
+
* const AtLeastTen = greaterThanOrEqualTo(10)(Number);
|
|
2506
|
+
*
|
|
2507
|
+
* expectOk(AtLeastTen.fromUnknown(10), 10);
|
|
2508
|
+
* ```
|
|
1381
2509
|
*
|
|
1382
2510
|
* @group Number
|
|
1383
2511
|
*/
|
|
@@ -1392,7 +2520,17 @@ export const greaterThanOrEqualTo = (min) => (parent) => {
|
|
|
1392
2520
|
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be greater than or equal to ${error.min}.`);
|
|
1393
2521
|
};
|
|
1394
2522
|
/**
|
|
1395
|
-
*
|
|
2523
|
+
* Number {@link Brand} requiring a value less than `max`.
|
|
2524
|
+
*
|
|
2525
|
+
* ### Example
|
|
2526
|
+
*
|
|
2527
|
+
* ```ts
|
|
2528
|
+
* import { Number, lessThan } from "@evolu/common";
|
|
2529
|
+
*
|
|
2530
|
+
* const LessThanTen = lessThan(10)(Number);
|
|
2531
|
+
*
|
|
2532
|
+
* expectOk(LessThanTen.fromUnknown(9), 9);
|
|
2533
|
+
* ```
|
|
1396
2534
|
*
|
|
1397
2535
|
* @group Number
|
|
1398
2536
|
*/
|
|
@@ -1410,7 +2548,17 @@ export const lessThan = (max) => (parent) => {
|
|
|
1410
2548
|
export const Age = /*#__PURE__*/ brand("Age",
|
|
1411
2549
|
/*#__PURE__*/ lessThan(200)(NonNegativeInt));
|
|
1412
2550
|
/**
|
|
1413
|
-
*
|
|
2551
|
+
* Number {@link Brand} requiring a value less than or equal to `max`.
|
|
2552
|
+
*
|
|
2553
|
+
* ### Example
|
|
2554
|
+
*
|
|
2555
|
+
* ```ts
|
|
2556
|
+
* import { Number, lessThanOrEqualTo } from "@evolu/common";
|
|
2557
|
+
*
|
|
2558
|
+
* const AtMostTen = lessThanOrEqualTo(10)(Number);
|
|
2559
|
+
*
|
|
2560
|
+
* expectOk(AtMostTen.fromUnknown(10), 10);
|
|
2561
|
+
* ```
|
|
1414
2562
|
*
|
|
1415
2563
|
* @group Number
|
|
1416
2564
|
*/
|
|
@@ -1430,46 +2578,167 @@ export const lessThanOrEqualTo = (max) => (parent) => {
|
|
|
1430
2578
|
export const Ratio = /*#__PURE__*/ brand("Ratio",
|
|
1431
2579
|
/*#__PURE__*/ lessThanOrEqualTo(1)(NonNegativeFiniteNumber));
|
|
1432
2580
|
/**
|
|
1433
|
-
* Canonical string representation of a
|
|
2581
|
+
* Canonical string representation of a signed base-10 decimal value.
|
|
1434
2582
|
*
|
|
1435
2583
|
* Use this Type when a decimal value must remain exact instead of being
|
|
1436
2584
|
* converted to an IEEE-754 number. Equivalent values have one accepted
|
|
1437
|
-
* representation, so leading zeroes, trailing fractional zeroes,
|
|
1438
|
-
* exponent notation are rejected.
|
|
2585
|
+
* representation, so leading zeroes, trailing fractional zeroes, `-0`, plus
|
|
2586
|
+
* signs, and exponent notation are rejected.
|
|
1439
2587
|
*
|
|
1440
2588
|
* The decoded value remains a string. Arithmetic requires an explicit decimal
|
|
1441
2589
|
* or fixed-point representation.
|
|
1442
2590
|
*
|
|
2591
|
+
* TypeScript template literal types can describe a fixed number of digit
|
|
2592
|
+
* positions, but not the arbitrarily long integer and fractional parts accepted
|
|
2593
|
+
* here. `DecimalString` therefore uses a {@link Brand} so its TypeScript type
|
|
2594
|
+
* does not accept strings that have not been validated.
|
|
2595
|
+
*
|
|
2596
|
+
* Use these predefined Types or their corresponding factories to add sign
|
|
2597
|
+
* constraints to compatible decimal string Types:
|
|
2598
|
+
*
|
|
2599
|
+
* - {@link NonNegativeDecimalString} / {@link nonNegativeDecimalString}
|
|
2600
|
+
* - {@link PositiveDecimalString} / {@link positiveDecimalString}
|
|
2601
|
+
* - {@link NonPositiveDecimalString} / {@link nonPositiveDecimalString}
|
|
2602
|
+
* - {@link NegativeDecimalString} / {@link negativeDecimalString}
|
|
2603
|
+
*
|
|
1443
2604
|
* ### Example
|
|
1444
2605
|
*
|
|
1445
2606
|
* ```ts
|
|
1446
|
-
* import {
|
|
2607
|
+
* import { DecimalString } from "@evolu/common";
|
|
1447
2608
|
*
|
|
1448
|
-
* expectOk(
|
|
1449
|
-
* expectOk(
|
|
1450
|
-
* expectOk(
|
|
2609
|
+
* expectOk(DecimalString.fromUnknown("-10.25"), "-10.25");
|
|
2610
|
+
* expectOk(DecimalString.fromUnknown("0"), "0");
|
|
2611
|
+
* expectOk(DecimalString.fromUnknown("10.25"), "10.25");
|
|
1451
2612
|
*
|
|
1452
|
-
* expectErr(
|
|
1453
|
-
* type: "
|
|
1454
|
-
* value: "
|
|
1455
|
-
* });
|
|
1456
|
-
* expectErr(PositiveDecimalString.fromUnknown("0.30"), {
|
|
1457
|
-
* type: "PositiveDecimalString",
|
|
1458
|
-
* value: "0.30",
|
|
2613
|
+
* expectErr(DecimalString.fromUnknown("10.250"), {
|
|
2614
|
+
* type: "DecimalString",
|
|
2615
|
+
* value: "10.250",
|
|
1459
2616
|
* });
|
|
1460
2617
|
* ```
|
|
1461
2618
|
*
|
|
1462
2619
|
* @group Number
|
|
1463
2620
|
*/
|
|
1464
|
-
export const
|
|
2621
|
+
export const DecimalString = /*#__PURE__*/ brand("DecimalString", String, (value) => /^(?:0|-?(?:[1-9]\d*|(?:0|[1-9]\d*)\.\d*[1-9]))$/.test(value)
|
|
2622
|
+
? ok()
|
|
2623
|
+
: err({ type: "DecimalString", value }), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a canonical decimal string.`);
|
|
2624
|
+
/**
|
|
2625
|
+
* {@link DecimalString} Brand requiring a value greater than or equal to zero.
|
|
2626
|
+
*
|
|
2627
|
+
* ### Example
|
|
2628
|
+
*
|
|
2629
|
+
* ```ts
|
|
2630
|
+
* import { DecimalString, nonNegativeDecimalString } from "@evolu/common";
|
|
2631
|
+
*
|
|
2632
|
+
* const NonNegative = nonNegativeDecimalString(DecimalString);
|
|
2633
|
+
*
|
|
2634
|
+
* expectOk(NonNegative.fromUnknown("0.5"), "0.5");
|
|
2635
|
+
* ```
|
|
2636
|
+
*
|
|
2637
|
+
* @group Number
|
|
2638
|
+
*/
|
|
2639
|
+
export const nonNegativeDecimalString = (parent) => brand("NonNegativeDecimalString", parent, (value) => value[0] !== "-"
|
|
2640
|
+
? ok()
|
|
2641
|
+
: err({
|
|
2642
|
+
type: "NonNegativeDecimalString",
|
|
2643
|
+
value,
|
|
2644
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a non-negative decimal string.`);
|
|
2645
|
+
/**
|
|
2646
|
+
* Non-negative {@link DecimalString}.
|
|
2647
|
+
*
|
|
2648
|
+
* @group Number
|
|
2649
|
+
*/
|
|
2650
|
+
export const NonNegativeDecimalString =
|
|
2651
|
+
/*#__PURE__*/ nonNegativeDecimalString(DecimalString);
|
|
2652
|
+
/**
|
|
2653
|
+
* {@link DecimalString} Brand requiring a value greater than zero.
|
|
2654
|
+
*
|
|
2655
|
+
* ### Example
|
|
2656
|
+
*
|
|
2657
|
+
* ```ts
|
|
2658
|
+
* import { DecimalString, positiveDecimalString } from "@evolu/common";
|
|
2659
|
+
*
|
|
2660
|
+
* const Positive = positiveDecimalString(DecimalString);
|
|
2661
|
+
*
|
|
2662
|
+
* expectOk(Positive.fromUnknown("0.5"), "0.5");
|
|
2663
|
+
* ```
|
|
2664
|
+
*
|
|
2665
|
+
* @group Number
|
|
2666
|
+
*/
|
|
2667
|
+
export const positiveDecimalString = (parent) => brand("PositiveDecimalString", parent, (value) => value !== "0" && value[0] !== "-"
|
|
1465
2668
|
? ok()
|
|
1466
2669
|
: err({
|
|
1467
2670
|
type: "PositiveDecimalString",
|
|
1468
2671
|
value,
|
|
1469
|
-
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a
|
|
2672
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a positive decimal string.`);
|
|
1470
2673
|
/**
|
|
1471
|
-
*
|
|
1472
|
-
*
|
|
2674
|
+
* Positive {@link DecimalString}.
|
|
2675
|
+
*
|
|
2676
|
+
* Also satisfies {@link NonNegativeDecimalString}, so it can be used wherever a
|
|
2677
|
+
* non-negative decimal string is required.
|
|
2678
|
+
*
|
|
2679
|
+
* @group Number
|
|
2680
|
+
*/
|
|
2681
|
+
export const PositiveDecimalString = /*#__PURE__*/ positiveDecimalString(NonNegativeDecimalString);
|
|
2682
|
+
/**
|
|
2683
|
+
* {@link DecimalString} Brand requiring a value less than or equal to zero.
|
|
2684
|
+
*
|
|
2685
|
+
* ### Example
|
|
2686
|
+
*
|
|
2687
|
+
* ```ts
|
|
2688
|
+
* import { DecimalString, nonPositiveDecimalString } from "@evolu/common";
|
|
2689
|
+
*
|
|
2690
|
+
* const NonPositive = nonPositiveDecimalString(DecimalString);
|
|
2691
|
+
*
|
|
2692
|
+
* expectOk(NonPositive.fromUnknown("-0.5"), "-0.5");
|
|
2693
|
+
* ```
|
|
2694
|
+
*
|
|
2695
|
+
* @group Number
|
|
2696
|
+
*/
|
|
2697
|
+
export const nonPositiveDecimalString = (parent) => brand("NonPositiveDecimalString", parent, (value) => value === "0" || value[0] === "-"
|
|
2698
|
+
? ok()
|
|
2699
|
+
: err({
|
|
2700
|
+
type: "NonPositiveDecimalString",
|
|
2701
|
+
value,
|
|
2702
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a non-positive decimal string.`);
|
|
2703
|
+
/**
|
|
2704
|
+
* Non-positive {@link DecimalString}.
|
|
2705
|
+
*
|
|
2706
|
+
* @group Number
|
|
2707
|
+
*/
|
|
2708
|
+
export const NonPositiveDecimalString =
|
|
2709
|
+
/*#__PURE__*/ nonPositiveDecimalString(DecimalString);
|
|
2710
|
+
/**
|
|
2711
|
+
* {@link DecimalString} Brand requiring a value less than zero.
|
|
2712
|
+
*
|
|
2713
|
+
* ### Example
|
|
2714
|
+
*
|
|
2715
|
+
* ```ts
|
|
2716
|
+
* import { DecimalString, negativeDecimalString } from "@evolu/common";
|
|
2717
|
+
*
|
|
2718
|
+
* const Negative = negativeDecimalString(DecimalString);
|
|
2719
|
+
*
|
|
2720
|
+
* expectOk(Negative.fromUnknown("-0.5"), "-0.5");
|
|
2721
|
+
* ```
|
|
2722
|
+
*
|
|
2723
|
+
* @group Number
|
|
2724
|
+
*/
|
|
2725
|
+
export const negativeDecimalString = (parent) => brand("NegativeDecimalString", parent, (value) => value[0] === "-"
|
|
2726
|
+
? ok()
|
|
2727
|
+
: err({
|
|
2728
|
+
type: "NegativeDecimalString",
|
|
2729
|
+
value,
|
|
2730
|
+
}), (error) => `The value ${safelyStringifyUnknownValue(error.value)} must be a negative decimal string.`);
|
|
2731
|
+
/**
|
|
2732
|
+
* Negative {@link DecimalString}.
|
|
2733
|
+
*
|
|
2734
|
+
* Also satisfies {@link NonPositiveDecimalString}, so it can be used wherever a
|
|
2735
|
+
* non-positive decimal string is required.
|
|
2736
|
+
*
|
|
2737
|
+
* @group Number
|
|
2738
|
+
*/
|
|
2739
|
+
export const NegativeDecimalString = /*#__PURE__*/ negativeDecimalString(NonPositiveDecimalString);
|
|
2740
|
+
/**
|
|
2741
|
+
* Number {@link Brand} requiring an exact decimal multiple of `divisor`.
|
|
1473
2742
|
*
|
|
1474
2743
|
* The divisor must be one canonical positive decimal string literal because its
|
|
1475
2744
|
* exact value is encoded in the resulting Brand name. The declaration is
|
|
@@ -1542,7 +2811,17 @@ const decimalStringToParts = (value) => {
|
|
|
1542
2811
|
return { coefficient, exponent };
|
|
1543
2812
|
};
|
|
1544
2813
|
/**
|
|
1545
|
-
*
|
|
2814
|
+
* Number {@link Brand} requiring a value within an inclusive range.
|
|
2815
|
+
*
|
|
2816
|
+
* ### Example
|
|
2817
|
+
*
|
|
2818
|
+
* ```ts
|
|
2819
|
+
* import { Number, between } from "@evolu/common";
|
|
2820
|
+
*
|
|
2821
|
+
* const Percentage = between(0, 100)(Number);
|
|
2822
|
+
*
|
|
2823
|
+
* expectOk(Percentage.fromUnknown(75), 75);
|
|
2824
|
+
* ```
|
|
1546
2825
|
*
|
|
1547
2826
|
* @group Number
|
|
1548
2827
|
*/
|
|
@@ -1645,20 +2924,6 @@ const isArrayCollection = (value, isElement) => {
|
|
|
1645
2924
|
}
|
|
1646
2925
|
return true;
|
|
1647
2926
|
};
|
|
1648
|
-
const formatArrayError = (error) => {
|
|
1649
|
-
if (error.reason.kind === "NotArray") {
|
|
1650
|
-
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not an array.`;
|
|
1651
|
-
}
|
|
1652
|
-
const issue = error.reason.issues[0];
|
|
1653
|
-
switch (issue.kind) {
|
|
1654
|
-
case "Hole":
|
|
1655
|
-
return `An array element at index ${issue.index} is missing.`;
|
|
1656
|
-
case "Accessor":
|
|
1657
|
-
return `An array element at index ${issue.index} must be a data property.`;
|
|
1658
|
-
case "ExcessProperty":
|
|
1659
|
-
return "An excess Array property is not allowed. Remove it or use a different Type.";
|
|
1660
|
-
}
|
|
1661
|
-
};
|
|
1662
2927
|
const arrayRuntimeConfig = {
|
|
1663
2928
|
name: "Array",
|
|
1664
2929
|
typeByElement: arrayTypeByElement,
|
|
@@ -1666,7 +2931,22 @@ const arrayRuntimeConfig = {
|
|
|
1666
2931
|
validateItems: (value, validateElement, options) => validateArrayItems(value, validateElement, options, false),
|
|
1667
2932
|
encode: encodeArrayCollection,
|
|
1668
2933
|
is: isArrayCollection,
|
|
1669
|
-
formatError:
|
|
2934
|
+
formatError: ((error) => {
|
|
2935
|
+
if (error.reason.kind === "NotArray") {
|
|
2936
|
+
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not an array.`;
|
|
2937
|
+
}
|
|
2938
|
+
const issue = error.reason.issues[0];
|
|
2939
|
+
switch (issue.kind) {
|
|
2940
|
+
case "Hole":
|
|
2941
|
+
return `An array element at index ${issue.index} is missing.`;
|
|
2942
|
+
case "Accessor":
|
|
2943
|
+
return `An array element at index ${issue.index} must be a data property.`;
|
|
2944
|
+
case "ExcessProperty":
|
|
2945
|
+
return "An excess Array property is not allowed. Remove it or use a different Type.";
|
|
2946
|
+
case "Element":
|
|
2947
|
+
return `An array element at index ${issue.index} is invalid.`;
|
|
2948
|
+
}
|
|
2949
|
+
}),
|
|
1670
2950
|
};
|
|
1671
2951
|
const validateArrayItems = (value, validate, options, checkStructure) => validateIndexedArrayItems("Array", value, validate, options, checkStructure);
|
|
1672
2952
|
// Array and Tuple share their structural and element validation traversal.
|
|
@@ -1751,6 +3031,17 @@ const copyArrayPrefix = (value, endIndex) => {
|
|
|
1751
3031
|
* iteration order. Classification uses the realm-neutral object tag and
|
|
1752
3032
|
* prototype structure under Evolu Type's trusted JavaScript policy.
|
|
1753
3033
|
*
|
|
3034
|
+
* ### Example
|
|
3035
|
+
*
|
|
3036
|
+
* ```ts
|
|
3037
|
+
* import { String, set } from "@evolu/common";
|
|
3038
|
+
*
|
|
3039
|
+
* const Tags = set(String);
|
|
3040
|
+
* const tags = new Set(["local-first", "offline"]);
|
|
3041
|
+
*
|
|
3042
|
+
* expectOk(Tags.fromUnknown(tags), tags);
|
|
3043
|
+
* ```
|
|
3044
|
+
*
|
|
1754
3045
|
* @group Collection
|
|
1755
3046
|
*/
|
|
1756
3047
|
export const set = (element) => createHomogeneousCollectionType(element, setRuntimeConfig);
|
|
@@ -1824,16 +3115,6 @@ const isSetCollection = (value, isElement) => {
|
|
|
1824
3115
|
}
|
|
1825
3116
|
return true;
|
|
1826
3117
|
};
|
|
1827
|
-
const formatSetError = (error) => {
|
|
1828
|
-
if (error.reason.kind === "NotSet") {
|
|
1829
|
-
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Set.`;
|
|
1830
|
-
}
|
|
1831
|
-
if (error.reason.kind === "UnexpectedPrototype") {
|
|
1832
|
-
return "The value is a Set subclass, but a Set Output must be a direct Set.";
|
|
1833
|
-
}
|
|
1834
|
-
const issue = error.reason.issues[0];
|
|
1835
|
-
return `An excess Set property ${safelyStringifyUnknownValue(issue.key)} is not allowed.`;
|
|
1836
|
-
};
|
|
1837
3118
|
const hasDirectSetPrototype = (value) => {
|
|
1838
3119
|
const prototype = globalThis.Object.getPrototypeOf(value);
|
|
1839
3120
|
if (prototype === null)
|
|
@@ -1849,7 +3130,19 @@ const setRuntimeConfig = {
|
|
|
1849
3130
|
validateItems: (value, validateElement, options) => validateSetItems(value, validateElement, options, false),
|
|
1850
3131
|
encode: encodeSetCollection,
|
|
1851
3132
|
is: isSetCollection,
|
|
1852
|
-
formatError:
|
|
3133
|
+
formatError: ((error) => {
|
|
3134
|
+
if (error.reason.kind === "NotSet")
|
|
3135
|
+
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Set.`;
|
|
3136
|
+
if (error.reason.kind === "UnexpectedPrototype")
|
|
3137
|
+
return "The value is an instance of a Set subclass, but a Set Output must be a direct Set instance.";
|
|
3138
|
+
const issue = error.reason.issues[0];
|
|
3139
|
+
switch (issue.kind) {
|
|
3140
|
+
case "ExcessProperty":
|
|
3141
|
+
return `An excess Set property ${safelyStringifyUnknownValue(issue.key)} is not allowed.`;
|
|
3142
|
+
case "Element":
|
|
3143
|
+
return `A Set element at index ${issue.index} is invalid.`;
|
|
3144
|
+
}
|
|
3145
|
+
}),
|
|
1853
3146
|
};
|
|
1854
3147
|
const validateSetItems = (value, validate, options, checkStructure) => {
|
|
1855
3148
|
let issues;
|
|
@@ -1914,12 +3207,10 @@ const createTupleType = (typeElements) => {
|
|
|
1914
3207
|
const fromUnknown = (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element.fromUnknown(item, elementOptions), options);
|
|
1915
3208
|
const validateOutput = (value, options = firstValidationOptions) => validate(value, (element, item, elementOptions) => element[outputValidationSymbol](item, elementOptions), options);
|
|
1916
3209
|
const formatError = (error) => {
|
|
1917
|
-
if (error.reason.kind === "NotArray")
|
|
3210
|
+
if (error.reason.kind === "NotArray")
|
|
1918
3211
|
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a tuple.`;
|
|
1919
|
-
|
|
1920
|
-
if (error.reason.kind === "InvalidLength") {
|
|
3212
|
+
if (error.reason.kind === "InvalidLength")
|
|
1921
3213
|
return `A Tuple must contain exactly ${error.reason.expected} elements, but the value contains ${error.reason.actual}.`;
|
|
1922
|
-
}
|
|
1923
3214
|
const issue = error.reason.issues[0];
|
|
1924
3215
|
switch (issue.kind) {
|
|
1925
3216
|
case "Hole":
|
|
@@ -1928,6 +3219,8 @@ const createTupleType = (typeElements) => {
|
|
|
1928
3219
|
return `A Tuple element at index ${issue.index} must be a data property.`;
|
|
1929
3220
|
case "ExcessProperty":
|
|
1930
3221
|
return "An excess Tuple property is not allowed. Remove it or use a different Type.";
|
|
3222
|
+
case "Element":
|
|
3223
|
+
return `A Tuple element at index ${issue.index} is invalid.`;
|
|
1931
3224
|
}
|
|
1932
3225
|
};
|
|
1933
3226
|
const rootElements = typeElements.map(getTerminalRuntimeNode);
|
|
@@ -1974,6 +3267,55 @@ const createTupleType = (typeElements) => {
|
|
|
1974
3267
|
return createTypeNode("Tuple", parent, fromUnknown, is, validateOutput, from, to, getTypeIssues, { elements: typeElements });
|
|
1975
3268
|
};
|
|
1976
3269
|
const validateTupleItems = (value, elements, validate, options, checkStructure) => validateIndexedArrayItems("Tuple", value, (value, elementOptions, index) => validate(elements[index], value, elementOptions, index), options, checkStructure);
|
|
3270
|
+
/**
|
|
3271
|
+
* Decimal digit from `"0"` to `"9"`.
|
|
3272
|
+
*
|
|
3273
|
+
* @group String
|
|
3274
|
+
*/
|
|
3275
|
+
export const Digit = /*#__PURE__*/ union("0", "1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
3276
|
+
/**
|
|
3277
|
+
* Decimal digit from `"1"` to `"9"`.
|
|
3278
|
+
*
|
|
3279
|
+
* @group String
|
|
3280
|
+
*/
|
|
3281
|
+
export const Digit1To9 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6", "7", "8", "9");
|
|
3282
|
+
/**
|
|
3283
|
+
* Decimal string from `"1"` to `"6"`.
|
|
3284
|
+
*
|
|
3285
|
+
* @group String
|
|
3286
|
+
*/
|
|
3287
|
+
export const Digit1To6 = /*#__PURE__*/ union("1", "2", "3", "4", "5", "6");
|
|
3288
|
+
/**
|
|
3289
|
+
* Decimal string from `"1"` to `"23"`.
|
|
3290
|
+
*
|
|
3291
|
+
* @group String
|
|
3292
|
+
*/
|
|
3293
|
+
export const Digit1To23 = /*#__PURE__*/ union(Digit1To9,
|
|
3294
|
+
/*#__PURE__*/ templateLiteral("1", Digit),
|
|
3295
|
+
/*#__PURE__*/ templateLiteral("2", /*#__PURE__*/ union("0", "1", "2", "3")));
|
|
3296
|
+
/**
|
|
3297
|
+
* Decimal string from `"1"` to `"51"`.
|
|
3298
|
+
*
|
|
3299
|
+
* @group String
|
|
3300
|
+
*/
|
|
3301
|
+
export const Digit1To51 = /*#__PURE__*/ union(Digit1To9,
|
|
3302
|
+
/*#__PURE__*/ templateLiteral(/*#__PURE__*/ union("1", "2", "3", "4"), Digit),
|
|
3303
|
+
/*#__PURE__*/ templateLiteral("5", /*#__PURE__*/ union("0", "1")));
|
|
3304
|
+
/**
|
|
3305
|
+
* Decimal string from `"1"` to `"99"`.
|
|
3306
|
+
*
|
|
3307
|
+
* @group String
|
|
3308
|
+
*/
|
|
3309
|
+
export const Digit1To99 = /*#__PURE__*/ union(Digit1To9,
|
|
3310
|
+
/*#__PURE__*/ templateLiteral(Digit1To9, Digit));
|
|
3311
|
+
/**
|
|
3312
|
+
* Decimal string from `"1"` to `"59"`.
|
|
3313
|
+
*
|
|
3314
|
+
* @group String
|
|
3315
|
+
*/
|
|
3316
|
+
export const Digit1To59 = /*#__PURE__*/ union(Digit1To9,
|
|
3317
|
+
/*#__PURE__*/ templateLiteral(
|
|
3318
|
+
/*#__PURE__*/ union("1", "2", "3", "4", "5"), Digit));
|
|
1977
3319
|
const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (error, mode) => {
|
|
1978
3320
|
const objectError = error;
|
|
1979
3321
|
if (objectError.reason.kind !== "Properties") {
|
|
@@ -2014,21 +3356,9 @@ const createObjectRuntimeTypeIssues = (defaultFormatter, props, recordType) => (
|
|
|
2014
3356
|
return recordType[getRuntimeTypeIssuesSymbol](propertyError, mode);
|
|
2015
3357
|
});
|
|
2016
3358
|
};
|
|
2017
|
-
const
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
}
|
|
2021
|
-
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
2022
|
-
assertNonNullable(key);
|
|
2023
|
-
const propertyError = error.reason.errors[key];
|
|
2024
|
-
assertNonNullable(propertyError);
|
|
2025
|
-
if (propertyError.type === "ObjectPropertyAccess") {
|
|
2026
|
-
return propertyError.reason === "Accessor"
|
|
2027
|
-
? "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type."
|
|
2028
|
-
: "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
2029
|
-
}
|
|
2030
|
-
return "An Object property key must be a string. Remove it or use a different Type.";
|
|
2031
|
-
};
|
|
3359
|
+
const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
3360
|
+
? `A value ${safelyStringifyUnknownValue(reason.value)} is not an object.`
|
|
3361
|
+
: "The value is an object, but an Object Output must be a plain object or have a null prototype.";
|
|
2032
3362
|
/**
|
|
2033
3363
|
* A {@link Type} for readonly plain objects with unknown property values.
|
|
2034
3364
|
*
|
|
@@ -2043,7 +3373,7 @@ const formatPlainObjectError = (error) => {
|
|
|
2043
3373
|
* values have their own Types, and {@link instanceOf} when an instance belongs
|
|
2044
3374
|
* to the domain.
|
|
2045
3375
|
*
|
|
2046
|
-
* @group Base
|
|
3376
|
+
* @group Base
|
|
2047
3377
|
*/
|
|
2048
3378
|
export const Object = /*#__PURE__*/ createRootType("Object", (value, options = firstValidationOptions) => {
|
|
2049
3379
|
if (value === null || typeof value !== "object") {
|
|
@@ -2093,15 +3423,56 @@ export const Object = /*#__PURE__*/ createRootType("Object", (value, options = f
|
|
|
2093
3423
|
type: "Object",
|
|
2094
3424
|
reason: { kind: "Properties", errors },
|
|
2095
3425
|
});
|
|
2096
|
-
},
|
|
2097
|
-
|
|
3426
|
+
}, (error) => {
|
|
3427
|
+
if (error.reason.kind !== "Properties")
|
|
3428
|
+
return formatPlainObjectRootError(error.reason);
|
|
3429
|
+
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
3430
|
+
assertNonNullable(key);
|
|
3431
|
+
const propertyError = error.reason.errors[key];
|
|
3432
|
+
assertNonNullable(propertyError);
|
|
3433
|
+
if (propertyError.type === "ObjectPropertyAccess") {
|
|
3434
|
+
switch (propertyError.reason) {
|
|
3435
|
+
case "Accessor":
|
|
3436
|
+
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3437
|
+
case "NonEnumerable":
|
|
3438
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3439
|
+
}
|
|
3440
|
+
}
|
|
3441
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
3442
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
3443
|
+
if (typeof key === "symbol")
|
|
3444
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3445
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
3446
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3447
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3448
|
+
},
|
|
3449
|
+
/*#__PURE__*/ createObjectRuntimeTypeIssues(((error) => {
|
|
3450
|
+
if (error.reason.kind !== "Properties")
|
|
3451
|
+
return formatPlainObjectRootError(error.reason);
|
|
3452
|
+
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
3453
|
+
assertNonNullable(key);
|
|
3454
|
+
const propertyError = error.reason.errors[key];
|
|
3455
|
+
assertNonNullable(propertyError);
|
|
3456
|
+
if (propertyError.type === "ObjectPropertyAccess") {
|
|
3457
|
+
switch (propertyError.reason) {
|
|
3458
|
+
case "Accessor":
|
|
3459
|
+
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3460
|
+
case "NonEnumerable":
|
|
3461
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
3462
|
+
}
|
|
3463
|
+
}
|
|
3464
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
3465
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
3466
|
+
if (typeof key === "symbol")
|
|
3467
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3468
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
3469
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3470
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
3471
|
+
})));
|
|
2098
3472
|
const isPlainObject = (value) => {
|
|
2099
3473
|
const prototype = globalThis.Object.getPrototypeOf(value);
|
|
2100
3474
|
return (prototype === null || globalThis.Object.getPrototypeOf(prototype) === null);
|
|
2101
3475
|
};
|
|
2102
|
-
const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
2103
|
-
? `A value ${safelyStringifyUnknownValue(reason.value)} is not an object.`
|
|
2104
|
-
: "The value is an object, but an Object Output must be a plain object or have a null prototype.";
|
|
2105
3476
|
/**
|
|
2106
3477
|
* Record {@link Type}.
|
|
2107
3478
|
*
|
|
@@ -2132,20 +3503,13 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
2132
3503
|
*
|
|
2133
3504
|
* ```ts
|
|
2134
3505
|
* import {
|
|
2135
|
-
*
|
|
3506
|
+
* Int64FromInt64String,
|
|
2136
3507
|
* String,
|
|
2137
|
-
* ok,
|
|
2138
3508
|
* record,
|
|
2139
|
-
*
|
|
2140
|
-
* type Result,
|
|
3509
|
+
* type Int64,
|
|
2141
3510
|
* } from "@evolu/common";
|
|
2142
3511
|
*
|
|
2143
|
-
* const
|
|
2144
|
-
* from: (value) => ok(globalThis.Number(value)),
|
|
2145
|
-
* to: globalThis.String,
|
|
2146
|
-
* });
|
|
2147
|
-
*
|
|
2148
|
-
* const ScoresByUser = record(String, ScoreFromString);
|
|
3512
|
+
* const ScoresByUser = record(String, Int64FromInt64String);
|
|
2149
3513
|
* type ScoresByUser = typeof ScoresByUser.Output;
|
|
2150
3514
|
*
|
|
2151
3515
|
* // Validate an unknown value.
|
|
@@ -2154,7 +3518,7 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
2154
3518
|
* grace: "20",
|
|
2155
3519
|
* });
|
|
2156
3520
|
*
|
|
2157
|
-
* expectOk(scoresFromUnknown, { ada:
|
|
3521
|
+
* expectOk(scoresFromUnknown, { ada: 10n, grace: 20n });
|
|
2158
3522
|
*
|
|
2159
3523
|
* // Validate keys and values with their root Types.
|
|
2160
3524
|
* const scoresInput = ScoresByUser.parent.fromUnknown({
|
|
@@ -2166,11 +3530,10 @@ const formatPlainObjectRootError = (reason) => reason.kind === "NotObject"
|
|
|
2166
3530
|
* // Run the remaining key and value stages.
|
|
2167
3531
|
* const scoresFromInput = ScoresByUser.from.parent(scoresInput.value);
|
|
2168
3532
|
*
|
|
2169
|
-
*
|
|
2170
|
-
*
|
|
3533
|
+
* expectOk(scoresFromInput, { ada: 10n, grace: 20n });
|
|
3534
|
+
* expectTypeOf(scoresFromInput.value).toEqualTypeOf<
|
|
3535
|
+
* Readonly<Partial<Record<string, Int64>>>
|
|
2171
3536
|
* >();
|
|
2172
|
-
*
|
|
2173
|
-
* expectOk(scoresFromInput, { ada: 10, grace: 20 });
|
|
2174
3537
|
* ```
|
|
2175
3538
|
*
|
|
2176
3539
|
* Note that TypeScript does not model an object's runtime prototype. This can
|
|
@@ -2234,18 +3597,20 @@ export const record = (key, value) => {
|
|
|
2234
3597
|
const fromUnknown = (input, options = firstValidationOptions) => validate(input, typeKey.fromUnknown, typeValue.fromUnknown, options);
|
|
2235
3598
|
const validateOutput = (input, options = firstValidationOptions) => validate(input, typeKey[outputValidationSymbol], typeValue[outputValidationSymbol], options);
|
|
2236
3599
|
const formatError = (error) => {
|
|
2237
|
-
if (error.reason.kind === "NotRecord")
|
|
2238
|
-
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a
|
|
2239
|
-
|
|
2240
|
-
if (error.reason.kind === "NotPlainRecord") {
|
|
3600
|
+
if (error.reason.kind === "NotRecord")
|
|
3601
|
+
return `A value ${safelyStringifyUnknownValue(error.reason.value)} is not a Record.`;
|
|
3602
|
+
if (error.reason.kind === "NotPlainRecord")
|
|
2241
3603
|
return "The value is an object, but a Record Output must be a plain object or have a null prototype.";
|
|
2242
|
-
}
|
|
2243
3604
|
const issue = error.reason.issues[0];
|
|
2244
3605
|
switch (issue.kind) {
|
|
3606
|
+
case "Key":
|
|
3607
|
+
return `Property key ${safelyStringifyUnknownValue(issue.key)} is invalid.`;
|
|
3608
|
+
case "Value":
|
|
3609
|
+
return `The value of property ${safelyStringifyUnknownValue(issue.key)} is invalid.`;
|
|
2245
3610
|
case "Accessor":
|
|
2246
|
-
return `A
|
|
3611
|
+
return `A Record property ${safelyStringifyUnknownValue(issue.key)} must be a data property.`;
|
|
2247
3612
|
case "NonEnumerable":
|
|
2248
|
-
return `A
|
|
3613
|
+
return `A Record property ${safelyStringifyUnknownValue(issue.key)} must be enumerable.`;
|
|
2249
3614
|
case "Collision":
|
|
2250
3615
|
return `Record keys ${safelyStringifyUnknownValue(issue.previousKey)} and ${safelyStringifyUnknownValue(issue.key)} decode to the same key ${safelyStringifyUnknownValue(issue.outputKey)}.`;
|
|
2251
3616
|
}
|
|
@@ -2557,25 +3922,27 @@ const createObjectType = (props, recordType) => {
|
|
|
2557
3922
|
const fromUnknown = (value, options = firstValidationOptions) => validate(value, options, false);
|
|
2558
3923
|
const validateOutput = (value, options = firstValidationOptions) => validate(value, options, true);
|
|
2559
3924
|
const formatError = (error) => {
|
|
2560
|
-
if (error.reason.kind !== "Properties")
|
|
3925
|
+
if (error.reason.kind !== "Properties")
|
|
2561
3926
|
return formatPlainObjectRootError(error.reason);
|
|
2562
|
-
}
|
|
2563
3927
|
const key = Reflect.ownKeys(error.reason.errors).at(0);
|
|
2564
3928
|
assertNonNullable(key);
|
|
2565
3929
|
const propertyError = error.reason.errors[key];
|
|
3930
|
+
assertNonNullable(propertyError);
|
|
2566
3931
|
if (propertyError.type === "ObjectPropertyAccess") {
|
|
2567
3932
|
switch (propertyError.reason) {
|
|
2568
3933
|
case "Accessor":
|
|
2569
3934
|
return "An Object property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
2570
3935
|
case "NonEnumerable":
|
|
2571
|
-
return "An Object property must be enumerable.";
|
|
3936
|
+
return "An Object property must be enumerable. Make it enumerable or use a different Type.";
|
|
2572
3937
|
}
|
|
2573
3938
|
}
|
|
2574
|
-
if (
|
|
2575
|
-
|
|
2576
|
-
|
|
2577
|
-
|
|
2578
|
-
|
|
3939
|
+
if (propertyError.type === "ObjectMissingProperty")
|
|
3940
|
+
return `The required property ${safelyStringifyUnknownValue(key)} is missing.`;
|
|
3941
|
+
if (typeof key === "symbol")
|
|
3942
|
+
return "An Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3943
|
+
if (propertyError.type === "ObjectExcessProperty")
|
|
3944
|
+
return `The property ${safelyStringifyUnknownValue(key)} is not allowed. Remove it or use a different Type.`;
|
|
3945
|
+
return `The property ${safelyStringifyUnknownValue(key)} is invalid.`;
|
|
2579
3946
|
};
|
|
2580
3947
|
const rootProps = createMutableRecord();
|
|
2581
3948
|
let hasNonRootType = false;
|
|
@@ -2746,7 +4113,7 @@ const createRecordPropertyError = (issue) => ({
|
|
|
2746
4113
|
reason: { kind: "Entries", issues: [issue] },
|
|
2747
4114
|
});
|
|
2748
4115
|
/**
|
|
2749
|
-
*
|
|
4116
|
+
* Object {@link Type} with every property optional.
|
|
2750
4117
|
*
|
|
2751
4118
|
* No property is required, but every present property must still satisfy its
|
|
2752
4119
|
* Type.
|
|
@@ -2777,7 +4144,8 @@ export const partial = (props, ..._validation) => {
|
|
|
2777
4144
|
return createObjectType(partialProps);
|
|
2778
4145
|
};
|
|
2779
4146
|
/**
|
|
2780
|
-
*
|
|
4147
|
+
* Object {@link Type} making every property whose Union Type includes
|
|
4148
|
+
* {@link Null} optional.
|
|
2781
4149
|
*
|
|
2782
4150
|
* The property retains its original Union Type, so consumers may omit it, set
|
|
2783
4151
|
* it to `null`, or provide any other member of that Union. Properties without
|
|
@@ -2822,7 +4190,18 @@ export const nullableToOptional = (props, ..._validation) => {
|
|
|
2822
4190
|
return createObjectType(optionalProps);
|
|
2823
4191
|
};
|
|
2824
4192
|
/**
|
|
2825
|
-
*
|
|
4193
|
+
* Object {@link Type} without the selected declared properties.
|
|
4194
|
+
*
|
|
4195
|
+
* ### Example
|
|
4196
|
+
*
|
|
4197
|
+
* ```ts
|
|
4198
|
+
* import { String, object, omit } from "@evolu/common";
|
|
4199
|
+
*
|
|
4200
|
+
* const User = object({ name: String, password: String });
|
|
4201
|
+
* const PublicUser = omit(User, "password");
|
|
4202
|
+
*
|
|
4203
|
+
* expectOk(PublicUser.fromUnknown({ name: "Ada" }), { name: "Ada" });
|
|
4204
|
+
* ```
|
|
2826
4205
|
*
|
|
2827
4206
|
* @group Objects
|
|
2828
4207
|
*/
|
|
@@ -2976,22 +4355,21 @@ export function discriminatedUnion(...keyOrMembers) {
|
|
|
2976
4355
|
};
|
|
2977
4356
|
const route = (value) => membersByDiscriminator.get(Reflect.get(value, key));
|
|
2978
4357
|
const formatError = (error) => {
|
|
2979
|
-
|
|
2980
|
-
switch (reason.kind) {
|
|
4358
|
+
switch (error.reason.kind) {
|
|
2981
4359
|
case "Object":
|
|
2982
|
-
return formatPlainObjectRootError(reason.error.reason);
|
|
4360
|
+
return formatPlainObjectRootError(error.reason.error.reason);
|
|
2983
4361
|
case "PropertyAccess": {
|
|
2984
|
-
const property = `The discriminator property ${safelyStringifyUnknownValue(reason.key)}`;
|
|
2985
|
-
if (reason.reason === "Accessor")
|
|
4362
|
+
const property = `The discriminator property ${safelyStringifyUnknownValue(error.reason.key)}`;
|
|
4363
|
+
if (error.reason.reason === "Accessor")
|
|
2986
4364
|
return `${property} must be a data property.`;
|
|
2987
|
-
|
|
2988
|
-
if (reason.reason === "Inherited") {
|
|
4365
|
+
if (error.reason.reason === "Inherited")
|
|
2989
4366
|
return `${property} must be an own property.`;
|
|
2990
|
-
}
|
|
2991
4367
|
return `${property} must be enumerable.`;
|
|
2992
4368
|
}
|
|
2993
4369
|
case "Discriminator":
|
|
2994
|
-
return `The discriminator property ${safelyStringifyUnknownValue(reason.key)} has an unexpected value ${safelyStringifyUnknownValue(reason.value)}.`;
|
|
4370
|
+
return `The discriminator property ${safelyStringifyUnknownValue(error.reason.key)} has an unexpected value ${safelyStringifyUnknownValue(error.reason.value)}.`;
|
|
4371
|
+
case "Member":
|
|
4372
|
+
return `The selected variant ${safelyStringifyUnknownValue(error.reason.discriminator)} is invalid.`;
|
|
2995
4373
|
}
|
|
2996
4374
|
};
|
|
2997
4375
|
const defaultFormatter = formatError;
|
|
@@ -3272,29 +4650,6 @@ const validateJsonValue = (value, options = firstValidationOptions) => {
|
|
|
3272
4650
|
},
|
|
3273
4651
|
});
|
|
3274
4652
|
};
|
|
3275
|
-
const formatJsonValueError = (error) => {
|
|
3276
|
-
const issue = error.reason.issues[0];
|
|
3277
|
-
switch (issue.kind) {
|
|
3278
|
-
case "InvalidType":
|
|
3279
|
-
return `A value ${safelyStringifyUnknownValue(issue.value)} is not a JSON value.`;
|
|
3280
|
-
case "NonFiniteNumber":
|
|
3281
|
-
return "A JSON number must be finite.";
|
|
3282
|
-
case "UnexpectedPrototype":
|
|
3283
|
-
return "The value is an object, but a JsonValue Object must be a plain object or have a null prototype.";
|
|
3284
|
-
case "Accessor":
|
|
3285
|
-
return "A JSON property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
3286
|
-
case "NonEnumerable":
|
|
3287
|
-
return "A JSON Object property must be enumerable. Remove it or use a different Type.";
|
|
3288
|
-
case "SymbolProperty":
|
|
3289
|
-
return "A JSON Object property key must be a string. Remove the symbol property or use a different Type.";
|
|
3290
|
-
case "Hole":
|
|
3291
|
-
return "A JSON Array element is missing.";
|
|
3292
|
-
case "ExcessProperty":
|
|
3293
|
-
return "An excess JSON Array property is not allowed. Remove it or use a different Type.";
|
|
3294
|
-
case "CircularReference":
|
|
3295
|
-
return "A JsonValue must not contain circular references.";
|
|
3296
|
-
}
|
|
3297
|
-
};
|
|
3298
4653
|
const getJsonValueRuntimeTypeIssues = (error, mode) => {
|
|
3299
4654
|
const jsonValueError = error;
|
|
3300
4655
|
const issues = mode === "first"
|
|
@@ -3309,7 +4664,29 @@ const getJsonValueRuntimeTypeIssues = (error, mode) => {
|
|
|
3309
4664
|
reason: { kind: "Issues", issues: [issue] },
|
|
3310
4665
|
},
|
|
3311
4666
|
path: issue.path,
|
|
3312
|
-
formatError:
|
|
4667
|
+
formatError: ((error) => {
|
|
4668
|
+
const issue = error.reason.issues[0];
|
|
4669
|
+
switch (issue.kind) {
|
|
4670
|
+
case "InvalidType":
|
|
4671
|
+
return `A value ${safelyStringifyUnknownValue(issue.value)} is not a JSON value.`;
|
|
4672
|
+
case "NonFiniteNumber":
|
|
4673
|
+
return "A JSON number must be finite.";
|
|
4674
|
+
case "UnexpectedPrototype":
|
|
4675
|
+
return "The value is an object, but a JsonValue object must be a plain object or have a null prototype.";
|
|
4676
|
+
case "Accessor":
|
|
4677
|
+
return "A JSON property must be a data property. Materialize accessor values into plain data before using this Type or use a different Type.";
|
|
4678
|
+
case "NonEnumerable":
|
|
4679
|
+
return "A JSON object property must be enumerable. Remove it or use a different Type.";
|
|
4680
|
+
case "SymbolProperty":
|
|
4681
|
+
return "A JSON object property key must be a string. Remove the symbol property or use a different Type.";
|
|
4682
|
+
case "Hole":
|
|
4683
|
+
return "A JSON array element is missing.";
|
|
4684
|
+
case "ExcessProperty":
|
|
4685
|
+
return "An excess JSON array property is not allowed. Remove it or use a different Type.";
|
|
4686
|
+
case "CircularReference":
|
|
4687
|
+
return "A JsonValue must not contain circular references.";
|
|
4688
|
+
}
|
|
4689
|
+
}),
|
|
3313
4690
|
}));
|
|
3314
4691
|
};
|
|
3315
4692
|
const parseJson = (value) => globalThis.JSON.parse(value);
|
|
@@ -3379,6 +4756,21 @@ const stringifyJsonValue = (value) => {
|
|
|
3379
4756
|
/**
|
|
3380
4757
|
* Exact root Type for {@link JsonValue} data trees.
|
|
3381
4758
|
*
|
|
4759
|
+
* ### Example
|
|
4760
|
+
*
|
|
4761
|
+
* ```ts
|
|
4762
|
+
* import { JsonValue, type JsonValueInput } from "@evolu/common";
|
|
4763
|
+
*
|
|
4764
|
+
* const input: JsonValueInput = {
|
|
4765
|
+
* name: "Ada",
|
|
4766
|
+
* scores: [10, 20],
|
|
4767
|
+
* };
|
|
4768
|
+
* const result = JsonValue.fromUnknown(input);
|
|
4769
|
+
*
|
|
4770
|
+
* expectOk(result, input);
|
|
4771
|
+
* expectTypeOf(result.value).toEqualTypeOf<JsonValue>();
|
|
4772
|
+
* ```
|
|
4773
|
+
*
|
|
3382
4774
|
* @group JSON
|
|
3383
4775
|
*/
|
|
3384
4776
|
export const JsonValue =
|
|
@@ -3411,21 +4803,52 @@ export const Json = /*#__PURE__*/ brand("Json", String, (value) => {
|
|
|
3411
4803
|
/**
|
|
3412
4804
|
* Totally parses proven {@link Json} text into an exact {@link JsonValue}.
|
|
3413
4805
|
*
|
|
4806
|
+
* ### Example
|
|
4807
|
+
*
|
|
4808
|
+
* ```ts
|
|
4809
|
+
* import { Json, jsonToJsonValue } from "@evolu/common";
|
|
4810
|
+
*
|
|
4811
|
+
* const value = jsonToJsonValue(Json.orThrow('{"name":"Ada"}'));
|
|
4812
|
+
*
|
|
4813
|
+
* expect(value).toEqual({ name: "Ada" });
|
|
4814
|
+
* ```
|
|
4815
|
+
*
|
|
3414
4816
|
* @group JSON
|
|
3415
4817
|
*/
|
|
3416
4818
|
export const jsonToJsonValue = (value) => parseJson(value);
|
|
3417
4819
|
/**
|
|
3418
4820
|
* Totally encodes an exact {@link JsonValue} as canonical {@link Json} text.
|
|
3419
4821
|
*
|
|
4822
|
+
* ### Example
|
|
4823
|
+
*
|
|
4824
|
+
* ```ts
|
|
4825
|
+
* import { jsonValueToJson, type JsonValue } from "@evolu/common";
|
|
4826
|
+
*
|
|
4827
|
+
* const value: JsonValue = { name: "Ada" };
|
|
4828
|
+
*
|
|
4829
|
+
* expect(jsonValueToJson(value)).toBe('{"name":"Ada"}');
|
|
4830
|
+
* ```
|
|
4831
|
+
*
|
|
3420
4832
|
* @group JSON
|
|
3421
4833
|
*/
|
|
3422
4834
|
export const jsonValueToJson = (value) => stringifyJsonValue(JsonValue.to(value));
|
|
3423
4835
|
/**
|
|
3424
|
-
* {@link Json}
|
|
4836
|
+
* Transformation {@link Type} that parses {@link Json} into {@link JsonValue}.
|
|
3425
4837
|
*
|
|
3426
4838
|
* Decoding unknown input first validates the Json Brand. Starting from the
|
|
3427
4839
|
* typed Json parent is infallible. Encoding canonicalizes JSON text.
|
|
3428
4840
|
*
|
|
4841
|
+
* ### Example
|
|
4842
|
+
*
|
|
4843
|
+
* ```ts
|
|
4844
|
+
* import { JsonValueFromJson } from "@evolu/common";
|
|
4845
|
+
*
|
|
4846
|
+
* const result = JsonValueFromJson.fromUnknown('{ "name": "Ada" }');
|
|
4847
|
+
*
|
|
4848
|
+
* expectOk(result, { name: "Ada" });
|
|
4849
|
+
* expect(JsonValueFromJson.to(result.value)).toBe('{"name":"Ada"}');
|
|
4850
|
+
* ```
|
|
4851
|
+
*
|
|
3429
4852
|
* @group JSON
|
|
3430
4853
|
*/
|
|
3431
4854
|
export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Json, JsonValue, {
|
|
@@ -3433,7 +4856,7 @@ export const JsonValueFromJson = /*#__PURE__*/ transform("JsonValueFromJson", Js
|
|
|
3433
4856
|
to: stringifyJsonValue,
|
|
3434
4857
|
});
|
|
3435
4858
|
/**
|
|
3436
|
-
*
|
|
4859
|
+
* Branded {@link Json} Type and total conversions for another Type.
|
|
3437
4860
|
*
|
|
3438
4861
|
* Use this factory when a domain value must be stored as JSON text while its
|
|
3439
4862
|
* exact Type remains visible to TypeScript, such as a JSON column in an Evolu
|