typia 13.2.0 → 13.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (92) hide show
  1. package/lib/http.d.ts +3 -3
  2. package/lib/http.js.map +1 -1
  3. package/lib/http.mjs.map +1 -1
  4. package/lib/internal/_assertGuard.js +13 -3
  5. package/lib/internal/_assertGuard.js.map +1 -1
  6. package/lib/internal/_assertGuard.mjs +4 -2
  7. package/lib/internal/_assertGuard.mjs.map +1 -1
  8. package/lib/internal/_isFormatIdnHostname.js +5 -1
  9. package/lib/internal/_isFormatIdnHostname.js.map +1 -1
  10. package/lib/internal/_isFormatIdnHostname.mjs +1 -1
  11. package/lib/internal/_isFormatIdnHostname.mjs.map +1 -1
  12. package/lib/internal/_isFormatTime.js +19 -2
  13. package/lib/internal/_isFormatTime.js.map +1 -1
  14. package/lib/internal/_isFormatTime.mjs +9 -2
  15. package/lib/internal/_isFormatTime.mjs.map +1 -1
  16. package/lib/internal/_randomArray.js +37 -4
  17. package/lib/internal/_randomArray.js.map +1 -1
  18. package/lib/internal/_randomArray.mjs +16 -3
  19. package/lib/internal/_randomArray.mjs.map +1 -1
  20. package/lib/internal/_randomFormatByte.js +15 -12
  21. package/lib/internal/_randomFormatByte.js.map +1 -1
  22. package/lib/internal/_randomFormatByte.mjs +11 -10
  23. package/lib/internal/_randomFormatByte.mjs.map +1 -1
  24. package/lib/internal/_randomFormatDatetime.mjs +1 -1
  25. package/lib/internal/_randomFormatHostname.d.ts +2 -2
  26. package/lib/internal/_randomFormatHostname.js +2 -2
  27. package/lib/internal/_randomFormatHostname.mjs +2 -2
  28. package/lib/internal/_randomFormatHostname.mjs.map +1 -1
  29. package/lib/internal/_randomFormatIdnHostname.js +9 -10
  30. package/lib/internal/_randomFormatIdnHostname.js.map +1 -1
  31. package/lib/internal/_randomFormatIdnHostname.mjs +3 -4
  32. package/lib/internal/_randomFormatIdnHostname.mjs.map +1 -1
  33. package/lib/internal/_randomFormatRegex.js +4 -4
  34. package/lib/internal/_randomFormatRegex.js.map +1 -1
  35. package/lib/internal/_randomFormatRegex.mjs +1 -2
  36. package/lib/internal/_randomFormatRegex.mjs.map +1 -1
  37. package/lib/internal/_randomFormatTime.js +15 -10
  38. package/lib/internal/_randomFormatTime.js.map +1 -1
  39. package/lib/internal/_randomFormatTime.mjs +6 -3
  40. package/lib/internal/_randomFormatTime.mjs.map +1 -1
  41. package/lib/internal/private/__randomComposition.d.ts +8 -0
  42. package/lib/internal/private/__randomComposition.js +21 -1
  43. package/lib/internal/private/__randomComposition.js.map +1 -1
  44. package/lib/internal/private/__randomComposition.mjs +18 -1
  45. package/lib/internal/private/__randomComposition.mjs.map +1 -1
  46. package/lib/json.d.ts +2 -2
  47. package/lib/json.js.map +1 -1
  48. package/lib/json.mjs.map +1 -1
  49. package/lib/module.d.ts +28 -13
  50. package/lib/module.js.map +1 -1
  51. package/lib/module.mjs.map +1 -1
  52. package/lib/notations.d.ts +4 -4
  53. package/lib/notations.js.map +1 -1
  54. package/lib/notations.mjs.map +1 -1
  55. package/lib/plain.d.ts +3 -3
  56. package/lib/plain.js.map +1 -1
  57. package/lib/plain.mjs.map +1 -1
  58. package/lib/protobuf.d.ts +2 -2
  59. package/lib/protobuf.js.map +1 -1
  60. package/lib/protobuf.mjs.map +1 -1
  61. package/native/cmd/ttsc-typia/assert_guard_factory_contract_test.go +17 -4
  62. package/native/cmd/ttsc-typia/create_assert_error_factory_arity_test.go +309 -0
  63. package/native/cmd/ttsc-typia/local_function_named_like_typia_operation_transform_test.go +185 -0
  64. package/native/cmd/ttsc-typia/recursive_conditional_alias_name_transform_test.go +307 -0
  65. package/native/cmd/ttsc-typia/shadowed_typia_module_declaration_diagnostic_test.go +152 -0
  66. package/native/cmd/ttsc-typia/template_literal_type_tags_transform_test.go +1 -3
  67. package/native/cmd/ttsc-typia/transform_helpers_test.go +92 -26
  68. package/native/core/factories/FormatCheatSheet.go +2 -2
  69. package/native/core/factories/MetadataCommentTagFactory.go +26 -11
  70. package/native/core/factories/factory_utility_coverage_test.go +896 -907
  71. package/native/core/factories/internal/metadata/MetadataHelper.go +5 -28
  72. package/native/core/factories/metadata_comment_tag_factory_coverage_test.go +2 -2
  73. package/native/core/programmers/RandomProgrammer.go +5 -2
  74. package/native/core/schemas/metadata/MetadataCollection.go +47 -5
  75. package/native/transform/CallExpressionTransformer.go +167 -0
  76. package/package.json +3 -3
  77. package/src/http.ts +18 -4
  78. package/src/internal/_assertGuard.ts +13 -2
  79. package/src/internal/_isFormatIdnHostname.ts +5 -1
  80. package/src/internal/_isFormatTime.ts +19 -2
  81. package/src/internal/_randomArray.ts +42 -6
  82. package/src/internal/_randomFormatByte.ts +17 -12
  83. package/src/internal/_randomFormatHostname.ts +2 -2
  84. package/src/internal/_randomFormatIdnHostname.ts +8 -9
  85. package/src/internal/_randomFormatRegex.ts +4 -6
  86. package/src/internal/_randomFormatTime.ts +15 -10
  87. package/src/internal/private/__randomComposition.ts +24 -0
  88. package/src/json.ts +16 -4
  89. package/src/module.ts +56 -22
  90. package/src/notations.ts +16 -4
  91. package/src/plain.ts +12 -3
  92. package/src/protobuf.ts +16 -4
