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

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 (103) hide show
  1. package/lib/converters/LlmSchemaConverter.d.ts +0 -1
  2. package/lib/converters/LlmSchemaConverter.js +4 -31
  3. package/lib/converters/LlmSchemaConverter.js.map +1 -1
  4. package/lib/converters/LlmSchemaConverter.mjs +2 -32
  5. package/lib/converters/LlmSchemaConverter.mjs.map +1 -1
  6. package/lib/http/HttpLlm.js +4 -5
  7. package/lib/http/HttpLlm.js.map +1 -1
  8. package/lib/http/HttpLlm.mjs +0 -1
  9. package/lib/http/HttpLlm.mjs.map +1 -1
  10. package/lib/http/internal/HttpLlmApplicationComposer.js +3 -4
  11. package/lib/http/internal/HttpLlmApplicationComposer.js.map +1 -1
  12. package/lib/http/internal/HttpLlmApplicationComposer.mjs +5 -2
  13. package/lib/http/internal/HttpLlmApplicationComposer.mjs.map +1 -1
  14. package/lib/index.mjs +9 -9
  15. package/lib/utils/LlmJson.mjs +9 -2
  16. package/lib/utils/LlmJson.mjs.map +1 -1
  17. package/lib/utils/internal/stringifyValidationFailure.js +17 -15
  18. package/lib/utils/internal/stringifyValidationFailure.js.map +1 -1
  19. package/lib/utils/internal/stringifyValidationFailure.mjs +17 -15
  20. package/lib/utils/internal/stringifyValidationFailure.mjs.map +1 -1
  21. package/lib/validators/internal/OpenApiOneOfValidator.mjs +5 -1
  22. package/lib/validators/internal/OpenApiOneOfValidator.mjs.map +1 -1
  23. package/package.json +2 -2
  24. package/src/converters/LlmSchemaConverter.ts +617 -647
  25. package/src/converters/OpenApiConverter.ts +285 -285
  26. package/src/converters/index.ts +5 -5
  27. package/src/converters/internal/LlmDescriptionInverter.ts +178 -178
  28. package/src/converters/internal/LlmParametersComposer.ts +52 -52
  29. package/src/converters/internal/OpenApiConstraintShifter.ts +154 -154
  30. package/src/converters/internal/OpenApiExclusiveEmender.ts +46 -46
  31. package/src/converters/internal/OpenApiV3Downgrader.ts +355 -355
  32. package/src/converters/internal/OpenApiV3Upgrader.ts +470 -470
  33. package/src/converters/internal/OpenApiV3_1Upgrader.ts +685 -685
  34. package/src/converters/internal/SwaggerV2Downgrader.ts +424 -424
  35. package/src/converters/internal/SwaggerV2Upgrader.ts +523 -523
  36. package/src/http/HttpError.ts +107 -107
  37. package/src/http/HttpLlm.ts +166 -167
  38. package/src/http/HttpMigration.ts +92 -92
  39. package/src/http/index.ts +3 -3
  40. package/src/http/internal/HttpLlmApplicationComposer.ts +360 -361
  41. package/src/http/internal/HttpLlmFunctionFetcher.ts +37 -37
  42. package/src/http/internal/HttpMigrateApplicationComposer.ts +56 -56
  43. package/src/http/internal/HttpMigrateRouteAccessor.ts +135 -135
  44. package/src/http/internal/HttpMigrateRouteComposer.ts +505 -505
  45. package/src/http/internal/HttpMigrateRouteFetcher.ts +203 -203
  46. package/src/index.ts +4 -4
  47. package/src/utils/ArrayUtil.ts +42 -42
  48. package/src/utils/LlmJson.ts +141 -141
  49. package/src/utils/MapUtil.ts +15 -15
  50. package/src/utils/NamingConvention.ts +205 -205
  51. package/src/utils/Singleton.ts +17 -17
  52. package/src/utils/StringUtil.ts +14 -14
  53. package/src/utils/dedent.ts +57 -57
  54. package/src/utils/index.ts +8 -8
  55. package/src/utils/internal/EndpointUtil.ts +44 -44
  56. package/src/utils/internal/JsonDescriptor.ts +70 -70
  57. package/src/utils/internal/OpenApiTypeCheckerBase.ts +822 -822
  58. package/src/utils/internal/coerceLlmArguments.ts +314 -314
  59. package/src/utils/internal/parseLenientJson.ts +894 -894
  60. package/src/utils/internal/stringifyValidationFailure.ts +415 -411
  61. package/src/validators/LlmTypeChecker.ts +402 -402
  62. package/src/validators/OpenApiTypeChecker.ts +297 -297
  63. package/src/validators/OpenApiV3TypeChecker.ts +70 -70
  64. package/src/validators/OpenApiV3_1TypeChecker.ts +86 -86
  65. package/src/validators/OpenApiValidator.ts +94 -94
  66. package/src/validators/SwaggerV2TypeChecker.ts +71 -71
  67. package/src/validators/functional/_isBigintString.ts +8 -8
  68. package/src/validators/functional/_isFormatByte.ts +7 -7
  69. package/src/validators/functional/_isFormatDate.ts +3 -3
  70. package/src/validators/functional/_isFormatDateTime.ts +4 -4
  71. package/src/validators/functional/_isFormatDuration.ts +4 -4
  72. package/src/validators/functional/_isFormatEmail.ts +4 -4
  73. package/src/validators/functional/_isFormatHostname.ts +4 -4
  74. package/src/validators/functional/_isFormatIdnEmail.ts +4 -4
  75. package/src/validators/functional/_isFormatIdnHostname.ts +4 -4
  76. package/src/validators/functional/_isFormatIpv4.ts +4 -4
  77. package/src/validators/functional/_isFormatIpv6.ts +4 -4
  78. package/src/validators/functional/_isFormatIri.ts +3 -3
  79. package/src/validators/functional/_isFormatIriReference.ts +4 -4
  80. package/src/validators/functional/_isFormatJsonPointer.ts +3 -3
  81. package/src/validators/functional/_isFormatPassword.ts +1 -1
  82. package/src/validators/functional/_isFormatRegex.ts +8 -8
  83. package/src/validators/functional/_isFormatRelativeJsonPointer.ts +4 -4
  84. package/src/validators/functional/_isFormatTime.ts +4 -4
  85. package/src/validators/functional/_isFormatUri.ts +6 -6
  86. package/src/validators/functional/_isFormatUriReference.ts +5 -5
  87. package/src/validators/functional/_isFormatUriTemplate.ts +4 -4
  88. package/src/validators/functional/_isFormatUrl.ts +4 -4
  89. package/src/validators/functional/_isFormatUuid.ts +3 -3
  90. package/src/validators/functional/_isUniqueItems.ts +159 -159
  91. package/src/validators/index.ts +14 -14
  92. package/src/validators/internal/IOpenApiValidatorContext.ts +17 -17
  93. package/src/validators/internal/OpenApiArrayValidator.ts +49 -49
  94. package/src/validators/internal/OpenApiBooleanValidator.ts +11 -11
  95. package/src/validators/internal/OpenApiConstantValidator.ts +11 -11
  96. package/src/validators/internal/OpenApiIntegerValidator.ts +49 -49
  97. package/src/validators/internal/OpenApiNumberValidator.ts +48 -48
  98. package/src/validators/internal/OpenApiObjectValidator.ts +83 -83
  99. package/src/validators/internal/OpenApiOneOfValidator.ts +309 -309
  100. package/src/validators/internal/OpenApiSchemaNamingRule.ts +124 -124
  101. package/src/validators/internal/OpenApiStationValidator.ts +115 -115
  102. package/src/validators/internal/OpenApiStringValidator.ts +88 -88
  103. 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
+ }