@evolu/common 8.9.0 → 8.10.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/Bytes.d.ts +647 -0
- package/dist/src/Bytes.d.ts.map +1 -0
- package/dist/src/{Binary.js → Bytes.js} +266 -16
- package/dist/src/Config.d.ts +142 -0
- package/dist/src/Config.d.ts.map +1 -0
- package/dist/src/Config.js +181 -0
- package/dist/src/Fs.d.ts +302 -0
- package/dist/src/Fs.d.ts.map +1 -0
- package/dist/src/Fs.js +111 -0
- package/dist/src/Number.d.ts +50 -7
- package/dist/src/Number.d.ts.map +1 -1
- package/dist/src/Number.js +47 -8
- package/dist/src/Object.d.ts +32 -0
- package/dist/src/Object.d.ts.map +1 -1
- package/dist/src/Object.js +41 -0
- package/dist/src/Sqlite.js +1 -1
- package/dist/src/Time.d.ts +74 -12
- package/dist/src/Time.d.ts.map +1 -1
- package/dist/src/Time.js +40 -2
- package/dist/src/Type.d.ts +1451 -79
- package/dist/src/Type.d.ts.map +1 -1
- package/dist/src/Type.js +1303 -96
- package/dist/src/index.d.ts +3 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -1
- package/dist/src/intl/_en.d.ts +24 -1
- package/dist/src/intl/_en.d.ts.map +1 -1
- package/dist/src/intl/_en.js +20 -0
- package/dist/src/intl/ar.d.ts +24 -1
- package/dist/src/intl/ar.d.ts.map +1 -1
- package/dist/src/intl/ar.js +20 -0
- package/dist/src/intl/bn.d.ts +24 -1
- package/dist/src/intl/bn.d.ts.map +1 -1
- package/dist/src/intl/bn.js +20 -0
- package/dist/src/intl/ca.d.ts +24 -1
- package/dist/src/intl/ca.d.ts.map +1 -1
- package/dist/src/intl/ca.js +20 -0
- package/dist/src/intl/cs.d.ts +24 -1
- package/dist/src/intl/cs.d.ts.map +1 -1
- package/dist/src/intl/cs.js +20 -0
- package/dist/src/intl/da.d.ts +24 -1
- package/dist/src/intl/da.d.ts.map +1 -1
- package/dist/src/intl/da.js +20 -0
- package/dist/src/intl/de.d.ts +24 -1
- package/dist/src/intl/de.d.ts.map +1 -1
- package/dist/src/intl/de.js +20 -0
- package/dist/src/intl/el.d.ts +24 -1
- package/dist/src/intl/el.d.ts.map +1 -1
- package/dist/src/intl/el.js +20 -0
- package/dist/src/intl/es.d.ts +24 -1
- package/dist/src/intl/es.d.ts.map +1 -1
- package/dist/src/intl/es.js +20 -0
- package/dist/src/intl/fa.d.ts +24 -1
- package/dist/src/intl/fa.d.ts.map +1 -1
- package/dist/src/intl/fa.js +20 -0
- package/dist/src/intl/fi.d.ts +24 -1
- package/dist/src/intl/fi.d.ts.map +1 -1
- package/dist/src/intl/fi.js +20 -0
- package/dist/src/intl/fil.d.ts +24 -1
- package/dist/src/intl/fil.d.ts.map +1 -1
- package/dist/src/intl/fil.js +20 -0
- package/dist/src/intl/fr.d.ts +24 -1
- package/dist/src/intl/fr.d.ts.map +1 -1
- package/dist/src/intl/fr.js +20 -0
- package/dist/src/intl/he.d.ts +24 -1
- package/dist/src/intl/he.d.ts.map +1 -1
- package/dist/src/intl/he.js +20 -0
- package/dist/src/intl/hi.d.ts +24 -1
- package/dist/src/intl/hi.d.ts.map +1 -1
- package/dist/src/intl/hi.js +20 -0
- package/dist/src/intl/hr.d.ts +24 -1
- package/dist/src/intl/hr.d.ts.map +1 -1
- package/dist/src/intl/hr.js +20 -0
- package/dist/src/intl/hu.d.ts +22 -1
- package/dist/src/intl/hu.d.ts.map +1 -1
- package/dist/src/intl/hu.js +18 -0
- package/dist/src/intl/id.d.ts +24 -1
- package/dist/src/intl/id.d.ts.map +1 -1
- package/dist/src/intl/id.js +20 -0
- package/dist/src/intl/it.d.ts +24 -1
- package/dist/src/intl/it.d.ts.map +1 -1
- package/dist/src/intl/it.js +20 -0
- package/dist/src/intl/ja.d.ts +24 -1
- package/dist/src/intl/ja.d.ts.map +1 -1
- package/dist/src/intl/ja.js +20 -0
- package/dist/src/intl/ko.d.ts +24 -1
- package/dist/src/intl/ko.d.ts.map +1 -1
- package/dist/src/intl/ko.js +20 -0
- package/dist/src/intl/ml.d.ts +24 -1
- package/dist/src/intl/ml.d.ts.map +1 -1
- package/dist/src/intl/ml.js +20 -0
- package/dist/src/intl/mr.d.ts +24 -1
- package/dist/src/intl/mr.d.ts.map +1 -1
- package/dist/src/intl/mr.js +20 -0
- package/dist/src/intl/ms.d.ts +24 -1
- package/dist/src/intl/ms.d.ts.map +1 -1
- package/dist/src/intl/ms.js +20 -0
- package/dist/src/intl/nb.d.ts +22 -1
- package/dist/src/intl/nb.d.ts.map +1 -1
- package/dist/src/intl/nb.js +18 -0
- package/dist/src/intl/nl.d.ts +24 -1
- package/dist/src/intl/nl.d.ts.map +1 -1
- package/dist/src/intl/nl.js +20 -0
- package/dist/src/intl/pa.d.ts +24 -1
- package/dist/src/intl/pa.d.ts.map +1 -1
- package/dist/src/intl/pa.js +20 -0
- package/dist/src/intl/pl.d.ts +23 -0
- package/dist/src/intl/pl.d.ts.map +1 -1
- package/dist/src/intl/pl.js +20 -0
- package/dist/src/intl/pt-BR.d.ts +24 -1
- package/dist/src/intl/pt-BR.d.ts.map +1 -1
- package/dist/src/intl/pt-BR.js +20 -0
- package/dist/src/intl/pt.d.ts +24 -1
- package/dist/src/intl/pt.d.ts.map +1 -1
- package/dist/src/intl/pt.js +20 -0
- package/dist/src/intl/ro.d.ts +24 -1
- package/dist/src/intl/ro.d.ts.map +1 -1
- package/dist/src/intl/ro.js +20 -0
- package/dist/src/intl/sk.d.ts +24 -1
- package/dist/src/intl/sk.d.ts.map +1 -1
- package/dist/src/intl/sk.js +20 -0
- package/dist/src/intl/sl.d.ts +24 -1
- package/dist/src/intl/sl.d.ts.map +1 -1
- package/dist/src/intl/sl.js +20 -0
- package/dist/src/intl/sv.d.ts +24 -1
- package/dist/src/intl/sv.d.ts.map +1 -1
- package/dist/src/intl/sv.js +20 -0
- package/dist/src/intl/sw.d.ts +21 -0
- package/dist/src/intl/sw.d.ts.map +1 -1
- package/dist/src/intl/sw.js +18 -0
- package/dist/src/intl/ta.d.ts +24 -1
- package/dist/src/intl/ta.d.ts.map +1 -1
- package/dist/src/intl/ta.js +20 -0
- package/dist/src/intl/te.d.ts +24 -1
- package/dist/src/intl/te.d.ts.map +1 -1
- package/dist/src/intl/te.js +20 -0
- package/dist/src/intl/th.d.ts +24 -1
- package/dist/src/intl/th.d.ts.map +1 -1
- package/dist/src/intl/th.js +20 -0
- package/dist/src/intl/tr.d.ts +24 -1
- package/dist/src/intl/tr.d.ts.map +1 -1
- package/dist/src/intl/tr.js +20 -0
- package/dist/src/intl/uk.d.ts +80 -57
- package/dist/src/intl/uk.d.ts.map +1 -1
- package/dist/src/intl/uk.js +174 -149
- package/dist/src/intl/ur.d.ts +24 -1
- package/dist/src/intl/ur.d.ts.map +1 -1
- package/dist/src/intl/ur.js +20 -0
- package/dist/src/intl/vi.d.ts +24 -1
- package/dist/src/intl/vi.d.ts.map +1 -1
- package/dist/src/intl/vi.js +20 -0
- package/dist/src/intl/zh-CN.d.ts +24 -1
- package/dist/src/intl/zh-CN.d.ts.map +1 -1
- package/dist/src/intl/zh-CN.js +20 -0
- package/dist/src/intl/zh-TW.d.ts +24 -1
- package/dist/src/intl/zh-TW.d.ts.map +1 -1
- package/dist/src/intl/zh-TW.js +20 -0
- package/dist/src/local-first/Protocol.d.ts +1 -1
- package/dist/src/local-first/Protocol.d.ts.map +1 -1
- package/dist/src/local-first/Protocol.js +1 -1
- package/dist/src/local-first/Schema.d.ts +10 -0
- package/dist/src/local-first/Schema.d.ts.map +1 -1
- package/dist/src/local-first/Storage.js +1 -1
- package/dist/src/local-first/Timestamp.js +1 -1
- package/package.json +1 -1
- package/src/{Binary.test.ts → Bytes.test.ts} +286 -1
- package/src/{Binary.ts → Bytes.ts} +652 -21
- package/src/Config.test.ts +672 -0
- package/src/Config.ts +410 -0
- package/src/Fs.test.ts +105 -0
- package/src/Fs.ts +414 -0
- package/src/Number.test.ts +82 -18
- package/src/Number.ts +76 -8
- package/src/Object.test.ts +115 -1
- package/src/Object.ts +44 -0
- package/src/Sqlite.ts +1 -1
- package/src/Time.test.ts +82 -11
- package/src/Time.ts +141 -16
- package/src/Type.test.ts +3114 -157
- package/src/Type.ts +2553 -196
- package/src/index.ts +3 -1
- package/src/intl/_en.ts +70 -0
- package/src/intl/ar.ts +71 -0
- package/src/intl/bn.ts +70 -0
- package/src/intl/ca.ts +70 -0
- package/src/intl/cs.ts +70 -0
- package/src/intl/da.ts +70 -0
- package/src/intl/de.ts +70 -0
- package/src/intl/el.ts +70 -0
- package/src/intl/es.ts +70 -0
- package/src/intl/fa.ts +70 -0
- package/src/intl/fi.ts +70 -0
- package/src/intl/fil.ts +70 -0
- package/src/intl/fr.ts +70 -0
- package/src/intl/he.ts +70 -0
- package/src/intl/hi.ts +70 -0
- package/src/intl/hr.ts +70 -0
- package/src/intl/hu.ts +69 -0
- package/src/intl/id.ts +70 -0
- package/src/intl/intl.test.ts +819 -1
- package/src/intl/it.ts +70 -0
- package/src/intl/ja.ts +70 -0
- package/src/intl/ko.ts +68 -0
- package/src/intl/ml.ts +70 -0
- package/src/intl/mr.ts +70 -0
- package/src/intl/ms.ts +71 -0
- package/src/intl/nb.ts +69 -0
- package/src/intl/nl.ts +70 -0
- package/src/intl/pa.ts +70 -0
- package/src/intl/pl.ts +63 -0
- package/src/intl/pt-BR.ts +70 -0
- package/src/intl/pt.ts +71 -0
- package/src/intl/ro.ts +70 -0
- package/src/intl/sk.ts +71 -0
- package/src/intl/sl.ts +70 -0
- package/src/intl/sv.ts +70 -0
- package/src/intl/sw.ts +62 -0
- package/src/intl/ta.ts +70 -0
- package/src/intl/te.ts +70 -0
- package/src/intl/th.ts +68 -0
- package/src/intl/tr.ts +70 -0
- package/src/intl/uk.ts +228 -155
- package/src/intl/ur.ts +70 -0
- package/src/intl/vi.ts +70 -0
- package/src/intl/zh-CN.ts +68 -0
- package/src/intl/zh-TW.ts +68 -0
- package/src/local-first/Protocol.test.ts +1 -1
- package/src/local-first/Protocol.ts +1 -1
- package/src/local-first/Schema.ts +11 -0
- package/src/local-first/Storage.ts +1 -1
- package/src/local-first/Timestamp.ts +1 -1
- package/dist/src/Binary.d.ts +0 -254
- package/dist/src/Binary.d.ts.map +0 -1
package/src/Type.test.ts
CHANGED
|
@@ -15,6 +15,12 @@ import {
|
|
|
15
15
|
assertThrowsInstanceOf,
|
|
16
16
|
assertTrue,
|
|
17
17
|
} from "./Assert.ts";
|
|
18
|
+
import {
|
|
19
|
+
formatCapitalizedError,
|
|
20
|
+
formatUncapitalizedError,
|
|
21
|
+
formatUppercasedError,
|
|
22
|
+
formatLowercasedError,
|
|
23
|
+
} from "./intl/_en.ts";
|
|
18
24
|
import * as cs from "./intl/cs.ts";
|
|
19
25
|
import {
|
|
20
26
|
allResult,
|
|
@@ -36,15 +42,54 @@ import {
|
|
|
36
42
|
between,
|
|
37
43
|
BigInt,
|
|
38
44
|
Boolean,
|
|
45
|
+
BooleanFromString,
|
|
39
46
|
brand,
|
|
47
|
+
CamelCaseIdentifier,
|
|
48
|
+
CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
49
|
+
camelCaseToPascalCase,
|
|
50
|
+
camelCaseToSnakeCase,
|
|
51
|
+
camelCaseToKebabCase,
|
|
52
|
+
camelCaseToConstantCase,
|
|
53
|
+
pascalCaseToCamelCase,
|
|
54
|
+
pascalCaseToSnakeCase,
|
|
55
|
+
pascalCaseToKebabCase,
|
|
56
|
+
pascalCaseToConstantCase,
|
|
57
|
+
snakeCaseToCamelCase,
|
|
58
|
+
snakeCaseToPascalCase,
|
|
59
|
+
snakeCaseToKebabCase,
|
|
60
|
+
snakeCaseToConstantCase,
|
|
61
|
+
kebabCaseToCamelCase,
|
|
62
|
+
kebabCaseToPascalCase,
|
|
63
|
+
kebabCaseToSnakeCase,
|
|
64
|
+
kebabCaseToConstantCase,
|
|
65
|
+
constantCaseToCamelCase,
|
|
66
|
+
constantCaseToPascalCase,
|
|
67
|
+
constantCaseToSnakeCase,
|
|
68
|
+
constantCaseToKebabCase,
|
|
69
|
+
ConstantCaseIdentifier,
|
|
70
|
+
identifier,
|
|
71
|
+
KebabCaseIdentifier,
|
|
72
|
+
PascalCaseIdentifier,
|
|
73
|
+
SnakeCaseIdentifier,
|
|
74
|
+
capitalize,
|
|
40
75
|
capitalized,
|
|
76
|
+
uncapitalize,
|
|
77
|
+
uncapitalized,
|
|
78
|
+
uppercase,
|
|
79
|
+
uppercased,
|
|
80
|
+
lowercase,
|
|
81
|
+
lowercased,
|
|
41
82
|
CapitalizedString,
|
|
83
|
+
UncapitalizedString,
|
|
84
|
+
UppercasedString,
|
|
85
|
+
LowercasedString,
|
|
42
86
|
instanceOf,
|
|
43
87
|
objectTag,
|
|
44
88
|
createId,
|
|
45
89
|
createIdAsUuidv7,
|
|
46
90
|
createIdFromString,
|
|
47
91
|
createType,
|
|
92
|
+
createTypeWithError,
|
|
48
93
|
Data,
|
|
49
94
|
Date,
|
|
50
95
|
DateIso,
|
|
@@ -66,6 +111,7 @@ import {
|
|
|
66
111
|
greaterThanOrEqualTo,
|
|
67
112
|
int,
|
|
68
113
|
Int,
|
|
114
|
+
IntFromString,
|
|
69
115
|
Int64,
|
|
70
116
|
Int64FromInt64String,
|
|
71
117
|
Int64String,
|
|
@@ -121,10 +167,13 @@ import {
|
|
|
121
167
|
Null,
|
|
122
168
|
nullishOr,
|
|
123
169
|
nullOr,
|
|
170
|
+
withDefault,
|
|
171
|
+
type Defaulted,
|
|
124
172
|
nullableToOptional,
|
|
125
173
|
Number,
|
|
126
174
|
Object,
|
|
127
175
|
object,
|
|
176
|
+
objectKeys,
|
|
128
177
|
omit,
|
|
129
178
|
onePositiveInt,
|
|
130
179
|
optional,
|
|
@@ -135,12 +184,16 @@ import {
|
|
|
135
184
|
PositiveFiniteNumber,
|
|
136
185
|
PositiveInt,
|
|
137
186
|
PositiveNumber,
|
|
187
|
+
Port,
|
|
188
|
+
PortFromString,
|
|
189
|
+
prefixed,
|
|
138
190
|
Ratio,
|
|
139
191
|
record,
|
|
140
192
|
regex,
|
|
141
193
|
result,
|
|
142
194
|
nextResult,
|
|
143
195
|
set,
|
|
196
|
+
startsWith,
|
|
144
197
|
String,
|
|
145
198
|
Symbol,
|
|
146
199
|
SimplePassword,
|
|
@@ -174,8 +227,8 @@ import {
|
|
|
174
227
|
type ArrayType,
|
|
175
228
|
type BrandFactory,
|
|
176
229
|
type BrandType,
|
|
230
|
+
type StartsWithError,
|
|
177
231
|
type Base64UrlError,
|
|
178
|
-
type CapitalizedError,
|
|
179
232
|
type DataError,
|
|
180
233
|
type DataIssue,
|
|
181
234
|
type DataType,
|
|
@@ -190,8 +243,12 @@ import {
|
|
|
190
243
|
type ExtractTyped,
|
|
191
244
|
type FiniteError,
|
|
192
245
|
type GreaterThanError,
|
|
246
|
+
type IdentifierCasing,
|
|
247
|
+
type IdentifierError,
|
|
193
248
|
type InferErrors,
|
|
194
249
|
type InferType,
|
|
250
|
+
type TypeIssue,
|
|
251
|
+
typeErrorToIssues,
|
|
195
252
|
type InstanceConstructor,
|
|
196
253
|
type IsData,
|
|
197
254
|
type InstanceOfError,
|
|
@@ -295,6 +352,8 @@ import {
|
|
|
295
352
|
type UnionMemberError,
|
|
296
353
|
type UnionType,
|
|
297
354
|
type ValidationOptions,
|
|
355
|
+
type ValidateLiteral,
|
|
356
|
+
type ValidateOutput,
|
|
298
357
|
} from "./Type.ts";
|
|
299
358
|
|
|
300
359
|
const createNullRecord = <T extends object>(entries: T): T =>
|
|
@@ -463,7 +522,14 @@ describe("Type", () => {
|
|
|
463
522
|
Base64Url,
|
|
464
523
|
BigInt,
|
|
465
524
|
Boolean,
|
|
525
|
+
BooleanFromString,
|
|
526
|
+
CamelCaseIdentifier,
|
|
527
|
+
CamelCaseIdentifierFromConstantCaseIdentifier,
|
|
466
528
|
CapitalizedString,
|
|
529
|
+
UncapitalizedString,
|
|
530
|
+
UppercasedString,
|
|
531
|
+
LowercasedString,
|
|
532
|
+
ConstantCaseIdentifier,
|
|
467
533
|
Data,
|
|
468
534
|
Date,
|
|
469
535
|
DateIso,
|
|
@@ -482,6 +548,7 @@ describe("Type", () => {
|
|
|
482
548
|
Id,
|
|
483
549
|
IdBytes,
|
|
484
550
|
Int,
|
|
551
|
+
IntFromString,
|
|
485
552
|
Int64,
|
|
486
553
|
Int64FromInt64String,
|
|
487
554
|
Int64String,
|
|
@@ -490,6 +557,7 @@ describe("Type", () => {
|
|
|
490
557
|
JsonObject,
|
|
491
558
|
JsonValue,
|
|
492
559
|
JsonValueFromJson,
|
|
560
|
+
KebabCaseIdentifier,
|
|
493
561
|
NegativeDecimalString,
|
|
494
562
|
NegativeInt,
|
|
495
563
|
NegativeNumber,
|
|
@@ -510,12 +578,16 @@ describe("Type", () => {
|
|
|
510
578
|
Null,
|
|
511
579
|
Number,
|
|
512
580
|
Object,
|
|
581
|
+
PascalCaseIdentifier,
|
|
513
582
|
PositiveDecimalString,
|
|
514
583
|
PositiveFiniteNumber,
|
|
515
584
|
PositiveInt,
|
|
516
585
|
PositiveNumber,
|
|
586
|
+
Port,
|
|
587
|
+
PortFromString,
|
|
517
588
|
Ratio,
|
|
518
589
|
SimplePassword,
|
|
590
|
+
SnakeCaseIdentifier,
|
|
519
591
|
String,
|
|
520
592
|
Symbol,
|
|
521
593
|
TrimmedString,
|
|
@@ -593,7 +665,11 @@ describe("Type", () => {
|
|
|
593
665
|
"Between6-7",
|
|
594
666
|
"BigInt",
|
|
595
667
|
"Boolean",
|
|
668
|
+
"BooleanFromString",
|
|
669
|
+
"CamelCaseIdentifier",
|
|
670
|
+
"CamelCaseIdentifierFromConstantCaseIdentifier",
|
|
596
671
|
"Capitalized",
|
|
672
|
+
"ConstantCaseIdentifier",
|
|
597
673
|
"Data",
|
|
598
674
|
"Date",
|
|
599
675
|
"DateIso",
|
|
@@ -612,6 +688,7 @@ describe("Type", () => {
|
|
|
612
688
|
"Int64",
|
|
613
689
|
"Int64FromInt64String",
|
|
614
690
|
"Int64String",
|
|
691
|
+
"IntFromString",
|
|
615
692
|
"IntrospectionChild",
|
|
616
693
|
"IntrospectionKey",
|
|
617
694
|
"IntrospectionRegex",
|
|
@@ -619,13 +696,16 @@ describe("Type", () => {
|
|
|
619
696
|
"Json",
|
|
620
697
|
"JsonValue",
|
|
621
698
|
"JsonValueFromJson",
|
|
699
|
+
"KebabCaseIdentifier",
|
|
622
700
|
"Length16",
|
|
623
701
|
"Length4",
|
|
624
702
|
"LessThan200",
|
|
625
703
|
"LessThan3",
|
|
626
704
|
"LessThanOrEqualTo1",
|
|
627
705
|
"LessThanOrEqualTo4",
|
|
706
|
+
"LessThanOrEqualTo65535",
|
|
628
707
|
"Literal",
|
|
708
|
+
"Lowercased",
|
|
629
709
|
"Map",
|
|
630
710
|
"MaxLength100",
|
|
631
711
|
"MaxLength1000",
|
|
@@ -648,12 +728,16 @@ describe("Type", () => {
|
|
|
648
728
|
"Number",
|
|
649
729
|
"NumberFromString",
|
|
650
730
|
"Object",
|
|
731
|
+
"PascalCaseIdentifier",
|
|
732
|
+
"Port",
|
|
733
|
+
"PortFromString",
|
|
651
734
|
"Positive",
|
|
652
735
|
"PositiveDecimalString",
|
|
653
736
|
"Ratio",
|
|
654
737
|
"Record",
|
|
655
738
|
"Set",
|
|
656
739
|
"SimplePassword",
|
|
740
|
+
"SnakeCaseIdentifier",
|
|
657
741
|
"String",
|
|
658
742
|
"Symbol",
|
|
659
743
|
"TemplateLiteral",
|
|
@@ -661,8 +745,10 @@ describe("Type", () => {
|
|
|
661
745
|
"Tuple",
|
|
662
746
|
"UInt64",
|
|
663
747
|
"Uint8Array",
|
|
748
|
+
"Uncapitalized",
|
|
664
749
|
"Union",
|
|
665
750
|
"Unknown",
|
|
751
|
+
"Uppercased",
|
|
666
752
|
"UrlSafeString",
|
|
667
753
|
] as const;
|
|
668
754
|
|
|
@@ -1076,11 +1162,108 @@ describe("Type operations", () => {
|
|
|
1076
1162
|
it("map only errors returned after the typed Input boundary", () => {
|
|
1077
1163
|
const error = { type: "Positive", value: 0 } as const;
|
|
1078
1164
|
|
|
1079
|
-
assertAssertionError(
|
|
1165
|
+
assertAssertionError(
|
|
1166
|
+
() => PositiveNumber.orThrow(0),
|
|
1167
|
+
PositiveNumber.formatError(error),
|
|
1168
|
+
error,
|
|
1169
|
+
);
|
|
1080
1170
|
assertSame(PositiveNumber.orNull(0), null);
|
|
1081
1171
|
});
|
|
1082
1172
|
});
|
|
1083
1173
|
|
|
1174
|
+
describe("Type.orThrow", () => {
|
|
1175
|
+
it("formats a returned validation error once and preserves its identity", () => {
|
|
1176
|
+
const cause = { type: "Text", value: "invalid" } as const;
|
|
1177
|
+
const decode = mock.fn(() => err(cause));
|
|
1178
|
+
const format = mock.fn(
|
|
1179
|
+
(_error: typeof cause) => "Expected configured text.",
|
|
1180
|
+
);
|
|
1181
|
+
const Text = transform(
|
|
1182
|
+
"Text",
|
|
1183
|
+
String,
|
|
1184
|
+
String,
|
|
1185
|
+
{
|
|
1186
|
+
from: decode,
|
|
1187
|
+
to: (value) => value,
|
|
1188
|
+
},
|
|
1189
|
+
format,
|
|
1190
|
+
);
|
|
1191
|
+
|
|
1192
|
+
const thrown = assertThrowsInstanceOf(() => Text.orThrow("invalid"), Error);
|
|
1193
|
+
|
|
1194
|
+
assertEqual(thrown.message, "Expected configured text.");
|
|
1195
|
+
assertSame(thrown.cause, cause);
|
|
1196
|
+
assertEqual(decode.mock.callCount(), 1);
|
|
1197
|
+
assertEqual(format.mock.callCount(), 1);
|
|
1198
|
+
assertSame(format.mock.calls[0]?.arguments[0], cause);
|
|
1199
|
+
});
|
|
1200
|
+
});
|
|
1201
|
+
|
|
1202
|
+
describe("typeErrorToIssues", () => {
|
|
1203
|
+
it("formats collected errors without decoding again", () => {
|
|
1204
|
+
const decode = mock.fn((value: string) => ok(value));
|
|
1205
|
+
const Text = transform("Text", String, String, {
|
|
1206
|
+
from: decode,
|
|
1207
|
+
to: (value) => value,
|
|
1208
|
+
});
|
|
1209
|
+
const Model = object({ text: Text, counts: array(IntFromString) });
|
|
1210
|
+
const result = Model.fromUnknown(
|
|
1211
|
+
{ text: "ok", counts: ["bad", "worse"] },
|
|
1212
|
+
{ errors: "all" },
|
|
1213
|
+
);
|
|
1214
|
+
assertErr(result);
|
|
1215
|
+
assertEqual(typeErrorToIssues(Model, result.error), [
|
|
1216
|
+
{
|
|
1217
|
+
path: ["counts", 0],
|
|
1218
|
+
message: 'The value "bad" is not a decimal integer.',
|
|
1219
|
+
},
|
|
1220
|
+
{
|
|
1221
|
+
path: ["counts", 1],
|
|
1222
|
+
message: 'The value "worse" is not a decimal integer.',
|
|
1223
|
+
},
|
|
1224
|
+
]);
|
|
1225
|
+
assertEqual(decode.mock.callCount(), 1);
|
|
1226
|
+
});
|
|
1227
|
+
|
|
1228
|
+
it("retains root paths and only the errors collected during decoding", () => {
|
|
1229
|
+
const root = String.fromUnknown(1);
|
|
1230
|
+
assertErr(root);
|
|
1231
|
+
const issues = typeErrorToIssues(String, root.error);
|
|
1232
|
+
assertType<typeof issues, NonEmptyReadonlyArray<TypeIssue>>();
|
|
1233
|
+
assertEqual(issues, [{ path: [], message: "A value 1 is not a string." }]);
|
|
1234
|
+
const Values = array(IntFromString);
|
|
1235
|
+
const result = Values.fromUnknown(["bad", "worse"]);
|
|
1236
|
+
assertErr(result);
|
|
1237
|
+
assertEqual(typeErrorToIssues(Values, result.error), [
|
|
1238
|
+
{ path: [0], message: 'The value "bad" is not a decimal integer.' },
|
|
1239
|
+
]);
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
it("uses localized messages for nested issues", () => {
|
|
1243
|
+
const Model = object({ labels: array(minLength(1)(String)) });
|
|
1244
|
+
const LocalizedModel = localizeTypes(
|
|
1245
|
+
{ Model },
|
|
1246
|
+
{
|
|
1247
|
+
cs: {
|
|
1248
|
+
Array: cs.formatArrayError,
|
|
1249
|
+
MinLength1: cs.formatMinLengthError,
|
|
1250
|
+
Object: cs.formatObjectError,
|
|
1251
|
+
String: cs.formatStringError,
|
|
1252
|
+
},
|
|
1253
|
+
},
|
|
1254
|
+
).cs.Model;
|
|
1255
|
+
const result = LocalizedModel.fromUnknown(
|
|
1256
|
+
{ labels: [1, ""] },
|
|
1257
|
+
{ errors: "all" },
|
|
1258
|
+
);
|
|
1259
|
+
assertErr(result);
|
|
1260
|
+
assertEqual(typeErrorToIssues(LocalizedModel, result.error), [
|
|
1261
|
+
{ path: ["labels", 0], message: "Hodnota 1 musí být text." },
|
|
1262
|
+
{ path: ["labels", 1], message: "Text nesmí být prázdný." },
|
|
1263
|
+
]);
|
|
1264
|
+
});
|
|
1265
|
+
});
|
|
1266
|
+
|
|
1084
1267
|
describe("Standard Schema", () => {
|
|
1085
1268
|
it("infers the exact Input and Output", () => {
|
|
1086
1269
|
const _NumberFromString = setupNumberFromString();
|
|
@@ -1302,7 +1485,11 @@ describe("Standard Schema", () => {
|
|
|
1302
1485
|
assertEqual(result, {
|
|
1303
1486
|
issues: [
|
|
1304
1487
|
{
|
|
1305
|
-
message:
|
|
1488
|
+
message: [
|
|
1489
|
+
"A value does not match any allowed variant.",
|
|
1490
|
+
"- 0: String: A value null is not a string.",
|
|
1491
|
+
"- 1: Number: A value null is not a number.",
|
|
1492
|
+
].join("\n"),
|
|
1306
1493
|
path: ["value"],
|
|
1307
1494
|
},
|
|
1308
1495
|
],
|
|
@@ -1702,6 +1889,158 @@ describe("localizeTypes", () => {
|
|
|
1702
1889
|
);
|
|
1703
1890
|
});
|
|
1704
1891
|
|
|
1892
|
+
it("preserves configured defaults while localizing their Types", () => {
|
|
1893
|
+
const defaultValue = { enabled: true };
|
|
1894
|
+
const Selected = createType(
|
|
1895
|
+
"Selected",
|
|
1896
|
+
(value): Result<typeof defaultValue, TypeError<"Selected">> =>
|
|
1897
|
+
value === defaultValue ? ok(defaultValue) : err({ type: "Selected" }),
|
|
1898
|
+
() => "Expected the selected object.",
|
|
1899
|
+
);
|
|
1900
|
+
const Model = object({
|
|
1901
|
+
replaced: withDefault(optional(Selected), defaultValue),
|
|
1902
|
+
preserved: withDefault(optional(Selected), defaultValue, {
|
|
1903
|
+
strategy: "preserve",
|
|
1904
|
+
}),
|
|
1905
|
+
});
|
|
1906
|
+
const formatters = {
|
|
1907
|
+
test: {
|
|
1908
|
+
Object: () => "Localized Object.",
|
|
1909
|
+
Selected: () => "Localized Selected.",
|
|
1910
|
+
},
|
|
1911
|
+
};
|
|
1912
|
+
const localized = localizeTypes({ Model, Selected }, formatters).test;
|
|
1913
|
+
|
|
1914
|
+
for (const types of [
|
|
1915
|
+
localized,
|
|
1916
|
+
localizeTypes(localized, formatters).test,
|
|
1917
|
+
]) {
|
|
1918
|
+
for (const property of [
|
|
1919
|
+
types.Model.props.replaced,
|
|
1920
|
+
types.Model.props.preserved,
|
|
1921
|
+
]) {
|
|
1922
|
+
assertSame(property.value, defaultValue);
|
|
1923
|
+
assertSame(property.type, types.Selected);
|
|
1924
|
+
assertTrue(property.type.is(property.value));
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
const Reconstructed = object(types.Model.props);
|
|
1928
|
+
const decoded = Reconstructed.orThrow({});
|
|
1929
|
+
assertSame(decoded.replaced, defaultValue);
|
|
1930
|
+
assertSame(decoded.preserved.value, defaultValue);
|
|
1931
|
+
assertEqual(Reconstructed.to(decoded), { replaced: defaultValue });
|
|
1932
|
+
|
|
1933
|
+
const invalid = Reconstructed.fromUnknown({ replaced: {} });
|
|
1934
|
+
assertErr(invalid);
|
|
1935
|
+
assertEqual(
|
|
1936
|
+
Reconstructed.formatError(invalid.error),
|
|
1937
|
+
"Localized Selected.",
|
|
1938
|
+
);
|
|
1939
|
+
}
|
|
1940
|
+
});
|
|
1941
|
+
|
|
1942
|
+
it("keeps configured default data opaque", () => {
|
|
1943
|
+
const cyclicObject: { self?: unknown } = {};
|
|
1944
|
+
cyclicObject.self = cyclicObject;
|
|
1945
|
+
const cyclicArray: Array<unknown> = [];
|
|
1946
|
+
cyclicArray.push(cyclicArray);
|
|
1947
|
+
const accessor = globalThis.Object.defineProperty({}, "value", {
|
|
1948
|
+
enumerable: true,
|
|
1949
|
+
get: () => {
|
|
1950
|
+
throw new Error("A default getter must not be read.");
|
|
1951
|
+
},
|
|
1952
|
+
});
|
|
1953
|
+
|
|
1954
|
+
for (const defaultValue of [
|
|
1955
|
+
cyclicObject,
|
|
1956
|
+
cyclicArray,
|
|
1957
|
+
accessor,
|
|
1958
|
+
String,
|
|
1959
|
+
{ "~evolu/instance": "Type" },
|
|
1960
|
+
]) {
|
|
1961
|
+
const Model = object({
|
|
1962
|
+
replaced: withDefault(optional(Unknown), defaultValue),
|
|
1963
|
+
preserved: withDefault(optional(Unknown), defaultValue, {
|
|
1964
|
+
strategy: "preserve",
|
|
1965
|
+
}),
|
|
1966
|
+
});
|
|
1967
|
+
const localized = localizeTypes(
|
|
1968
|
+
{ Model },
|
|
1969
|
+
{ test: { Object: () => "Localized Object." } },
|
|
1970
|
+
).test.Model;
|
|
1971
|
+
|
|
1972
|
+
assertSame(localized.props.replaced.value, defaultValue);
|
|
1973
|
+
assertSame(localized.props.preserved.value, defaultValue);
|
|
1974
|
+
const decoded = localized.orThrow({});
|
|
1975
|
+
assertSame(decoded.replaced, defaultValue);
|
|
1976
|
+
assertSame(decoded.preserved.value, defaultValue);
|
|
1977
|
+
const encoded = localized.to(decoded);
|
|
1978
|
+
assertSame(encoded.replaced, defaultValue);
|
|
1979
|
+
assertFalse(globalThis.Object.hasOwn(encoded, "preserved"));
|
|
1980
|
+
}
|
|
1981
|
+
});
|
|
1982
|
+
|
|
1983
|
+
it("retains localized errors when making defaulted properties partial", () => {
|
|
1984
|
+
const Model = object({
|
|
1985
|
+
replaced: withDefault(optional(Boolean), true),
|
|
1986
|
+
preserved: withDefault(optional(Boolean), true, {
|
|
1987
|
+
strategy: "preserve",
|
|
1988
|
+
}),
|
|
1989
|
+
ordinary: optional(Boolean),
|
|
1990
|
+
});
|
|
1991
|
+
const first = localizeTypes(
|
|
1992
|
+
{ Model },
|
|
1993
|
+
{
|
|
1994
|
+
test: {
|
|
1995
|
+
Boolean: () => "First Boolean.",
|
|
1996
|
+
Object: () => "First Object.",
|
|
1997
|
+
},
|
|
1998
|
+
},
|
|
1999
|
+
).test.Model;
|
|
2000
|
+
const second = localizeTypes(
|
|
2001
|
+
{ Model: first },
|
|
2002
|
+
{
|
|
2003
|
+
test: {
|
|
2004
|
+
Boolean: () => "Second Boolean.",
|
|
2005
|
+
Object: () => "Second Object.",
|
|
2006
|
+
},
|
|
2007
|
+
},
|
|
2008
|
+
).test.Model;
|
|
2009
|
+
|
|
2010
|
+
for (const [model, message] of [
|
|
2011
|
+
[first, "First Boolean."],
|
|
2012
|
+
[second, "Second Boolean."],
|
|
2013
|
+
] as const) {
|
|
2014
|
+
const Partial = partial(model.props);
|
|
2015
|
+
const input = { replaced: "bad", preserved: "bad", ordinary: "bad" };
|
|
2016
|
+
const result = Partial.fromUnknown(input, { errors: "all" });
|
|
2017
|
+
assertErr(result);
|
|
2018
|
+
const issues = ["replaced", "preserved", "ordinary"].map((key) => ({
|
|
2019
|
+
path: [key],
|
|
2020
|
+
message,
|
|
2021
|
+
}));
|
|
2022
|
+
assertEqual(typeErrorToIssues(Partial, result.error), issues);
|
|
2023
|
+
assertEqual(Partial.formatError(result.error), message);
|
|
2024
|
+
assertEqual(Partial["~standard"].validate(input), { issues });
|
|
2025
|
+
assertSame(Partial.props.replaced.type.parent, model.props.replaced.type);
|
|
2026
|
+
assertSame(
|
|
2027
|
+
Partial.props.preserved.type.parent,
|
|
2028
|
+
model.props.preserved.type,
|
|
2029
|
+
);
|
|
2030
|
+
|
|
2031
|
+
assertOk(Partial.fromUnknown({}), {});
|
|
2032
|
+
assertEqual(Partial.to({}), {});
|
|
2033
|
+
const supplied = { replaced: false, preserved: false, ordinary: false };
|
|
2034
|
+
const decoded = Partial.orThrow(supplied);
|
|
2035
|
+
assertEqual(decoded, {
|
|
2036
|
+
replaced: false,
|
|
2037
|
+
preserved: { value: false, defaultUsed: false },
|
|
2038
|
+
ordinary: false,
|
|
2039
|
+
});
|
|
2040
|
+
assertEqual(Partial.to(decoded), supplied);
|
|
2041
|
+
}
|
|
2042
|
+
});
|
|
2043
|
+
|
|
1705
2044
|
it("preserves localized child errors when composed afterward", async () => {
|
|
1706
2045
|
const LocalizedString = localizeTypes(
|
|
1707
2046
|
{ String },
|
|
@@ -1832,6 +2171,11 @@ describe("localizeTypes", () => {
|
|
|
1832
2171
|
assertEqual(types.Strings.to(["Evolu"]), ["Evolu"]);
|
|
1833
2172
|
|
|
1834
2173
|
const positiveError = { type: "Positive", value: 0 } as const;
|
|
2174
|
+
assertAssertionError(
|
|
2175
|
+
() => types.PositiveNumber.orThrow(0),
|
|
2176
|
+
"Localized Positive.",
|
|
2177
|
+
positiveError,
|
|
2178
|
+
);
|
|
1835
2179
|
for (const operation of [
|
|
1836
2180
|
() => types.PositiveNumber.from(0 as PositiveNumber),
|
|
1837
2181
|
() => types.PositiveNumber.to(0 as PositiveNumber),
|
|
@@ -1960,7 +2304,7 @@ describe("localizeTypes", () => {
|
|
|
1960
2304
|
>();
|
|
1961
2305
|
});
|
|
1962
2306
|
|
|
1963
|
-
it("
|
|
2307
|
+
it("localizes a Union summary and its retained member failures", () => {
|
|
1964
2308
|
const Value = union(String, Number);
|
|
1965
2309
|
const types = localizeTypes(
|
|
1966
2310
|
{ Value },
|
|
@@ -1976,7 +2320,10 @@ describe("localizeTypes", () => {
|
|
|
1976
2320
|
const stringResult = types.Value.members[0].fromUnknown(1);
|
|
1977
2321
|
|
|
1978
2322
|
assertErr(result);
|
|
1979
|
-
assertEqual(
|
|
2323
|
+
assertEqual(
|
|
2324
|
+
types.Value.formatError(result.error),
|
|
2325
|
+
"Localized Union.\n- 0: String: Localized String.",
|
|
2326
|
+
);
|
|
1980
2327
|
assertFalse(globalThis.Object.is(types.Value.members[0], String));
|
|
1981
2328
|
assertFalse(globalThis.Object.is(types.Value.members[1], Number));
|
|
1982
2329
|
assertErr(stringResult);
|
|
@@ -2003,6 +2350,49 @@ describe("localizeTypes", () => {
|
|
|
2003
2350
|
>();
|
|
2004
2351
|
});
|
|
2005
2352
|
|
|
2353
|
+
it("localizes nested Union details and can localize the result again", async () => {
|
|
2354
|
+
const Value = object({
|
|
2355
|
+
field: union(object({ count: union(String, Number) }), Null),
|
|
2356
|
+
});
|
|
2357
|
+
const localized = localizeTypes(
|
|
2358
|
+
{ Value },
|
|
2359
|
+
{
|
|
2360
|
+
first: {
|
|
2361
|
+
Object: () => "Object one.",
|
|
2362
|
+
String: () => "String one.",
|
|
2363
|
+
Number: () => "Number one.",
|
|
2364
|
+
Literal: () => "Literal one.",
|
|
2365
|
+
Union: () => "Union one.",
|
|
2366
|
+
},
|
|
2367
|
+
},
|
|
2368
|
+
).first;
|
|
2369
|
+
const second = localizeTypes(localized, {
|
|
2370
|
+
second: {
|
|
2371
|
+
Object: () => "Object two.",
|
|
2372
|
+
String: () => "String two.",
|
|
2373
|
+
Number: () => "Number two.",
|
|
2374
|
+
Literal: () => "Literal two.",
|
|
2375
|
+
Union: () => "Union two.",
|
|
2376
|
+
},
|
|
2377
|
+
}).second;
|
|
2378
|
+
const input = { field: { count: false } };
|
|
2379
|
+
const result = second.Value.fromUnknown(input, { errors: "all" });
|
|
2380
|
+
assertErr(result);
|
|
2381
|
+
const message = [
|
|
2382
|
+
"Union two.",
|
|
2383
|
+
'- 0: Object["count"]: Union two.',
|
|
2384
|
+
" - 0: String: String two.",
|
|
2385
|
+
" - 1: Number: Number two.",
|
|
2386
|
+
"- 1: Literal: Literal two.",
|
|
2387
|
+
].join("\n");
|
|
2388
|
+
const issues = [{ path: ["field"], message }];
|
|
2389
|
+
assertEqual(second.Value.formatError(result.error), message);
|
|
2390
|
+
assertEqual(typeErrorToIssues(second.Value, result.error), issues);
|
|
2391
|
+
assertEqual(await second.Value["~standard"].validate(input), { issues });
|
|
2392
|
+
assertFalse(localized.Value.formatError(result.error).includes("two."));
|
|
2393
|
+
assertFalse(Value.formatError(result.error).includes("two."));
|
|
2394
|
+
});
|
|
2395
|
+
|
|
2006
2396
|
it("localizes TemplateLiteral captures and their reflected Tuple", () => {
|
|
2007
2397
|
const Part = literal("a");
|
|
2008
2398
|
const Value = templateLiteralParser(Part);
|
|
@@ -3191,6 +3581,149 @@ describe("createType", () => {
|
|
|
3191
3581
|
});
|
|
3192
3582
|
});
|
|
3193
3583
|
|
|
3584
|
+
describe("createTypeWithError", () => {
|
|
3585
|
+
it("delegates validation and error collection without callback options", () => {
|
|
3586
|
+
const Syntax = object({ name: String, count: Number });
|
|
3587
|
+
interface ModelError extends TypeError<"Model"> {
|
|
3588
|
+
readonly cause: InferErrors<typeof Syntax>;
|
|
3589
|
+
readonly value: unknown;
|
|
3590
|
+
}
|
|
3591
|
+
const mapError = mock.fn(
|
|
3592
|
+
(cause: InferErrors<typeof Syntax>, value: unknown): ModelError => ({
|
|
3593
|
+
type: "Model",
|
|
3594
|
+
cause,
|
|
3595
|
+
value,
|
|
3596
|
+
}),
|
|
3597
|
+
);
|
|
3598
|
+
const Model = createTypeWithError("Model", Syntax, mapError, (error) => {
|
|
3599
|
+
assertType<typeof error, ModelError>();
|
|
3600
|
+
return "Invalid model.";
|
|
3601
|
+
});
|
|
3602
|
+
assertType<typeof Model.Input, typeof Syntax.Output>();
|
|
3603
|
+
assertType<typeof Model.Output, typeof Syntax.Output>();
|
|
3604
|
+
assertType<typeof Model.CanonicalInput, typeof Syntax.Output>();
|
|
3605
|
+
assertType<InferErrors<typeof Model>, ModelError>();
|
|
3606
|
+
assertType<typeof Model.parent, null>();
|
|
3607
|
+
assertSame(Model.parent, null);
|
|
3608
|
+
|
|
3609
|
+
const valid = { name: "Evolu", count: 1 };
|
|
3610
|
+
for (const result of [Model.fromUnknown(valid), Model.from(valid)]) {
|
|
3611
|
+
assertOk(result);
|
|
3612
|
+
assertSame(result.value, valid);
|
|
3613
|
+
}
|
|
3614
|
+
assertSame(Model.to(valid), valid);
|
|
3615
|
+
assertSame(Model.orThrow(valid), valid);
|
|
3616
|
+
assertSame(Model.orNull(valid), valid);
|
|
3617
|
+
assertTrue(Model.is(valid));
|
|
3618
|
+
assertType(Model, valid);
|
|
3619
|
+
assertEqual(Model["~standard"].validate(valid), { value: valid });
|
|
3620
|
+
assertSame(mapError.mock.callCount(), 0);
|
|
3621
|
+
|
|
3622
|
+
const invalid = { name: 1, count: "bad" };
|
|
3623
|
+
for (const options of [
|
|
3624
|
+
undefined,
|
|
3625
|
+
{ errors: "first" },
|
|
3626
|
+
{ errors: "all" },
|
|
3627
|
+
] as const) {
|
|
3628
|
+
const result = Model.fromUnknown(invalid, options);
|
|
3629
|
+
const source = Syntax.fromUnknown(invalid, options);
|
|
3630
|
+
assertErr(result);
|
|
3631
|
+
assertErr(source);
|
|
3632
|
+
assertEqual(result.error, {
|
|
3633
|
+
type: "Model",
|
|
3634
|
+
cause: source.error,
|
|
3635
|
+
value: invalid,
|
|
3636
|
+
});
|
|
3637
|
+
assertSame(result.error.value, invalid);
|
|
3638
|
+
assertEqual(Model.formatError(result.error), "Invalid model.");
|
|
3639
|
+
assertEqual(typeErrorToIssues(Model, result.error), [
|
|
3640
|
+
{ path: [], message: "Invalid model." },
|
|
3641
|
+
]);
|
|
3642
|
+
}
|
|
3643
|
+
assertFalse(Model.is(invalid));
|
|
3644
|
+
const standard = Model["~standard"].validate(invalid);
|
|
3645
|
+
assertEqual(standard, {
|
|
3646
|
+
issues: [{ path: [], message: "Invalid model." }],
|
|
3647
|
+
});
|
|
3648
|
+
const collected = mapError.mock.calls.at(-1)!.arguments[0];
|
|
3649
|
+
assertEqual(collected, {
|
|
3650
|
+
type: "Object",
|
|
3651
|
+
reason: {
|
|
3652
|
+
kind: "Properties",
|
|
3653
|
+
errors: {
|
|
3654
|
+
name: { type: "TypeOf", expected: "String", value: 1 },
|
|
3655
|
+
count: { type: "TypeOf", expected: "Number", value: "bad" },
|
|
3656
|
+
},
|
|
3657
|
+
},
|
|
3658
|
+
});
|
|
3659
|
+
assertAssertionError(
|
|
3660
|
+
() =>
|
|
3661
|
+
Model.from(invalid as unknown as typeof Model.Output, {
|
|
3662
|
+
errors: "all",
|
|
3663
|
+
}),
|
|
3664
|
+
"Expected Model.",
|
|
3665
|
+
{ type: "Model", cause: collected, value: invalid },
|
|
3666
|
+
);
|
|
3667
|
+
});
|
|
3668
|
+
|
|
3669
|
+
it("preserves all alternatives inside localized structural Types", () => {
|
|
3670
|
+
const Value = createTypeWithError(
|
|
3671
|
+
"Value",
|
|
3672
|
+
union(String, Number),
|
|
3673
|
+
(cause) => ({ type: "Value" as const, cause }),
|
|
3674
|
+
() => "Invalid value.",
|
|
3675
|
+
);
|
|
3676
|
+
const { custom } = localizeTypes(
|
|
3677
|
+
{ Settings: object({ value: Value }) },
|
|
3678
|
+
{ custom: { Object: () => "Object.", Value: () => "Custom value." } },
|
|
3679
|
+
);
|
|
3680
|
+
const result = custom.Settings.fromUnknown(
|
|
3681
|
+
{ value: false },
|
|
3682
|
+
{ errors: "all" },
|
|
3683
|
+
);
|
|
3684
|
+
assertErr(result);
|
|
3685
|
+
assertTrue(result.error.reason.kind === "Properties");
|
|
3686
|
+
const error = result.error.reason.errors.value;
|
|
3687
|
+
assertTrue(error?.type === "Value");
|
|
3688
|
+
assertEqual(error.cause.errors.length, 2);
|
|
3689
|
+
assertEqual(typeErrorToIssues(custom.Settings, result.error), [
|
|
3690
|
+
{ path: ["value"], message: "Custom value." },
|
|
3691
|
+
]);
|
|
3692
|
+
});
|
|
3693
|
+
|
|
3694
|
+
it("rejects transforming sources and incompatible error names", () => {
|
|
3695
|
+
const compileTimeAssertions = (
|
|
3696
|
+
uncertain: typeof String | typeof Number,
|
|
3697
|
+
) => {
|
|
3698
|
+
createTypeWithError(
|
|
3699
|
+
"Value",
|
|
3700
|
+
// @ts-expect-error Source Type must use identity encoding.
|
|
3701
|
+
BooleanFromString,
|
|
3702
|
+
(cause) => ({ type: "Value" as const, cause }),
|
|
3703
|
+
() => "Invalid value.",
|
|
3704
|
+
);
|
|
3705
|
+
createTypeWithError(
|
|
3706
|
+
"Value",
|
|
3707
|
+
// @ts-expect-error Output Type must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
3708
|
+
uncertain,
|
|
3709
|
+
(cause) => ({ type: "Value" as const, cause }),
|
|
3710
|
+
() => "Invalid value.",
|
|
3711
|
+
);
|
|
3712
|
+
createTypeWithError(
|
|
3713
|
+
"Value",
|
|
3714
|
+
String,
|
|
3715
|
+
// @ts-expect-error The mapped error discriminant must match the Type name "Value".
|
|
3716
|
+
() => ({ type: "Other" as const }),
|
|
3717
|
+
() => "Invalid value.",
|
|
3718
|
+
);
|
|
3719
|
+
};
|
|
3720
|
+
assertType<
|
|
3721
|
+
typeof compileTimeAssertions,
|
|
3722
|
+
(uncertain: typeof String | typeof Number) => void
|
|
3723
|
+
>();
|
|
3724
|
+
});
|
|
3725
|
+
});
|
|
3726
|
+
|
|
3194
3727
|
describe("transform", () => {
|
|
3195
3728
|
it("creates a Type between encoded and output Types", () => {
|
|
3196
3729
|
const NumberFromString = setupNumberFromString();
|
|
@@ -4432,7 +4965,47 @@ describe("instanceOf", () => {
|
|
|
4432
4965
|
});
|
|
4433
4966
|
});
|
|
4434
4967
|
|
|
4968
|
+
it("ValidateOutput accepts concrete Types and rejects unions of Type nodes", () => {
|
|
4969
|
+
const defineOutput = <T extends AnyType>(type: T & ValidateOutput<T>): T =>
|
|
4970
|
+
type;
|
|
4971
|
+
const Value = union(String, Number);
|
|
4972
|
+
assertSame(defineOutput(Value), Value);
|
|
4973
|
+
assertType<ValidateOutput<typeof String>, typeof String>();
|
|
4974
|
+
assertType<ValidateOutput<typeof Value>, typeof Value>();
|
|
4975
|
+
const uncertain = String as typeof String | typeof Number;
|
|
4976
|
+
const reject = () => {
|
|
4977
|
+
// @ts-expect-error Output Type must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
4978
|
+
defineOutput(uncertain);
|
|
4979
|
+
};
|
|
4980
|
+
assertType<typeof reject, () => void>();
|
|
4981
|
+
});
|
|
4982
|
+
|
|
4435
4983
|
describe("literal", () => {
|
|
4984
|
+
it("exposes its compile-time literal guard for other factories", () => {
|
|
4985
|
+
assertType<ValidateLiteral<"APP_">, "APP_">();
|
|
4986
|
+
assertType<ValidateLiteral<42>, 42>();
|
|
4987
|
+
assertType<ValidateLiteral<42n>, 42n>();
|
|
4988
|
+
assertType<ValidateLiteral<true>, true>();
|
|
4989
|
+
assertType<ValidateLiteral<null>, null>();
|
|
4990
|
+
assertType<ValidateLiteral<undefined>, undefined>();
|
|
4991
|
+
|
|
4992
|
+
const definePrefix = <Prefix extends string>(
|
|
4993
|
+
prefix: Prefix & ValidateLiteral<Prefix>,
|
|
4994
|
+
): Prefix => prefix;
|
|
4995
|
+
assertEqual(definePrefix("APP_"), "APP_");
|
|
4996
|
+
const reject = () => {
|
|
4997
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
4998
|
+
definePrefix("APP_" as string);
|
|
4999
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
5000
|
+
definePrefix("APP_" as "APP_" | "OTHER_");
|
|
5001
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
5002
|
+
definePrefix(TrimmedString.orThrow("APP_"));
|
|
5003
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
5004
|
+
definePrefix("APP_" as `APP_${string}`);
|
|
5005
|
+
};
|
|
5006
|
+
assertType<typeof reject, () => void>();
|
|
5007
|
+
});
|
|
5008
|
+
|
|
4436
5009
|
const Hello = literal("Hello");
|
|
4437
5010
|
|
|
4438
5011
|
it("creates a Literal Type with its primitive Type as parent", () => {
|
|
@@ -4494,7 +5067,10 @@ describe("literal", () => {
|
|
|
4494
5067
|
|
|
4495
5068
|
assertEqual(Hello.orThrow("Hello"), "Hello");
|
|
4496
5069
|
const error = assertThrowsInstanceOf(() => Hello.orThrow("World"), Error);
|
|
4497
|
-
|
|
5070
|
+
assertEqual(
|
|
5071
|
+
error.message,
|
|
5072
|
+
Hello.formatError({ type: "Literal", expected: "Hello", value: "World" }),
|
|
5073
|
+
);
|
|
4498
5074
|
assertEqual(Hello.orNull("Hello"), "Hello");
|
|
4499
5075
|
assertSame(Hello.orNull("World"), null);
|
|
4500
5076
|
});
|
|
@@ -5094,7 +5670,7 @@ describe("union", () => {
|
|
|
5094
5670
|
}
|
|
5095
5671
|
});
|
|
5096
5672
|
|
|
5097
|
-
it("formats
|
|
5673
|
+
it("formats retained member errors in one message", () => {
|
|
5098
5674
|
const result = StringOrNumber.fromUnknown(true, { errors: "all" });
|
|
5099
5675
|
|
|
5100
5676
|
assertErr(result, {
|
|
@@ -5113,7 +5689,11 @@ describe("union", () => {
|
|
|
5113
5689
|
|
|
5114
5690
|
assertEqual(
|
|
5115
5691
|
StringOrNumber.formatError(result.error),
|
|
5116
|
-
|
|
5692
|
+
[
|
|
5693
|
+
"A value does not match any allowed variant.",
|
|
5694
|
+
"- 0: String: A value true is not a string.",
|
|
5695
|
+
"- 1: Number: A value true is not a number.",
|
|
5696
|
+
].join("\n"),
|
|
5117
5697
|
);
|
|
5118
5698
|
assertType<
|
|
5119
5699
|
Parameters<typeof StringOrNumber.formatError>[0],
|
|
@@ -5121,6 +5701,120 @@ describe("union", () => {
|
|
|
5121
5701
|
>();
|
|
5122
5702
|
});
|
|
5123
5703
|
|
|
5704
|
+
describe("error reporting", () => {
|
|
5705
|
+
it("formats only retained failures without validating again", () => {
|
|
5706
|
+
let calls = 0;
|
|
5707
|
+
const Counted = brand(
|
|
5708
|
+
"Counted",
|
|
5709
|
+
String,
|
|
5710
|
+
(): Result<void, TypeError<"Counted">> => {
|
|
5711
|
+
calls++;
|
|
5712
|
+
return err({ type: "Counted" });
|
|
5713
|
+
},
|
|
5714
|
+
() => "Counted failure.",
|
|
5715
|
+
);
|
|
5716
|
+
const Value = undefinedOr(Counted);
|
|
5717
|
+
const result = Value.fromUnknown("invalid");
|
|
5718
|
+
assertErr(result);
|
|
5719
|
+
const expected =
|
|
5720
|
+
"A value does not match any allowed variant.\n- 0: Counted: Counted failure.";
|
|
5721
|
+
|
|
5722
|
+
assertEqual(Value.formatError(result.error), expected);
|
|
5723
|
+
assertEqual(typeErrorToIssues(Value, result.error), [
|
|
5724
|
+
{ path: [], message: expected },
|
|
5725
|
+
]);
|
|
5726
|
+
assertEqual(calls, 1);
|
|
5727
|
+
|
|
5728
|
+
const thrown = assertThrowsInstanceOf(
|
|
5729
|
+
() => Value.orThrow("invalid"),
|
|
5730
|
+
Error,
|
|
5731
|
+
);
|
|
5732
|
+
assertEqual(thrown.message, expected);
|
|
5733
|
+
assertEqual(thrown.cause, result.error);
|
|
5734
|
+
assertEqual(calls, 2);
|
|
5735
|
+
});
|
|
5736
|
+
|
|
5737
|
+
it("keeps nested alternatives inside one issue at the enclosing path", async () => {
|
|
5738
|
+
const Value = object({
|
|
5739
|
+
config: union(
|
|
5740
|
+
object({ ports: array(undefinedOr(PortFromString)) }),
|
|
5741
|
+
Null,
|
|
5742
|
+
),
|
|
5743
|
+
});
|
|
5744
|
+
const input = { config: { ports: ["65536"] } };
|
|
5745
|
+
const result = Value.fromUnknown(input, { errors: "all" });
|
|
5746
|
+
assertErr(result);
|
|
5747
|
+
const issues = typeErrorToIssues(Value, result.error);
|
|
5748
|
+
assertLength(issues, 1);
|
|
5749
|
+
assertEqual(issues[0].path, ["config"]);
|
|
5750
|
+
assertTrue(
|
|
5751
|
+
issues[0].message.includes(
|
|
5752
|
+
'- 0: Object["ports"][0]: A value does not match any allowed variant.\n - 0: PortFromString:',
|
|
5753
|
+
),
|
|
5754
|
+
);
|
|
5755
|
+
assertTrue(
|
|
5756
|
+
issues[0].message.includes(
|
|
5757
|
+
"The value 65536 must be less than or equal to 65535.",
|
|
5758
|
+
),
|
|
5759
|
+
);
|
|
5760
|
+
assertEqual(Value.formatError(result.error), issues[0].message);
|
|
5761
|
+
assertEqual(await Value["~standard"].validate(input), { issues });
|
|
5762
|
+
});
|
|
5763
|
+
|
|
5764
|
+
it("renders symbol member paths without adding them to the union path", () => {
|
|
5765
|
+
const Value = union(object({}), Null);
|
|
5766
|
+
const result = Value.fromUnknown({
|
|
5767
|
+
[globalThis.Symbol.for("setting")]: "bad",
|
|
5768
|
+
});
|
|
5769
|
+
assertErr(result);
|
|
5770
|
+
const issues = typeErrorToIssues(Value, result.error);
|
|
5771
|
+
assertLength(issues, 1);
|
|
5772
|
+
assertEqual(issues[0].path, []);
|
|
5773
|
+
assertTrue(issues[0].message.includes("- 0: Object[Symbol(setting)]:"));
|
|
5774
|
+
});
|
|
5775
|
+
|
|
5776
|
+
it("uses the correct member pipeline at parent boundaries", () => {
|
|
5777
|
+
const Value = undefinedOr(PortFromString);
|
|
5778
|
+
const input = Value.parent.fromUnknown(42);
|
|
5779
|
+
assertErr(input);
|
|
5780
|
+
assertEqual(
|
|
5781
|
+
Value.parent.formatError(input.error),
|
|
5782
|
+
[
|
|
5783
|
+
"A value does not match any allowed variant.",
|
|
5784
|
+
"- 0: String: A value 42 is not a string.",
|
|
5785
|
+
].join("\n"),
|
|
5786
|
+
);
|
|
5787
|
+
|
|
5788
|
+
const remaining = Value.from.parent("65536", { errors: "all" });
|
|
5789
|
+
assertErr(remaining);
|
|
5790
|
+
assertEqual(
|
|
5791
|
+
Value.formatError(remaining.error),
|
|
5792
|
+
[
|
|
5793
|
+
"A value does not match any allowed variant.",
|
|
5794
|
+
"- 0: PortFromString: The value 65536 must be less than or equal to 65535.",
|
|
5795
|
+
].join("\n"),
|
|
5796
|
+
);
|
|
5797
|
+
});
|
|
5798
|
+
|
|
5799
|
+
it("preserves specific failures through nullish defaults", () => {
|
|
5800
|
+
for (const Value of [
|
|
5801
|
+
withDefault(nullOr(PortFromString), Port.orThrow(4000)),
|
|
5802
|
+
withDefault(undefinedOr(PortFromString), Port.orThrow(4000)),
|
|
5803
|
+
withDefault(nullishOr(PortFromString), Port.orThrow(4000)),
|
|
5804
|
+
]) {
|
|
5805
|
+
const error = assertThrowsInstanceOf(
|
|
5806
|
+
() => Value.orThrow("65536"),
|
|
5807
|
+
Error,
|
|
5808
|
+
);
|
|
5809
|
+
assertTrue(
|
|
5810
|
+
error.message.includes(
|
|
5811
|
+
"The value 65536 must be less than or equal to 65535.",
|
|
5812
|
+
),
|
|
5813
|
+
);
|
|
5814
|
+
}
|
|
5815
|
+
});
|
|
5816
|
+
});
|
|
5817
|
+
|
|
5124
5818
|
it("cannot fail when one member is infallible", () => {
|
|
5125
5819
|
const StringOrUnknown = union(String, Unknown);
|
|
5126
5820
|
const result = StringOrUnknown.fromUnknown(true);
|
|
@@ -5378,7 +6072,13 @@ describe("union", () => {
|
|
|
5378
6072
|
);
|
|
5379
6073
|
assertSame(Value.orNull(input), null);
|
|
5380
6074
|
const error = assertThrowsInstanceOf(() => Value.orThrow(input), Error);
|
|
5381
|
-
|
|
6075
|
+
assertEqual(
|
|
6076
|
+
error.message,
|
|
6077
|
+
[
|
|
6078
|
+
"A value does not match any allowed variant.",
|
|
6079
|
+
'- 0: Literal: The value "World" is not strictly equal to the expected literal: Hello.',
|
|
6080
|
+
].join("\n"),
|
|
6081
|
+
);
|
|
5382
6082
|
});
|
|
5383
6083
|
|
|
5384
6084
|
it("validates only the remaining Type after a validated Union through from.parent", () => {
|
|
@@ -7080,7 +7780,7 @@ describe("brand", () => {
|
|
|
7080
7780
|
assertType<typeof value, typeof Label.Output>();
|
|
7081
7781
|
assertType<Parameters<typeof Label.orThrow>[0], typeof Label.Input>();
|
|
7082
7782
|
validations.length = 0;
|
|
7083
|
-
assertAssertionError(() => Label.orThrow(" value "), "
|
|
7783
|
+
assertAssertionError(() => Label.orThrow(" value "), "TrimmedString", {
|
|
7084
7784
|
type: "TrimmedString",
|
|
7085
7785
|
value: " value ",
|
|
7086
7786
|
});
|
|
@@ -7357,22 +8057,301 @@ describe("brand", () => {
|
|
|
7357
8057
|
});
|
|
7358
8058
|
});
|
|
7359
8059
|
|
|
7360
|
-
describe("
|
|
7361
|
-
|
|
7362
|
-
|
|
8060
|
+
describe("Text casing", () => {
|
|
8061
|
+
const cases = [
|
|
8062
|
+
{
|
|
8063
|
+
type: CapitalizedString,
|
|
8064
|
+
convert: capitalize,
|
|
8065
|
+
input: "hello WORLD",
|
|
8066
|
+
expected: "Hello WORLD",
|
|
8067
|
+
description: "capitalized",
|
|
8068
|
+
},
|
|
8069
|
+
{
|
|
8070
|
+
type: UncapitalizedString,
|
|
8071
|
+
convert: uncapitalize,
|
|
8072
|
+
input: "Hello WORLD",
|
|
8073
|
+
expected: "hello WORLD",
|
|
8074
|
+
description: "uncapitalized",
|
|
8075
|
+
},
|
|
8076
|
+
{
|
|
8077
|
+
type: UppercasedString,
|
|
8078
|
+
convert: uppercase,
|
|
8079
|
+
input: "hello World",
|
|
8080
|
+
expected: "HELLO WORLD",
|
|
8081
|
+
description: "uppercased",
|
|
8082
|
+
},
|
|
8083
|
+
{
|
|
8084
|
+
type: LowercasedString,
|
|
8085
|
+
convert: lowercase,
|
|
8086
|
+
input: "Hello WORLD",
|
|
8087
|
+
expected: "hello world",
|
|
8088
|
+
description: "lowercased",
|
|
8089
|
+
},
|
|
8090
|
+
] as const;
|
|
8091
|
+
|
|
8092
|
+
for (const { type, convert, input, expected, description } of cases) {
|
|
8093
|
+
it(`validates ${description} text without changing it`, () => {
|
|
8094
|
+
assertOk(type.fromUnknown(expected), expected);
|
|
8095
|
+
assertOk(type.from.parent(expected), expected);
|
|
8096
|
+
assertEqual(type.to("123"), "123");
|
|
8097
|
+
assertErr(type.fromUnknown(input), { type: type.name, value: input });
|
|
8098
|
+
assertErr(type.fromUnknown(123), {
|
|
8099
|
+
type: "TypeOf",
|
|
8100
|
+
expected: "String",
|
|
8101
|
+
value: 123,
|
|
8102
|
+
});
|
|
8103
|
+
for (const value of ["", "123", "😀", " ", "\uD800", "\uDC00"]) {
|
|
8104
|
+
assertOk(type.fromUnknown(value), value);
|
|
8105
|
+
assertEqual(convert(value), value);
|
|
8106
|
+
}
|
|
8107
|
+
assertEqual(convert(input), expected);
|
|
8108
|
+
assertEqual(convert(expected), expected);
|
|
8109
|
+
});
|
|
7363
8110
|
}
|
|
7364
8111
|
|
|
7365
|
-
|
|
7366
|
-
|
|
7367
|
-
|
|
7368
|
-
|
|
7369
|
-
|
|
7370
|
-
|
|
7371
|
-
|
|
7372
|
-
|
|
7373
|
-
|
|
7374
|
-
|
|
7375
|
-
|
|
8112
|
+
it("uses intrinsic types and accepts known literals directly", () => {
|
|
8113
|
+
assertType<CapitalizedString, Capitalize<string>>();
|
|
8114
|
+
assertType<UncapitalizedString, Uncapitalize<string>>();
|
|
8115
|
+
assertType<UppercasedString, Uppercase<string>>();
|
|
8116
|
+
assertType<LowercasedString, Lowercase<string>>();
|
|
8117
|
+
assertType<ReturnType<typeof capitalize>, Capitalize<string>>();
|
|
8118
|
+
assertType<ReturnType<typeof uncapitalize>, Uncapitalize<string>>();
|
|
8119
|
+
assertType<ReturnType<typeof uppercase>, Uppercase<string>>();
|
|
8120
|
+
assertType<ReturnType<typeof lowercase>, Lowercase<string>>();
|
|
8121
|
+
const title: CapitalizedString = "Hello world";
|
|
8122
|
+
const sentence: UncapitalizedString = "hello WORLD";
|
|
8123
|
+
const shout: UppercasedString = "HELLO WORLD";
|
|
8124
|
+
const quiet: LowercasedString = "hello world";
|
|
8125
|
+
assertOk(CapitalizedString.fromUnknown(title), title);
|
|
8126
|
+
assertOk(UncapitalizedString.fromUnknown(sentence), sentence);
|
|
8127
|
+
assertOk(UppercasedString.fromUnknown(shout), shout);
|
|
8128
|
+
assertOk(LowercasedString.fromUnknown(quiet), quiet);
|
|
8129
|
+
// @ts-expect-error A lowercase initial does not satisfy Capitalize<string>.
|
|
8130
|
+
const _title: CapitalizedString = "hello";
|
|
8131
|
+
// @ts-expect-error An uppercase initial does not satisfy Uncapitalize<string>.
|
|
8132
|
+
const _sentence: UncapitalizedString = "Hello";
|
|
8133
|
+
// @ts-expect-error A lowercase character does not satisfy Uppercase<string>.
|
|
8134
|
+
const _shout: UppercasedString = "HELLo";
|
|
8135
|
+
// @ts-expect-error An uppercase character does not satisfy Lowercase<string>.
|
|
8136
|
+
const _quiet: LowercasedString = "hellO";
|
|
8137
|
+
});
|
|
8138
|
+
|
|
8139
|
+
it("infers converted literals, unions, and runtime strings", () => {
|
|
8140
|
+
const capital = capitalize("hello");
|
|
8141
|
+
const uncapital = uncapitalize("Hello");
|
|
8142
|
+
const upper = uppercase("hello");
|
|
8143
|
+
const lower = lowercase("HELLO");
|
|
8144
|
+
assertType<typeof capital, "Hello">();
|
|
8145
|
+
assertType<typeof uncapital, "hello">();
|
|
8146
|
+
assertType<typeof upper, "HELLO">();
|
|
8147
|
+
assertType<typeof lower, "hello">();
|
|
8148
|
+
assertEqual(
|
|
8149
|
+
[capital, uncapital, upper, lower],
|
|
8150
|
+
["Hello", "hello", "HELLO", "hello"],
|
|
8151
|
+
);
|
|
8152
|
+
|
|
8153
|
+
for (const value of ["hello", "WORLD"] as const) {
|
|
8154
|
+
const capital = capitalize(value);
|
|
8155
|
+
const uncapital = uncapitalize(value);
|
|
8156
|
+
const upper = uppercase(value);
|
|
8157
|
+
const lower = lowercase(value);
|
|
8158
|
+
assertType<typeof capital, "Hello" | "WORLD">();
|
|
8159
|
+
assertType<typeof uncapital, "hello" | "wORLD">();
|
|
8160
|
+
assertType<typeof upper, "HELLO" | "WORLD">();
|
|
8161
|
+
assertType<typeof lower, "hello" | "world">();
|
|
8162
|
+
}
|
|
8163
|
+
|
|
8164
|
+
const value = String.orThrow("Hello");
|
|
8165
|
+
const runtimeCapital = capitalize(value);
|
|
8166
|
+
const runtimeUncapital = uncapitalize(value);
|
|
8167
|
+
const runtimeUpper = uppercase(value);
|
|
8168
|
+
const runtimeLower = lowercase(value);
|
|
8169
|
+
assertType<typeof runtimeCapital, Capitalize<string>>();
|
|
8170
|
+
assertType<typeof runtimeUncapital, Uncapitalize<string>>();
|
|
8171
|
+
assertType<typeof runtimeUpper, Uppercase<string>>();
|
|
8172
|
+
assertType<typeof runtimeLower, Lowercase<string>>();
|
|
8173
|
+
|
|
8174
|
+
const astralCapital = capitalize("𐐨x");
|
|
8175
|
+
const astralUncapital = uncapitalize("𐐀X");
|
|
8176
|
+
const expandedUpper = uppercase("ß");
|
|
8177
|
+
const expandedLower = lowercase("İ");
|
|
8178
|
+
assertType<typeof astralCapital, "𐐀x">();
|
|
8179
|
+
assertType<typeof astralUncapital, "𐐨X">();
|
|
8180
|
+
assertType<typeof expandedUpper, "SS">();
|
|
8181
|
+
assertType<typeof expandedLower, "i\u0307">();
|
|
8182
|
+
assertEqual(
|
|
8183
|
+
[astralCapital, astralUncapital, expandedUpper, expandedLower],
|
|
8184
|
+
["𐐀x", "𐐨X", "SS", "i\u0307"],
|
|
8185
|
+
);
|
|
8186
|
+
});
|
|
8187
|
+
|
|
8188
|
+
it("matches TS7 Unicode mappings and handles expansions and uncased initials", () => {
|
|
8189
|
+
assertType<Capitalize<"𐐨x">, "𐐀x">();
|
|
8190
|
+
assertType<Uncapitalize<"𐐀X">, "𐐨X">();
|
|
8191
|
+
assertType<Uppercase<"ß">, "SS">();
|
|
8192
|
+
assertType<Lowercase<"İ">, "i\u0307">();
|
|
8193
|
+
// @ts-expect-error A lowercase astral initial does not satisfy Capitalize<string>.
|
|
8194
|
+
const _lowerAstral: CapitalizedString = "𐐨x";
|
|
8195
|
+
// @ts-expect-error An uppercase astral initial does not satisfy Uncapitalize<string>.
|
|
8196
|
+
const _upperAstral: UncapitalizedString = "𐐀x";
|
|
8197
|
+
assertEqual(capitalize("𐐨x"), "𐐀x");
|
|
8198
|
+
assertEqual(uncapitalize("𐐀X"), "𐐨X");
|
|
8199
|
+
assertEqual(capitalize("ßabc"), "SSabc");
|
|
8200
|
+
assertEqual(uncapitalize("İABC"), "i\u0307ABC");
|
|
8201
|
+
assertEqual(uppercase("Straße"), "STRASSE");
|
|
8202
|
+
assertEqual(lowercase("ΟΣ"), "ος");
|
|
8203
|
+
assertEqual(capitalize("e\u0301clair"), "E\u0301clair");
|
|
8204
|
+
assertEqual(uncapitalize("E\u0301CLAIR"), "e\u0301CLAIR");
|
|
8205
|
+
for (const { type, convert } of cases) {
|
|
8206
|
+
for (const input of [
|
|
8207
|
+
"𐐨𐐀",
|
|
8208
|
+
"ßİ",
|
|
8209
|
+
"ffile",
|
|
8210
|
+
"Džuro",
|
|
8211
|
+
"ΟΣ",
|
|
8212
|
+
"😀hello",
|
|
8213
|
+
" hello",
|
|
8214
|
+
"1ABC",
|
|
8215
|
+
"\uD800x",
|
|
8216
|
+
"\uDC00X",
|
|
8217
|
+
]) {
|
|
8218
|
+
const output = convert(input);
|
|
8219
|
+
assertTrue(type.is(output));
|
|
8220
|
+
assertEqual(convert(output), output);
|
|
8221
|
+
}
|
|
8222
|
+
}
|
|
8223
|
+
assertOk(CapitalizedString.fromUnknown("😀hello"), "😀hello");
|
|
8224
|
+
assertOk(UncapitalizedString.fromUnknown("😀HELLO"), "😀HELLO");
|
|
8225
|
+
assertErr(CapitalizedString.fromUnknown("𐐨x"));
|
|
8226
|
+
assertErr(UncapitalizedString.fromUnknown("𐐀x"));
|
|
8227
|
+
});
|
|
8228
|
+
|
|
8229
|
+
it("preserves parent constraints during validation and drops them during conversion", () => {
|
|
8230
|
+
const Short = maxLength(1)(String);
|
|
8231
|
+
const Capitalized = capitalized(Short);
|
|
8232
|
+
const Uncapitalized = uncapitalized(Short);
|
|
8233
|
+
const Uppercased = uppercased(Short);
|
|
8234
|
+
const Lowercased = lowercased(Short);
|
|
8235
|
+
assertType<
|
|
8236
|
+
typeof Capitalized.Output,
|
|
8237
|
+
string & Brand<"MaxLength1"> & Capitalize<string>
|
|
8238
|
+
>();
|
|
8239
|
+
assertType<
|
|
8240
|
+
typeof Uncapitalized.Output,
|
|
8241
|
+
string & Brand<"MaxLength1"> & Uncapitalize<string>
|
|
8242
|
+
>();
|
|
8243
|
+
assertType<
|
|
8244
|
+
typeof Uppercased.Output,
|
|
8245
|
+
string & Brand<"MaxLength1"> & Uppercase<string>
|
|
8246
|
+
>();
|
|
8247
|
+
assertType<
|
|
8248
|
+
typeof Lowercased.Output,
|
|
8249
|
+
string & Brand<"MaxLength1"> & Lowercase<string>
|
|
8250
|
+
>();
|
|
8251
|
+
assertType<
|
|
8252
|
+
InferErrors<typeof Capitalized>,
|
|
8253
|
+
typeof Short.Error | typeof String.Error | typeof Capitalized.Error
|
|
8254
|
+
>();
|
|
8255
|
+
for (const [type, valid] of [
|
|
8256
|
+
[Capitalized, "A"],
|
|
8257
|
+
[Uncapitalized, "a"],
|
|
8258
|
+
[Uppercased, "A"],
|
|
8259
|
+
[Lowercased, "a"],
|
|
8260
|
+
] as const) {
|
|
8261
|
+
assertSame(type.parent, Short);
|
|
8262
|
+
assertOk(type.fromUnknown(valid), valid);
|
|
8263
|
+
assertErr(type.fromUnknown(valid.repeat(2)), {
|
|
8264
|
+
type: "MaxLength1",
|
|
8265
|
+
value: valid.repeat(2),
|
|
8266
|
+
max: 1,
|
|
8267
|
+
});
|
|
8268
|
+
}
|
|
8269
|
+
const sharpS = Short.orThrow("ß");
|
|
8270
|
+
const dottedI = Short.orThrow("İ");
|
|
8271
|
+
const capital = capitalize(sharpS);
|
|
8272
|
+
const uncapital = uncapitalize(dottedI);
|
|
8273
|
+
const upper = uppercase(sharpS);
|
|
8274
|
+
const lower = lowercase(dottedI);
|
|
8275
|
+
assertEqual(capital, "SS");
|
|
8276
|
+
assertEqual(uncapital, "i\u0307");
|
|
8277
|
+
assertEqual(upper, "SS");
|
|
8278
|
+
assertEqual(lower, "i\u0307");
|
|
8279
|
+
// @ts-expect-error Capitalization does not retain the MaxLength1 brand.
|
|
8280
|
+
const _capital: typeof sharpS = capital;
|
|
8281
|
+
// @ts-expect-error Uncapitalization does not retain the MaxLength1 brand.
|
|
8282
|
+
const _uncapital: typeof dottedI = uncapital;
|
|
8283
|
+
// @ts-expect-error Uppercasing does not retain the MaxLength1 brand.
|
|
8284
|
+
const _upper: typeof sharpS = upper;
|
|
8285
|
+
// @ts-expect-error Lowercasing does not retain the MaxLength1 brand.
|
|
8286
|
+
const _lower: typeof dottedI = lower;
|
|
8287
|
+
});
|
|
8288
|
+
|
|
8289
|
+
it("preserves a parent's decoding and encoding", () => {
|
|
8290
|
+
const PrefixedString = transform("PrefixedString", String, String, {
|
|
8291
|
+
from: (value) => ok(value.slice(1)),
|
|
8292
|
+
to: (value) => `!${value}`,
|
|
8293
|
+
});
|
|
8294
|
+
for (const [type, value] of [
|
|
8295
|
+
[capitalized(PrefixedString), "Hello"],
|
|
8296
|
+
[uncapitalized(PrefixedString), "hello"],
|
|
8297
|
+
[uppercased(PrefixedString), "HELLO"],
|
|
8298
|
+
[lowercased(PrefixedString), "hello"],
|
|
8299
|
+
] as const) {
|
|
8300
|
+
const result = type.fromUnknown(`!${value}`);
|
|
8301
|
+
assertOk(result);
|
|
8302
|
+
assertEqual(result.value, value);
|
|
8303
|
+
assertEqual(type.to("123"), "!123");
|
|
8304
|
+
assertErr(type.fromUnknown(null));
|
|
8305
|
+
}
|
|
8306
|
+
});
|
|
8307
|
+
|
|
8308
|
+
it("formats casing errors and matches the default English messages", () => {
|
|
8309
|
+
assertEqual(
|
|
8310
|
+
formatUncapitalizedError({ type: "Uncapitalized", value: "Hello" }),
|
|
8311
|
+
'The value "Hello" must not start with an uppercase letter.',
|
|
8312
|
+
);
|
|
8313
|
+
assertEqual(
|
|
8314
|
+
cs.formatUncapitalizedError({ type: "Uncapitalized", value: "Hello" }),
|
|
8315
|
+
'Hodnota "Hello" nesmí začínat velkým písmenem.',
|
|
8316
|
+
);
|
|
8317
|
+
assertEqual(
|
|
8318
|
+
formatCapitalizedError({ type: "Capitalized", value: "hello" }),
|
|
8319
|
+
CapitalizedString.formatError({ type: "Capitalized", value: "hello" }),
|
|
8320
|
+
);
|
|
8321
|
+
assertEqual(
|
|
8322
|
+
formatUncapitalizedError({ type: "Uncapitalized", value: "Hello" }),
|
|
8323
|
+
UncapitalizedString.formatError({
|
|
8324
|
+
type: "Uncapitalized",
|
|
8325
|
+
value: "Hello",
|
|
8326
|
+
}),
|
|
8327
|
+
);
|
|
8328
|
+
assertEqual(
|
|
8329
|
+
formatUppercasedError({ type: "Uppercased", value: "Hello" }),
|
|
8330
|
+
UppercasedString.formatError({ type: "Uppercased", value: "Hello" }),
|
|
8331
|
+
);
|
|
8332
|
+
assertEqual(
|
|
8333
|
+
formatLowercasedError({ type: "Lowercased", value: "Hello" }),
|
|
8334
|
+
LowercasedString.formatError({ type: "Lowercased", value: "Hello" }),
|
|
8335
|
+
);
|
|
8336
|
+
});
|
|
8337
|
+
});
|
|
8338
|
+
|
|
8339
|
+
describe("BrandFactory", () => {
|
|
8340
|
+
interface NonEmptyError extends TypeError<"NonEmpty"> {
|
|
8341
|
+
readonly value: { readonly length: number };
|
|
8342
|
+
}
|
|
8343
|
+
|
|
8344
|
+
const createNonEmpty = () => {
|
|
8345
|
+
const validations: Array<{ readonly length: number }> = [];
|
|
8346
|
+
const nonEmpty: BrandFactory<
|
|
8347
|
+
"NonEmpty",
|
|
8348
|
+
{ readonly length: number },
|
|
8349
|
+
NonEmptyError
|
|
8350
|
+
> = (parent) =>
|
|
8351
|
+
brand(
|
|
8352
|
+
"NonEmpty",
|
|
8353
|
+
parent,
|
|
8354
|
+
(value) => {
|
|
7376
8355
|
validations.push(value);
|
|
7377
8356
|
|
|
7378
8357
|
return value.length > 0 ? ok() : err({ type: "NonEmpty", value });
|
|
@@ -7613,81 +8592,1048 @@ describe("BrandFactory", () => {
|
|
|
7613
8592
|
});
|
|
7614
8593
|
|
|
7615
8594
|
describe("Type Factory", () => {
|
|
7616
|
-
describe("
|
|
8595
|
+
describe("identifier", () => {
|
|
8596
|
+
const formats = [
|
|
8597
|
+
{
|
|
8598
|
+
type: CamelCaseIdentifier,
|
|
8599
|
+
casing: "camelCase",
|
|
8600
|
+
valid: [
|
|
8601
|
+
"a",
|
|
8602
|
+
"a0",
|
|
8603
|
+
"http2Port",
|
|
8604
|
+
"v2beta",
|
|
8605
|
+
"v2Beta",
|
|
8606
|
+
"maxOwnerBytes",
|
|
8607
|
+
"httpUrl",
|
|
8608
|
+
"httpURL",
|
|
8609
|
+
"aBC",
|
|
8610
|
+
],
|
|
8611
|
+
invalid: ["Http2Port", "HTTP2_PORT", "http_port", "http-port"],
|
|
8612
|
+
},
|
|
8613
|
+
{
|
|
8614
|
+
type: PascalCaseIdentifier,
|
|
8615
|
+
casing: "PascalCase",
|
|
8616
|
+
valid: [
|
|
8617
|
+
"A",
|
|
8618
|
+
"A0",
|
|
8619
|
+
"Http2Port",
|
|
8620
|
+
"V2beta",
|
|
8621
|
+
"V2Beta",
|
|
8622
|
+
"MaxOwnerBytes",
|
|
8623
|
+
"HttpUrl",
|
|
8624
|
+
"HttpURL",
|
|
8625
|
+
"ABC",
|
|
8626
|
+
],
|
|
8627
|
+
invalid: ["http2Port", "HTTP2_PORT", "http_port", "http-port"],
|
|
8628
|
+
},
|
|
8629
|
+
{
|
|
8630
|
+
type: SnakeCaseIdentifier,
|
|
8631
|
+
casing: "snake_case",
|
|
8632
|
+
valid: [
|
|
8633
|
+
"a",
|
|
8634
|
+
"a0",
|
|
8635
|
+
"http2_port",
|
|
8636
|
+
"v2beta",
|
|
8637
|
+
"v2_beta",
|
|
8638
|
+
"max_owner_bytes",
|
|
8639
|
+
"http_url",
|
|
8640
|
+
"http_u_r_l",
|
|
8641
|
+
"a_b_c",
|
|
8642
|
+
],
|
|
8643
|
+
invalid: [
|
|
8644
|
+
"HTTP2_PORT",
|
|
8645
|
+
"httpPort",
|
|
8646
|
+
"http-port",
|
|
8647
|
+
"_port",
|
|
8648
|
+
"port_",
|
|
8649
|
+
"http__port",
|
|
8650
|
+
"http_2_port",
|
|
8651
|
+
],
|
|
8652
|
+
},
|
|
8653
|
+
{
|
|
8654
|
+
type: KebabCaseIdentifier,
|
|
8655
|
+
casing: "kebab-case",
|
|
8656
|
+
valid: [
|
|
8657
|
+
"a",
|
|
8658
|
+
"a0",
|
|
8659
|
+
"http2-port",
|
|
8660
|
+
"v2beta",
|
|
8661
|
+
"v2-beta",
|
|
8662
|
+
"max-owner-bytes",
|
|
8663
|
+
"http-url",
|
|
8664
|
+
"http-u-r-l",
|
|
8665
|
+
"a-b-c",
|
|
8666
|
+
],
|
|
8667
|
+
invalid: [
|
|
8668
|
+
"HTTP2-PORT",
|
|
8669
|
+
"httpPort",
|
|
8670
|
+
"http_port",
|
|
8671
|
+
"-port",
|
|
8672
|
+
"port-",
|
|
8673
|
+
"http--port",
|
|
8674
|
+
"http-2-port",
|
|
8675
|
+
],
|
|
8676
|
+
},
|
|
8677
|
+
{
|
|
8678
|
+
type: ConstantCaseIdentifier,
|
|
8679
|
+
casing: "CONSTANT_CASE",
|
|
8680
|
+
valid: [
|
|
8681
|
+
"A",
|
|
8682
|
+
"A0",
|
|
8683
|
+
"HTTP2_PORT",
|
|
8684
|
+
"V2BETA",
|
|
8685
|
+
"V2_BETA",
|
|
8686
|
+
"MAX_OWNER_BYTES",
|
|
8687
|
+
"HTTP_URL",
|
|
8688
|
+
"HTTP_U_R_L",
|
|
8689
|
+
"A_B_C",
|
|
8690
|
+
],
|
|
8691
|
+
invalid: [
|
|
8692
|
+
"http2_port",
|
|
8693
|
+
"httpPort",
|
|
8694
|
+
"HTTP-PORT",
|
|
8695
|
+
"_PORT",
|
|
8696
|
+
"PORT_",
|
|
8697
|
+
"HTTP__PORT",
|
|
8698
|
+
"HTTP_2_PORT",
|
|
8699
|
+
],
|
|
8700
|
+
},
|
|
8701
|
+
] as const;
|
|
8702
|
+
|
|
8703
|
+
for (const { type, casing, valid, invalid } of formats) {
|
|
8704
|
+
it(`${type.name} accepts only its complete ASCII grammar`, () => {
|
|
8705
|
+
for (const value of valid) {
|
|
8706
|
+
assertOk(type.fromUnknown(value), value);
|
|
8707
|
+
assertTrue(type.is(value));
|
|
8708
|
+
}
|
|
8709
|
+
for (const value of [
|
|
8710
|
+
...invalid,
|
|
8711
|
+
"",
|
|
8712
|
+
"0",
|
|
8713
|
+
"2FA",
|
|
8714
|
+
"2fa",
|
|
8715
|
+
" hello",
|
|
8716
|
+
"hello ",
|
|
8717
|
+
"hello world",
|
|
8718
|
+
"HELLO WORLD",
|
|
8719
|
+
"hello.world",
|
|
8720
|
+
"HELLO.WORLD",
|
|
8721
|
+
"déjà",
|
|
8722
|
+
"DÉJÀ",
|
|
8723
|
+
"𐐀x",
|
|
8724
|
+
"😀",
|
|
8725
|
+
"a\u0301",
|
|
8726
|
+
"A\u0301",
|
|
8727
|
+
...valid.flatMap((value) =>
|
|
8728
|
+
["\n", "\r", "\r\n", "\t", "\0", "\u2028", "\u2029"].map(
|
|
8729
|
+
(suffix) => value + suffix,
|
|
8730
|
+
),
|
|
8731
|
+
),
|
|
8732
|
+
]) {
|
|
8733
|
+
assertErr(type.fromUnknown(value), {
|
|
8734
|
+
type: type.name,
|
|
8735
|
+
casing,
|
|
8736
|
+
value,
|
|
8737
|
+
});
|
|
8738
|
+
assertFalse(type.is(value));
|
|
8739
|
+
}
|
|
8740
|
+
assertErr(type.fromUnknown(123), {
|
|
8741
|
+
type: "TypeOf",
|
|
8742
|
+
expected: "String",
|
|
8743
|
+
value: 123,
|
|
8744
|
+
});
|
|
8745
|
+
const empty = type.fromUnknown("");
|
|
8746
|
+
assertErr(empty);
|
|
8747
|
+
assertEqual(
|
|
8748
|
+
typeErrorToIssues(type, empty.error)[0]?.message,
|
|
8749
|
+
`The value "" is not a ${casing} identifier.`,
|
|
8750
|
+
);
|
|
8751
|
+
});
|
|
8752
|
+
}
|
|
8753
|
+
|
|
8754
|
+
it("exposes distinct brands and composes validation constraints", () => {
|
|
8755
|
+
assertType<
|
|
8756
|
+
CamelCaseIdentifier,
|
|
8757
|
+
string & Brand<"CamelCaseIdentifier">
|
|
8758
|
+
>();
|
|
8759
|
+
assertType<
|
|
8760
|
+
PascalCaseIdentifier,
|
|
8761
|
+
string & Brand<"PascalCaseIdentifier">
|
|
8762
|
+
>();
|
|
8763
|
+
assertType<
|
|
8764
|
+
SnakeCaseIdentifier,
|
|
8765
|
+
string & Brand<"SnakeCaseIdentifier">
|
|
8766
|
+
>();
|
|
8767
|
+
assertType<
|
|
8768
|
+
KebabCaseIdentifier,
|
|
8769
|
+
string & Brand<"KebabCaseIdentifier">
|
|
8770
|
+
>();
|
|
8771
|
+
assertType<
|
|
8772
|
+
ConstantCaseIdentifier,
|
|
8773
|
+
string & Brand<"ConstantCaseIdentifier">
|
|
8774
|
+
>();
|
|
8775
|
+
assertType<
|
|
8776
|
+
IdentifierError<"camelCase">["type"],
|
|
8777
|
+
"CamelCaseIdentifier"
|
|
8778
|
+
>();
|
|
8779
|
+
assertType<IdentifierError["casing"], IdentifierCasing>();
|
|
8780
|
+
|
|
8781
|
+
const ShortCamel = identifier("camelCase")(maxLength(3)(String));
|
|
8782
|
+
assertType<
|
|
8783
|
+
typeof ShortCamel.Output,
|
|
8784
|
+
string & Brand<"MaxLength3"> & Brand<"CamelCaseIdentifier">
|
|
8785
|
+
>();
|
|
8786
|
+
assertOk(ShortCamel.fromUnknown("aBC"), "aBC");
|
|
8787
|
+
assertErr(ShortCamel.fromUnknown("long"), {
|
|
8788
|
+
type: "MaxLength3",
|
|
8789
|
+
value: "long",
|
|
8790
|
+
max: 3,
|
|
8791
|
+
});
|
|
8792
|
+
assertErr(ShortCamel.fromUnknown("Ab"), {
|
|
8793
|
+
type: "CamelCaseIdentifier",
|
|
8794
|
+
casing: "camelCase",
|
|
8795
|
+
value: "Ab",
|
|
8796
|
+
});
|
|
8797
|
+
assertOk(
|
|
8798
|
+
identifier("PascalCase")(maxLength(3)(String)).fromUnknown("ABC"),
|
|
8799
|
+
"ABC",
|
|
8800
|
+
);
|
|
8801
|
+
assertOk(
|
|
8802
|
+
identifier("snake_case")(maxLength(3)(String)).fromUnknown("a_b"),
|
|
8803
|
+
"a_b",
|
|
8804
|
+
);
|
|
8805
|
+
assertOk(
|
|
8806
|
+
identifier("kebab-case")(maxLength(3)(String)).fromUnknown("a-b"),
|
|
8807
|
+
"a-b",
|
|
8808
|
+
);
|
|
8809
|
+
assertOk(
|
|
8810
|
+
identifier("CONSTANT_CASE")(maxLength(3)(String)).fromUnknown("A_B"),
|
|
8811
|
+
"A_B",
|
|
8812
|
+
);
|
|
8813
|
+
});
|
|
8814
|
+
});
|
|
8815
|
+
|
|
8816
|
+
describe("naming conversions", () => {
|
|
8817
|
+
const conversions = [
|
|
8818
|
+
{
|
|
8819
|
+
from: "CamelCase",
|
|
8820
|
+
to: "PascalCase",
|
|
8821
|
+
convert: (value: string) =>
|
|
8822
|
+
camelCaseToPascalCase(CamelCaseIdentifier.orThrow(value)),
|
|
8823
|
+
},
|
|
8824
|
+
{
|
|
8825
|
+
from: "CamelCase",
|
|
8826
|
+
to: "SnakeCase",
|
|
8827
|
+
convert: (value: string) =>
|
|
8828
|
+
camelCaseToSnakeCase(CamelCaseIdentifier.orThrow(value)),
|
|
8829
|
+
},
|
|
8830
|
+
{
|
|
8831
|
+
from: "CamelCase",
|
|
8832
|
+
to: "KebabCase",
|
|
8833
|
+
convert: (value: string) =>
|
|
8834
|
+
camelCaseToKebabCase(CamelCaseIdentifier.orThrow(value)),
|
|
8835
|
+
},
|
|
8836
|
+
{
|
|
8837
|
+
from: "CamelCase",
|
|
8838
|
+
to: "ConstantCase",
|
|
8839
|
+
convert: (value: string) =>
|
|
8840
|
+
camelCaseToConstantCase(CamelCaseIdentifier.orThrow(value)),
|
|
8841
|
+
},
|
|
8842
|
+
{
|
|
8843
|
+
from: "PascalCase",
|
|
8844
|
+
to: "CamelCase",
|
|
8845
|
+
convert: (value: string) =>
|
|
8846
|
+
pascalCaseToCamelCase(PascalCaseIdentifier.orThrow(value)),
|
|
8847
|
+
},
|
|
8848
|
+
{
|
|
8849
|
+
from: "PascalCase",
|
|
8850
|
+
to: "SnakeCase",
|
|
8851
|
+
convert: (value: string) =>
|
|
8852
|
+
pascalCaseToSnakeCase(PascalCaseIdentifier.orThrow(value)),
|
|
8853
|
+
},
|
|
8854
|
+
{
|
|
8855
|
+
from: "PascalCase",
|
|
8856
|
+
to: "KebabCase",
|
|
8857
|
+
convert: (value: string) =>
|
|
8858
|
+
pascalCaseToKebabCase(PascalCaseIdentifier.orThrow(value)),
|
|
8859
|
+
},
|
|
8860
|
+
{
|
|
8861
|
+
from: "PascalCase",
|
|
8862
|
+
to: "ConstantCase",
|
|
8863
|
+
convert: (value: string) =>
|
|
8864
|
+
pascalCaseToConstantCase(PascalCaseIdentifier.orThrow(value)),
|
|
8865
|
+
},
|
|
8866
|
+
{
|
|
8867
|
+
from: "SnakeCase",
|
|
8868
|
+
to: "CamelCase",
|
|
8869
|
+
convert: (value: string) =>
|
|
8870
|
+
snakeCaseToCamelCase(SnakeCaseIdentifier.orThrow(value)),
|
|
8871
|
+
},
|
|
8872
|
+
{
|
|
8873
|
+
from: "SnakeCase",
|
|
8874
|
+
to: "PascalCase",
|
|
8875
|
+
convert: (value: string) =>
|
|
8876
|
+
snakeCaseToPascalCase(SnakeCaseIdentifier.orThrow(value)),
|
|
8877
|
+
},
|
|
8878
|
+
{
|
|
8879
|
+
from: "SnakeCase",
|
|
8880
|
+
to: "KebabCase",
|
|
8881
|
+
convert: (value: string) =>
|
|
8882
|
+
snakeCaseToKebabCase(SnakeCaseIdentifier.orThrow(value)),
|
|
8883
|
+
},
|
|
8884
|
+
{
|
|
8885
|
+
from: "SnakeCase",
|
|
8886
|
+
to: "ConstantCase",
|
|
8887
|
+
convert: (value: string) =>
|
|
8888
|
+
snakeCaseToConstantCase(SnakeCaseIdentifier.orThrow(value)),
|
|
8889
|
+
},
|
|
8890
|
+
{
|
|
8891
|
+
from: "KebabCase",
|
|
8892
|
+
to: "CamelCase",
|
|
8893
|
+
convert: (value: string) =>
|
|
8894
|
+
kebabCaseToCamelCase(KebabCaseIdentifier.orThrow(value)),
|
|
8895
|
+
},
|
|
8896
|
+
{
|
|
8897
|
+
from: "KebabCase",
|
|
8898
|
+
to: "PascalCase",
|
|
8899
|
+
convert: (value: string) =>
|
|
8900
|
+
kebabCaseToPascalCase(KebabCaseIdentifier.orThrow(value)),
|
|
8901
|
+
},
|
|
8902
|
+
{
|
|
8903
|
+
from: "KebabCase",
|
|
8904
|
+
to: "SnakeCase",
|
|
8905
|
+
convert: (value: string) =>
|
|
8906
|
+
kebabCaseToSnakeCase(KebabCaseIdentifier.orThrow(value)),
|
|
8907
|
+
},
|
|
8908
|
+
{
|
|
8909
|
+
from: "KebabCase",
|
|
8910
|
+
to: "ConstantCase",
|
|
8911
|
+
convert: (value: string) =>
|
|
8912
|
+
kebabCaseToConstantCase(KebabCaseIdentifier.orThrow(value)),
|
|
8913
|
+
},
|
|
8914
|
+
{
|
|
8915
|
+
from: "ConstantCase",
|
|
8916
|
+
to: "CamelCase",
|
|
8917
|
+
convert: (value: string) =>
|
|
8918
|
+
constantCaseToCamelCase(ConstantCaseIdentifier.orThrow(value)),
|
|
8919
|
+
},
|
|
8920
|
+
{
|
|
8921
|
+
from: "ConstantCase",
|
|
8922
|
+
to: "PascalCase",
|
|
8923
|
+
convert: (value: string) =>
|
|
8924
|
+
constantCaseToPascalCase(ConstantCaseIdentifier.orThrow(value)),
|
|
8925
|
+
},
|
|
8926
|
+
{
|
|
8927
|
+
from: "ConstantCase",
|
|
8928
|
+
to: "SnakeCase",
|
|
8929
|
+
convert: (value: string) =>
|
|
8930
|
+
constantCaseToSnakeCase(ConstantCaseIdentifier.orThrow(value)),
|
|
8931
|
+
},
|
|
8932
|
+
{
|
|
8933
|
+
from: "ConstantCase",
|
|
8934
|
+
to: "KebabCase",
|
|
8935
|
+
convert: (value: string) =>
|
|
8936
|
+
constantCaseToKebabCase(ConstantCaseIdentifier.orThrow(value)),
|
|
8937
|
+
},
|
|
8938
|
+
] as const;
|
|
8939
|
+
|
|
8940
|
+
it("preserves the exact spelling for every pair of formats", () => {
|
|
8941
|
+
const spellings = [
|
|
8942
|
+
{
|
|
8943
|
+
CamelCase: "maxOwnerBytes",
|
|
8944
|
+
PascalCase: "MaxOwnerBytes",
|
|
8945
|
+
SnakeCase: "max_owner_bytes",
|
|
8946
|
+
KebabCase: "max-owner-bytes",
|
|
8947
|
+
ConstantCase: "MAX_OWNER_BYTES",
|
|
8948
|
+
},
|
|
8949
|
+
{
|
|
8950
|
+
CamelCase: "http2Port",
|
|
8951
|
+
PascalCase: "Http2Port",
|
|
8952
|
+
SnakeCase: "http2_port",
|
|
8953
|
+
KebabCase: "http2-port",
|
|
8954
|
+
ConstantCase: "HTTP2_PORT",
|
|
8955
|
+
},
|
|
8956
|
+
{
|
|
8957
|
+
CamelCase: "httpUrl",
|
|
8958
|
+
PascalCase: "HttpUrl",
|
|
8959
|
+
SnakeCase: "http_url",
|
|
8960
|
+
KebabCase: "http-url",
|
|
8961
|
+
ConstantCase: "HTTP_URL",
|
|
8962
|
+
},
|
|
8963
|
+
{
|
|
8964
|
+
CamelCase: "httpURL",
|
|
8965
|
+
PascalCase: "HttpURL",
|
|
8966
|
+
SnakeCase: "http_u_r_l",
|
|
8967
|
+
KebabCase: "http-u-r-l",
|
|
8968
|
+
ConstantCase: "HTTP_U_R_L",
|
|
8969
|
+
},
|
|
8970
|
+
{
|
|
8971
|
+
CamelCase: "aBC",
|
|
8972
|
+
PascalCase: "ABC",
|
|
8973
|
+
SnakeCase: "a_b_c",
|
|
8974
|
+
KebabCase: "a-b-c",
|
|
8975
|
+
ConstantCase: "A_B_C",
|
|
8976
|
+
},
|
|
8977
|
+
{
|
|
8978
|
+
CamelCase: "v2Beta",
|
|
8979
|
+
PascalCase: "V2Beta",
|
|
8980
|
+
SnakeCase: "v2_beta",
|
|
8981
|
+
KebabCase: "v2-beta",
|
|
8982
|
+
ConstantCase: "V2_BETA",
|
|
8983
|
+
},
|
|
8984
|
+
{
|
|
8985
|
+
CamelCase: "v2beta",
|
|
8986
|
+
PascalCase: "V2beta",
|
|
8987
|
+
SnakeCase: "v2beta",
|
|
8988
|
+
KebabCase: "v2beta",
|
|
8989
|
+
ConstantCase: "V2BETA",
|
|
8990
|
+
},
|
|
8991
|
+
{
|
|
8992
|
+
CamelCase: "a",
|
|
8993
|
+
PascalCase: "A",
|
|
8994
|
+
SnakeCase: "a",
|
|
8995
|
+
KebabCase: "a",
|
|
8996
|
+
ConstantCase: "A",
|
|
8997
|
+
},
|
|
8998
|
+
{
|
|
8999
|
+
CamelCase: "a0B2c",
|
|
9000
|
+
PascalCase: "A0B2c",
|
|
9001
|
+
SnakeCase: "a0_b2c",
|
|
9002
|
+
KebabCase: "a0-b2c",
|
|
9003
|
+
ConstantCase: "A0_B2C",
|
|
9004
|
+
},
|
|
9005
|
+
{
|
|
9006
|
+
CamelCase: "max2faAttempts",
|
|
9007
|
+
PascalCase: "Max2faAttempts",
|
|
9008
|
+
SnakeCase: "max2fa_attempts",
|
|
9009
|
+
KebabCase: "max2fa-attempts",
|
|
9010
|
+
ConstantCase: "MAX2FA_ATTEMPTS",
|
|
9011
|
+
},
|
|
9012
|
+
];
|
|
9013
|
+
for (const values of spellings) {
|
|
9014
|
+
for (const { from, to, convert } of conversions) {
|
|
9015
|
+
assertEqual(convert(values[from]), values[to]);
|
|
9016
|
+
}
|
|
9017
|
+
}
|
|
9018
|
+
});
|
|
9019
|
+
|
|
9020
|
+
it("round-trips generated word sequences through every format", () => {
|
|
9021
|
+
const words = ["a", "b", "z", "a0", "b2", "ab", "z9a", "http2", "url"];
|
|
9022
|
+
const formats = {
|
|
9023
|
+
CamelCase: CamelCaseIdentifier,
|
|
9024
|
+
PascalCase: PascalCaseIdentifier,
|
|
9025
|
+
SnakeCase: SnakeCaseIdentifier,
|
|
9026
|
+
KebabCase: KebabCaseIdentifier,
|
|
9027
|
+
ConstantCase: ConstantCaseIdentifier,
|
|
9028
|
+
};
|
|
9029
|
+
for (const first of words) {
|
|
9030
|
+
for (const second of words) {
|
|
9031
|
+
for (const third of words) {
|
|
9032
|
+
const parts = [first, second, third];
|
|
9033
|
+
const titled = parts.map(
|
|
9034
|
+
(word) => word.charAt(0).toUpperCase() + word.slice(1),
|
|
9035
|
+
);
|
|
9036
|
+
const spellings = {
|
|
9037
|
+
CamelCase: first + titled.slice(1).join(""),
|
|
9038
|
+
PascalCase: titled.join(""),
|
|
9039
|
+
SnakeCase: parts.join("_"),
|
|
9040
|
+
KebabCase: parts.join("-"),
|
|
9041
|
+
ConstantCase: parts.join("_").toUpperCase(),
|
|
9042
|
+
};
|
|
9043
|
+
for (const { from, to, convert } of conversions) {
|
|
9044
|
+
const converted = convert(spellings[from]);
|
|
9045
|
+
assertTrue(formats[to].is(converted));
|
|
9046
|
+
assertEqual(converted, spellings[to]);
|
|
9047
|
+
const reverse = conversions.find(
|
|
9048
|
+
(conversion) =>
|
|
9049
|
+
conversion.from === to && conversion.to === from,
|
|
9050
|
+
);
|
|
9051
|
+
nodeAssert.ok(reverse);
|
|
9052
|
+
assertEqual(reverse.convert(converted), spellings[from]);
|
|
9053
|
+
}
|
|
9054
|
+
}
|
|
9055
|
+
}
|
|
9056
|
+
}
|
|
9057
|
+
});
|
|
9058
|
+
|
|
9059
|
+
it("requires the source brand and returns only the destination brand", () => {
|
|
9060
|
+
assertType<
|
|
9061
|
+
Parameters<typeof camelCaseToPascalCase>[0],
|
|
9062
|
+
CamelCaseIdentifier
|
|
9063
|
+
>();
|
|
9064
|
+
assertType<
|
|
9065
|
+
ReturnType<typeof camelCaseToPascalCase>,
|
|
9066
|
+
PascalCaseIdentifier
|
|
9067
|
+
>();
|
|
9068
|
+
assertType<
|
|
9069
|
+
Parameters<typeof camelCaseToSnakeCase>[0],
|
|
9070
|
+
CamelCaseIdentifier
|
|
9071
|
+
>();
|
|
9072
|
+
assertType<
|
|
9073
|
+
ReturnType<typeof camelCaseToSnakeCase>,
|
|
9074
|
+
SnakeCaseIdentifier
|
|
9075
|
+
>();
|
|
9076
|
+
assertType<
|
|
9077
|
+
Parameters<typeof camelCaseToKebabCase>[0],
|
|
9078
|
+
CamelCaseIdentifier
|
|
9079
|
+
>();
|
|
9080
|
+
assertType<
|
|
9081
|
+
ReturnType<typeof camelCaseToKebabCase>,
|
|
9082
|
+
KebabCaseIdentifier
|
|
9083
|
+
>();
|
|
9084
|
+
assertType<
|
|
9085
|
+
Parameters<typeof camelCaseToConstantCase>[0],
|
|
9086
|
+
CamelCaseIdentifier
|
|
9087
|
+
>();
|
|
9088
|
+
assertType<
|
|
9089
|
+
ReturnType<typeof camelCaseToConstantCase>,
|
|
9090
|
+
ConstantCaseIdentifier
|
|
9091
|
+
>();
|
|
9092
|
+
assertType<
|
|
9093
|
+
Parameters<typeof pascalCaseToCamelCase>[0],
|
|
9094
|
+
PascalCaseIdentifier
|
|
9095
|
+
>();
|
|
9096
|
+
assertType<
|
|
9097
|
+
ReturnType<typeof pascalCaseToCamelCase>,
|
|
9098
|
+
CamelCaseIdentifier
|
|
9099
|
+
>();
|
|
9100
|
+
assertType<
|
|
9101
|
+
Parameters<typeof pascalCaseToSnakeCase>[0],
|
|
9102
|
+
PascalCaseIdentifier
|
|
9103
|
+
>();
|
|
9104
|
+
assertType<
|
|
9105
|
+
ReturnType<typeof pascalCaseToSnakeCase>,
|
|
9106
|
+
SnakeCaseIdentifier
|
|
9107
|
+
>();
|
|
9108
|
+
assertType<
|
|
9109
|
+
Parameters<typeof pascalCaseToKebabCase>[0],
|
|
9110
|
+
PascalCaseIdentifier
|
|
9111
|
+
>();
|
|
9112
|
+
assertType<
|
|
9113
|
+
ReturnType<typeof pascalCaseToKebabCase>,
|
|
9114
|
+
KebabCaseIdentifier
|
|
9115
|
+
>();
|
|
9116
|
+
assertType<
|
|
9117
|
+
Parameters<typeof pascalCaseToConstantCase>[0],
|
|
9118
|
+
PascalCaseIdentifier
|
|
9119
|
+
>();
|
|
9120
|
+
assertType<
|
|
9121
|
+
ReturnType<typeof pascalCaseToConstantCase>,
|
|
9122
|
+
ConstantCaseIdentifier
|
|
9123
|
+
>();
|
|
9124
|
+
assertType<
|
|
9125
|
+
Parameters<typeof snakeCaseToCamelCase>[0],
|
|
9126
|
+
SnakeCaseIdentifier
|
|
9127
|
+
>();
|
|
9128
|
+
assertType<
|
|
9129
|
+
ReturnType<typeof snakeCaseToCamelCase>,
|
|
9130
|
+
CamelCaseIdentifier
|
|
9131
|
+
>();
|
|
9132
|
+
assertType<
|
|
9133
|
+
Parameters<typeof snakeCaseToPascalCase>[0],
|
|
9134
|
+
SnakeCaseIdentifier
|
|
9135
|
+
>();
|
|
9136
|
+
assertType<
|
|
9137
|
+
ReturnType<typeof snakeCaseToPascalCase>,
|
|
9138
|
+
PascalCaseIdentifier
|
|
9139
|
+
>();
|
|
9140
|
+
assertType<
|
|
9141
|
+
Parameters<typeof snakeCaseToKebabCase>[0],
|
|
9142
|
+
SnakeCaseIdentifier
|
|
9143
|
+
>();
|
|
9144
|
+
assertType<
|
|
9145
|
+
ReturnType<typeof snakeCaseToKebabCase>,
|
|
9146
|
+
KebabCaseIdentifier
|
|
9147
|
+
>();
|
|
9148
|
+
assertType<
|
|
9149
|
+
Parameters<typeof snakeCaseToConstantCase>[0],
|
|
9150
|
+
SnakeCaseIdentifier
|
|
9151
|
+
>();
|
|
9152
|
+
assertType<
|
|
9153
|
+
ReturnType<typeof snakeCaseToConstantCase>,
|
|
9154
|
+
ConstantCaseIdentifier
|
|
9155
|
+
>();
|
|
9156
|
+
assertType<
|
|
9157
|
+
Parameters<typeof kebabCaseToCamelCase>[0],
|
|
9158
|
+
KebabCaseIdentifier
|
|
9159
|
+
>();
|
|
9160
|
+
assertType<
|
|
9161
|
+
ReturnType<typeof kebabCaseToCamelCase>,
|
|
9162
|
+
CamelCaseIdentifier
|
|
9163
|
+
>();
|
|
9164
|
+
assertType<
|
|
9165
|
+
Parameters<typeof kebabCaseToPascalCase>[0],
|
|
9166
|
+
KebabCaseIdentifier
|
|
9167
|
+
>();
|
|
9168
|
+
assertType<
|
|
9169
|
+
ReturnType<typeof kebabCaseToPascalCase>,
|
|
9170
|
+
PascalCaseIdentifier
|
|
9171
|
+
>();
|
|
9172
|
+
assertType<
|
|
9173
|
+
Parameters<typeof kebabCaseToSnakeCase>[0],
|
|
9174
|
+
KebabCaseIdentifier
|
|
9175
|
+
>();
|
|
9176
|
+
assertType<
|
|
9177
|
+
ReturnType<typeof kebabCaseToSnakeCase>,
|
|
9178
|
+
SnakeCaseIdentifier
|
|
9179
|
+
>();
|
|
9180
|
+
assertType<
|
|
9181
|
+
Parameters<typeof kebabCaseToConstantCase>[0],
|
|
9182
|
+
KebabCaseIdentifier
|
|
9183
|
+
>();
|
|
9184
|
+
assertType<
|
|
9185
|
+
ReturnType<typeof kebabCaseToConstantCase>,
|
|
9186
|
+
ConstantCaseIdentifier
|
|
9187
|
+
>();
|
|
9188
|
+
assertType<
|
|
9189
|
+
Parameters<typeof constantCaseToCamelCase>[0],
|
|
9190
|
+
ConstantCaseIdentifier
|
|
9191
|
+
>();
|
|
9192
|
+
assertType<
|
|
9193
|
+
ReturnType<typeof constantCaseToCamelCase>,
|
|
9194
|
+
CamelCaseIdentifier
|
|
9195
|
+
>();
|
|
9196
|
+
assertType<
|
|
9197
|
+
Parameters<typeof constantCaseToPascalCase>[0],
|
|
9198
|
+
ConstantCaseIdentifier
|
|
9199
|
+
>();
|
|
9200
|
+
assertType<
|
|
9201
|
+
ReturnType<typeof constantCaseToPascalCase>,
|
|
9202
|
+
PascalCaseIdentifier
|
|
9203
|
+
>();
|
|
9204
|
+
assertType<
|
|
9205
|
+
Parameters<typeof constantCaseToSnakeCase>[0],
|
|
9206
|
+
ConstantCaseIdentifier
|
|
9207
|
+
>();
|
|
9208
|
+
assertType<
|
|
9209
|
+
ReturnType<typeof constantCaseToSnakeCase>,
|
|
9210
|
+
SnakeCaseIdentifier
|
|
9211
|
+
>();
|
|
9212
|
+
assertType<
|
|
9213
|
+
Parameters<typeof constantCaseToKebabCase>[0],
|
|
9214
|
+
ConstantCaseIdentifier
|
|
9215
|
+
>();
|
|
9216
|
+
assertType<
|
|
9217
|
+
ReturnType<typeof constantCaseToKebabCase>,
|
|
9218
|
+
KebabCaseIdentifier
|
|
9219
|
+
>();
|
|
9220
|
+
const short = maxLength(3)(CamelCaseIdentifier).orThrow("aBC");
|
|
9221
|
+
const constant = camelCaseToConstantCase(short);
|
|
9222
|
+
assertEqual(constant, "A_B_C");
|
|
9223
|
+
assertType<typeof constant, ConstantCaseIdentifier>();
|
|
9224
|
+
const compileTimeAssertions = () => {
|
|
9225
|
+
// @ts-expect-error Requires the CamelCaseIdentifier brand.
|
|
9226
|
+
camelCaseToPascalCase("example");
|
|
9227
|
+
// @ts-expect-error Requires the CamelCaseIdentifier brand.
|
|
9228
|
+
camelCaseToSnakeCase("example");
|
|
9229
|
+
// @ts-expect-error Requires the CamelCaseIdentifier brand.
|
|
9230
|
+
camelCaseToKebabCase("example");
|
|
9231
|
+
// @ts-expect-error Requires the CamelCaseIdentifier brand.
|
|
9232
|
+
camelCaseToConstantCase("example");
|
|
9233
|
+
// @ts-expect-error Requires the PascalCaseIdentifier brand.
|
|
9234
|
+
pascalCaseToCamelCase("example");
|
|
9235
|
+
// @ts-expect-error Requires the PascalCaseIdentifier brand.
|
|
9236
|
+
pascalCaseToSnakeCase("example");
|
|
9237
|
+
// @ts-expect-error Requires the PascalCaseIdentifier brand.
|
|
9238
|
+
pascalCaseToKebabCase("example");
|
|
9239
|
+
// @ts-expect-error Requires the PascalCaseIdentifier brand.
|
|
9240
|
+
pascalCaseToConstantCase("example");
|
|
9241
|
+
// @ts-expect-error Requires the SnakeCaseIdentifier brand.
|
|
9242
|
+
snakeCaseToCamelCase("example");
|
|
9243
|
+
// @ts-expect-error Requires the SnakeCaseIdentifier brand.
|
|
9244
|
+
snakeCaseToPascalCase("example");
|
|
9245
|
+
// @ts-expect-error Requires the SnakeCaseIdentifier brand.
|
|
9246
|
+
snakeCaseToKebabCase("example");
|
|
9247
|
+
// @ts-expect-error Requires the SnakeCaseIdentifier brand.
|
|
9248
|
+
snakeCaseToConstantCase("example");
|
|
9249
|
+
// @ts-expect-error Requires the KebabCaseIdentifier brand.
|
|
9250
|
+
kebabCaseToCamelCase("example");
|
|
9251
|
+
// @ts-expect-error Requires the KebabCaseIdentifier brand.
|
|
9252
|
+
kebabCaseToPascalCase("example");
|
|
9253
|
+
// @ts-expect-error Requires the KebabCaseIdentifier brand.
|
|
9254
|
+
kebabCaseToSnakeCase("example");
|
|
9255
|
+
// @ts-expect-error Requires the KebabCaseIdentifier brand.
|
|
9256
|
+
kebabCaseToConstantCase("example");
|
|
9257
|
+
// @ts-expect-error Requires the ConstantCaseIdentifier brand.
|
|
9258
|
+
constantCaseToCamelCase("example");
|
|
9259
|
+
// @ts-expect-error Requires the ConstantCaseIdentifier brand.
|
|
9260
|
+
constantCaseToPascalCase("example");
|
|
9261
|
+
// @ts-expect-error Requires the ConstantCaseIdentifier brand.
|
|
9262
|
+
constantCaseToSnakeCase("example");
|
|
9263
|
+
// @ts-expect-error Requires the ConstantCaseIdentifier brand.
|
|
9264
|
+
constantCaseToKebabCase("example");
|
|
9265
|
+
// @ts-expect-error ConstantCaseIdentifier does not prove the CamelCaseIdentifier brand.
|
|
9266
|
+
camelCaseToSnakeCase(constant);
|
|
9267
|
+
// @ts-expect-error Conversion does not retain the input's MaxLength3 brand.
|
|
9268
|
+
const _short: string & Brand<"MaxLength3"> = constant;
|
|
9269
|
+
// @ts-expect-error Uppercase text does not prove the ConstantCaseIdentifier grammar.
|
|
9270
|
+
const _constant: ConstantCaseIdentifier =
|
|
9271
|
+
"HTTP PORT" as Uppercase<string>;
|
|
9272
|
+
};
|
|
9273
|
+
assertType<typeof compileTimeAssertions, () => void>();
|
|
9274
|
+
});
|
|
9275
|
+
});
|
|
9276
|
+
|
|
9277
|
+
describe("trimmed", () => {
|
|
7617
9278
|
it("is a reusable Brand Factory", () => {
|
|
7618
9279
|
assertType<
|
|
7619
|
-
typeof
|
|
7620
|
-
BrandFactory<"
|
|
9280
|
+
typeof trimmed,
|
|
9281
|
+
BrandFactory<"Trimmed", string, TrimmedError>
|
|
9282
|
+
>();
|
|
9283
|
+
});
|
|
9284
|
+
|
|
9285
|
+
describe("Type", () => {
|
|
9286
|
+
describe("TrimmedString", () => {
|
|
9287
|
+
it("accepts only strings without surrounding whitespace", () => {
|
|
9288
|
+
assertEqual(TrimmedString.from.parent("Evolu"), ok("Evolu"));
|
|
9289
|
+
assertEqual(
|
|
9290
|
+
TrimmedString.from.parent(" Evolu "),
|
|
9291
|
+
err({ type: "Trimmed", value: " Evolu " }),
|
|
9292
|
+
);
|
|
9293
|
+
assertEqual(
|
|
9294
|
+
TrimmedString.formatError({
|
|
9295
|
+
type: "Trimmed",
|
|
9296
|
+
value: " Evolu ",
|
|
9297
|
+
}),
|
|
9298
|
+
'The value " Evolu " must be trimmed.',
|
|
9299
|
+
);
|
|
9300
|
+
assertType<
|
|
9301
|
+
typeof TrimmedString.Output,
|
|
9302
|
+
string & Brand<"Trimmed">
|
|
9303
|
+
>();
|
|
9304
|
+
});
|
|
9305
|
+
});
|
|
9306
|
+
});
|
|
9307
|
+
});
|
|
9308
|
+
|
|
9309
|
+
describe("trim", () => {
|
|
9310
|
+
it("returns a TrimmedString", () => {
|
|
9311
|
+
const value = trim(" Evolu ");
|
|
9312
|
+
|
|
9313
|
+
assertEqual(value, "Evolu");
|
|
9314
|
+
assertType<typeof value, TrimmedString>();
|
|
9315
|
+
});
|
|
9316
|
+
});
|
|
9317
|
+
|
|
9318
|
+
describe("startsWith", () => {
|
|
9319
|
+
it("preserves the full string and parent constraints", () => {
|
|
9320
|
+
const Parent = maxLength(12)(TrimmedString);
|
|
9321
|
+
const AppName = startsWith("APP_")(Parent);
|
|
9322
|
+
const input = Parent.orThrow("APP_PORT");
|
|
9323
|
+
const result = AppName.from.parent(input);
|
|
9324
|
+
|
|
9325
|
+
assertOk(result, input);
|
|
9326
|
+
assertSame(result.value, input);
|
|
9327
|
+
assertSame(AppName.to(result.value), input);
|
|
9328
|
+
assertSame(AppName.parent, Parent);
|
|
9329
|
+
assertEqual(AppName.name, "StartsWithAPP_");
|
|
9330
|
+
assertType<typeof AppName.name, "StartsWithAPP_">();
|
|
9331
|
+
assertType<typeof AppName.parent, typeof Parent>();
|
|
9332
|
+
assertType<
|
|
9333
|
+
typeof AppName.Output,
|
|
9334
|
+
typeof Parent.Output & Brand<"StartsWithAPP_">
|
|
9335
|
+
>();
|
|
9336
|
+
assertType<typeof AppName.Error, StartsWithError<"APP_">>();
|
|
9337
|
+
assertType<
|
|
9338
|
+
Parameters<typeof AppName.from.parent>[0],
|
|
9339
|
+
typeof Parent.Output
|
|
9340
|
+
>();
|
|
9341
|
+
assertErr(AppName.fromUnknown(" APP_PORT"), {
|
|
9342
|
+
type: "Trimmed",
|
|
9343
|
+
value: " APP_PORT",
|
|
9344
|
+
});
|
|
9345
|
+
assertErr(AppName.fromUnknown("APP_LONG_NAME"), {
|
|
9346
|
+
type: "MaxLength12",
|
|
9347
|
+
value: "APP_LONG_NAME",
|
|
9348
|
+
max: 12,
|
|
9349
|
+
});
|
|
9350
|
+
assertErr(AppName.fromUnknown(1), {
|
|
9351
|
+
type: "TypeOf",
|
|
9352
|
+
expected: "String",
|
|
9353
|
+
value: 1,
|
|
9354
|
+
});
|
|
9355
|
+
});
|
|
9356
|
+
|
|
9357
|
+
it("matches the exact prefix at the start without removing it", () => {
|
|
9358
|
+
const AppName = startsWith("APP_")(String);
|
|
9359
|
+
for (const value of ["APP_", "APP_PORT", "APP_APP_PORT", "APP_\n"]) {
|
|
9360
|
+
assertOk(AppName.fromUnknown(value), value);
|
|
9361
|
+
}
|
|
9362
|
+
for (const value of ["", "APP", "app_PORT", "XAPP_PORT"]) {
|
|
9363
|
+
assertErr(AppName.fromUnknown(value), {
|
|
9364
|
+
type: "StartsWithAPP_",
|
|
9365
|
+
value,
|
|
9366
|
+
prefix: "APP_",
|
|
9367
|
+
});
|
|
9368
|
+
}
|
|
9369
|
+
assertEqual(
|
|
9370
|
+
AppName.formatError({
|
|
9371
|
+
type: "StartsWithAPP_",
|
|
9372
|
+
value: "PORT",
|
|
9373
|
+
prefix: "APP_",
|
|
9374
|
+
}),
|
|
9375
|
+
'The value "PORT" must start with "APP_".',
|
|
9376
|
+
);
|
|
9377
|
+
});
|
|
9378
|
+
|
|
9379
|
+
it("accepts an empty prefix and matches punctuation and Unicode literally", () => {
|
|
9380
|
+
const AnyString = startsWith("")(String);
|
|
9381
|
+
for (const value of ["", "PORT", "\n", "😀"]) {
|
|
9382
|
+
assertOk(AnyString.fromUnknown(value), value);
|
|
9383
|
+
}
|
|
9384
|
+
assertErr(startsWith("")(TrimmedString).fromUnknown(" "));
|
|
9385
|
+
assertOk(
|
|
9386
|
+
startsWith("[a].*")(String).fromUnknown("[a].*value"),
|
|
9387
|
+
"[a].*value",
|
|
9388
|
+
);
|
|
9389
|
+
assertErr(startsWith("[a].*")(String).fromUnknown("abc"));
|
|
9390
|
+
assertOk(startsWith("😀:")(String).fromUnknown("😀:value"), "😀:value");
|
|
9391
|
+
assertErr(startsWith("é")(String).fromUnknown("e\u0301"));
|
|
9392
|
+
});
|
|
9393
|
+
|
|
9394
|
+
it("validates transformed parent output and preserves its encoder", () => {
|
|
9395
|
+
const Parent = transform("LowercasePrefixInput", String, String, {
|
|
9396
|
+
from: (value) => ok(value.toLowerCase()),
|
|
9397
|
+
to: (value) => value.toUpperCase(),
|
|
9398
|
+
});
|
|
9399
|
+
const AppName = startsWith("app_")(Parent);
|
|
9400
|
+
const result = AppName.fromUnknown("APP_PORT");
|
|
9401
|
+
|
|
9402
|
+
assertOk(result, "app_port");
|
|
9403
|
+
assertEqual(AppName.to(result.value), "APP_PORT");
|
|
9404
|
+
assertErr(AppName.fromUnknown("OTHER_PORT"), {
|
|
9405
|
+
type: "StartsWithapp_",
|
|
9406
|
+
value: "other_port",
|
|
9407
|
+
prefix: "app_",
|
|
9408
|
+
});
|
|
9409
|
+
});
|
|
9410
|
+
|
|
9411
|
+
it("requires a concrete prefix and keeps different prefix brands distinct", () => {
|
|
9412
|
+
const AppName = startsWith("APP_")(String);
|
|
9413
|
+
const OtherName = startsWith("OTHER_")(String);
|
|
9414
|
+
const otherName = OtherName.orThrow("OTHER_PORT");
|
|
9415
|
+
const widened = globalThis.String("APP_");
|
|
9416
|
+
const unionPrefix = "APP_" as "APP_" | "OTHER_";
|
|
9417
|
+
const branded = TrimmedString.orThrow("APP_");
|
|
9418
|
+
const templatePrefix = "APP_" as `APP_${string}`;
|
|
9419
|
+
const compileTimeAssertions = () => {
|
|
9420
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9421
|
+
startsWith(widened);
|
|
9422
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9423
|
+
startsWith(unionPrefix);
|
|
9424
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9425
|
+
startsWith(branded);
|
|
9426
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9427
|
+
startsWith(templatePrefix);
|
|
9428
|
+
// @ts-expect-error The parent Type must output strings.
|
|
9429
|
+
startsWith("APP_")(Number);
|
|
9430
|
+
// @ts-expect-error Error type must not duplicate an error inherited from the parent Type.
|
|
9431
|
+
startsWith("APP_")(AppName);
|
|
9432
|
+
// @ts-expect-error OTHER_ values do not carry the StartsWithAPP_ brand.
|
|
9433
|
+
AppName.from(otherName);
|
|
9434
|
+
};
|
|
9435
|
+
|
|
9436
|
+
assertType<
|
|
9437
|
+
typeof compileTimeAssertions extends (
|
|
9438
|
+
...args: Array<never>
|
|
9439
|
+
) => unknown
|
|
9440
|
+
? true
|
|
9441
|
+
: false,
|
|
9442
|
+
true
|
|
9443
|
+
>();
|
|
9444
|
+
});
|
|
9445
|
+
});
|
|
9446
|
+
|
|
9447
|
+
describe("prefixed", () => {
|
|
9448
|
+
it("preserves the suffix Type and applies its constraints after stripping", () => {
|
|
9449
|
+
const Suffix = maxLength(4)(ConstantCaseIdentifier);
|
|
9450
|
+
const Key = prefixed("app:")(Suffix);
|
|
9451
|
+
const result = Key.fromUnknown("app:PORT");
|
|
9452
|
+
|
|
9453
|
+
assertOk(result, "PORT");
|
|
9454
|
+
assertEqual(Key.to(result.value), "app:PORT");
|
|
9455
|
+
assertSame(Key.output, Suffix);
|
|
9456
|
+
assertEqual(Key.name, "Prefixedapp:");
|
|
9457
|
+
assertEqual(Key.parent.name, "StartsWithapp:");
|
|
9458
|
+
assertType<typeof Key.Output, typeof Suffix.Output>();
|
|
9459
|
+
assertType<typeof Key.output, typeof Suffix>();
|
|
9460
|
+
assertType<typeof Key.name, "Prefixedapp:">();
|
|
9461
|
+
assertType<
|
|
9462
|
+
typeof Key.Error,
|
|
9463
|
+
TransformOutputError<
|
|
9464
|
+
"Prefixedapp:",
|
|
9465
|
+
IdentifierError<"CONSTANT_CASE"> | MaxLengthError<4>
|
|
9466
|
+
>
|
|
9467
|
+
>();
|
|
9468
|
+
assertType<
|
|
9469
|
+
Parameters<typeof Key.from.parent>[0],
|
|
9470
|
+
string & Brand<"StartsWithapp:">
|
|
9471
|
+
>();
|
|
9472
|
+
assertType<Parameters<typeof Key.from.parent.parent>[0], string>();
|
|
9473
|
+
assertType<Parameters<typeof Key.from>[0], typeof Suffix.Output>();
|
|
9474
|
+
assertType<
|
|
9475
|
+
ReturnType<typeof Key.to>,
|
|
9476
|
+
string & Brand<"StartsWithapp:">
|
|
7621
9477
|
>();
|
|
9478
|
+
assertOk(Key.from.parent(Key.parent.orThrow("app:PORT")), "PORT");
|
|
9479
|
+
assertOk(Key.from.parent.parent("app:PORT"), "PORT");
|
|
9480
|
+
assertOk(Key.from(result.value), result.value);
|
|
9481
|
+
assertTrue(Key.is("PORT"));
|
|
9482
|
+
assertFalse(Key.is("app:PORT"));
|
|
9483
|
+
assertErr(Key.fromUnknown("app:PORTS"), {
|
|
9484
|
+
type: "Prefixedapp:",
|
|
9485
|
+
outputError: { type: "MaxLength4", value: "PORTS", max: 4 },
|
|
9486
|
+
});
|
|
7622
9487
|
});
|
|
7623
9488
|
|
|
7624
|
-
|
|
7625
|
-
|
|
7626
|
-
|
|
7627
|
-
|
|
7628
|
-
|
|
7629
|
-
|
|
7630
|
-
|
|
7631
|
-
|
|
7632
|
-
|
|
7633
|
-
|
|
7634
|
-
|
|
7635
|
-
|
|
7636
|
-
|
|
7637
|
-
|
|
7638
|
-
|
|
7639
|
-
|
|
7640
|
-
|
|
7641
|
-
|
|
7642
|
-
|
|
7643
|
-
|
|
7644
|
-
|
|
7645
|
-
|
|
7646
|
-
|
|
7647
|
-
>();
|
|
7648
|
-
});
|
|
9489
|
+
it("reports prefix and suffix errors through their existing formatters", () => {
|
|
9490
|
+
const Key = prefixed("APP_")(ConstantCaseIdentifier);
|
|
9491
|
+
for (const value of ["PORT", "app_PORT", "XAPP_PORT"]) {
|
|
9492
|
+
const result = Key.fromUnknown(value);
|
|
9493
|
+
assertErr(result, { type: "StartsWithAPP_", value, prefix: "APP_" });
|
|
9494
|
+
assertEqual(
|
|
9495
|
+
Key.formatError(result.error),
|
|
9496
|
+
`The value "${value}" must start with "APP_".`,
|
|
9497
|
+
);
|
|
9498
|
+
}
|
|
9499
|
+
assertErr(Key.fromUnknown(1), {
|
|
9500
|
+
type: "TypeOf",
|
|
9501
|
+
expected: "String",
|
|
9502
|
+
value: 1,
|
|
9503
|
+
});
|
|
9504
|
+
const invalid = Key.fromUnknown("APP_port");
|
|
9505
|
+
assertErr(invalid, {
|
|
9506
|
+
type: "PrefixedAPP_",
|
|
9507
|
+
outputError: {
|
|
9508
|
+
type: "ConstantCaseIdentifier",
|
|
9509
|
+
value: "port",
|
|
9510
|
+
casing: "CONSTANT_CASE",
|
|
9511
|
+
},
|
|
7649
9512
|
});
|
|
9513
|
+
assertEqual(typeErrorToIssues(Key, invalid.error), [
|
|
9514
|
+
{
|
|
9515
|
+
path: [],
|
|
9516
|
+
message: 'The value "port" is not a CONSTANT_CASE identifier.',
|
|
9517
|
+
},
|
|
9518
|
+
]);
|
|
9519
|
+
assertErr(Key.fromUnknown("APP_"));
|
|
7650
9520
|
});
|
|
7651
|
-
});
|
|
7652
9521
|
|
|
7653
|
-
|
|
7654
|
-
|
|
7655
|
-
|
|
7656
|
-
typeof trimmed,
|
|
7657
|
-
BrandFactory<"Trimmed", string, TrimmedError>
|
|
7658
|
-
>();
|
|
7659
|
-
});
|
|
9522
|
+
it("runs the wrapped decoder and canonical encoder for non-string Outputs", () => {
|
|
9523
|
+
const Setting = prefixed("port:")(PortFromString);
|
|
9524
|
+
const result = Setting.fromUnknown("port:04000");
|
|
7660
9525
|
|
|
7661
|
-
|
|
7662
|
-
|
|
7663
|
-
|
|
7664
|
-
|
|
7665
|
-
|
|
7666
|
-
|
|
7667
|
-
|
|
7668
|
-
|
|
7669
|
-
|
|
7670
|
-
|
|
7671
|
-
|
|
7672
|
-
|
|
7673
|
-
|
|
7674
|
-
|
|
7675
|
-
);
|
|
7676
|
-
assertType<
|
|
7677
|
-
typeof TrimmedString.Output,
|
|
7678
|
-
string & Brand<"Trimmed">
|
|
7679
|
-
>();
|
|
7680
|
-
});
|
|
9526
|
+
assertOk(result, 4000);
|
|
9527
|
+
assertType<typeof result.value, Port>();
|
|
9528
|
+
assertEqual(Setting.to(result.value), "port:4000");
|
|
9529
|
+
assertOk(Setting.fromUnknown(Setting.to(result.value)), result.value);
|
|
9530
|
+
assertErr(Setting.fromUnknown("port:65536"), {
|
|
9531
|
+
type: "Prefixedport:",
|
|
9532
|
+
outputError: {
|
|
9533
|
+
type: "PortFromString",
|
|
9534
|
+
outputError: {
|
|
9535
|
+
type: "LessThanOrEqualTo65535",
|
|
9536
|
+
value: 65536,
|
|
9537
|
+
max: 65535,
|
|
9538
|
+
},
|
|
9539
|
+
},
|
|
7681
9540
|
});
|
|
9541
|
+
assertTrue(Setting.is(4000));
|
|
9542
|
+
assertFalse(Setting.is("port:4000"));
|
|
7682
9543
|
});
|
|
7683
|
-
});
|
|
7684
9544
|
|
|
7685
|
-
|
|
7686
|
-
|
|
7687
|
-
const value
|
|
9545
|
+
it("removes one literal prefix, including empty and Unicode prefixes", () => {
|
|
9546
|
+
const Empty = prefixed("")(String);
|
|
9547
|
+
for (const value of ["", "PORT", "\n"]) {
|
|
9548
|
+
assertOk(Empty.fromUnknown(value), value);
|
|
9549
|
+
assertEqual(Empty.to(value), value);
|
|
9550
|
+
}
|
|
9551
|
+
const App = prefixed("APP_")(String);
|
|
9552
|
+
assertOk(App.fromUnknown("APP_"), "");
|
|
9553
|
+
assertOk(App.fromUnknown("APP_APP_PORT"), "APP_PORT");
|
|
9554
|
+
assertEqual(App.to("APP_PORT"), "APP_APP_PORT");
|
|
9555
|
+
const Unicode = prefixed("😀:")(String);
|
|
9556
|
+
assertOk(Unicode.fromUnknown("😀:PORT"), "PORT");
|
|
9557
|
+
assertEqual(Unicode.to("PORT"), "😀:PORT");
|
|
9558
|
+
const Punctuation = prefixed("[a].*")(String);
|
|
9559
|
+
assertOk(Punctuation.fromUnknown("[a].*PORT"), "PORT");
|
|
9560
|
+
assertEqual(Punctuation.to("PORT"), "[a].*PORT");
|
|
9561
|
+
assertErr(Punctuation.fromUnknown("abcPORT"));
|
|
9562
|
+
});
|
|
7688
9563
|
|
|
7689
|
-
|
|
7690
|
-
|
|
9564
|
+
it("composes nested prefixes in both directions", () => {
|
|
9565
|
+
const Nested = prefixed("outer/")(prefixed("inner/")(PortFromString));
|
|
9566
|
+
const result = Nested.fromUnknown("outer/inner/04000");
|
|
9567
|
+
assertOk(result, 4000);
|
|
9568
|
+
assertEqual(Nested.to(result.value), "outer/inner/4000");
|
|
9569
|
+
assertErr(Nested.fromUnknown("outer/4000"));
|
|
9570
|
+
|
|
9571
|
+
const Repeated = prefixed("APP_")(prefixed("APP_")(String));
|
|
9572
|
+
assertOk(Repeated.fromUnknown("APP_APP_PORT"), "PORT");
|
|
9573
|
+
assertEqual(Repeated.to("PORT"), "APP_APP_PORT");
|
|
9574
|
+
|
|
9575
|
+
const Literal = prefixed("APP_")(literal("PORT"));
|
|
9576
|
+
assertOk(Literal.fromUnknown("APP_PORT"), "PORT");
|
|
9577
|
+
assertErr(Literal.fromUnknown("APP_OTHER"));
|
|
9578
|
+
assertEqual(Literal.to("PORT"), "APP_PORT");
|
|
9579
|
+
assertType<typeof Literal.Output, "PORT">();
|
|
9580
|
+
});
|
|
9581
|
+
|
|
9582
|
+
it("decodes and encodes record keys without filtering mismatched prefixes", () => {
|
|
9583
|
+
const Settings = record(
|
|
9584
|
+
prefixed("APP_")(ConstantCaseIdentifier),
|
|
9585
|
+
String,
|
|
9586
|
+
);
|
|
9587
|
+
const result = Settings.fromUnknown({ APP_PORT: "4000" });
|
|
9588
|
+
assertOk(result, { PORT: "4000" });
|
|
9589
|
+
assertEqual(Settings.to(result.value), { APP_PORT: "4000" });
|
|
9590
|
+
assertErr(Settings.fromUnknown({ PORT: "4000" }));
|
|
9591
|
+
});
|
|
9592
|
+
|
|
9593
|
+
it("rejects uncertain prefixes and Types without a string input boundary", () => {
|
|
9594
|
+
const widened = globalThis.String("APP_");
|
|
9595
|
+
const unionPrefix = "APP_" as "APP_" | "OTHER_";
|
|
9596
|
+
const branded = TrimmedString.orThrow("APP_");
|
|
9597
|
+
const templatePrefix = "APP_" as `APP_${string}`;
|
|
9598
|
+
const uncertain = String as
|
|
9599
|
+
typeof String | typeof ConstantCaseIdentifier;
|
|
9600
|
+
const Setting = prefixed("port:")(PortFromString);
|
|
9601
|
+
const LiteralRoot = createType(
|
|
9602
|
+
"LiteralRoot",
|
|
9603
|
+
(value): Result<"PORT", TypeError<"LiteralRoot">> =>
|
|
9604
|
+
value === "PORT" ? ok(value) : err({ type: "LiteralRoot" }),
|
|
9605
|
+
() => "Expected PORT.",
|
|
9606
|
+
);
|
|
9607
|
+
const compileTimeAssertions = () => {
|
|
9608
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9609
|
+
prefixed(widened);
|
|
9610
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9611
|
+
prefixed(unionPrefix);
|
|
9612
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9613
|
+
prefixed(branded);
|
|
9614
|
+
// @ts-expect-error Expected must be one concrete literal value.
|
|
9615
|
+
prefixed(templatePrefix);
|
|
9616
|
+
// @ts-expect-error The wrapped Type must have string Input and CanonicalInput.
|
|
9617
|
+
prefixed("APP_")(Number);
|
|
9618
|
+
// @ts-expect-error The wrapped Type must have string Input and CanonicalInput.
|
|
9619
|
+
prefixed("APP_")(object({ port: Port }));
|
|
9620
|
+
// @ts-expect-error Output Type must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
9621
|
+
prefixed("APP_")(uncertain);
|
|
9622
|
+
// @ts-expect-error Prefixed Type Input must accept every string.
|
|
9623
|
+
prefixed("APP_")(LiteralRoot);
|
|
9624
|
+
// @ts-expect-error The typed parent boundary requires the StartsWithport: brand.
|
|
9625
|
+
Setting.from.parent("port:4000");
|
|
9626
|
+
// @ts-expect-error Encoding requires a validated Port, not a plain number.
|
|
9627
|
+
Setting.to(4000);
|
|
9628
|
+
};
|
|
9629
|
+
assertType<
|
|
9630
|
+
typeof compileTimeAssertions extends (
|
|
9631
|
+
...args: Array<never>
|
|
9632
|
+
) => unknown
|
|
9633
|
+
? true
|
|
9634
|
+
: false,
|
|
9635
|
+
true
|
|
9636
|
+
>();
|
|
7691
9637
|
});
|
|
7692
9638
|
});
|
|
7693
9639
|
|
|
@@ -8869,6 +10815,97 @@ describe("BrandFactory", () => {
|
|
|
8869
10815
|
});
|
|
8870
10816
|
});
|
|
8871
10817
|
|
|
10818
|
+
describe("Port", () => {
|
|
10819
|
+
it("accepts integer ports including zero and both range boundaries", () => {
|
|
10820
|
+
for (const value of [0, 1, 4000, 65535]) {
|
|
10821
|
+
assertOk(Port.fromUnknown(value), value);
|
|
10822
|
+
const port = Port.orThrow(value);
|
|
10823
|
+
assertType<typeof port, Port>();
|
|
10824
|
+
assertSame(Port.to(port), value);
|
|
10825
|
+
}
|
|
10826
|
+
assertSame(Port.orThrow(-0), -0);
|
|
10827
|
+
assertType<
|
|
10828
|
+
Port,
|
|
10829
|
+
NonNegativeInt & Brand<"LessThanOrEqualTo65535"> & Brand<"Port">
|
|
10830
|
+
>();
|
|
10831
|
+
// @ts-expect-error A number does not carry the Port brand.
|
|
10832
|
+
const _port: Port = 4000;
|
|
10833
|
+
});
|
|
10834
|
+
|
|
10835
|
+
it("rejects values outside the port domain", () => {
|
|
10836
|
+
assertErr(Port.fromUnknown(-1), { type: "NonNegative", value: -1 });
|
|
10837
|
+
assertErr(Port.fromUnknown(65536), {
|
|
10838
|
+
type: "LessThanOrEqualTo65535",
|
|
10839
|
+
value: 65536,
|
|
10840
|
+
max: 65535,
|
|
10841
|
+
});
|
|
10842
|
+
assertErr(Port.fromUnknown(4000.5), { type: "Int", value: 4000.5 });
|
|
10843
|
+
for (const value of ["4000", null, NaN, Infinity, -Infinity]) {
|
|
10844
|
+
assertErr(Port.fromUnknown(value));
|
|
10845
|
+
}
|
|
10846
|
+
});
|
|
10847
|
+
});
|
|
10848
|
+
|
|
10849
|
+
describe("PortFromString", () => {
|
|
10850
|
+
it("reuses decimal integer parsing and encodes validated ports", () => {
|
|
10851
|
+
for (const [input, output] of [
|
|
10852
|
+
["0", 0],
|
|
10853
|
+
["04000", 4000],
|
|
10854
|
+
["65535", 65535],
|
|
10855
|
+
] as const) {
|
|
10856
|
+
assertOk(PortFromString.fromUnknown(input), output);
|
|
10857
|
+
const port = PortFromString.orThrow(input);
|
|
10858
|
+
assertType<typeof port, Port>();
|
|
10859
|
+
assertEqual(PortFromString.to(port), globalThis.String(output));
|
|
10860
|
+
}
|
|
10861
|
+
assertOk(PortFromString.from.parent(Int.orThrow(4000)), 4000);
|
|
10862
|
+
assertEqual(PortFromString.to(Port.orThrow(4000)), "4000");
|
|
10863
|
+
assertSame(PortFromString.orThrow("-0"), -0);
|
|
10864
|
+
assertEqual(PortFromString.to(Port.orThrow(-0)), "-0");
|
|
10865
|
+
});
|
|
10866
|
+
|
|
10867
|
+
it("preserves parsing errors and formats port range errors", () => {
|
|
10868
|
+
for (const value of [
|
|
10869
|
+
"",
|
|
10870
|
+
"http",
|
|
10871
|
+
" 4000",
|
|
10872
|
+
"+4000",
|
|
10873
|
+
"4000.5",
|
|
10874
|
+
"4e3",
|
|
10875
|
+
"0x10",
|
|
10876
|
+
]) {
|
|
10877
|
+
assertErr(PortFromString.fromUnknown(value), {
|
|
10878
|
+
type: "IntFromString",
|
|
10879
|
+
value,
|
|
10880
|
+
});
|
|
10881
|
+
}
|
|
10882
|
+
assertErr(PortFromString.fromUnknown(4000));
|
|
10883
|
+
assertErr(PortFromString.fromUnknown("9007199254740992"));
|
|
10884
|
+
const negative = PortFromString.fromUnknown("-1");
|
|
10885
|
+
assertErr(negative, {
|
|
10886
|
+
type: "PortFromString",
|
|
10887
|
+
outputError: { type: "NonNegative", value: -1 },
|
|
10888
|
+
});
|
|
10889
|
+
assertEqual(
|
|
10890
|
+
PortFromString.formatError(negative.error),
|
|
10891
|
+
"The value -1 must be non-negative (>= 0).",
|
|
10892
|
+
);
|
|
10893
|
+
const tooLarge = PortFromString.fromUnknown("65536");
|
|
10894
|
+
assertErr(tooLarge, {
|
|
10895
|
+
type: "PortFromString",
|
|
10896
|
+
outputError: {
|
|
10897
|
+
type: "LessThanOrEqualTo65535",
|
|
10898
|
+
value: 65536,
|
|
10899
|
+
max: 65535,
|
|
10900
|
+
},
|
|
10901
|
+
});
|
|
10902
|
+
assertEqual(
|
|
10903
|
+
PortFromString.formatError(tooLarge.error),
|
|
10904
|
+
"The value 65536 must be less than or equal to 65535.",
|
|
10905
|
+
);
|
|
10906
|
+
});
|
|
10907
|
+
});
|
|
10908
|
+
|
|
8872
10909
|
describe("Ratio", () => {
|
|
8873
10910
|
it("accepts finite numbers from zero to one", () => {
|
|
8874
10911
|
assertEqual(Ratio.from.parent.parent.parent.parent.parent(0), ok(0));
|
|
@@ -11053,7 +13090,7 @@ describe("array", () => {
|
|
|
11053
13090
|
const value: ReadonlyArray<number> = [1, 2];
|
|
11054
13091
|
|
|
11055
13092
|
assertSame(UserIds.orThrow(value), value);
|
|
11056
|
-
assertAssertionError(() => UserIds.orThrow([1, -2, 3]), "
|
|
13093
|
+
assertAssertionError(() => UserIds.orThrow([1, -2, 3]), "PositiveInt", {
|
|
11057
13094
|
type: "Array",
|
|
11058
13095
|
reason: {
|
|
11059
13096
|
kind: "Items",
|
|
@@ -11072,7 +13109,7 @@ describe("array", () => {
|
|
|
11072
13109
|
const { UserIds } = setupUserIds();
|
|
11073
13110
|
assertAssertionError(
|
|
11074
13111
|
() => UserIds.orThrow([0, -1], { errors: "all" }),
|
|
11075
|
-
"
|
|
13112
|
+
"PositiveInt",
|
|
11076
13113
|
{
|
|
11077
13114
|
type: "Array",
|
|
11078
13115
|
reason: {
|
|
@@ -13731,26 +15768,541 @@ describe("record", () => {
|
|
|
13731
15768
|
kind: "Entries",
|
|
13732
15769
|
issues: [{ kind: "NonEnumerable", key: "allowed" }],
|
|
13733
15770
|
},
|
|
13734
|
-
}),
|
|
13735
|
-
'A Record property "allowed" must be enumerable.',
|
|
15771
|
+
}),
|
|
15772
|
+
'A Record property "allowed" must be enumerable.',
|
|
15773
|
+
);
|
|
15774
|
+
assertEqual(
|
|
15775
|
+
Transformed.formatError({
|
|
15776
|
+
type: "Record",
|
|
15777
|
+
reason: {
|
|
15778
|
+
kind: "Entries",
|
|
15779
|
+
issues: [
|
|
15780
|
+
{
|
|
15781
|
+
kind: "Collision",
|
|
15782
|
+
key: "a",
|
|
15783
|
+
previousKey: "A",
|
|
15784
|
+
outputKey: "a",
|
|
15785
|
+
},
|
|
15786
|
+
],
|
|
15787
|
+
},
|
|
15788
|
+
}),
|
|
15789
|
+
'Record keys "A" and "a" decode to the same key "a".',
|
|
15790
|
+
);
|
|
15791
|
+
});
|
|
15792
|
+
});
|
|
15793
|
+
|
|
15794
|
+
describe("withDefault", () => {
|
|
15795
|
+
it("accepts nullish defaults when they belong to the wrapped Output", () => {
|
|
15796
|
+
const Nullable = withDefault(nullOr(Boolean), null);
|
|
15797
|
+
assertType<typeof Nullable.Output, boolean | null>();
|
|
15798
|
+
assertOk(Nullable.fromUnknown(null), null);
|
|
15799
|
+
assertEqual(Nullable.to(null), null);
|
|
15800
|
+
assertOk(Nullable.fromUnknown(Nullable.to(false)), false);
|
|
15801
|
+
|
|
15802
|
+
const UndefinedDefault = withDefault(undefinedOr(Boolean), undefined, {
|
|
15803
|
+
strategy: "preserve",
|
|
15804
|
+
});
|
|
15805
|
+
assertType<
|
|
15806
|
+
typeof UndefinedDefault.Output,
|
|
15807
|
+
Defaulted<boolean, undefined, "undefined">
|
|
15808
|
+
>();
|
|
15809
|
+
const decoded = UndefinedDefault.fromUnknown(undefined);
|
|
15810
|
+
assertOk(decoded, {
|
|
15811
|
+
value: undefined,
|
|
15812
|
+
defaultUsed: true,
|
|
15813
|
+
original: "undefined",
|
|
15814
|
+
});
|
|
15815
|
+
assertSame(UndefinedDefault.to(decoded.value), undefined);
|
|
15816
|
+
assertOk(
|
|
15817
|
+
UndefinedDefault.fromUnknown(UndefinedDefault.to(decoded.value)),
|
|
15818
|
+
decoded.value,
|
|
15819
|
+
);
|
|
15820
|
+
|
|
15821
|
+
const Model = object({
|
|
15822
|
+
value: withDefault(optional(nullOr(Boolean)), null, {
|
|
15823
|
+
strategy: "preserve",
|
|
15824
|
+
}),
|
|
15825
|
+
});
|
|
15826
|
+
const missing = Model.fromUnknown({});
|
|
15827
|
+
assertOk(missing, {
|
|
15828
|
+
value: { value: null, defaultUsed: true, original: "missing" },
|
|
15829
|
+
});
|
|
15830
|
+
assertEqual(Model.to(missing.value), {});
|
|
15831
|
+
assertOk(Model.fromUnknown({ value: null }), {
|
|
15832
|
+
value: { value: null, defaultUsed: true, original: "null" },
|
|
15833
|
+
});
|
|
15834
|
+
});
|
|
15835
|
+
|
|
15836
|
+
it("reuses configured object defaults by reference", () => {
|
|
15837
|
+
const defaultValue = { enabled: true };
|
|
15838
|
+
const Value = nullOr(object({ enabled: Boolean }));
|
|
15839
|
+
const Replaced = withDefault(Value, defaultValue);
|
|
15840
|
+
const Preserved = withDefault(Value, defaultValue, {
|
|
15841
|
+
strategy: "preserve",
|
|
15842
|
+
});
|
|
15843
|
+
|
|
15844
|
+
assertSame(Replaced.orThrow(null), defaultValue);
|
|
15845
|
+
assertSame(Replaced.orThrow(null), defaultValue);
|
|
15846
|
+
assertSame(Preserved.orThrow(null).value, defaultValue);
|
|
15847
|
+
assertSame(Preserved.orThrow(null).value, defaultValue);
|
|
15848
|
+
});
|
|
15849
|
+
|
|
15850
|
+
describe("replace", () => {
|
|
15851
|
+
it("accepts object defaults directly, including objects with a value property", () => {
|
|
15852
|
+
const Value = withDefault(nullOr(object({ value: Boolean })), {
|
|
15853
|
+
value: true,
|
|
15854
|
+
});
|
|
15855
|
+
|
|
15856
|
+
assertType<typeof Value.Output.value, boolean>();
|
|
15857
|
+
assertOk(Value.fromUnknown(null), { value: true });
|
|
15858
|
+
assertOk(Value.fromUnknown({ value: false }), { value: false });
|
|
15859
|
+
assertEqual(Value.to({ value: true }), { value: true });
|
|
15860
|
+
});
|
|
15861
|
+
|
|
15862
|
+
it("defaults only decoded absence and emits supplied canonical data", () => {
|
|
15863
|
+
const Value = withDefault(nullishOr(BooleanFromString), true);
|
|
15864
|
+
|
|
15865
|
+
assertType<typeof Value.Output, boolean>();
|
|
15866
|
+
assertType<typeof Value.Input, string | null | undefined>();
|
|
15867
|
+
for (const input of [null, undefined, "true"]) {
|
|
15868
|
+
assertOk(Value.fromUnknown(input), true);
|
|
15869
|
+
}
|
|
15870
|
+
assertOk(Value.fromUnknown("false"), false);
|
|
15871
|
+
assertErr(Value.fromUnknown("wrong"));
|
|
15872
|
+
assertEqual(Value.to(true), "true");
|
|
15873
|
+
assertEqual(Value.to(false), "false");
|
|
15874
|
+
assertOk(Value.fromUnknown(Value.to(false)), false);
|
|
15875
|
+
assertOk(Value.from.parent(null), true);
|
|
15876
|
+
assertEqual(Value.to.parent(true), true);
|
|
15877
|
+
assertFalse(Value.is(null));
|
|
15878
|
+
assertThrowsInstanceOf(() => Value.to(null as never), Error);
|
|
15879
|
+
});
|
|
15880
|
+
|
|
15881
|
+
it("makes optional input required in the output without accepting undefined", () => {
|
|
15882
|
+
const Model = object({
|
|
15883
|
+
enabled: withDefault(optional(Boolean), true),
|
|
15884
|
+
});
|
|
15885
|
+
|
|
15886
|
+
assertType<typeof Model.Input.enabled, boolean | undefined>();
|
|
15887
|
+
assertType<typeof Model.Output.enabled, boolean>();
|
|
15888
|
+
assertOk(Model.fromUnknown({}), { enabled: true });
|
|
15889
|
+
assertOk(Model.from.parent({}), { enabled: true });
|
|
15890
|
+
assertOk(Model.parent.fromUnknown({}), {});
|
|
15891
|
+
assertErr(Model.fromUnknown({ enabled: undefined }));
|
|
15892
|
+
assertErr(Model.fromUnknown({ enabled: null }));
|
|
15893
|
+
assertFalse(Model.is({}));
|
|
15894
|
+
assertTrue(Model.is({ enabled: false }));
|
|
15895
|
+
assertEqual(Model.to({ enabled: true }), { enabled: true });
|
|
15896
|
+
assertOk(Model.fromUnknown(Model.to({ enabled: false })), {
|
|
15897
|
+
enabled: false,
|
|
15898
|
+
});
|
|
15899
|
+
});
|
|
15900
|
+
});
|
|
15901
|
+
|
|
15902
|
+
describe("preserve", () => {
|
|
15903
|
+
it("distinguishes all absence states and explicit values equal to the default", () => {
|
|
15904
|
+
const Model = object({
|
|
15905
|
+
enabled: withDefault(optional(nullishOr(BooleanFromString)), true, {
|
|
15906
|
+
strategy: "preserve",
|
|
15907
|
+
}),
|
|
15908
|
+
});
|
|
15909
|
+
assertType<typeof Model.Output.enabled, Defaulted<boolean, true>>();
|
|
15910
|
+
const cases = [
|
|
15911
|
+
[{}, { value: true, defaultUsed: true, original: "missing" }],
|
|
15912
|
+
[
|
|
15913
|
+
{ enabled: null },
|
|
15914
|
+
{ value: true, defaultUsed: true, original: "null" },
|
|
15915
|
+
],
|
|
15916
|
+
[
|
|
15917
|
+
{ enabled: undefined },
|
|
15918
|
+
{ value: true, defaultUsed: true, original: "undefined" },
|
|
15919
|
+
],
|
|
15920
|
+
[{ enabled: "true" }, { value: true, defaultUsed: false }],
|
|
15921
|
+
[{ enabled: "false" }, { value: false, defaultUsed: false }],
|
|
15922
|
+
] as const;
|
|
15923
|
+
|
|
15924
|
+
for (const [input, enabled] of cases) {
|
|
15925
|
+
const result = Model.fromUnknown(input);
|
|
15926
|
+
assertOk(result, { enabled });
|
|
15927
|
+
assertTrue(Model.is(result.value));
|
|
15928
|
+
const encoded = Model.to(result.value);
|
|
15929
|
+
assertEqual(encoded, input);
|
|
15930
|
+
assertOk(Model.fromUnknown(encoded), result.value);
|
|
15931
|
+
assertEqual(
|
|
15932
|
+
globalThis.Object.hasOwn(encoded, "enabled"),
|
|
15933
|
+
globalThis.Object.hasOwn(input, "enabled"),
|
|
15934
|
+
);
|
|
15935
|
+
}
|
|
15936
|
+
assertOk(Model.from.parent({ enabled: "false" }), {
|
|
15937
|
+
enabled: { value: false, defaultUsed: false },
|
|
15938
|
+
});
|
|
15939
|
+
});
|
|
15940
|
+
|
|
15941
|
+
it("narrows original states and rejects impossible output shapes", () => {
|
|
15942
|
+
const Value = withDefault(nullOr(Boolean), true, {
|
|
15943
|
+
strategy: "preserve",
|
|
15944
|
+
});
|
|
15945
|
+
assertType<typeof Value.Output, Defaulted<boolean, true, "null">>();
|
|
15946
|
+
assertOk(Value.fromUnknown(null), {
|
|
15947
|
+
value: true,
|
|
15948
|
+
defaultUsed: true,
|
|
15949
|
+
original: "null",
|
|
15950
|
+
});
|
|
15951
|
+
assertEqual(
|
|
15952
|
+
Value.to({ value: true, defaultUsed: true, original: "null" }),
|
|
15953
|
+
null,
|
|
15954
|
+
);
|
|
15955
|
+
assertFalse(
|
|
15956
|
+
Value.is({ value: false, defaultUsed: true, original: "null" }),
|
|
15957
|
+
);
|
|
15958
|
+
assertFalse(
|
|
15959
|
+
Value.is({ value: true, defaultUsed: true, original: "undefined" }),
|
|
15960
|
+
);
|
|
15961
|
+
assertFalse(
|
|
15962
|
+
Value.is({ value: true, defaultUsed: true, original: "missing" }),
|
|
15963
|
+
);
|
|
15964
|
+
assertFalse(Value.is({ value: null, defaultUsed: false }));
|
|
15965
|
+
assertFalse(
|
|
15966
|
+
Value.is({ value: true, defaultUsed: false, original: "null" }),
|
|
15967
|
+
);
|
|
15968
|
+
const error = assertThrowsInstanceOf(
|
|
15969
|
+
() =>
|
|
15970
|
+
Value.to({
|
|
15971
|
+
value: false,
|
|
15972
|
+
defaultUsed: true,
|
|
15973
|
+
original: "null",
|
|
15974
|
+
} as never),
|
|
15975
|
+
Error,
|
|
15976
|
+
);
|
|
15977
|
+
assertTrue(error.message.length > 0);
|
|
15978
|
+
assertEqual(
|
|
15979
|
+
Value.formatError(error.cause as never),
|
|
15980
|
+
"A preserved default must equal the configured default value.",
|
|
15981
|
+
);
|
|
15982
|
+
for (const [output, message] of [
|
|
15983
|
+
[
|
|
15984
|
+
{ value: null, defaultUsed: false },
|
|
15985
|
+
"The value must be a non-nullish Output of its Type or, in replacement mode, the configured default.",
|
|
15986
|
+
],
|
|
15987
|
+
[
|
|
15988
|
+
{ value: true, defaultUsed: true, original: "missing" },
|
|
15989
|
+
"The original absence must be handled by this default declaration.",
|
|
15990
|
+
],
|
|
15991
|
+
] as const) {
|
|
15992
|
+
const invalid = assertThrowsInstanceOf(
|
|
15993
|
+
() => Value.to(output as never),
|
|
15994
|
+
Error,
|
|
15995
|
+
);
|
|
15996
|
+
assertEqual(Value.formatError(invalid.cause as never), message);
|
|
15997
|
+
}
|
|
15998
|
+
});
|
|
15999
|
+
|
|
16000
|
+
it("compares structured defaults by value", () => {
|
|
16001
|
+
const Value = withDefault(
|
|
16002
|
+
nullOr(object({ enabled: Boolean })),
|
|
16003
|
+
{ enabled: true },
|
|
16004
|
+
{ strategy: "preserve" },
|
|
16005
|
+
);
|
|
16006
|
+
const output = {
|
|
16007
|
+
value: { enabled: true },
|
|
16008
|
+
defaultUsed: true,
|
|
16009
|
+
original: "null",
|
|
16010
|
+
} as const;
|
|
16011
|
+
assertTrue(Value.is(output));
|
|
16012
|
+
assertEqual(Value.to(output), null);
|
|
16013
|
+
assertOk(Value.fromUnknown(null), output);
|
|
16014
|
+
assertFalse(Value.is({ ...output, value: { enabled: false } }));
|
|
16015
|
+
});
|
|
16016
|
+
|
|
16017
|
+
it("also validates preserved default values against the wrapped Type", () => {
|
|
16018
|
+
const selected = { enabled: true };
|
|
16019
|
+
const Selected = createType(
|
|
16020
|
+
"Selected",
|
|
16021
|
+
(value): Result<typeof selected, TypeError<"Selected">> =>
|
|
16022
|
+
value === selected ? ok(selected) : err({ type: "Selected" }),
|
|
16023
|
+
() => "Expected the selected object.",
|
|
16024
|
+
);
|
|
16025
|
+
const Value = withDefault(nullOr(Selected), selected, {
|
|
16026
|
+
strategy: "preserve",
|
|
16027
|
+
});
|
|
16028
|
+
assertTrue(
|
|
16029
|
+
Value.is({ value: selected, defaultUsed: true, original: "null" }),
|
|
16030
|
+
);
|
|
16031
|
+
assertFalse(
|
|
16032
|
+
Value.is({
|
|
16033
|
+
value: { ...selected },
|
|
16034
|
+
defaultUsed: true,
|
|
16035
|
+
original: "null",
|
|
16036
|
+
}),
|
|
16037
|
+
);
|
|
16038
|
+
});
|
|
16039
|
+
|
|
16040
|
+
it("uses decoded absence rather than raw input spelling", () => {
|
|
16041
|
+
const Input = transform("AbsentString", String, nullOr(Boolean), {
|
|
16042
|
+
from: (value) => ok(value === "absent" ? null : value === "true"),
|
|
16043
|
+
to: (value) => (value === null ? "absent" : value ? "true" : "false"),
|
|
16044
|
+
});
|
|
16045
|
+
const Value = withDefault(Input, false, { strategy: "preserve" });
|
|
16046
|
+
const decoded = Value.fromUnknown("absent");
|
|
16047
|
+
assertOk(decoded, { value: false, defaultUsed: true, original: "null" });
|
|
16048
|
+
assertEqual(Value.to(decoded.value), "absent");
|
|
16049
|
+
assertOk(Value.fromUnknown("other"), {
|
|
16050
|
+
value: false,
|
|
16051
|
+
defaultUsed: false,
|
|
16052
|
+
});
|
|
16053
|
+
});
|
|
16054
|
+
});
|
|
16055
|
+
|
|
16056
|
+
describe("composition", () => {
|
|
16057
|
+
it("round-trips through arrays and JSON", () => {
|
|
16058
|
+
const Values = array(
|
|
16059
|
+
withDefault(undefinedOr(BooleanFromString), false, {
|
|
16060
|
+
strategy: "preserve",
|
|
16061
|
+
}),
|
|
16062
|
+
);
|
|
16063
|
+
const values = Values.fromUnknown([undefined, "true"]);
|
|
16064
|
+
assertOk(values, [
|
|
16065
|
+
{ value: false, defaultUsed: true, original: "undefined" },
|
|
16066
|
+
{ value: true, defaultUsed: false },
|
|
16067
|
+
]);
|
|
16068
|
+
assertEqual(Values.to(values.value), [undefined, "true"]);
|
|
16069
|
+
assertOk(Values.from.parent([undefined, true]), values.value);
|
|
16070
|
+
|
|
16071
|
+
const Model = object({
|
|
16072
|
+
enabled: withDefault(optional(Boolean), true, { strategy: "preserve" }),
|
|
16073
|
+
});
|
|
16074
|
+
const [ModelJson, modelToJson, jsonToModel] = json(
|
|
16075
|
+
Model,
|
|
16076
|
+
"DefaultSettingsJson",
|
|
16077
|
+
);
|
|
16078
|
+
const missing = jsonToModel(ModelJson.orThrow("{}"));
|
|
16079
|
+
assertEqual(missing, {
|
|
16080
|
+
enabled: { value: true, defaultUsed: true, original: "missing" },
|
|
16081
|
+
});
|
|
16082
|
+
assertEqual(modelToJson(missing), "{}");
|
|
16083
|
+
assertEqual(jsonToModel(modelToJson(missing)), missing);
|
|
16084
|
+
assertFalse(
|
|
16085
|
+
Model.is({
|
|
16086
|
+
enabled: { value: false, defaultUsed: true, original: "missing" },
|
|
16087
|
+
}),
|
|
16088
|
+
);
|
|
16089
|
+
});
|
|
16090
|
+
|
|
16091
|
+
it("composes nested objects, ordinary optional fields, and rest records", () => {
|
|
16092
|
+
const Model = object({
|
|
16093
|
+
inner: object({
|
|
16094
|
+
enabled: withDefault(optional(BooleanFromString), true, {
|
|
16095
|
+
strategy: "preserve",
|
|
16096
|
+
}),
|
|
16097
|
+
count: Int64FromInt64String,
|
|
16098
|
+
note: optional(String),
|
|
16099
|
+
}),
|
|
16100
|
+
});
|
|
16101
|
+
const decoded = Model.fromUnknown({ inner: { count: "42" } });
|
|
16102
|
+
assertOk(decoded, {
|
|
16103
|
+
inner: {
|
|
16104
|
+
enabled: { value: true, defaultUsed: true, original: "missing" },
|
|
16105
|
+
count: 42n,
|
|
16106
|
+
},
|
|
16107
|
+
});
|
|
16108
|
+
assertEqual(Model.to(decoded.value), { inner: { count: "42" } });
|
|
16109
|
+
|
|
16110
|
+
const Open = object(
|
|
16111
|
+
{
|
|
16112
|
+
enabled: withDefault(optional(Boolean), true),
|
|
16113
|
+
},
|
|
16114
|
+
record(String, Boolean),
|
|
16115
|
+
);
|
|
16116
|
+
assertOk(Open.fromUnknown({ other: false }), {
|
|
16117
|
+
enabled: true,
|
|
16118
|
+
other: false,
|
|
16119
|
+
});
|
|
16120
|
+
assertEqual(Open.to({ enabled: true, other: false }), {
|
|
16121
|
+
enabled: true,
|
|
16122
|
+
other: false,
|
|
16123
|
+
});
|
|
16124
|
+
assertErr(Open.fromUnknown({ other: "false" }));
|
|
16125
|
+
});
|
|
16126
|
+
|
|
16127
|
+
it("partial disables missing defaults for non-nullish Types in both strategies", () => {
|
|
16128
|
+
const Replaced = partial({
|
|
16129
|
+
enabled: withDefault(optional(Boolean), true),
|
|
16130
|
+
});
|
|
16131
|
+
const Preserved = partial({
|
|
16132
|
+
enabled: withDefault(optional(Boolean), true, { strategy: "preserve" }),
|
|
16133
|
+
});
|
|
16134
|
+
|
|
16135
|
+
assertType<typeof Replaced.Output.enabled, boolean | undefined>();
|
|
16136
|
+
assertType<
|
|
16137
|
+
typeof Preserved.Output.enabled,
|
|
16138
|
+
Defaulted<boolean, true, never> | undefined
|
|
16139
|
+
>();
|
|
16140
|
+
assertOk(Replaced.fromUnknown({}), {});
|
|
16141
|
+
assertOk(Preserved.fromUnknown({}), {});
|
|
16142
|
+
assertOk(Replaced.fromUnknown({ enabled: false }), { enabled: false });
|
|
16143
|
+
const supplied = Preserved.orThrow({ enabled: false });
|
|
16144
|
+
assertEqual(supplied, { enabled: { value: false, defaultUsed: false } });
|
|
16145
|
+
assertEqual(Preserved.to(supplied), { enabled: false });
|
|
16146
|
+
assertEqual(Replaced.to({}), {});
|
|
16147
|
+
assertEqual(Preserved.to({}), {});
|
|
16148
|
+
assertErr(Replaced.fromUnknown({ enabled: undefined }));
|
|
16149
|
+
assertErr(Preserved.fromUnknown({ enabled: null }));
|
|
16150
|
+
});
|
|
16151
|
+
|
|
16152
|
+
it("partial makes missing outputs optional and retains defaults for present null", () => {
|
|
16153
|
+
const Model = partial({
|
|
16154
|
+
enabled: withDefault(optional(nullOr(Boolean)), true, {
|
|
16155
|
+
strategy: "preserve",
|
|
16156
|
+
}),
|
|
16157
|
+
});
|
|
16158
|
+
assertType<
|
|
16159
|
+
typeof Model.Output.enabled,
|
|
16160
|
+
Defaulted<boolean, true, "null"> | undefined
|
|
16161
|
+
>();
|
|
16162
|
+
assertOk(Model.fromUnknown({}), {});
|
|
16163
|
+
const decoded = Model.fromUnknown({ enabled: null });
|
|
16164
|
+
assertOk(decoded, {
|
|
16165
|
+
enabled: { value: true, defaultUsed: true, original: "null" },
|
|
16166
|
+
});
|
|
16167
|
+
assertEqual(Model.to(decoded.value), { enabled: null });
|
|
16168
|
+
});
|
|
16169
|
+
|
|
16170
|
+
it("partial retains replacement defaults for present null", () => {
|
|
16171
|
+
const Model = partial({
|
|
16172
|
+
enabled: withDefault(optional(nullOr(Boolean)), true),
|
|
16173
|
+
});
|
|
16174
|
+
|
|
16175
|
+
assertType<typeof Model.Output.enabled, boolean | undefined>();
|
|
16176
|
+
assertOk(Model.fromUnknown({}), {});
|
|
16177
|
+
assertOk(Model.fromUnknown({ enabled: null }), { enabled: true });
|
|
16178
|
+
assertOk(Model.fromUnknown({ enabled: false }), { enabled: false });
|
|
16179
|
+
assertEqual(Model.to({ enabled: true }), { enabled: true });
|
|
16180
|
+
assertEqual(Model.to({}), {});
|
|
16181
|
+
});
|
|
16182
|
+
|
|
16183
|
+
it("retains error paths, all errors, and never reads accessors", () => {
|
|
16184
|
+
const Model = object({
|
|
16185
|
+
enabled: withDefault(optional(BooleanFromString), true, {
|
|
16186
|
+
strategy: "preserve",
|
|
16187
|
+
}),
|
|
16188
|
+
count: Int64FromInt64String,
|
|
16189
|
+
});
|
|
16190
|
+
const failed = Model.fromUnknown(
|
|
16191
|
+
{ enabled: "bad", count: "bad" },
|
|
16192
|
+
{ errors: "all" },
|
|
16193
|
+
);
|
|
16194
|
+
assertErr(failed);
|
|
16195
|
+
const issues = typeErrorToIssues(Model, failed.error);
|
|
16196
|
+
assertEqual(
|
|
16197
|
+
issues.map((issue) => issue.path),
|
|
16198
|
+
[["enabled"], ["count"]],
|
|
16199
|
+
);
|
|
16200
|
+
assertTrue(Model.formatError(failed.error).length > 0);
|
|
16201
|
+
const invalidUnion = withDefault(nullOr(BooleanFromString), true, {
|
|
16202
|
+
strategy: "preserve",
|
|
16203
|
+
});
|
|
16204
|
+
const result = invalidUnion.fromUnknown("bad");
|
|
16205
|
+
assertErr(result);
|
|
16206
|
+
assertTrue(invalidUnion.formatError(result.error).length > 0);
|
|
16207
|
+
|
|
16208
|
+
let reads = 0;
|
|
16209
|
+
const input = {
|
|
16210
|
+
get enabled() {
|
|
16211
|
+
reads++;
|
|
16212
|
+
return "true";
|
|
16213
|
+
},
|
|
16214
|
+
count: "42",
|
|
16215
|
+
};
|
|
16216
|
+
assertErr(Model.fromUnknown(input));
|
|
16217
|
+
assertEqual(reads, 0);
|
|
16218
|
+
assertErr(Model.fromUnknown({ extra: 1, count: "42" }));
|
|
16219
|
+
assertErr(Model.fromUnknown(null));
|
|
16220
|
+
assertErr(Model.fromUnknown([]));
|
|
16221
|
+
assertThrowsInstanceOf(
|
|
16222
|
+
() =>
|
|
16223
|
+
Model.to({
|
|
16224
|
+
enabled: { value: true, defaultUsed: false },
|
|
16225
|
+
count: 0 as never,
|
|
16226
|
+
}),
|
|
16227
|
+
Error,
|
|
16228
|
+
);
|
|
16229
|
+
});
|
|
16230
|
+
});
|
|
16231
|
+
|
|
16232
|
+
it("rejects invalid declarations and explicit replacement strategies", () => {
|
|
16233
|
+
assertThrowsInstanceOf(
|
|
16234
|
+
() => withDefault(optional(Boolean), null as never),
|
|
16235
|
+
Error,
|
|
13736
16236
|
);
|
|
13737
|
-
|
|
13738
|
-
|
|
13739
|
-
|
|
13740
|
-
reason: {
|
|
13741
|
-
kind: "Entries",
|
|
13742
|
-
issues: [
|
|
13743
|
-
{
|
|
13744
|
-
kind: "Collision",
|
|
13745
|
-
key: "a",
|
|
13746
|
-
previousKey: "A",
|
|
13747
|
-
outputKey: "a",
|
|
13748
|
-
},
|
|
13749
|
-
],
|
|
13750
|
-
},
|
|
13751
|
-
}),
|
|
13752
|
-
'Record keys "A" and "a" decode to the same key "a".',
|
|
16237
|
+
assertThrowsInstanceOf(
|
|
16238
|
+
() => withDefault(nullOr(Boolean), "true" as never),
|
|
16239
|
+
Error,
|
|
13753
16240
|
);
|
|
16241
|
+
for (const strategy of ["replace", "other"]) {
|
|
16242
|
+
for (const property of [Boolean, optional(Boolean)]) {
|
|
16243
|
+
const error = assertThrowsInstanceOf(
|
|
16244
|
+
() =>
|
|
16245
|
+
withDefault(property as never, true, {
|
|
16246
|
+
strategy: strategy as never,
|
|
16247
|
+
}),
|
|
16248
|
+
Error,
|
|
16249
|
+
);
|
|
16250
|
+
assertEqual(
|
|
16251
|
+
error.message,
|
|
16252
|
+
'withDefault strategy must be omitted or "preserve".',
|
|
16253
|
+
);
|
|
16254
|
+
}
|
|
16255
|
+
}
|
|
16256
|
+
if (false as boolean) {
|
|
16257
|
+
// @ts-expect-error withDefault requires an optional property or a Type whose Output includes null or undefined.
|
|
16258
|
+
withDefault(Boolean, true);
|
|
16259
|
+
// @ts-expect-error withDefault requires an optional property or a Type whose Output includes null or undefined.
|
|
16260
|
+
withDefault(Boolean, true, { strategy: "preserve" });
|
|
16261
|
+
// @ts-expect-error A Boolean default must be a decoded boolean.
|
|
16262
|
+
withDefault(nullOr(BooleanFromString), "true");
|
|
16263
|
+
// @ts-expect-error A Boolean default cannot be null.
|
|
16264
|
+
withDefault(optional(Boolean), null);
|
|
16265
|
+
// @ts-expect-error The default value is required.
|
|
16266
|
+
withDefault(optional(Boolean));
|
|
16267
|
+
// @ts-expect-error A Boolean default must be passed directly, not wrapped in options.
|
|
16268
|
+
withDefault(optional(Boolean), { value: true });
|
|
16269
|
+
// @ts-expect-error The third argument must explicitly request preservation.
|
|
16270
|
+
withDefault(optional(Boolean), true, {});
|
|
16271
|
+
// @ts-expect-error Explicit replacement strategies are not accepted.
|
|
16272
|
+
withDefault(optional(Boolean), true, { strategy: "replace" });
|
|
16273
|
+
// @ts-expect-error Explicit replacement strategies are not accepted.
|
|
16274
|
+
withDefault(nullOr(Boolean), true, { strategy: "replace" });
|
|
16275
|
+
const replacement = { strategy: "replace" } as const;
|
|
16276
|
+
// @ts-expect-error Explicit replacement strategies are not accepted, including in variables.
|
|
16277
|
+
withDefault(optional(Boolean), true, replacement);
|
|
16278
|
+
// @ts-expect-error Explicit replacement strategies are not accepted, including in variables.
|
|
16279
|
+
withDefault(nullOr(Boolean), true, replacement);
|
|
16280
|
+
const uncertain = optional(Boolean) as OptionalProperty<
|
|
16281
|
+
typeof Boolean | typeof String
|
|
16282
|
+
>;
|
|
16283
|
+
// @ts-expect-error Property must use one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
16284
|
+
withDefault(uncertain, true);
|
|
16285
|
+
const erased = optional(Boolean) as OptionalProperty<TypeNode>;
|
|
16286
|
+
// @ts-expect-error Property must use one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
16287
|
+
withDefault(erased, true);
|
|
16288
|
+
const Conflicting = createType(
|
|
16289
|
+
"WithDefault",
|
|
16290
|
+
(_value): Result<boolean, TypeError<"WithDefault">> =>
|
|
16291
|
+
err({ type: "WithDefault" }),
|
|
16292
|
+
() => "Invalid flag.",
|
|
16293
|
+
);
|
|
16294
|
+
// @ts-expect-error Error type must not duplicate an error inherited from the parent Type.
|
|
16295
|
+
withDefault(Conflicting, true);
|
|
16296
|
+
// @ts-expect-error Error type must not duplicate an error inherited from the parent Type.
|
|
16297
|
+
withDefault(optional(Conflicting), true, { strategy: "preserve" });
|
|
16298
|
+
// @ts-expect-error Output properties with defaults are required.
|
|
16299
|
+
const invalid: typeof objectWithDefault.Output = {};
|
|
16300
|
+
void invalid;
|
|
16301
|
+
}
|
|
16302
|
+
const objectWithDefault = object({
|
|
16303
|
+
value: withDefault(optional(Boolean), true),
|
|
16304
|
+
});
|
|
16305
|
+
assertOk(objectWithDefault.fromUnknown({}), { value: true });
|
|
13754
16306
|
});
|
|
13755
16307
|
});
|
|
13756
16308
|
|
|
@@ -16223,60 +18775,126 @@ describe("object", () => {
|
|
|
16223
18775
|
>();
|
|
16224
18776
|
});
|
|
16225
18777
|
|
|
16226
|
-
|
|
16227
|
-
|
|
16228
|
-
|
|
16229
|
-
|
|
16230
|
-
|
|
16231
|
-
|
|
18778
|
+
describe("omit", () => {
|
|
18779
|
+
it("removes declared properties and preserves an Object Record", () => {
|
|
18780
|
+
const User = object({ name: String, age: Number });
|
|
18781
|
+
const WithoutAge = omit(User, "age");
|
|
18782
|
+
const WithoutNameAndAge = omit(User, "name", "age");
|
|
18783
|
+
const keys = ["name", "age"] as const;
|
|
18784
|
+
const Empty = omit(User, ...keys);
|
|
16232
18785
|
|
|
16233
|
-
|
|
16234
|
-
|
|
16235
|
-
|
|
16236
|
-
|
|
16237
|
-
|
|
16238
|
-
|
|
16239
|
-
|
|
16240
|
-
|
|
16241
|
-
|
|
16242
|
-
|
|
16243
|
-
|
|
16244
|
-
|
|
16245
|
-
|
|
16246
|
-
|
|
16247
|
-
|
|
16248
|
-
|
|
16249
|
-
|
|
16250
|
-
|
|
16251
|
-
|
|
16252
|
-
|
|
16253
|
-
|
|
16254
|
-
|
|
18786
|
+
assertEqual(WithoutAge.props, { name: String });
|
|
18787
|
+
assertOk(WithoutAge.fromUnknown({ name: "Ada" }), { name: "Ada" });
|
|
18788
|
+
assertFalse(WithoutAge.fromUnknown({ name: "Ada", age: 1 }).ok);
|
|
18789
|
+
assertType<
|
|
18790
|
+
typeof WithoutAge,
|
|
18791
|
+
ObjectType<{ readonly name: typeof String }>
|
|
18792
|
+
>();
|
|
18793
|
+
assertEqual(WithoutNameAndAge.props, {});
|
|
18794
|
+
assertOk(WithoutNameAndAge.fromUnknown({}), {});
|
|
18795
|
+
assertType<typeof WithoutNameAndAge, ObjectType<{}>>();
|
|
18796
|
+
assertEqual(Empty.props, {});
|
|
18797
|
+
assertType<typeof Empty, ObjectType<{}>>();
|
|
18798
|
+
|
|
18799
|
+
const compileTimeAssertions = (
|
|
18800
|
+
key: "name" | "age",
|
|
18801
|
+
keys: ReadonlyArray<"name" | "age">,
|
|
18802
|
+
) => {
|
|
18803
|
+
// @ts-expect-error A runtime key must identify one statically known property.
|
|
18804
|
+
omit(User, key);
|
|
18805
|
+
// @ts-expect-error A runtime array does not guarantee which properties are omitted.
|
|
18806
|
+
omit(User, ...keys);
|
|
18807
|
+
};
|
|
16255
18808
|
|
|
16256
|
-
|
|
16257
|
-
|
|
16258
|
-
|
|
16259
|
-
|
|
16260
|
-
|
|
18809
|
+
const Metadata = object(
|
|
18810
|
+
{ name: String, age: Number },
|
|
18811
|
+
record(String, Unknown),
|
|
18812
|
+
);
|
|
18813
|
+
const MetadataWithoutAge = omit(Metadata, "age");
|
|
16261
18814
|
|
|
16262
|
-
|
|
16263
|
-
|
|
16264
|
-
|
|
16265
|
-
|
|
16266
|
-
|
|
16267
|
-
|
|
16268
|
-
|
|
16269
|
-
|
|
16270
|
-
|
|
16271
|
-
|
|
16272
|
-
|
|
16273
|
-
|
|
16274
|
-
|
|
16275
|
-
|
|
16276
|
-
|
|
16277
|
-
|
|
16278
|
-
|
|
16279
|
-
|
|
18815
|
+
assertSame(MetadataWithoutAge.record, Metadata.record);
|
|
18816
|
+
assertType<
|
|
18817
|
+
typeof MetadataWithoutAge,
|
|
18818
|
+
ObjectType<
|
|
18819
|
+
{ readonly name: typeof String },
|
|
18820
|
+
RecordType<typeof String, typeof Unknown>
|
|
18821
|
+
>
|
|
18822
|
+
>();
|
|
18823
|
+
assertOk(
|
|
18824
|
+
MetadataWithoutAge.fromUnknown({ name: "Ada", age: "unknown" }),
|
|
18825
|
+
{ name: "Ada", age: "unknown" },
|
|
18826
|
+
);
|
|
18827
|
+
assertType<
|
|
18828
|
+
typeof compileTimeAssertions extends (
|
|
18829
|
+
...args: Array<never>
|
|
18830
|
+
) => unknown
|
|
18831
|
+
? true
|
|
18832
|
+
: false,
|
|
18833
|
+
true
|
|
18834
|
+
>();
|
|
18835
|
+
});
|
|
18836
|
+
|
|
18837
|
+
it("retains defaulted fields and can remove every default", () => {
|
|
18838
|
+
const Model = object({
|
|
18839
|
+
enabled: withDefault(optional(Boolean), true),
|
|
18840
|
+
tracked: withDefault(optional(Boolean), false, {
|
|
18841
|
+
strategy: "preserve",
|
|
18842
|
+
}),
|
|
18843
|
+
name: String,
|
|
18844
|
+
});
|
|
18845
|
+
const Defaults = omit(Model, "name");
|
|
18846
|
+
assertType<typeof Defaults.Output.enabled, boolean>();
|
|
18847
|
+
assertType<
|
|
18848
|
+
typeof Defaults.Output.tracked,
|
|
18849
|
+
Defaulted<boolean, false, "missing">
|
|
18850
|
+
>();
|
|
18851
|
+
const decoded = Defaults.orThrow({});
|
|
18852
|
+
assertEqual(decoded, {
|
|
18853
|
+
enabled: true,
|
|
18854
|
+
tracked: { value: false, defaultUsed: true, original: "missing" },
|
|
18855
|
+
});
|
|
18856
|
+
assertEqual(Defaults.to(decoded), { enabled: true });
|
|
18857
|
+
const Named = omit(Model, "enabled", "tracked");
|
|
18858
|
+
assertType<
|
|
18859
|
+
typeof Named,
|
|
18860
|
+
ObjectType<{ readonly name: typeof String }>
|
|
18861
|
+
>();
|
|
18862
|
+
assertOk(Named.fromUnknown({ name: "Ada" }), { name: "Ada" });
|
|
18863
|
+
assertErr(Named.fromUnknown({ name: "Ada", enabled: true }));
|
|
18864
|
+
const Empty = omit(Model, "enabled", "tracked", "name");
|
|
18865
|
+
assertType<typeof Empty, ObjectType<{}>>();
|
|
18866
|
+
assertOk(Empty.fromUnknown({}), {});
|
|
18867
|
+
});
|
|
18868
|
+
|
|
18869
|
+
it("preserves rest record inference when retaining or removing a default", () => {
|
|
18870
|
+
const Model = object(
|
|
18871
|
+
{
|
|
18872
|
+
enabled: withDefault(optional(Boolean), true),
|
|
18873
|
+
name: String,
|
|
18874
|
+
},
|
|
18875
|
+
record(String, Unknown),
|
|
18876
|
+
);
|
|
18877
|
+
const Defaults = omit(Model, "name");
|
|
18878
|
+
assertSame(Defaults.record, Model.record);
|
|
18879
|
+
assertType<typeof Defaults.Output.enabled, boolean>();
|
|
18880
|
+
assertOk(Defaults.fromUnknown({ other: "value" }), {
|
|
18881
|
+
enabled: true,
|
|
18882
|
+
other: "value",
|
|
18883
|
+
});
|
|
18884
|
+
const Named = omit(Model, "enabled");
|
|
18885
|
+
assertType<
|
|
18886
|
+
typeof Named,
|
|
18887
|
+
ObjectType<
|
|
18888
|
+
{ readonly name: typeof String },
|
|
18889
|
+
RecordType<typeof String, typeof Unknown>
|
|
18890
|
+
>
|
|
18891
|
+
>();
|
|
18892
|
+
assertSame(Named.record, Model.record);
|
|
18893
|
+
assertOk(Named.fromUnknown({ name: "Ada", enabled: "rest" }), {
|
|
18894
|
+
name: "Ada",
|
|
18895
|
+
enabled: "rest",
|
|
18896
|
+
});
|
|
18897
|
+
});
|
|
16280
18898
|
});
|
|
16281
18899
|
});
|
|
16282
18900
|
});
|
|
@@ -19753,6 +22371,39 @@ describe("json", () => {
|
|
|
19753
22371
|
}
|
|
19754
22372
|
});
|
|
19755
22373
|
|
|
22374
|
+
it("formats plain and localized orThrow failures without parsing twice", () => {
|
|
22375
|
+
const [ValueJson] = json(String, "ValueJson");
|
|
22376
|
+
const LocalizedValueJson = localizeTypes(
|
|
22377
|
+
{ ValueJson },
|
|
22378
|
+
{
|
|
22379
|
+
test: {
|
|
22380
|
+
Json: () => "Localized Json.",
|
|
22381
|
+
String: () => "Localized String.",
|
|
22382
|
+
},
|
|
22383
|
+
},
|
|
22384
|
+
).test.ValueJson;
|
|
22385
|
+
|
|
22386
|
+
for (const type of [ValueJson, LocalizedValueJson]) {
|
|
22387
|
+
for (const input of ["1", "{"]) {
|
|
22388
|
+
const result = type.fromUnknown(input);
|
|
22389
|
+
assertErr(result);
|
|
22390
|
+
const message = type.formatError(result.error);
|
|
22391
|
+
const parse = mock.method(JSON, "parse");
|
|
22392
|
+
|
|
22393
|
+
try {
|
|
22394
|
+
assertAssertionError(
|
|
22395
|
+
() => type.orThrow(input),
|
|
22396
|
+
message,
|
|
22397
|
+
result.error,
|
|
22398
|
+
);
|
|
22399
|
+
assertEqual(parse.mock.callCount(), 1);
|
|
22400
|
+
} finally {
|
|
22401
|
+
parse.mock.restore();
|
|
22402
|
+
}
|
|
22403
|
+
}
|
|
22404
|
+
}
|
|
22405
|
+
});
|
|
22406
|
+
|
|
19756
22407
|
it("accepts only Types with a JSON-compatible canonical Input", () => {
|
|
19757
22408
|
const compileTimeAssertions = () => {
|
|
19758
22409
|
interface StringTree {
|
|
@@ -20463,3 +23114,309 @@ describe("design decisions", () => {
|
|
|
20463
23114
|
});
|
|
20464
23115
|
});
|
|
20465
23116
|
});
|
|
23117
|
+
|
|
23118
|
+
describe("BooleanFromString", () => {
|
|
23119
|
+
it("parses exactly true and false", () => {
|
|
23120
|
+
assertOk(BooleanFromString.fromUnknown("true"), true);
|
|
23121
|
+
assertOk(BooleanFromString.fromUnknown("false"), false);
|
|
23122
|
+
|
|
23123
|
+
assertEqual(BooleanFromString.to(true), "true");
|
|
23124
|
+
assertEqual(BooleanFromString.to(false), "false");
|
|
23125
|
+
});
|
|
23126
|
+
|
|
23127
|
+
it("rejects other strings and non-strings", () => {
|
|
23128
|
+
for (const value of [
|
|
23129
|
+
"TRUE",
|
|
23130
|
+
"False",
|
|
23131
|
+
"1",
|
|
23132
|
+
"0",
|
|
23133
|
+
"yes",
|
|
23134
|
+
"no",
|
|
23135
|
+
"on",
|
|
23136
|
+
"off",
|
|
23137
|
+
" true",
|
|
23138
|
+
"",
|
|
23139
|
+
]) {
|
|
23140
|
+
const invalid = BooleanFromString.fromUnknown(value);
|
|
23141
|
+
assertErr(invalid, { type: "BooleanFromString", value });
|
|
23142
|
+
assertEqual(
|
|
23143
|
+
BooleanFromString.formatError(invalid.error),
|
|
23144
|
+
`The value ${JSON.stringify(value)} is not a boolean. Use true or false.`,
|
|
23145
|
+
);
|
|
23146
|
+
}
|
|
23147
|
+
|
|
23148
|
+
assertErr(BooleanFromString.fromUnknown(true), {
|
|
23149
|
+
type: "TypeOf",
|
|
23150
|
+
expected: "String",
|
|
23151
|
+
value: true,
|
|
23152
|
+
});
|
|
23153
|
+
});
|
|
23154
|
+
});
|
|
23155
|
+
|
|
23156
|
+
describe("IntFromString", () => {
|
|
23157
|
+
it("parses decimal integers", () => {
|
|
23158
|
+
assertOk(IntFromString.fromUnknown("4000"), 4000);
|
|
23159
|
+
assertOk(IntFromString.fromUnknown("-1"), -1);
|
|
23160
|
+
assertOk(IntFromString.fromUnknown("007"), 7);
|
|
23161
|
+
assertOk(
|
|
23162
|
+
IntFromString.fromUnknown("9007199254740991"),
|
|
23163
|
+
globalThis.Number.MAX_SAFE_INTEGER,
|
|
23164
|
+
);
|
|
23165
|
+
assertOk(
|
|
23166
|
+
IntFromString.fromUnknown("-9007199254740991"),
|
|
23167
|
+
globalThis.Number.MIN_SAFE_INTEGER,
|
|
23168
|
+
);
|
|
23169
|
+
assertEqual(IntFromString.to(IntFromString.orThrow("42")), "42");
|
|
23170
|
+
|
|
23171
|
+
const negativeZero = IntFromString.orThrow("-0");
|
|
23172
|
+
assertSame(negativeZero, -0);
|
|
23173
|
+
assertEqual(IntFromString.to(negativeZero), "-0");
|
|
23174
|
+
|
|
23175
|
+
{
|
|
23176
|
+
const value = IntFromString.orThrow("1");
|
|
23177
|
+
assertType<typeof value, Int>();
|
|
23178
|
+
}
|
|
23179
|
+
});
|
|
23180
|
+
|
|
23181
|
+
it("rejects non-integer text", () => {
|
|
23182
|
+
for (const value of ["", " 1", "1 ", "+1", "1.5", "1e3", "0x10", "NaN"]) {
|
|
23183
|
+
const invalid = IntFromString.fromUnknown(value);
|
|
23184
|
+
assertErr(invalid, { type: "IntFromString", value });
|
|
23185
|
+
assertEqual(
|
|
23186
|
+
IntFromString.formatError(invalid.error),
|
|
23187
|
+
`The value ${JSON.stringify(value)} is not a decimal integer.`,
|
|
23188
|
+
);
|
|
23189
|
+
}
|
|
23190
|
+
});
|
|
23191
|
+
|
|
23192
|
+
it("validates parsed numbers with Int", () => {
|
|
23193
|
+
for (const value of [
|
|
23194
|
+
"9007199254740992",
|
|
23195
|
+
"9007199254740993",
|
|
23196
|
+
"-9007199254740992",
|
|
23197
|
+
"-9007199254740993",
|
|
23198
|
+
]) {
|
|
23199
|
+
const number = globalThis.Number(value);
|
|
23200
|
+
const invalid = IntFromString.fromUnknown(value);
|
|
23201
|
+
assertErr(invalid, {
|
|
23202
|
+
type: "IntFromString",
|
|
23203
|
+
outputError: { type: "Int", value: number },
|
|
23204
|
+
});
|
|
23205
|
+
assertEqual(
|
|
23206
|
+
IntFromString.formatError(invalid.error),
|
|
23207
|
+
`The value ${number} must be a safe integer.`,
|
|
23208
|
+
);
|
|
23209
|
+
}
|
|
23210
|
+
|
|
23211
|
+
const value = "9".repeat(309);
|
|
23212
|
+
const invalid = IntFromString.fromUnknown(value);
|
|
23213
|
+
assertErr(invalid, {
|
|
23214
|
+
type: "IntFromString",
|
|
23215
|
+
outputError: {
|
|
23216
|
+
type: "Finite",
|
|
23217
|
+
value: globalThis.Number.POSITIVE_INFINITY,
|
|
23218
|
+
},
|
|
23219
|
+
});
|
|
23220
|
+
assertEqual(
|
|
23221
|
+
IntFromString.formatError(invalid.error),
|
|
23222
|
+
"The value Infinity must be finite.",
|
|
23223
|
+
);
|
|
23224
|
+
});
|
|
23225
|
+
});
|
|
23226
|
+
|
|
23227
|
+
describe("objectKeys", () => {
|
|
23228
|
+
it("includes missing and excess properties in the error type", () => {
|
|
23229
|
+
const Model = objectKeys(String)(object({ port: Number }));
|
|
23230
|
+
const result = Model.fromUnknown({ extra: 0 }, { errors: "all" });
|
|
23231
|
+
assertErr(result);
|
|
23232
|
+
assertTrue("error" in result.error);
|
|
23233
|
+
const error = result.error.error;
|
|
23234
|
+
assertTrue(error.reason.kind === "Properties");
|
|
23235
|
+
const errors = error.reason.errors;
|
|
23236
|
+
|
|
23237
|
+
assertType<
|
|
23238
|
+
(typeof errors)[string],
|
|
23239
|
+
| TypeOfError<"Number">
|
|
23240
|
+
| ObjectPropertyAccessError
|
|
23241
|
+
| ObjectMissingPropertyError
|
|
23242
|
+
| ObjectExcessPropertyError
|
|
23243
|
+
| undefined
|
|
23244
|
+
>();
|
|
23245
|
+
assertEqual(errors.port, { type: "ObjectMissingProperty" });
|
|
23246
|
+
assertEqual(errors.extra, { type: "ObjectExcessProperty" });
|
|
23247
|
+
});
|
|
23248
|
+
|
|
23249
|
+
it("uses canonical key encodings and retains field codecs and optionality", () => {
|
|
23250
|
+
const Key = prefixed("APP_")(CamelCaseIdentifierFromConstantCaseIdentifier);
|
|
23251
|
+
const Output = object({
|
|
23252
|
+
http2Port: PortFromString,
|
|
23253
|
+
displayName: optional(String),
|
|
23254
|
+
});
|
|
23255
|
+
const Env = objectKeys(Key)(Output);
|
|
23256
|
+
const result = Env.fromUnknown({ APP_HTTP2_PORT: "04000" });
|
|
23257
|
+
assertOk(result, { http2Port: 4000 });
|
|
23258
|
+
assertType<typeof result.value, typeof Output.Output>();
|
|
23259
|
+
assertType(Env, result.value);
|
|
23260
|
+
assertSame(Env.key, Key);
|
|
23261
|
+
assertSame(Env.output, Output);
|
|
23262
|
+
assertEqual(Env.to(result.value), { APP_HTTP2_PORT: "4000" });
|
|
23263
|
+
assertOk(Env.from.parent({ APP_HTTP2_PORT: "4000" }), result.value);
|
|
23264
|
+
assertOk(Env.from(result.value), result.value);
|
|
23265
|
+
assertTrue(Env.is(result.value));
|
|
23266
|
+
assertFalse(Env.is({ http2Port: "4000" }));
|
|
23267
|
+
assertEqual(Env.orNull({ APP_HTTP2_PORT: "invalid" }), null);
|
|
23268
|
+
assertOk(
|
|
23269
|
+
Env.fromUnknown({ APP_HTTP2_PORT: "4000", APP_DISPLAY_NAME: "" }),
|
|
23270
|
+
{ http2Port: 4000, displayName: "" },
|
|
23271
|
+
);
|
|
23272
|
+
for (const value of [
|
|
23273
|
+
null,
|
|
23274
|
+
[],
|
|
23275
|
+
{ http2Port: "4000" },
|
|
23276
|
+
{ APP_HTTP2_PORT: "4000", HOME: "" },
|
|
23277
|
+
{ APP_HTTP2_PORT: "4000", APP_DISPLAY_NAME: undefined },
|
|
23278
|
+
]) {
|
|
23279
|
+
assertErr(Env.fromUnknown(value));
|
|
23280
|
+
}
|
|
23281
|
+
nodeAssert.throws(() => Env.to({ http2Port: 65536 as Port }));
|
|
23282
|
+
const outputError: TransformOutputError<
|
|
23283
|
+
"ObjectKeys",
|
|
23284
|
+
InferErrors<typeof Output>
|
|
23285
|
+
> = {
|
|
23286
|
+
type: "ObjectKeys",
|
|
23287
|
+
outputError: {
|
|
23288
|
+
type: "Object",
|
|
23289
|
+
reason: { kind: "NotObject", value: null },
|
|
23290
|
+
},
|
|
23291
|
+
};
|
|
23292
|
+
assertEqual(typeErrorToIssues(Env, outputError)[0]?.path, []);
|
|
23293
|
+
assertEqual(objectKeys(Key)(object({})).to({}), {});
|
|
23294
|
+
});
|
|
23295
|
+
|
|
23296
|
+
it("preserves external paths, nested paths, descriptors, and unknown-key errors", () => {
|
|
23297
|
+
const [Payload] = json(object({ items: array(Int) }), "ObjectKeysPayload");
|
|
23298
|
+
const Key = prefixed("APP_")(CamelCaseIdentifierFromConstantCaseIdentifier);
|
|
23299
|
+
const Env = objectKeys(Key)(
|
|
23300
|
+
object({ payload: Payload, port: PortFromString }),
|
|
23301
|
+
);
|
|
23302
|
+
const result = Env.fromUnknown(
|
|
23303
|
+
{ APP_PAYLOAD: '{"items":["bad"]}', APP_EXTRA: "value" },
|
|
23304
|
+
{ errors: "all" },
|
|
23305
|
+
);
|
|
23306
|
+
assertErr(result);
|
|
23307
|
+
assertEqual(
|
|
23308
|
+
typeErrorToIssues(Env, result.error).map(({ path }) => path),
|
|
23309
|
+
[["APP_PAYLOAD", "items", 0], ["APP_PORT"], ["APP_EXTRA"]],
|
|
23310
|
+
);
|
|
23311
|
+
let reads = 0;
|
|
23312
|
+
const accessor = globalThis.Object.defineProperty(
|
|
23313
|
+
{ APP_PORT: "4000" },
|
|
23314
|
+
"APP_PAYLOAD",
|
|
23315
|
+
{
|
|
23316
|
+
enumerable: true,
|
|
23317
|
+
get: () => {
|
|
23318
|
+
reads++;
|
|
23319
|
+
return "{}";
|
|
23320
|
+
},
|
|
23321
|
+
},
|
|
23322
|
+
);
|
|
23323
|
+
assertErr(Env.fromUnknown(accessor));
|
|
23324
|
+
assertEqual(reads, 0);
|
|
23325
|
+
assertErr(
|
|
23326
|
+
Env.fromUnknown(
|
|
23327
|
+
globalThis.Object.defineProperty({ APP_PORT: "4000" }, "APP_PAYLOAD", {
|
|
23328
|
+
value: "{}",
|
|
23329
|
+
}),
|
|
23330
|
+
),
|
|
23331
|
+
);
|
|
23332
|
+
const SpecialKey = transform("SpecialObjectKey", String, String, {
|
|
23333
|
+
from: (value) => ok(value === "__proto__" ? "proto" : value),
|
|
23334
|
+
to: (value) => (value === "proto" ? "__proto__" : value),
|
|
23335
|
+
});
|
|
23336
|
+
const Special = objectKeys(SpecialKey)(
|
|
23337
|
+
object({ proto: String, constructor: optional(String) }),
|
|
23338
|
+
);
|
|
23339
|
+
const special = Special.orThrow({ ["__proto__"]: "safe" });
|
|
23340
|
+
assertEqual(special, { proto: "safe" });
|
|
23341
|
+
assertEqual(Special.to(special), { ["__proto__"]: "safe" });
|
|
23342
|
+
const source = globalThis.Object.assign(globalThis.Object.create(null), {
|
|
23343
|
+
["__proto__"]: "safe",
|
|
23344
|
+
});
|
|
23345
|
+
assertOk(Special.fromUnknown(source), special);
|
|
23346
|
+
});
|
|
23347
|
+
|
|
23348
|
+
it("rejects invalid keys, duplicate encodings, and broken key codecs at construction", () => {
|
|
23349
|
+
nodeAssert.throws(() =>
|
|
23350
|
+
objectKeys(CamelCaseIdentifierFromConstantCaseIdentifier)(
|
|
23351
|
+
object({ BadKey: String }),
|
|
23352
|
+
),
|
|
23353
|
+
);
|
|
23354
|
+
const Broken = transform("BrokenObjectKey", String, String, {
|
|
23355
|
+
from: (value) => ok(value),
|
|
23356
|
+
to: () => "first",
|
|
23357
|
+
});
|
|
23358
|
+
nodeAssert.throws(
|
|
23359
|
+
() => objectKeys(Broken)(object({ first: String, second: String })),
|
|
23360
|
+
/Duplicate encoded object key/u,
|
|
23361
|
+
);
|
|
23362
|
+
nodeAssert.throws(
|
|
23363
|
+
() => objectKeys(Broken)(object({ second: String })),
|
|
23364
|
+
/decode its encoding/u,
|
|
23365
|
+
);
|
|
23366
|
+
const Rejecting = transform(
|
|
23367
|
+
"RejectingObjectKey",
|
|
23368
|
+
String,
|
|
23369
|
+
String,
|
|
23370
|
+
{
|
|
23371
|
+
from: () => err({ type: "RejectingObjectKey" as const }),
|
|
23372
|
+
to: (value) => value,
|
|
23373
|
+
},
|
|
23374
|
+
() => "Rejected key.",
|
|
23375
|
+
);
|
|
23376
|
+
nodeAssert.throws(
|
|
23377
|
+
() => objectKeys(Rejecting)(object({ key: String })),
|
|
23378
|
+
/decode its encoding/u,
|
|
23379
|
+
);
|
|
23380
|
+
});
|
|
23381
|
+
|
|
23382
|
+
it("requires string key codecs and a concrete strict object", () => {
|
|
23383
|
+
const reject = () => {
|
|
23384
|
+
// @ts-expect-error Key Output must be string.
|
|
23385
|
+
objectKeys(PortFromString);
|
|
23386
|
+
// @ts-expect-error objectKeys requires a strict Object Type without a record rest.
|
|
23387
|
+
objectKeys(String)(object({}, record(String, String)));
|
|
23388
|
+
// @ts-expect-error objectKeys requires an Object Type.
|
|
23389
|
+
objectKeys(String)(String);
|
|
23390
|
+
const objectType = (true as boolean)
|
|
23391
|
+
? object({ name: String })
|
|
23392
|
+
: object({ age: Number });
|
|
23393
|
+
// @ts-expect-error Output Type must be one concrete Type node. Pass a Union Type node instead of a union of Type nodes.
|
|
23394
|
+
objectKeys(String)(objectType);
|
|
23395
|
+
};
|
|
23396
|
+
assertType<typeof reject, () => void>();
|
|
23397
|
+
});
|
|
23398
|
+
});
|
|
23399
|
+
|
|
23400
|
+
describe("CamelCaseIdentifierFromConstantCaseIdentifier", () => {
|
|
23401
|
+
it("preserves identifier word boundaries in both directions", () => {
|
|
23402
|
+
for (const [encoded, decoded] of [
|
|
23403
|
+
["PORT", "port"],
|
|
23404
|
+
["HTTP2_PORT", "http2Port"],
|
|
23405
|
+
["H_T_T_P", "hTTP"],
|
|
23406
|
+
]) {
|
|
23407
|
+
const result =
|
|
23408
|
+
CamelCaseIdentifierFromConstantCaseIdentifier.fromUnknown(encoded);
|
|
23409
|
+
assertOk(result, decoded);
|
|
23410
|
+
assertEqual(
|
|
23411
|
+
CamelCaseIdentifierFromConstantCaseIdentifier.to(result.value),
|
|
23412
|
+
encoded,
|
|
23413
|
+
);
|
|
23414
|
+
}
|
|
23415
|
+
assertErr(
|
|
23416
|
+
CamelCaseIdentifierFromConstantCaseIdentifier.fromUnknown("HTTP__PORT"),
|
|
23417
|
+
);
|
|
23418
|
+
assertErr(
|
|
23419
|
+
CamelCaseIdentifierFromConstantCaseIdentifier.fromUnknown("http_PORT"),
|
|
23420
|
+
);
|
|
23421
|
+
});
|
|
23422
|
+
});
|