@shepherdjerred/helm-types 0.0.0-dev.706

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.
@@ -0,0 +1,509 @@
1
+ import type {
2
+ JSONSchemaProperty,
3
+ TypeScriptInterface,
4
+ TypeProperty,
5
+ } from "./types.ts";
6
+ import type { HelmValue } from "./schemas.ts";
7
+ import {
8
+ StringSchema,
9
+ NullSchema,
10
+ UndefinedSchema,
11
+ ArraySchema,
12
+ HelmValueSchema,
13
+ ActualBooleanSchema,
14
+ ActualNumberSchema,
15
+ StringBooleanSchema,
16
+ } from "./schemas.ts";
17
+ import { shouldAllowArbitraryProps, isK8sResourceSpec } from "./config.ts";
18
+ import {
19
+ sanitizePropertyName,
20
+ sanitizeTypeName,
21
+ capitalizeFirst,
22
+ } from "./utils.ts";
23
+ import type { PropertyConversionContext } from "./type-converter-helpers.ts";
24
+ import {
25
+ mergeDescriptions,
26
+ inferPrimitiveType,
27
+ augmentK8sResourceSpec,
28
+ } from "./type-converter-helpers.ts";
29
+
30
+ /**
31
+ * Convert JSON schema type to TypeScript type string
32
+ */
33
+ export function jsonSchemaToTypeScript(schema: JSONSchemaProperty): string {
34
+ // Handle oneOf - union of types
35
+ if (schema.oneOf) {
36
+ const types = schema.oneOf.map((s) => jsonSchemaToTypeScript(s));
37
+ return types.join(" | ");
38
+ }
39
+
40
+ // Handle anyOf - union of types
41
+ if (schema.anyOf) {
42
+ const types = schema.anyOf.map((s) => jsonSchemaToTypeScript(s));
43
+ return types.join(" | ");
44
+ }
45
+
46
+ // Handle enum
47
+ if (schema.enum) {
48
+ return schema.enum
49
+ .map((v) =>
50
+ StringSchema.safeParse(v).success ? `"${String(v)}"` : String(v),
51
+ )
52
+ .join(" | ");
53
+ }
54
+
55
+ // Handle array type
56
+ if (schema.type === "array" && schema.items) {
57
+ const itemType = jsonSchemaToTypeScript(schema.items);
58
+ return `${itemType}[]`;
59
+ }
60
+
61
+ // Handle basic types
62
+ const stringTypeCheck = StringSchema.safeParse(schema.type);
63
+ if (stringTypeCheck.success) {
64
+ switch (stringTypeCheck.data) {
65
+ case "string":
66
+ return "string";
67
+ case "number":
68
+ case "integer":
69
+ return "number";
70
+ case "boolean":
71
+ return "boolean";
72
+ case "object":
73
+ return "object";
74
+ case "array":
75
+ return "unknown[]";
76
+ case "null":
77
+ return "null";
78
+ default:
79
+ return "unknown";
80
+ }
81
+ }
82
+
83
+ // Handle multiple types
84
+ const arrayTypeCheck = ArraySchema.safeParse(schema.type);
85
+ if (arrayTypeCheck.success) {
86
+ return arrayTypeCheck.data
87
+ .map((t: unknown) => {
88
+ if (!StringSchema.safeParse(t).success) {
89
+ return "unknown";
90
+ }
91
+ const typeStr = String(t);
92
+ switch (typeStr) {
93
+ case "string":
94
+ return "string";
95
+ case "number":
96
+ case "integer":
97
+ return "number";
98
+ case "boolean":
99
+ return "boolean";
100
+ case "object":
101
+ return "object";
102
+ case "array":
103
+ return "unknown[]";
104
+ case "null":
105
+ return "null";
106
+ default:
107
+ return "unknown";
108
+ }
109
+ })
110
+ .join(" | ");
111
+ }
112
+
113
+ return "unknown";
114
+ }
115
+
116
+ /**
117
+ * Infer TypeScript type from actual runtime value
118
+ */
119
+ export function inferTypeFromValue(value: unknown): string | null {
120
+ // Check null/undefined
121
+ if (
122
+ NullSchema.safeParse(value).success ||
123
+ UndefinedSchema.safeParse(value).success
124
+ ) {
125
+ return null;
126
+ }
127
+
128
+ // Check for actual boolean
129
+ if (ActualBooleanSchema.safeParse(value).success) {
130
+ return "boolean";
131
+ }
132
+
133
+ // Check for actual number
134
+ if (ActualNumberSchema.safeParse(value).success) {
135
+ return "number";
136
+ }
137
+
138
+ // Check if it's a string that looks like a boolean
139
+ if (StringBooleanSchema.safeParse(value).success) {
140
+ return "boolean";
141
+ }
142
+
143
+ // Check if it's a string that looks like a number
144
+ const stringCheck = StringSchema.safeParse(value);
145
+ if (stringCheck.success) {
146
+ const trimmed = stringCheck.data.trim();
147
+ if (
148
+ trimmed !== "" &&
149
+ !Number.isNaN(Number(trimmed)) &&
150
+ Number.isFinite(Number(trimmed))
151
+ ) {
152
+ return "number";
153
+ }
154
+ }
155
+
156
+ // Check for array
157
+ if (ArraySchema.safeParse(value).success) {
158
+ return "array";
159
+ }
160
+
161
+ // Check for object
162
+ if (HelmValueSchema.safeParse(value).success) {
163
+ return "object";
164
+ }
165
+
166
+ // Plain string
167
+ if (StringSchema.safeParse(value).success) {
168
+ return "string";
169
+ }
170
+
171
+ return "unknown";
172
+ }
173
+
174
+ /**
175
+ * Check if inferred type is compatible with schema type
176
+ */
177
+ export function typesAreCompatible(
178
+ inferredType: string,
179
+ schemaType: string,
180
+ ): boolean {
181
+ // Exact match
182
+ if (inferredType === schemaType) {
183
+ return true;
184
+ }
185
+
186
+ // Check if the inferred type is part of a union in the schema
187
+ // For example: schemaType might be "number | \"default\"" and inferredType is "string"
188
+ const schemaTypes = schemaType
189
+ .split("|")
190
+ .map((t) => t.trim().replaceAll(/^["']|["']$/g, ""));
191
+
192
+ // If schema is a union, check if inferred type is compatible with any part
193
+ if (schemaTypes.length > 1) {
194
+ for (const st of schemaTypes) {
195
+ // Handle quoted strings in unions (like "default")
196
+ if (st.startsWith('"') && st.endsWith('"') && inferredType === "string") {
197
+ return true;
198
+ }
199
+ if (st === inferredType) {
200
+ return true;
201
+ }
202
+ // Arrays
203
+ if (st.endsWith("[]") && inferredType === "array") {
204
+ return true;
205
+ }
206
+ }
207
+ }
208
+
209
+ // Handle array types
210
+ if (schemaType.endsWith("[]") && inferredType === "array") {
211
+ return true;
212
+ }
213
+
214
+ // Handle specific string literals - if schema expects specific strings and value is a string
215
+ if (schemaType.includes('"') && inferredType === "string") {
216
+ return true;
217
+ }
218
+
219
+ // unknown is compatible with everything (schema might be less specific)
220
+ if (schemaType === "unknown" || inferredType === "unknown") {
221
+ return true;
222
+ }
223
+
224
+ return false;
225
+ }
226
+
227
+ /**
228
+ * Convert Helm values to TypeScript interface
229
+ */
230
+ export function convertToTypeScriptInterface(options: {
231
+ values: HelmValue;
232
+ interfaceName: string;
233
+ schema?: JSONSchemaProperty | null;
234
+ yamlComments?: Map<string, string>;
235
+ keyPrefix?: string;
236
+ chartName?: string;
237
+ }): TypeScriptInterface {
238
+ const keyPrefix = options.keyPrefix ?? "";
239
+ const properties: Record<string, TypeProperty> = {};
240
+ const schemaProps = options.schema?.properties;
241
+
242
+ for (const [key, value] of Object.entries(options.values)) {
243
+ const sanitizedKey = sanitizePropertyName(key);
244
+ const typeNameSuffix = sanitizeTypeName(key);
245
+ const propertySchema = schemaProps?.[key];
246
+ const fullKey = keyPrefix ? `${keyPrefix}.${key}` : key;
247
+ const yamlComment = options.yamlComments?.get(fullKey);
248
+
249
+ properties[sanitizedKey] = convertValueToProperty({
250
+ value,
251
+ nestedTypeName: `${options.interfaceName}${capitalizeFirst(typeNameSuffix)}`,
252
+ schema: propertySchema,
253
+ propertyName: key,
254
+ yamlComment,
255
+ yamlComments: options.yamlComments,
256
+ fullKey,
257
+ chartName: options.chartName,
258
+ });
259
+ }
260
+
261
+ // Check if this interface should allow arbitrary properties
262
+ const allowArbitraryProps =
263
+ options.chartName != null && options.chartName !== ""
264
+ ? shouldAllowArbitraryProps(
265
+ keyPrefix,
266
+ options.chartName,
267
+ keyPrefix.split(".").pop() ?? "",
268
+ options.yamlComments?.get(keyPrefix),
269
+ )
270
+ : false;
271
+
272
+ return {
273
+ name: options.interfaceName,
274
+ properties,
275
+ allowArbitraryProps,
276
+ };
277
+ }
278
+
279
+ /**
280
+ * Convert a value to a TypeProperty using JSON schema information
281
+ */
282
+ function convertWithSchema(
283
+ ctx: PropertyConversionContext & { schema: JSONSchemaProperty },
284
+ ): TypeProperty {
285
+ const {
286
+ value,
287
+ nestedTypeName,
288
+ schema,
289
+ propertyName,
290
+ yamlComment,
291
+ yamlComments,
292
+ fullKey,
293
+ chartName,
294
+ } = ctx;
295
+
296
+ // Infer the type from the actual value for comparison
297
+ const inferredType = inferTypeFromValue(value);
298
+ const schemaType = jsonSchemaToTypeScript(schema);
299
+
300
+ // Warn about type mismatches
301
+ if (
302
+ inferredType != null &&
303
+ inferredType !== "" &&
304
+ !typesAreCompatible(inferredType, schemaType)
305
+ ) {
306
+ const propName =
307
+ propertyName != null && propertyName !== "" ? `'${propertyName}': ` : "";
308
+ console.warn(
309
+ ` ⚠️ Type mismatch for ${propName}Schema says '${schemaType}' but value suggests '${inferredType}' (value: ${String(value).slice(0, 50)})`,
310
+ );
311
+ }
312
+
313
+ const description = mergeDescriptions(schema.description, yamlComment);
314
+ const defaultValue = schema.default === undefined ? value : schema.default;
315
+
316
+ // If schema defines it as an object with properties, recurse
317
+ const helmValueCheckForProps = HelmValueSchema.safeParse(value);
318
+ if (schema.properties && helmValueCheckForProps.success) {
319
+ const nestedInterface = convertToTypeScriptInterface({
320
+ values: helmValueCheckForProps.data,
321
+ interfaceName: nestedTypeName,
322
+ schema,
323
+ yamlComments,
324
+ keyPrefix: fullKey,
325
+ chartName,
326
+ });
327
+ return {
328
+ type: nestedTypeName,
329
+ optional: true,
330
+ nested: nestedInterface,
331
+ description,
332
+ default: defaultValue,
333
+ };
334
+ }
335
+
336
+ // Handle object types without explicit properties
337
+ const helmValueCheckForObject = HelmValueSchema.safeParse(value);
338
+ if (schemaType === "object" && helmValueCheckForObject.success) {
339
+ const nestedInterface = convertToTypeScriptInterface({
340
+ values: helmValueCheckForObject.data,
341
+ interfaceName: nestedTypeName,
342
+ yamlComments,
343
+ keyPrefix: fullKey,
344
+ chartName,
345
+ });
346
+ return {
347
+ type: nestedTypeName,
348
+ optional: true,
349
+ nested: nestedInterface,
350
+ description,
351
+ default: defaultValue,
352
+ };
353
+ }
354
+
355
+ return {
356
+ type: schemaType,
357
+ optional: true,
358
+ description,
359
+ default: defaultValue,
360
+ };
361
+ }
362
+
363
+ /**
364
+ * Infer array element type from sampled elements
365
+ */
366
+ function inferArrayType(
367
+ ctx: PropertyConversionContext,
368
+ arrayValue: unknown[],
369
+ ): TypeProperty {
370
+ const { nestedTypeName } = ctx;
371
+
372
+ if (arrayValue.length === 0) {
373
+ return { type: "unknown[]", optional: true };
374
+ }
375
+
376
+ // Sample multiple elements for better type inference
377
+ const elementTypes = new Set<string>();
378
+ const elementTypeProps: TypeProperty[] = [];
379
+ const sampleSize = Math.min(arrayValue.length, 3);
380
+
381
+ for (let i = 0; i < sampleSize; i++) {
382
+ const elementType = convertValueToProperty({
383
+ value: arrayValue[i],
384
+ nestedTypeName,
385
+ });
386
+ elementTypes.add(elementType.type);
387
+ elementTypeProps.push(elementType);
388
+ }
389
+
390
+ // If all elements have the same type, use that
391
+ if (elementTypes.size === 1) {
392
+ return inferUniformArrayType(ctx, elementTypes, elementTypeProps);
393
+ }
394
+
395
+ // If mixed types, use union type for common cases
396
+ const types = [...elementTypes].toSorted();
397
+ if (
398
+ types.length <= 3 &&
399
+ types.every((t) => ["string", "number", "boolean"].includes(t))
400
+ ) {
401
+ return { type: `(${types.join(" | ")})[]`, optional: true };
402
+ }
403
+
404
+ return { type: "unknown[]", optional: true };
405
+ }
406
+
407
+ /**
408
+ * Build TypeProperty for a uniform-type array
409
+ */
410
+ function inferUniformArrayType(
411
+ ctx: PropertyConversionContext,
412
+ elementTypes: Set<string>,
413
+ elementTypeProps: TypeProperty[],
414
+ ): TypeProperty {
415
+ const { nestedTypeName, chartName, fullKey, propertyName, yamlComment } = ctx;
416
+ const elementType = [...elementTypes][0];
417
+ const elementProp = elementTypeProps[0];
418
+ if (elementType == null || elementType === "" || !elementProp) {
419
+ return { type: "unknown[]", optional: true };
420
+ }
421
+
422
+ if (elementProp.nested) {
423
+ const arrayElementTypeName = `${nestedTypeName}Element`;
424
+ const allowArbitraryProps =
425
+ chartName != null && chartName !== "" && fullKey != null && fullKey !== ""
426
+ ? shouldAllowArbitraryProps(
427
+ fullKey,
428
+ chartName,
429
+ propertyName ?? "",
430
+ yamlComment,
431
+ )
432
+ : false;
433
+ const arrayElementInterface: TypeScriptInterface = {
434
+ name: arrayElementTypeName,
435
+ properties: elementProp.nested.properties,
436
+ allowArbitraryProps,
437
+ };
438
+ return {
439
+ type: `${arrayElementTypeName}[]`,
440
+ optional: true,
441
+ nested: arrayElementInterface,
442
+ };
443
+ }
444
+
445
+ return { type: `${elementType}[]`, optional: true };
446
+ }
447
+
448
+ function convertValueToProperty(opts: PropertyConversionContext): TypeProperty {
449
+ const {
450
+ value,
451
+ nestedTypeName,
452
+ schema,
453
+ propertyName,
454
+ yamlComment,
455
+ yamlComments,
456
+ fullKey,
457
+ chartName,
458
+ } = opts;
459
+
460
+ // If we have a JSON schema for this property, prefer it over inference
461
+ if (schema) {
462
+ return convertWithSchema({ ...opts, schema });
463
+ }
464
+
465
+ // Check for null/undefined first
466
+ if (
467
+ NullSchema.safeParse(value).success ||
468
+ UndefinedSchema.safeParse(value).success
469
+ ) {
470
+ return { type: "unknown", optional: true };
471
+ }
472
+
473
+ // Check for array (before coercion checks)
474
+ const arrayResult = ArraySchema.safeParse(value);
475
+ if (arrayResult.success) {
476
+ return inferArrayType(opts, arrayResult.data);
477
+ }
478
+
479
+ // Check for object (before primitive coercion checks)
480
+ const objectResult = HelmValueSchema.safeParse(value);
481
+ if (objectResult.success) {
482
+ const nestedInterface = convertToTypeScriptInterface({
483
+ values: objectResult.data,
484
+ interfaceName: nestedTypeName,
485
+ yamlComments,
486
+ keyPrefix: fullKey,
487
+ chartName,
488
+ });
489
+
490
+ if (
491
+ propertyName != null &&
492
+ propertyName !== "" &&
493
+ isK8sResourceSpec(propertyName)
494
+ ) {
495
+ augmentK8sResourceSpec(nestedInterface);
496
+ }
497
+
498
+ return {
499
+ type: nestedTypeName,
500
+ optional: true,
501
+ nested: nestedInterface,
502
+ description: yamlComment,
503
+ default: value,
504
+ };
505
+ }
506
+
507
+ // Infer primitive type from runtime value
508
+ return inferPrimitiveType(value, yamlComment);
509
+ }