@typia/utils 12.0.0-dev.20260309 → 12.0.0-dev.20260310

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/internal/HttpLlmApplicationComposer.mjs +5 -1
  2. package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
  3. package/lib/index.mjs +9 -9
  4. package/lib/utils/LlmJson.mjs +9 -2
  5. package/lib/utils/LlmJson.mjs.map +1 -1
  6. package/lib/utils/internal/stringifyValidationFailure.js +17 -15
  7. package/lib/utils/internal/stringifyValidationFailure.js.map +1 -1
  8. package/lib/utils/internal/stringifyValidationFailure.mjs +17 -15
  9. package/lib/utils/internal/stringifyValidationFailure.mjs.map +1 -1
  10. package/lib/validators/internal/OpenApiOneOfValidator.mjs +5 -1
  11. package/lib/validators/internal/OpenApiOneOfValidator.mjs.map +1 -1
  12. package/package.json +2 -2
  13. package/src/converters/LlmSchemaConverter.ts +647 -647
  14. package/src/converters/OpenApiConverter.ts +285 -285
  15. package/src/converters/index.ts +5 -5
  16. package/src/converters/internal/LlmDescriptionInverter.ts +178 -178
  17. package/src/converters/internal/LlmParametersComposer.ts +52 -52
  18. package/src/converters/internal/OpenApiConstraintShifter.ts +154 -154
  19. package/src/converters/internal/OpenApiExclusiveEmender.ts +46 -46
  20. package/src/converters/internal/OpenApiV3Downgrader.ts +355 -355
  21. package/src/converters/internal/OpenApiV3Upgrader.ts +470 -470
  22. package/src/converters/internal/OpenApiV3_1Upgrader.ts +685 -685
  23. package/src/converters/internal/SwaggerV2Downgrader.ts +424 -424
  24. package/src/converters/internal/SwaggerV2Upgrader.ts +523 -523
  25. package/src/http/HttpError.ts +107 -107
  26. package/src/http/HttpLlm.ts +167 -167
  27. package/src/http/HttpMigration.ts +92 -92
  28. package/src/http/index.ts +3 -3
  29. package/src/http/internal/HttpLlmApplicationComposer.ts +361 -361
  30. package/src/http/internal/HttpLlmFunctionFetcher.ts +37 -37
  31. package/src/http/internal/HttpMigrateApplicationComposer.ts +56 -56
  32. package/src/http/internal/HttpMigrateRouteAccessor.ts +135 -135
  33. package/src/http/internal/HttpMigrateRouteComposer.ts +505 -505
  34. package/src/http/internal/HttpMigrateRouteFetcher.ts +203 -203
  35. package/src/index.ts +4 -4
  36. package/src/utils/ArrayUtil.ts +42 -42
  37. package/src/utils/LlmJson.ts +141 -141
  38. package/src/utils/MapUtil.ts +15 -15
  39. package/src/utils/NamingConvention.ts +205 -205
  40. package/src/utils/Singleton.ts +17 -17
  41. package/src/utils/StringUtil.ts +14 -14
  42. package/src/utils/dedent.ts +57 -57
  43. package/src/utils/index.ts +8 -8
  44. package/src/utils/internal/EndpointUtil.ts +44 -44
  45. package/src/utils/internal/JsonDescriptor.ts +70 -70
  46. package/src/utils/internal/OpenApiTypeCheckerBase.ts +822 -822
  47. package/src/utils/internal/coerceLlmArguments.ts +314 -314
  48. package/src/utils/internal/parseLenientJson.ts +894 -894
  49. package/src/utils/internal/stringifyValidationFailure.ts +415 -411
  50. package/src/validators/LlmTypeChecker.ts +402 -402
  51. package/src/validators/OpenApiTypeChecker.ts +297 -297
  52. package/src/validators/OpenApiV3TypeChecker.ts +70 -70
  53. package/src/validators/OpenApiV3_1TypeChecker.ts +86 -86
  54. package/src/validators/OpenApiValidator.ts +94 -94
  55. package/src/validators/SwaggerV2TypeChecker.ts +71 -71
  56. package/src/validators/functional/_isBigintString.ts +8 -8
  57. package/src/validators/functional/_isFormatByte.ts +7 -7
  58. package/src/validators/functional/_isFormatDate.ts +3 -3
  59. package/src/validators/functional/_isFormatDateTime.ts +4 -4
  60. package/src/validators/functional/_isFormatDuration.ts +4 -4
  61. package/src/validators/functional/_isFormatEmail.ts +4 -4
  62. package/src/validators/functional/_isFormatHostname.ts +4 -4
  63. package/src/validators/functional/_isFormatIdnEmail.ts +4 -4
  64. package/src/validators/functional/_isFormatIdnHostname.ts +4 -4
  65. package/src/validators/functional/_isFormatIpv4.ts +4 -4
  66. package/src/validators/functional/_isFormatIpv6.ts +4 -4
  67. package/src/validators/functional/_isFormatIri.ts +3 -3
  68. package/src/validators/functional/_isFormatIriReference.ts +4 -4
  69. package/src/validators/functional/_isFormatJsonPointer.ts +3 -3
  70. package/src/validators/functional/_isFormatPassword.ts +1 -1
  71. package/src/validators/functional/_isFormatRegex.ts +8 -8
  72. package/src/validators/functional/_isFormatRelativeJsonPointer.ts +4 -4
  73. package/src/validators/functional/_isFormatTime.ts +4 -4
  74. package/src/validators/functional/_isFormatUri.ts +6 -6
  75. package/src/validators/functional/_isFormatUriReference.ts +5 -5
  76. package/src/validators/functional/_isFormatUriTemplate.ts +4 -4
  77. package/src/validators/functional/_isFormatUrl.ts +4 -4
  78. package/src/validators/functional/_isFormatUuid.ts +3 -3
  79. package/src/validators/functional/_isUniqueItems.ts +159 -159
  80. package/src/validators/index.ts +14 -14
  81. package/src/validators/internal/IOpenApiValidatorContext.ts +17 -17
  82. package/src/validators/internal/OpenApiArrayValidator.ts +49 -49
  83. package/src/validators/internal/OpenApiBooleanValidator.ts +11 -11
  84. package/src/validators/internal/OpenApiConstantValidator.ts +11 -11
  85. package/src/validators/internal/OpenApiIntegerValidator.ts +49 -49
  86. package/src/validators/internal/OpenApiNumberValidator.ts +48 -48
  87. package/src/validators/internal/OpenApiObjectValidator.ts +83 -83
  88. package/src/validators/internal/OpenApiOneOfValidator.ts +309 -309
  89. package/src/validators/internal/OpenApiSchemaNamingRule.ts +124 -124
  90. package/src/validators/internal/OpenApiStationValidator.ts +115 -115
  91. package/src/validators/internal/OpenApiStringValidator.ts +88 -88
  92. package/src/validators/internal/OpenApiTupleValidator.ts +55 -55