@@ -70,34 +70,11 @@ func metadata_type_full_name(
70
70
  return result
71
71
  }
72
72
 
73
- // metadata_type_symbol_name mirrors TypeFactory.getFullName({ aliasTypeArguments:
74
- // false }) from legacy typia: the name is derived from the type's *symbol*, not
75
- // from checker.typeToString. This matters for alias chains (e.g. MapAlias.MAP =
76
- // Map<K, V>) where typeToString prints the alias name ("MapAlias.MAP<...>")
77
- // instead of "Map<...>", which would defeat the Map/Set prefix guards.
78
- func metadata_type_symbol_name(checker *nativechecker.Checker, typ *nativechecker.Type) string {
79
- if checker == nil || typ == nil {
80
- return ""
81
- }
82
- symbol := typ.Symbol()
83
- if symbol == nil {
84
- return checker.TypeToString(typ)
85
- }
86
- name := metadata_symbol_name(symbol)
87
- generic := metadata_get_type_arguments(checker, typ)
88
- if len(generic) == 0 {
89
- return name
90
- }
91
- if name == "Promise" {
92
- return metadata_type_symbol_name(checker, generic[0])
93
- }
94
- names := make([]string, 0, len(generic))
95
- for _, child := range generic {
96
- names = append(names, metadata_type_symbol_name(checker, child))
97
- }
98
- return name + "<" + strings.Join(names, ", ") + ">"
99
- }
100
-
73
+ // metadata_type_symbol_base_name derives the name from the type's own *symbol*,
74
+ // never from checker.TypeToString. The Map / Set / native guards test this name
75
+ // against a prefix, and an alias chain (e.g. `type MAP = Map<K, V>`) makes
76
+ // TypeToString print the alias name ("MapAlias.MAP<...>") instead of "Map<...>",
77
+ // which would defeat those guards.
101
78
  func metadata_type_symbol_base_name(typ *nativechecker.Type) string {
102
79
  if typ == nil {
103
80
  return ""
@@ -124,8 +124,8 @@ func TestMetadataCommentTagFactoryCoverage(t *testing.T) {
124
124
  if len(reports) != decimalReportCount || len(decimal["number"]) != 1 || len(decimal["bigint"]) != 0 {
125
125
  t.Fatalf("decimal multipleOf must remain a number-only JSDoc tag: %#v, reports=%#v", decimal, reports[decimalReportCount:])
126
126
  }
127
- if decimal["number"][0].Validate != "$importInternal(\"_isMultipleOf\")($input, 0.01)" {
128
- t.Fatalf("decimal multipleOf must use the shared exact helper: %s", decimal["number"][0].Validate)
127
+ if decimal["number"][0].Validate != "$input % 0.01 === 0" {
128
+ t.Fatalf("decimal multipleOf must spell the same remainder check the MultipleOf type tag declares: %s", decimal["number"][0].Validate)
129
129
  }
130
130
  invalidBefore := len(reports)
131
131
  _ = metadataCommentTagFactory_parse_number(struct {
@@ -1442,8 +1442,11 @@ func randomProgrammer_is_typed_array(name string) bool {
1442
1442
  // `maximum` comment tags on a number-typed JSON schema, and `number` cannot
1443
1443
  // represent 2**63 - 1 or 2**64 - 1 -- both arrive rounded up to the next power of
1444
1444
  // two. That costs nothing here: the generator only picks a value from the range,
1445
- // and the TypedArray constructor wraps whatever it is handed. Validation owns
1446
- // exactness instead, in _isTypeInt64Bigint and its siblings.
1445
+ // and the TypedArray constructor wraps whatever it is handed. On the `number`
1446
+ // path validation owns exactness instead, in _isTypeInt64 and _isTypeUint64. The
1447
+ // `bigint` path owns none: Type<"int64"> declares no check and Type<"uint64">
1448
+ // only a lower bound, so that neither declaration names a runtime helper an
1449
+ // older `typia` cannot resolve (#2330). #2338 owns restoring the bound.
1447
1450
  func randomProgrammer_typed_array_range(name string) (string, string, string) {
1448
1451
  switch name {
1449
1452
  case "Uint8Array", "Uint8ClampedArray":
@@ -810,11 +810,31 @@ var metadataCollection_replacers = []metadataCollection_replacer{
810
810
  }
811
811
 
812
812
  func metadataCollection_getFullName(checker *nativechecker.Checker, typ *nativechecker.Type) string {
813
+ return metadataCollection_getFullNameOf(checker, typ, nil)
814
+ }
815
+
816
+ // metadataCollection_getFullNameOf carries the types whose name is still being
817
+ // composed on the current path. A recursive type graph reaches the same *Type
818
+ // again before its name is finished: `type Rec<T> = T extends Date ? string : T
819
+ // extends (infer U)[] ? Rec<U>[] : never` instantiated at `Date | Node[]`
820
+ // resolves to the union `string | Rec<Node>[]`, whose array member is a
821
+ // reference whose sole type argument is that very union. Walking union member ->
822
+ // array reference -> type argument returns to the union, so the unguarded walk
823
+ // never terminates and overflows the plugin's stack instead of naming the type
824
+ // (#2331). A type reached a second time on one path is therefore named by its
825
+ // own symbol with its arguments left unexpanded -- the recursion's placeholder,
826
+ // `Array<string | Array>` for the example above -- and a symbol-less one by the
827
+ // anonymous marker the collection already allocates unique names from. Expanding
828
+ // the checker's elided rendering there instead would name every such component
829
+ // after a several-hundred-character structural dump.
830
+ func metadataCollection_getFullNameOf(
831
+ checker *nativechecker.Checker,
832
+ typ *nativechecker.Type,
833
+ visiting map[*nativechecker.Type]bool,
834
+ ) string {
813
835
  if checker == nil || typ == nil {
814
836
  return "__type"
815
837
  }
816
- rendered := metadataCollection_sanitizeName(checker.TypeToString(typ))
817
-
818
838
  // Mirror TypeFactory.getFullName: prefer the alias symbol, then the type's
819
839
  // own symbol. getTypeNameSymbol already returns t.alias.symbol first (the
820
840
  // alias symbol), so a non-nil result that differs from typ.Symbol() means the
@@ -826,6 +846,13 @@ func metadataCollection_getFullName(checker *nativechecker.Checker, typ *nativec
826
846
  if symbol == nil {
827
847
  symbol = rawSymbol
828
848
  }
849
+ if visiting[typ] {
850
+ if symbol != nil {
851
+ return metadataCollection_getName(symbol)
852
+ }
853
+ return "__type"
854
+ }
855
+ rendered := metadataCollection_sanitizeName(checker.TypeToString(typ))
829
856
  if symbol == nil {
830
857
  if typ.IsUnion() || typ.IsIntersection() {
831
858
  joiner := " | "
@@ -834,9 +861,11 @@ func metadataCollection_getFullName(checker *nativechecker.Checker, typ *nativec
834
861
  }
835
862
  children := typ.Types()
836
863
  names := make([]string, 0, len(children))
864
+ visiting = metadataCollection_markVisiting(visiting, typ)
837
865
  for _, child := range children {
838
- names = append(names, metadataCollection_getFullName(checker, child))
866
+ names = append(names, metadataCollection_getFullNameOf(checker, child, visiting))
839
867
  }
868
+ delete(visiting, typ)
840
869
  return strings.Join(names, joiner)
841
870
  }
842
871
  if rendered != "" {
@@ -864,16 +893,29 @@ func metadataCollection_getFullName(checker *nativechecker.Checker, typ *nativec
864
893
  }
865
894
  return name
866
895
  }
896
+ visiting = metadataCollection_markVisiting(visiting, typ)
897
+ defer delete(visiting, typ)
867
898
  if name == "Promise" {
868
- return metadataCollection_getFullName(checker, generic[0])
899
+ return metadataCollection_getFullNameOf(checker, generic[0], visiting)
869
900
  }
870
901
  names := make([]string, 0, len(generic))
871
902
  for _, child := range generic {
872
- names = append(names, metadataCollection_getFullName(checker, child))
903
+ names = append(names, metadataCollection_getFullNameOf(checker, child, visiting))
873
904
  }
874
905
  return name + "<" + strings.Join(names, ", ") + ">"
875
906
  }
876
907
 
908
+ func metadataCollection_markVisiting(
909
+ visiting map[*nativechecker.Type]bool,
910
+ typ *nativechecker.Type,
911
+ ) map[*nativechecker.Type]bool {
912
+ if visiting == nil {
913
+ visiting = map[*nativechecker.Type]bool{}
914
+ }
915
+ visiting[typ] = true
916
+ return visiting
917
+ }
918
+
877
919
  func metadataCollection_getName(symbol *nativeast.Symbol) string {
878
920
  if symbol == nil || len(symbol.Declarations) == 0 || symbol.Declarations[0].Parent == nil {
879
921
  return "__type"
@@ -63,6 +63,7 @@ func (callExpressionTransformerNamespace) Transform(props CallExpressionTransfor
63
63
  location, _ := filepath.Abs(sourceFile.FileName())
64
64
  module, ok := callExpressionTransformer_targetModule(location)
65
65
  if ok == false {
66
+ callExpressionTransformer_rejectShadowedTypia(props, location)
66
67
  return props.Expression.AsNode()
67
68
  }
68
69
  typ := props.Context.Checker.GetTypeAtLocation(declaration)
@@ -127,6 +128,172 @@ func callExpressionTransformer_targetModule(location string) (string, bool) {
127
128
  return "", false
128
129
  }
129
130
 
131
+ // callExpressionTransformer_rejectShadowedTypia raises a diagnostic when a call
132
+ // the file-path identity test rejected is nevertheless a typia call the author
133
+ // expects to be transformed, and names the file that declared it instead.
134
+ //
135
+ // Attribution by declaration file is deliberate: accepting a callee declared
136
+ // anywhere else would let any package claim typia's transform, which is the
137
+ // boundary default_library_spoof_native_identity_transform_test.go and
138
+ // lib_replacement_native_identity_transform_test.go protect. That check is not
139
+ // relaxed here. Silence is what needs correcting. A project-local `declare
140
+ // module "typia"` moves the resolved signature's declaration out of typia's own
141
+ // files, so the call is left untransformed with no diagnostic at all and the
142
+ // failure surfaces only at run time as `no transform has been configured`
143
+ // (samchon/typia#2328).
144
+ //
145
+ // Three conditions have to hold together, so an unrelated local function that
146
+ // happens to share a name with a typia operation cannot trip this:
147
+ //
148
+ // 1. the callee spells an operation this transformer owns, in the namespace it
149
+ // actually lives in — `typia.json.assertParse`, never `typia.assertParse`;
150
+ // 2. the root of the callee expression resolves to an import binding; and
151
+ // 3. that import's module specifier is typia's own.
152
+ //
153
+ // Known gap: an alias chain is not followed, so a project that re-exports typia
154
+ // through its own barrel and then shadows it is still silent. The specifier of
155
+ // the import the call actually reads is the only evidence this collects.
156
+ func callExpressionTransformer_rejectShadowedTypia(props CallExpressionTransformer_TransformProps, location string) {
157
+ code, ok := callExpressionTransformer_calleeOperation(props.Expression.Expression)
158
+ if ok == false {
159
+ return
160
+ }
161
+ root := callExpressionTransformer_calleeRoot(props.Expression.Expression)
162
+ if root == nil || root.Kind != shimast.KindIdentifier {
163
+ return
164
+ }
165
+ symbol := props.Context.Checker.GetSymbolAtLocation(root)
166
+ if symbol == nil || symbol.Flags&shimast.SymbolFlagsAlias == 0 {
167
+ return
168
+ }
169
+ for _, declaration := range symbol.Declarations {
170
+ if callExpressionTransformer_importsTypia(declaration) == false {
171
+ continue
172
+ }
173
+ // State only what was observed. The declaration file is the fact; a
174
+ // `declare module "typia"` is the usual cause but not the only way to move
175
+ // a declaration, and naming it as the cause would misdirect anyone whose
176
+ // build got here another way.
177
+ panic(nativecontext.NewTransformerError(nativecontext.TransformerError_IProps{
178
+ Code: code,
179
+ Message: "this call was declared in " +
180
+ filepath.ToSlash(location) +
181
+ ", not in typia's own module, so no transform was applied. A project-local `declare module \"typia\"` is the usual cause; remove it, or give the redeclaration another module name.",
182
+ }))
183
+ }
184
+ }
185
+
186
+ // callExpressionTransformer_calleeOperation answers whether a callee expression
187
+ // spells one of this transformer's operations, and returns the diagnostic code
188
+ // for it — the written spelling, qualified as `typia.json.assertParse`.
189
+ //
190
+ // The namespace is part of the spelling. `typia.json.assertParse` names the
191
+ // `json` table, while `typia.createAssert` and a bare named import name the root
192
+ // `module` table; a root-level `typia.assertParse` matches nothing, because that
193
+ // operation does not live there. Matching a flat union of every table's names
194
+ // instead would report a module augmentation that adds an unrelated `parse` or
195
+ // `schema` to typia, which is a build that works today.
196
+ //
197
+ // Known gap: taking the namespace from the spelling means a renamed binding
198
+ // (`import { json as j } from "typia"; j.assertParse(...)`) is not recognized.
199
+ func callExpressionTransformer_calleeOperation(callee *shimast.Node) (string, bool) {
200
+ method := ""
201
+ first := true
202
+ // Segments to the left of the method, nearest first: `typia.json.assertParse`
203
+ // collects ["json", "typia"].
204
+ parts := []string{}
205
+ for current := callee; current != nil; {
206
+ if len(parts) > 2 {
207
+ return "", false
208
+ }
209
+ segment := ""
210
+ switch current.Kind {
211
+ case shimast.KindPropertyAccessExpression:
212
+ access := current.AsPropertyAccessExpression()
213
+ name := access.Name()
214
+ if name == nil || name.Kind != shimast.KindIdentifier {
215
+ return "", false
216
+ }
217
+ segment = name.Text()
218
+ current = access.Expression
219
+ case shimast.KindIdentifier:
220
+ segment = current.Text()
221
+ current = nil
222
+ default:
223
+ return "", false
224
+ }
225
+ if first {
226
+ method = segment
227
+ first = false
228
+ } else {
229
+ parts = append(parts, segment)
230
+ }
231
+ }
232
+ if method == "" || len(parts) > 2 {
233
+ return "", false
234
+ }
235
+ functors := callExpressionTransformer_FUNCTORS()
236
+ // `typia.json.assertParse` and `json.assertParse` both name the `json` table
237
+ // through the segment directly left of the method.
238
+ if len(parts) != 0 {
239
+ if _, found := functors[parts[0]][method]; found {
240
+ return "typia." + parts[0] + "." + method, true
241
+ }
242
+ }
243
+ // `typia.createAssert` and a bare `createAssert` name the root table. The
244
+ // qualifier, when present, is the imported binding rather than a namespace.
245
+ if len(parts) <= 1 {
246
+ if _, found := functors["module"][method]; found {
247
+ return "typia." + method, true
248
+ }
249
+ }
250
+ return "", false
251
+ }
252
+
253
+ // callExpressionTransformer_calleeRoot walks a property-access chain down to the
254
+ // expression it starts from, which is the binding that has to come from typia.
255
+ func callExpressionTransformer_calleeRoot(callee *shimast.Node) *shimast.Node {
256
+ current := callee
257
+ for current != nil && current.Kind == shimast.KindPropertyAccessExpression {
258
+ current = current.AsPropertyAccessExpression().Expression
259
+ }
260
+ return current
261
+ }
262
+
263
+ // callExpressionTransformer_importsTypia answers whether an alias declaration
264
+ // belongs to an import whose module specifier is typia's own. Both `import ...
265
+ // from "typia"` and `import typia = require("typia")` are matched; the walk
266
+ // stops at the source file so a local declaration can never reach an unrelated
267
+ // import above it.
268
+ func callExpressionTransformer_importsTypia(declaration *shimast.Node) bool {
269
+ for node := declaration; node != nil; node = node.Parent {
270
+ switch node.Kind {
271
+ case shimast.KindImportDeclaration:
272
+ return callExpressionTransformer_isTypiaSpecifier(node.ModuleSpecifier())
273
+ case shimast.KindImportEqualsDeclaration:
274
+ reference := node.AsImportEqualsDeclaration().ModuleReference
275
+ if reference == nil || reference.Kind != shimast.KindExternalModuleReference {
276
+ return false
277
+ }
278
+ return callExpressionTransformer_isTypiaSpecifier(reference.AsExternalModuleReference().Expression)
279
+ case shimast.KindSourceFile:
280
+ return false
281
+ }
282
+ }
283
+ return false
284
+ }
285
+
286
+ // callExpressionTransformer_isTypiaSpecifier accepts typia's own package name
287
+ // and its subpaths, and nothing that merely begins with those letters: a
288
+ // `typia-codegen` package is a different package.
289
+ func callExpressionTransformer_isTypiaSpecifier(specifier *shimast.Node) bool {
290
+ if specifier == nil || specifier.Kind != shimast.KindStringLiteral {
291
+ return false
292
+ }
293
+ text := specifier.AsStringLiteral().Text
294
+ return text == "typia" || strings.HasPrefix(text, "typia/")
295
+ }
296
+
130
297
  func callExpressionTransformer_sourceFile(node *shimast.Node) *shimast.SourceFile {
131
298
  for current := node; current != nil; current = current.Parent {
132
299
  if current.Kind == shimast.KindSourceFile {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "typia",
3
- "version": "13.2.0",
3
+ "version": "13.3.0",
4
4
  "description": "Superfast runtime validators with only one line",
5
5
  "main": "lib/index.js",
6
6
  "exports": {
@@ -50,8 +50,8 @@
50
50
  "inquirer": "^8.2.5",
51
51
  "randexp": "^0.5.3",
52
52
  "tinyglobby": "^0.2.12",
53
- "@typia/interface": "^13.2.0",
54
- "@typia/utils": "^13.2.0"
53
+ "@typia/interface": "^13.3.0",
54
+ "@typia/utils": "^13.3.0"
55
55
  },
56
56
  "peerDependencies": {
57
57
  "ttsc": ">=0.19.2"
package/src/http.ts CHANGED
@@ -550,10 +550,16 @@ export function createAssertFormData(
550
550
  */
551
551
  export function createAssertFormData<T extends object>(
552
552
  errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
553
- ): (input: FormData) => T;
553
+ ): (
554
+ input: FormData,
555
+ errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
556
+ ) => T;
554
557
 
555
558
  /** @internal */
556
- export function createAssertFormData<T>(): (input: FormData) => T {
559
+ export function createAssertFormData<T>(): (
560
+ input: FormData,
561
+ errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
562
+ ) => T {
557
563
  NoTransformConfigurationError("http.createAssertFormData");
558
564
  }
559
565
 
@@ -652,11 +658,15 @@ export function createAssertQuery(
652
658
  */
653
659
  export function createAssertQuery<T extends object>(
654
660
  errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
655
- ): (input: string | IReadableURLSearchParams) => T;
661
+ ): (
662
+ input: string | IReadableURLSearchParams,
663
+ errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
664
+ ) => T;
656
665
 
657
666
  /** @internal */
658
667
  export function createAssertQuery<T>(): (
659
668
  input: string | IReadableURLSearchParams,
669
+ errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
660
670
  ) => T {
661
671
  NoTransformConfigurationError("http.createAssertQuery");
662
672
  }
@@ -758,11 +768,15 @@ export function createAssertHeaders(
758
768
  */
759
769
  export function createAssertHeaders<T extends object>(
760
770
  errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
761
- ): (input: Record<string, string | string[] | undefined>) => T;
771
+ ): (
772
+ input: Record<string, string | string[] | undefined>,
773
+ errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
774
+ ) => T;
762
775
 
763
776
  /** @internal */
764
777
  export function createAssertHeaders<T>(): (
765
778
  input: Record<string, string | string[] | undefined>,
779
+ errorFactory?: undefined | ((props: TypeGuardError.IProps) => Error),
766
780
  ) => T {
767
781
  NoTransformConfigurationError("http.createAssertHeaders");
768
782
  }
@@ -6,8 +6,19 @@ export const _assertGuard = (
6
6
  factory?: (props: TypeGuardError.IProps) => Error,
7
7
  ): false => {
8
8
  if (exceptionable === true) {
9
- if (factory) throw factory(props);
10
- else throw new TypeGuardError(props);
9
+ // `factory` arrives from the generated function's second parameter, which
10
+ // a pointwise hand-off fills with something that is not a factory at all:
11
+ // `rows.map(assertUser)` passes the element index. A truthiness test then
12
+ // called a number and reported `factory is not a function`, hiding the
13
+ // real assertion failure — and only for indices other than 0. Require a
14
+ // callable, and fall back to TypeGuardError for everything else.
15
+ //
16
+ // A create-time factory is the default of that same parameter, so a
17
+ // non-callable argument has already overridden it by the time this runs
18
+ // and cannot be recovered here. TypeGuardError still names the property
19
+ // that failed, which `factory is not a function` never did.
20
+ if (typeof factory === "function") throw factory(props);
21
+ throw new TypeGuardError(props);
11
22
  }
12
23
  return false;
13
24
  };
@@ -1,4 +1,8 @@
1
1
  export const _isFormatIdnHostname = (str: string): boolean => PATTERN.test(str);
2
2
 
3
+ // The ASCII `hostname` structure with the label character class extended by the
4
+ // non-ASCII range: every valid host name is a valid IDN host name, so this must
5
+ // accept a single label, a one-to-63-character label in any position, and a
6
+ // name up to 253 characters, exactly as `_isFormatHostname` does (issue #2317).
3
7
  const PATTERN =
4
- /^([a-z0-9\u00a1-\uffff0-9]+(-[a-z0-9\u00a1-\uffff0-9]+)*\.)+[a-z\u00a1-\uffff]{2,}$/i;
8
+ /^(?=.{1,253}\.?$)[a-z0-9\u00a1-\uffff](?:[a-z0-9\u00a1-\uffff-]{0,61}[a-z0-9\u00a1-\uffff])?(?:\.[a-z0-9\u00a1-\uffff](?:[a-z0-9\u00a1-\uffff-]{0,61}[a-z0-9\u00a1-\uffff])?)*\.?$/i;
@@ -1,4 +1,21 @@
1
- export const _isFormatTime = (str: string): boolean => PATTERN.test(str);
1
+ export const _isFormatTime = (str: string): boolean => {
2
+ const match: RegExpExecArray | null = PATTERN.exec(str);
3
+ if (match === null) return false;
4
+ if (match[3] !== "60") return true;
5
+
6
+ // RFC 3339 admits `:60` only where a leap second is inserted, which is
7
+ // always 23:59:60 UTC. `date-time` decides that from the instant it can
8
+ // build; a clock carries no date, so it shifts itself by its own offset and
9
+ // asks the same question, wrapping the result back into the day.
10
+ const offset: number =
11
+ match[4] === undefined
12
+ ? 0
13
+ : (match[4] === "+" ? 1 : -1) *
14
+ (Number(match[5]) * 60 + Number(match[6]));
15
+ const minutes: number = Number(match[1]) * 60 + Number(match[2]) - offset;
16
+ return ((minutes % DAY) + DAY) % DAY === DAY - 1;
17
+ };
2
18
 
3
19
  const PATTERN =
4
- /^([01][0-9]|2[0-3]):[0-5][0-9]:[0-5][0-9](?:\.[0-9]{1,9})?(Z|[+-]([01][0-9]|2[0-3]):[0-5][0-9])$/i;
20
+ /^([01][0-9]|2[0-3]):([0-5][0-9]):([0-5][0-9]|60)(?:\.[0-9]+)?(?:[Zz]|([+-])((?:[01][0-9]|2[0-3])):([0-5][0-9]))$/;
21
+ const DAY = 24 * 60;
@@ -7,6 +7,16 @@ const DEFAULT_MIN_ITEMS = 1;
7
7
  const DEFAULT_RANGE = 5;
8
8
  const DEFAULT_RECURSIVE_RANGE = 2;
9
9
 
10
+ /**
11
+ * Consecutive duplicates that end a unique draw once its floor is satisfied.
12
+ *
13
+ * The worst case a domain can still hide is one value left among a handful:
14
+ * with one of four remaining, sixty-four misses in a row is a `0.75 ** 64`
15
+ * event, about one in a hundred million. Below the floor this limit does not
16
+ * apply at all, so it can never fail a request the domain could satisfy.
17
+ */
18
+ const STALE_LIMIT = 64;
19
+
10
20
  export const _randomArray = <T>(
11
21
  props: Omit<OpenApi.IJsonSchema.IArray, "items"> & {
12
22
  element: (index: number, count: number) => T;
@@ -28,16 +38,42 @@ export const _randomArray = <T>(
28
38
  });
29
39
  if (props.uniqueItems !== true)
30
40
  return new Array(count).fill(null).map((_, i) => props.element(i, count));
41
+ // How many distinct values the element can take is not knowable from here --
42
+ // `element` is a closure over whatever the transform emitted, from `boolean`
43
+ // to a recursive object -- so the count above is a preference, not a
44
+ // contract. Draw until the budget says the domain has nothing new left, then
45
+ // keep what was reached.
46
+ //
47
+ // Failing on that budget instead is what made `Array<boolean> &
48
+ // UniqueItems` throw on two draws in three: `[true, false]` satisfies the
49
+ // declaration at every count, and a count of five is unreachable for a
50
+ // domain of two however many times it is redrawn. Only a result below the
51
+ // floor is a real failure, which is still the honest outcome for a window
52
+ // like `MinItems<3>` over a domain of two.
53
+ //
54
+ // Below the floor the whole budget is spent, because stopping early there
55
+ // would fail a request the domain can still satisfy. Above it every further
56
+ // attempt is only buying length, so a run of `STALE_LIMIT` duplicates is
57
+ // taken as the domain saying it has nothing left. Without that, each nesting
58
+ // level of a small-domain unique array multiplies the full budget into the
59
+ // level above it.
31
60
  const elements: T[] = [];
32
61
  const maximumAttempts: number = count * 100 + 1000;
33
- for (let attempts = 0; elements.length !== count; attempts++) {
34
- if (attempts === maximumAttempts)
35
- throw new Error(
36
- "Unable to generate enough unique items; the element domain may be too small.",
37
- );
62
+ let stale: number = 0;
63
+ for (
64
+ let attempts = 0;
65
+ elements.length !== count && attempts !== maximumAttempts;
66
+ attempts++
67
+ ) {
38
68
  const candidate: T = props.element(elements.length, count);
39
- if (elements.every((element) => _isUniqueItems([element, candidate])))
69
+ if (elements.every((element) => _isUniqueItems([element, candidate]))) {
40
70
  elements.push(candidate);
71
+ stale = 0;
72
+ } else if (elements.length >= minimum && ++stale === STALE_LIMIT) break;
41
73
  }
74
+ if (elements.length < minimum)
75
+ throw new Error(
76
+ "Unable to generate enough unique items; the element domain may be too small.",
77
+ );
42
78
  return elements;
43
79
  };
@@ -1,18 +1,26 @@
1
- import { _randomString } from "./_randomString";
1
+ import { _randomInteger } from "./_randomInteger";
2
2
  import {
3
3
  _ILengthProps,
4
4
  _RANDOM_LENGTH_ERROR,
5
5
  _randomLengthPick,
6
6
  _randomLengthWindow,
7
7
  } from "./_randomStringLength";
8
+ import { __randomBase64 } from "./private/__randomComposition";
8
9
 
9
10
  export const _randomFormatByte = (props?: _ILengthProps): string => {
10
- if (props?.minLength === undefined && props?.maxLength === undefined)
11
- return FIXED;
12
11
  // Base64 encodes three bytes as four characters, so a valid length is always
13
- // a multiple of four; the window is raised to the first multiple it contains
14
- // and the draw steps by four from there. Every character `_randomString`
15
- // emits is in the base64 alphabet, so the result needs no padding.
12
+ // a multiple of four; an unconstrained draw spends between four and twelve
13
+ // groups, and a constrained one raises the window to the first multiple it
14
+ // contains and steps by four from there.
15
+ if (props?.minLength === undefined && props?.maxLength === undefined)
16
+ return __randomBase64(
17
+ 4 *
18
+ _randomInteger({
19
+ type: "integer",
20
+ minimum: DEFAULT_GROUP_MIN,
21
+ maximum: DEFAULT_GROUP_MAX,
22
+ }),
23
+ );
16
24
  const window = _randomLengthWindow(props, { minimum: 0, spread: 16 });
17
25
  const low: number = Math.ceil(window.low / 4) * 4;
18
26
  if (low > window.high) throw new Error(_RANDOM_LENGTH_ERROR);
@@ -23,11 +31,8 @@ export const _randomFormatByte = (props?: _ILengthProps): string => {
23
31
  low: 0,
24
32
  high: Math.floor((window.high - low) / 4),
25
33
  });
26
- return _randomString({
27
- type: "string",
28
- minLength: length,
29
- maxLength: length,
30
- });
34
+ return __randomBase64(length);
31
35
  };
32
36
 
33
- const FIXED = "vt7ekz4lIoNTTS9sDQYdWKharxIFAR54+z/umIxSgUM=";
37
+ const DEFAULT_GROUP_MIN = 4;
38
+ const DEFAULT_GROUP_MAX = 12;
@@ -29,8 +29,8 @@ const pickLength = (props: _ILengthProps): number => {
29
29
 
30
30
  /**
31
31
  * Builds dot-joined hostname labels totaling exactly `length` characters, each
32
- * label within 63 chars. Shared with the idn-hostname generator, which reserves
33
- * a fixed `.<tld>` suffix and realizes the rest as these leading labels.
32
+ * label within 63 chars. Shared with the idn-hostname generator, which realizes
33
+ * its length the same way since #2317 gave the two formats one structure.
34
34
  */
35
35
  export const _randomHostnameLabels = (length: number): string => {
36
36
  const parts: string[] = [];
@@ -6,20 +6,19 @@ import { _ILengthProps } from "./_randomStringLength";
6
6
  export const _randomFormatIdnHostname = (props?: _ILengthProps): string => {
7
7
  if (props?.minLength === undefined && props?.maxLength === undefined)
8
8
  return `${random(10)}.${random(3)}`;
9
- // `_isFormatIdnHostname` requires at least one label, a dot, and a >=2-char
10
- // TLD (`(…\.)+[a-z¡-￿]{2,}`), none of which the non-idn hostname generator
11
- // guarantees on its length path (it can emit a single dotless label). Reserve
12
- // a fixed `.<2-char TLD>` and realize the rest as dot-joined leading labels.
13
- const length: number = pickLength(props);
14
- return `${_randomHostnameLabels(length - 3)}.${random(2)}`;
9
+ // idn-hostname shares hostname's structure (a single label of 1..63 chars is
10
+ // valid, no dot or trailing label required), so the requested length is
11
+ // realized the same way: split it across dot-joined labels. #2317 dropped the
12
+ // mandatory dot and two-character trailing label this generator used to
13
+ // reserve, which is why it now realizes a length below four.
14
+ return _randomHostnameLabels(pickLength(props));
15
15
  };
16
16
 
17
17
  const MAX_TOTAL = 253;
18
- const MIN_TOTAL = 4; // the shortest idn hostname, `x.yy`: label(1) + `.` + tld(2)
19
18
 
20
19
  const pickLength = (props: _ILengthProps): number => {
21
- let low: number = props.minLength === undefined ? MIN_TOTAL : props.minLength;
22
- if (low < MIN_TOTAL) low = MIN_TOTAL;
20
+ let low: number = props.minLength === undefined ? 1 : props.minLength;
21
+ if (low < 1) low = 1;
23
22
  const high: number =
24
23
  props.maxLength === undefined
25
24
  ? Math.min(MAX_TOTAL, low + 14)
@@ -6,11 +6,12 @@ import {
6
6
  } from "./_randomStringLength";
7
7
 
8
8
  export const _randomFormatRegex = (props?: _ILengthProps): string => {
9
- if (props?.minLength === undefined && props?.maxLength === undefined)
10
- return FIXED;
11
9
  // Every character `_randomString` emits is a literal in regular expression
12
10
  // syntax, so a source of any length — including the empty source `new
13
- // RegExp("")` accepts — compiles.
11
+ // RegExp("")` accepts — compiles. An unconstrained draw is therefore an
12
+ // unconstrained string, and a constrained one fixes the length first.
13
+ if (props?.minLength === undefined && props?.maxLength === undefined)
14
+ return _randomString({ type: "string" });
14
15
  const length: number = _randomLengthPick(
15
16
  _randomLengthWindow(props, { minimum: 0, spread: 16 }),
16
17
  );
@@ -20,6 +21,3 @@ export const _randomFormatRegex = (props?: _ILengthProps): string => {
20
21
  maxLength: length,
21
22
  });
22
23
  };
23
-
24
- const FIXED =
25
- "/^(?:(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)\\.){3}(?:25[0-5]|2[0-4]\\d|1\\d\\d|[1-9]?\\d)$/";