@@ -1,411 +1,415 @@
1
- import { IValidation } from "@typia/interface";
2
-
3
- import { NamingConvention } from "../NamingConvention";
4
- import { dedent } from "../dedent";
5
-
6
- export function stringifyValidationFailure(
7
- failure: IValidation.IFailure,
8
- ): string {
9
- const usedErrors: Set<IValidation.IError> = new Set();
10
- // Pre-index errors by path for O(1) lookup
11
- const errorsByPath: Map<string, IValidation.IError[]> = new Map();
12
- for (const e of failure.errors) {
13
- const arr: IValidation.IError[] | undefined = errorsByPath.get(e.path);
14
- if (arr !== undefined) arr.push(e);
15
- else errorsByPath.set(e.path, [e]);
16
- }
17
- const jsonOutput = stringify({
18
- value: failure.data,
19
- errorsByPath,
20
- path: "$input",
21
- tab: 0,
22
- inArray: false,
23
- inToJson: false,
24
- usedErrors,
25
- });
26
-
27
- // Find errors that couldn't be embedded
28
- const unmappableErrors: IValidation.IError[] = failure.errors.filter(
29
- (e) => !usedErrors.has(e),
30
- );
31
-
32
- // If there are unmappable errors, append them as a separate block
33
- if (unmappableErrors.length > 0)
34
- return dedent`
35
- \`\`\`json
36
- ${jsonOutput}
37
- \`\`\`
38
-
39
- **Unmappable validation errors:**
40
- \`\`\`json
41
- ${JSON.stringify(unmappableErrors, null, 2)}
42
- \`\`\`
43
- `;
44
- return dedent`
45
- \`\`\`json
46
- ${jsonOutput}
47
- \`\`\`
48
- `;
49
- }
50
-
51
- function stringify(props: {
52
- value: unknown;
53
- errorsByPath: Map<string, IValidation.IError[]>;
54
- path: string;
55
- tab: number;
56
- inArray: boolean;
57
- inToJson: boolean;
58
- usedErrors: Set<IValidation.IError>;
59
- }): string {
60
- const { value, errorsByPath, path, tab, inArray, inToJson, usedErrors } =
61
- props;
62
- const indent: string = " ".repeat(tab);
63
- const errorComment: string = getErrorComment(path, errorsByPath, usedErrors);
64
-
65
- // Handle undefined in arrays
66
- if (inArray && value === undefined) {
67
- return `${indent}undefined${errorComment}`;
68
- }
69
-
70
- // Array
71
- if (Array.isArray(value)) {
72
- // Check for missing array element errors (path[])
73
- const missingElementErrors = getMissingArrayElementErrors(
74
- path,
75
- errorsByPath,
76
- usedErrors,
77
- );
78
- const hasMissingElements = missingElementErrors.length > 0;
79
-
80
- if (value.length === 0) {
81
- // Empty array but has missing element errors - show placeholders
82
- if (hasMissingElements) {
83
- const innerIndent = " ".repeat(tab + 1);
84
- const lines: string[] = [];
85
- lines.push(`${indent}[${errorComment}`);
86
- missingElementErrors.forEach((e, idx) => {
87
- const errComment = ` // ❌ ${JSON.stringify([{ path: e.path, expected: e.expected, description: e.description }])}`;
88
- const comma = idx < missingElementErrors.length - 1 ? "," : "";
89
- lines.push(`${innerIndent}undefined${comma}${errComment}`);
90
- });
91
- lines.push(`${indent}]`);
92
- return lines.join("\n");
93
- }
94
- return `${indent}[]${errorComment}`;
95
- }
96
-
97
- const lines: string[] = [];
98
- lines.push(`${indent}[${errorComment}`);
99
-
100
- value.forEach((item: unknown, index: number) => {
101
- const itemPath: string = `${path}[${index}]`;
102
- const isLastElement = index === value.length - 1;
103
- // If there are missing element errors, this is not truly the last line
104
- const needsComma = !isLastElement || hasMissingElements;
105
-
106
- let itemStr: string = stringify({
107
- value: item,
108
- errorsByPath,
109
- path: itemPath,
110
- tab: tab + 1,
111
- inArray: true,
112
- inToJson: false,
113
- usedErrors,
114
- });
115
- // Add comma before the error comment if not the last element
116
- if (needsComma) {
117
- itemStr = insertCommaBeforeComment(itemStr);
118
- }
119
- lines.push(itemStr);
120
- });
121
-
122
- // Add missing element placeholders at the end for each [] error
123
- if (hasMissingElements) {
124
- const innerIndent = " ".repeat(tab + 1);
125
- missingElementErrors.forEach((e, idx) => {
126
- const errComment = ` // ❌ ${JSON.stringify([{ path: e.path, expected: e.expected, description: e.description }])}`;
127
- const comma = idx < missingElementErrors.length - 1 ? "," : "";
128
- lines.push(`${innerIndent}undefined${comma}${errComment}`);
129
- });
130
- }
131
-
132
- lines.push(`${indent}]`);
133
- return lines.join("\n");
134
- }
135
-
136
- // Object
137
- if (typeof value === "object" && value !== null) {
138
- // Check for toJSON method
139
- // biome-ignore lint: intended
140
- if (!inToJson && typeof (value as any).toJSON === "function") {
141
- // biome-ignore lint: intended
142
- const jsonValue: unknown = (value as any).toJSON();
143
- return stringify({
144
- value: jsonValue,
145
- errorsByPath,
146
- path,
147
- tab,
148
- inArray,
149
- inToJson: true,
150
- usedErrors,
151
- });
152
- }
153
-
154
- // Get all entries from the object (including undefined values that have errors)
155
- const allEntries: [string, unknown][] = Object.entries(value);
156
-
157
- // Split into defined and undefined entries
158
- const definedEntries: [string, unknown][] = allEntries.filter(
159
- ([_, val]) => val !== undefined,
160
- );
161
- const undefinedEntryKeys: Set<string> = new Set(
162
- allEntries.filter(([_, val]) => val === undefined).map(([key]) => key),
163
- );
164
-
165
- // Find missing properties that have validation errors (not in object at all)
166
- const missingKeys: string[] = getMissingProperties(
167
- path,
168
- value,
169
- errorsByPath,
170
- );
171
-
172
- // Combine: defined entries + undefined entries with errors + missing properties
173
- const undefinedKeysWithErrors: string[] = Array.from(
174
- undefinedEntryKeys,
175
- ).filter((key) => {
176
- const propPath = NamingConvention.variable(key)
177
- ? `${path}.${key}`
178
- : `${path}[${JSON.stringify(key)}]`;
179
- return hasErrorsAtOrUnder(propPath, errorsByPath);
180
- });
181
-
182
- const allKeys: string[] = [
183
- ...definedEntries.map(([key]) => key),
184
- ...undefinedKeysWithErrors,
185
- ...missingKeys,
186
- ];
187
-
188
- if (allKeys.length === 0) {
189
- return `${indent}{}${errorComment}`;
190
- }
191
-
192
- const lines: string[] = [];
193
- lines.push(`${indent}{${errorComment}`);
194
-
195
- allKeys.forEach((key, index, array) => {
196
- const propPath: string = NamingConvention.variable(key)
197
- ? `${path}.${key}`
198
- : `${path}[${JSON.stringify(key)}]`;
199
- const propIndent: string = " ".repeat(tab + 1);
200
-
201
- // Get the value (undefined for missing properties or undefined entries)
202
- const val: unknown =
203
- missingKeys.includes(key) || undefinedKeysWithErrors.includes(key)
204
- ? undefined
205
- : // biome-ignore lint: intended
206
- (value as any)[key];
207
-
208
- // Primitive property value (including undefined for missing properties)
209
- if (
210
- val === undefined ||
211
- val === null ||
212
- typeof val === "boolean" ||
213
- typeof val === "number" ||
214
- typeof val === "string"
215
- ) {
216
- const propErrorComment: string = getErrorComment(
217
- propPath,
218
- errorsByPath,
219
- usedErrors,
220
- );
221
- const keyStr: string = JSON.stringify(key);
222
- const valueStr: string =
223
- val === undefined
224
- ? `${propIndent}${keyStr}: undefined`
225
- : `${propIndent}${keyStr}: ${JSON.stringify(val)}`;
226
- const withComma: string =
227
- index < array.length - 1 ? `${valueStr},` : valueStr;
228
- const line: string = withComma + propErrorComment;
229
- lines.push(line);
230
- }
231
- // Complex property value (object or array)
232
- else {
233
- const keyLine: string = `${propIndent}${JSON.stringify(key)}: `;
234
- let valStr: string = stringify({
235
- value: val,
236
- errorsByPath,
237
- path: propPath,
238
- tab: tab + 1,
239
- inArray: false,
240
- inToJson: false,
241
- usedErrors,
242
- });
243
- const valStrWithoutIndent: string = valStr.trimStart();
244
- // Add comma before the error comment if not the last property
245
- if (index < array.length - 1) {
246
- valStr = insertCommaBeforeComment(valStrWithoutIndent);
247
- } else {
248
- valStr = valStrWithoutIndent;
249
- }
250
- const combined: string = keyLine + valStr;
251
- lines.push(combined);
252
- }
253
- });
254
-
255
- lines.push(`${indent}}`);
256
- return lines.join("\n");
257
- }
258
-
259
- // Primitive types (null, boolean, number, string, undefined, etc.)
260
- const valStr: string =
261
- value === undefined
262
- ? "undefined"
263
- : (JSON.stringify(value) ?? String(value));
264
- return `${indent}${valStr}${errorComment}`;
265
- }
266
-
267
- /** Insert comma before inline error comment on the last line */
268
- function insertCommaBeforeComment(str: string): string {
269
- const lines: string[] = str.split("\n");
270
- const lastLine: string = lines[lines.length - 1]!;
271
- // Use specific error marker to avoid false positives with values containing " //"
272
- const commentIndex: number = lastLine.indexOf(" // ❌");
273
- if (commentIndex !== -1) {
274
- lines[lines.length - 1] = `${lastLine.slice(
275
- 0,
276
- commentIndex,
277
- )},${lastLine.slice(commentIndex)}`;
278
- } else {
279
- lines[lines.length - 1] += ",";
280
- }
281
- return lines.join("\n");
282
- }
283
-
284
- /** Get error comment for a given path */
285
- function getErrorComment(
286
- path: string,
287
- errorsByPath: Map<string, IValidation.IError[]>,
288
- usedErrors: Set<IValidation.IError>,
289
- ): string {
290
- const pathErrors: IValidation.IError[] | undefined = errorsByPath.get(path);
291
- if (pathErrors === undefined || pathErrors.length === 0) {
292
- return "";
293
- }
294
-
295
- // Mark these errors as used
296
- pathErrors.forEach((e) => usedErrors.add(e));
297
-
298
- return ` // ❌ ${JSON.stringify(
299
- pathErrors.map((e) => ({
300
- path: e.path,
301
- expected: e.expected,
302
- description: e.description,
303
- })),
304
- )}`;
305
- }
306
-
307
- /**
308
- * Check if there are missing array element errors (path ending with []) Returns
309
- * an array of error objects, one per missing element
310
- */
311
- function getMissingArrayElementErrors(
312
- path: string,
313
- errorsByPath: Map<string, IValidation.IError[]>,
314
- usedErrors: Set<IValidation.IError>,
315
- ): IValidation.IError[] {
316
- const wildcardPath = `${path}[]`;
317
- const missingErrors: IValidation.IError[] =
318
- errorsByPath.get(wildcardPath) ?? [];
319
-
320
- // Mark these errors as used
321
- missingErrors.forEach((e) => usedErrors.add(e));
322
-
323
- return missingErrors;
324
- }
325
-
326
- /** Check if any errors exist at or under the given path prefix */
327
- function hasErrorsAtOrUnder(
328
- pathPrefix: string,
329
- errorsByPath: Map<string, IValidation.IError[]>,
330
- ): boolean {
331
- for (const errorPath of errorsByPath.keys()) {
332
- if (errorPath.startsWith(pathPrefix)) {
333
- return true;
334
- }
335
- }
336
- return false;
337
- }
338
-
339
- /**
340
- * Find missing properties that have validation errors but don't exist in the
341
- * data Returns array of property keys that should be displayed as undefined
342
- */
343
- function getMissingProperties(
344
- path: string,
345
- value: object,
346
- errorsByPath: Map<string, IValidation.IError[]>,
347
- ): string[] {
348
- const missingKeys: Set<string> = new Set();
349
-
350
- for (const errorPath of errorsByPath.keys()) {
351
- // Check if error.path is a direct child of current path
352
- const childKey = extractDirectChildKey(path, errorPath);
353
- if (childKey !== null) {
354
- // Check if this property actually exists in the value
355
- if (!(childKey in value)) {
356
- missingKeys.add(childKey);
357
- }
358
- }
359
- }
360
-
361
- return Array.from(missingKeys);
362
- }
363
-
364
- /**
365
- * Extract direct child property key if errorPath is a direct child of
366
- * parentPath Returns null if not a direct child
367
- *
368
- * Examples:
369
- *
370
- * - ExtractDirectChildKey("$input", "$input.email") => "email"
371
- * - ExtractDirectChildKey("$input", "$input.user.email") => null (grandchild)
372
- * - ExtractDirectChildKey("$input.user", "$input.user.email") => "email"
373
- * - ExtractDirectChildKey("$input", "$input[0]") => null (array index, not object
374
- * property)
375
- * - ExtractDirectChildKey("$input", "$input["foo-bar"]") => "foo-bar"
376
- * - ExtractDirectChildKey("$input", "$input["foo"]["bar"]") => null (grandchild)
377
- */
378
- function extractDirectChildKey(
379
- parentPath: string,
380
- errorPath: string,
381
- ): string | null {
382
- if (!errorPath.startsWith(parentPath)) {
383
- return null;
384
- }
385
-
386
- const suffix = errorPath.slice(parentPath.length);
387
-
388
- // Match ".propertyName" pattern (direct child property with dot notation)
389
- // Should not contain additional dots or brackets after the property name
390
- const dotMatch = suffix.match(/^\.([^.[\]]+)$/);
391
- if (dotMatch !== null) {
392
- return dotMatch[1]!;
393
- }
394
-
395
- // Match '["key"]' pattern (direct child property with bracket notation)
396
- // The key is a JSON-encoded string
397
- const bracketMatch = suffix.match(/^\[("[^"\\]*(?:\\.[^"\\]*)*")\]$/);
398
- if (bracketMatch !== null) {
399
- try {
400
- const parsed = JSON.parse(bracketMatch[1]!);
401
- // Ensure it's a string key, not a number (array index)
402
- if (typeof parsed === "string") {
403
- return parsed;
404
- }
405
- } catch {
406
- // Invalid JSON, ignore
407
- }
408
- }
409
-
410
- return null;
411
- }
1
+ import { IValidation } from "@typia/interface";
2
+
3
+ import { NamingConvention } from "../NamingConvention";
4
+ import { dedent } from "../dedent";
5
+
6
+ export function stringifyValidationFailure(
7
+ failure: IValidation.IFailure,
8
+ ): string {
9
+ const usedErrors: Set<IValidation.IError> = new Set();
10
+ // Pre-index errors by path for O(1) lookup
11
+ const errorsByPath: Map<string, IValidation.IError[]> = new Map();
12
+ for (const e of failure.errors) {
13
+ const arr: IValidation.IError[] | undefined = errorsByPath.get(e.path);
14
+ if (arr !== undefined) arr.push(e);
15
+ else errorsByPath.set(e.path, [e]);
16
+ }
17
+ const jsonOutput = stringify({
18
+ value: failure.data,
19
+ errorsByPath,
20
+ path: "$input",
21
+ tab: 0,
22
+ inArray: false,
23
+ inToJson: false,
24
+ usedErrors,
25
+ });
26
+
27
+ // Find errors that couldn't be embedded
28
+ const unmappableErrors: IValidation.IError[] = failure.errors.filter(
29
+ (e) => !usedErrors.has(e),
30
+ );
31
+
32
+ // If there are unmappable errors, append them as a separate block
33
+ if (unmappableErrors.length > 0)
34
+ return dedent`
35
+ \`\`\`json
36
+ ${jsonOutput}
37
+ \`\`\`
38
+
39
+ **Unmappable validation errors:**
40
+ \`\`\`json
41
+ ${JSON.stringify(unmappableErrors, null, 2)}
42
+ \`\`\`
43
+ `;
44
+ return dedent`
45
+ \`\`\`json
46
+ ${jsonOutput}
47
+ \`\`\`
48
+ `;
49
+ }
50
+
51
+ function stringify(props: {
52
+ value: unknown;
53
+ errorsByPath: Map<string, IValidation.IError[]>;
54
+ path: string;
55
+ tab: number;
56
+ inArray: boolean;
57
+ inToJson: boolean;
58
+ usedErrors: Set<IValidation.IError>;
59
+ }): string {
60
+ const { value, errorsByPath, path, tab, inArray, inToJson, usedErrors } =
61
+ props;
62
+ const indent: string = " ".repeat(tab);
63
+ const errorComment: string = getErrorComment(path, errorsByPath, usedErrors);
64
+
65
+ // Handle undefined in arrays
66
+ if (inArray && value === undefined) {
67
+ return `${indent}undefined${errorComment}`;
68
+ }
69
+
70
+ // Array
71
+ if (Array.isArray(value)) {
72
+ // Check for missing array element errors (path[])
73
+ const missingElementErrors = getMissingArrayElementErrors(
74
+ path,
75
+ errorsByPath,
76
+ usedErrors,
77
+ );
78
+ const hasMissingElements = missingElementErrors.length > 0;
79
+
80
+ if (value.length === 0) {
81
+ // Empty array but has missing element errors - show placeholders
82
+ if (hasMissingElements) {
83
+ const innerIndent = " ".repeat(tab + 1);
84
+ const lines: string[] = [];
85
+ lines.push(`${indent}[${errorComment}`);
86
+ missingElementErrors.forEach((e, idx) => {
87
+ const errComment = ` // ❌ ${JSON.stringify([{ path: e.path, expected: e.expected, description: e.description }])}`;
88
+ const comma = idx < missingElementErrors.length - 1 ? "," : "";
89
+ lines.push(`${innerIndent}undefined${comma}${errComment}`);
90
+ });
91
+ lines.push(`${indent}]`);
92
+ return lines.join("\n");
93
+ }
94
+ return `${indent}[]${errorComment}`;
95
+ }
96
+
97
+ const lines: string[] = [];
98
+ lines.push(`${indent}[${errorComment}`);
99
+
100
+ value.forEach((item: unknown, index: number) => {
101
+ const itemPath: string = `${path}[${index}]`;
102
+ const isLastElement = index === value.length - 1;
103
+ // If there are missing element errors, this is not truly the last line
104
+ const needsComma = !isLastElement || hasMissingElements;
105
+
106
+ let itemStr: string = stringify({
107
+ value: item,
108
+ errorsByPath,
109
+ path: itemPath,
110
+ tab: tab + 1,
111
+ inArray: true,
112
+ inToJson: false,
113
+ usedErrors,
114
+ });
115
+ // Add comma before the error comment if not the last element
116
+ if (needsComma) {
117
+ itemStr = insertCommaBeforeComment(itemStr);
118
+ }
119
+ lines.push(itemStr);
120
+ });
121
+
122
+ // Add missing element placeholders at the end for each [] error
123
+ if (hasMissingElements) {
124
+ const innerIndent = " ".repeat(tab + 1);
125
+ missingElementErrors.forEach((e, idx) => {
126
+ const errComment = ` // ❌ ${JSON.stringify([{ path: e.path, expected: e.expected, description: e.description }])}`;
127
+ const comma = idx < missingElementErrors.length - 1 ? "," : "";
128
+ lines.push(`${innerIndent}undefined${comma}${errComment}`);
129
+ });
130
+ }
131
+
132
+ lines.push(`${indent}]`);
133
+ return lines.join("\n");
134
+ }
135
+
136
+ // Object
137
+ if (typeof value === "object" && value !== null) {
138
+ // Check for toJSON method
139
+ // biome-ignore lint: intended
140
+ if (!inToJson && typeof (value as any).toJSON === "function") {
141
+ // biome-ignore lint: intended
142
+ const jsonValue: unknown = (value as any).toJSON();
143
+ return stringify({
144
+ value: jsonValue,
145
+ errorsByPath,
146
+ path,
147
+ tab,
148
+ inArray,
149
+ inToJson: true,
150
+ usedErrors,
151
+ });
152
+ }
153
+
154
+ // Get all entries from the object (including undefined values that have errors)
155
+ const allEntries: [string, unknown][] = Object.entries(value);
156
+
157
+ // Split into defined and undefined entries
158
+ const definedEntries: [string, unknown][] = allEntries.filter(
159
+ ([_, val]) => val !== undefined,
160
+ );
161
+ const undefinedEntryKeys: Set<string> = new Set(
162
+ allEntries.filter(([_, val]) => val === undefined).map(([key]) => key),
163
+ );
164
+
165
+ // Find missing properties that have validation errors (not in object at all)
166
+ const missingKeys: string[] = getMissingProperties(
167
+ path,
168
+ value,
169
+ errorsByPath,
170
+ );
171
+
172
+ // Combine: defined entries + undefined entries with errors + missing properties
173
+ const undefinedKeysWithErrors: string[] = Array.from(
174
+ undefinedEntryKeys,
175
+ ).filter((key) => {
176
+ const propPath = NamingConvention.variable(key)
177
+ ? `${path}.${key}`
178
+ : `${path}[${JSON.stringify(key)}]`;
179
+ return hasErrorsAtOrUnder(propPath, errorsByPath);
180
+ });
181
+
182
+ const allKeys: string[] = [
183
+ ...definedEntries.map(([key]) => key),
184
+ ...undefinedKeysWithErrors,
185
+ ...missingKeys,
186
+ ];
187
+
188
+ if (allKeys.length === 0) {
189
+ return `${indent}{}${errorComment}`;
190
+ }
191
+
192
+ const lines: string[] = [];
193
+ lines.push(`${indent}{${errorComment}`);
194
+
195
+ allKeys.forEach((key, index, array) => {
196
+ const propPath: string = NamingConvention.variable(key)
197
+ ? `${path}.${key}`
198
+ : `${path}[${JSON.stringify(key)}]`;
199
+ const propIndent: string = " ".repeat(tab + 1);
200
+
201
+ // Get the value (undefined for missing properties or undefined entries)
202
+ const val: unknown =
203
+ missingKeys.includes(key) || undefinedKeysWithErrors.includes(key)
204
+ ? undefined
205
+ : // biome-ignore lint: intended
206
+ (value as any)[key];
207
+
208
+ // Primitive property value (including undefined for missing properties)
209
+ if (
210
+ val === undefined ||
211
+ val === null ||
212
+ typeof val === "boolean" ||
213
+ typeof val === "number" ||
214
+ typeof val === "string"
215
+ ) {
216
+ const propErrorComment: string = getErrorComment(
217
+ propPath,
218
+ errorsByPath,
219
+ usedErrors,
220
+ );
221
+ const keyStr: string = JSON.stringify(key);
222
+ const valueStr: string =
223
+ val === undefined
224
+ ? `${propIndent}${keyStr}: undefined`
225
+ : `${propIndent}${keyStr}: ${JSON.stringify(val)}`;
226
+ const withComma: string =
227
+ index < array.length - 1 ? `${valueStr},` : valueStr;
228
+ const line: string = withComma + propErrorComment;
229
+ lines.push(line);
230
+ }
231
+ // Complex property value (object or array)
232
+ else {
233
+ const keyLine: string = `${propIndent}${JSON.stringify(key)}: `;
234
+ let valStr: string = stringify({
235
+ value: val,
236
+ errorsByPath,
237
+ path: propPath,
238
+ tab: tab + 1,
239
+ inArray: false,
240
+ inToJson: false,
241
+ usedErrors,
242
+ });
243
+ const valStrWithoutIndent: string = valStr.trimStart();
244
+ // Add comma before the error comment if not the last property
245
+ if (index < array.length - 1) {
246
+ valStr = insertCommaBeforeComment(valStrWithoutIndent);
247
+ } else {
248
+ valStr = valStrWithoutIndent;
249
+ }
250
+ const combined: string = keyLine + valStr;
251
+ lines.push(combined);
252
+ }
253
+ });
254
+
255
+ lines.push(`${indent}}`);
256
+ return lines.join("\n");
257
+ }
258
+
259
+ // Primitive types (null, boolean, number, string, undefined, etc.)
260
+ const valStr: string =
261
+ value === undefined
262
+ ? "undefined"
263
+ : (JSON.stringify(value) ?? String(value));
264
+ return `${indent}${valStr}${errorComment}`;
265
+ }
266
+
267
+ /** Insert comma before inline error comment on the last line */
268
+ function insertCommaBeforeComment(str: string): string {
269
+ const lines: string[] = str.split("\n");
270
+ const lastLine: string = lines[lines.length - 1]!;
271
+ // Use specific error marker to avoid false positives with values containing " //"
272
+ const commentIndex: number = lastLine.lastIndexOf(" // ❌");
273
+ if (commentIndex !== -1) {
274
+ lines[lines.length - 1] = `${lastLine.slice(
275
+ 0,
276
+ commentIndex,
277
+ )},${lastLine.slice(commentIndex)}`;
278
+ } else {
279
+ lines[lines.length - 1] += ",";
280
+ }
281
+ return lines.join("\n");
282
+ }
283
+
284
+ /** Get error comment for a given path */
285
+ function getErrorComment(
286
+ path: string,
287
+ errorsByPath: Map<string, IValidation.IError[]>,
288
+ usedErrors: Set<IValidation.IError>,
289
+ ): string {
290
+ const pathErrors: IValidation.IError[] | undefined = errorsByPath.get(path);
291
+ if (pathErrors === undefined || pathErrors.length === 0) {
292
+ return "";
293
+ }
294
+
295
+ // Mark these errors as used
296
+ pathErrors.forEach((e) => usedErrors.add(e));
297
+
298
+ return ` // ❌ ${JSON.stringify(
299
+ pathErrors.map((e) => ({
300
+ path: e.path,
301
+ expected: e.expected,
302
+ description: e.description,
303
+ })),
304
+ )}`;
305
+ }
306
+
307
+ /**
308
+ * Check if there are missing array element errors (path ending with []) Returns
309
+ * an array of error objects, one per missing element
310
+ */
311
+ function getMissingArrayElementErrors(
312
+ path: string,
313
+ errorsByPath: Map<string, IValidation.IError[]>,
314
+ usedErrors: Set<IValidation.IError>,
315
+ ): IValidation.IError[] {
316
+ const wildcardPath = `${path}[]`;
317
+ const missingErrors: IValidation.IError[] =
318
+ errorsByPath.get(wildcardPath) ?? [];
319
+
320
+ // Mark these errors as used
321
+ missingErrors.forEach((e) => usedErrors.add(e));
322
+
323
+ return missingErrors;
324
+ }
325
+
326
+ /** Check if any errors exist at or under the given path prefix */
327
+ function hasErrorsAtOrUnder(
328
+ pathPrefix: string,
329
+ errorsByPath: Map<string, IValidation.IError[]>,
330
+ ): boolean {
331
+ for (const errorPath of errorsByPath.keys()) {
332
+ if (
333
+ errorPath === pathPrefix ||
334
+ errorPath.startsWith(pathPrefix + ".") ||
335
+ errorPath.startsWith(pathPrefix + "[")
336
+ ) {
337
+ return true;
338
+ }
339
+ }
340
+ return false;
341
+ }
342
+
343
+ /**
344
+ * Find missing properties that have validation errors but don't exist in the
345
+ * data Returns array of property keys that should be displayed as undefined
346
+ */
347
+ function getMissingProperties(
348
+ path: string,
349
+ value: object,
350
+ errorsByPath: Map<string, IValidation.IError[]>,
351
+ ): string[] {
352
+ const missingKeys: Set<string> = new Set();
353
+
354
+ for (const errorPath of errorsByPath.keys()) {
355
+ // Check if error.path is a direct child of current path
356
+ const childKey = extractDirectChildKey(path, errorPath);
357
+ if (childKey !== null) {
358
+ // Check if this property actually exists in the value
359
+ if (!(childKey in value)) {
360
+ missingKeys.add(childKey);
361
+ }
362
+ }
363
+ }
364
+
365
+ return Array.from(missingKeys);
366
+ }
367
+
368
+ /**
369
+ * Extract direct child property key if errorPath is a direct child of
370
+ * parentPath Returns null if not a direct child
371
+ *
372
+ * Examples:
373
+ *
374
+ * - ExtractDirectChildKey("$input", "$input.email") => "email"
375
+ * - ExtractDirectChildKey("$input", "$input.user.email") => null (grandchild)
376
+ * - ExtractDirectChildKey("$input.user", "$input.user.email") => "email"
377
+ * - ExtractDirectChildKey("$input", "$input[0]") => null (array index, not object
378
+ * property)
379
+ * - ExtractDirectChildKey("$input", "$input["foo-bar"]") => "foo-bar"
380
+ * - ExtractDirectChildKey("$input", "$input["foo"]["bar"]") => null (grandchild)
381
+ */
382
+ function extractDirectChildKey(
383
+ parentPath: string,
384
+ errorPath: string,
385
+ ): string | null {
386
+ if (!errorPath.startsWith(parentPath)) {
387
+ return null;
388
+ }
389
+
390
+ const suffix = errorPath.slice(parentPath.length);
391
+
392
+ // Match ".propertyName" pattern (direct child property with dot notation)
393
+ // Should not contain additional dots or brackets after the property name
394
+ const dotMatch = suffix.match(/^\.([^.[\]]+)$/);
395
+ if (dotMatch !== null) {
396
+ return dotMatch[1]!;
397
+ }
398
+
399
+ // Match '["key"]' pattern (direct child property with bracket notation)
400
+ // The key is a JSON-encoded string
401
+ const bracketMatch = suffix.match(/^\[("[^"\\]*(?:\\.[^"\\]*)*")\]$/);
402
+ if (bracketMatch !== null) {
403
+ try {
404
+ const parsed = JSON.parse(bracketMatch[1]!);
405
+ // Ensure it's a string key, not a number (array index)
406
+ if (typeof parsed === "string") {
407
+ return parsed;
408
+ }
409
+ } catch {
410
+ // Invalid JSON, ignore
411
+ }
412
+ }
413
+
414
+ return null;
415
+ }