@xrpckit/target-go-server 0.0.2 → 0.0.3

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,851 @@
1
+ import {
2
+ type ContractDefinition,
3
+ type Property,
4
+ type TypeDefinition,
5
+ type TypeReference,
6
+ toPascalCase,
7
+ type ValidationRules,
8
+ } from "@xrpckit/sdk";
9
+ import { GoBuilder } from "./go-builder";
10
+ import type { CollectedType } from "./type-collector";
11
+
12
+ export class GoValidationGenerator {
13
+ private w: GoBuilder;
14
+ private packageName: string;
15
+ private generatedValidations: Set<string> = new Set();
16
+
17
+ constructor(packageName = "server") {
18
+ this.w = new GoBuilder();
19
+ this.packageName = packageName;
20
+ }
21
+
22
+ /**
23
+ * Generate Go validation functions from a contract definition.
24
+ * @param contract - The contract definition (should have names assigned to inline types)
25
+ * @param collectedTypes - Optional pre-collected nested types from GoTypeCollector
26
+ */
27
+ generateValidation(
28
+ contract: ContractDefinition,
29
+ collectedTypes?: CollectedType[],
30
+ ): string {
31
+ const w = this.w.reset();
32
+ this.generatedValidations.clear();
33
+
34
+ // Determine which imports are needed based on validation rules in contract
35
+ const imports = new Set<string>(["fmt", "strings"]);
36
+
37
+ // Check if any validation rules require these imports
38
+ // Note: email uses mail.ParseAddress, not regex, so we check separately
39
+ const needsRegex = this.hasValidationRule(
40
+ contract,
41
+ collectedTypes,
42
+ (rules) => !!(rules.uuid || (rules.regex && !rules.email)), // regex but not email (email uses mail)
43
+ );
44
+ const needsMail = this.hasValidationRule(
45
+ contract,
46
+ collectedTypes,
47
+ (rules) => !!rules.email,
48
+ );
49
+ const needsURL = this.hasValidationRule(
50
+ contract,
51
+ collectedTypes,
52
+ (rules) => !!rules.url,
53
+ );
54
+
55
+ if (needsRegex) imports.add("regexp");
56
+ if (needsMail) imports.add("net/mail");
57
+ if (needsURL) imports.add("net/url");
58
+
59
+ w.package(this.packageName);
60
+ if (imports.size > 0) {
61
+ w.import(...Array.from(imports));
62
+ }
63
+
64
+ // Generate error types
65
+ this.generateErrorTypes(w);
66
+
67
+ // Generate validation functions for each type from contract
68
+ for (const type of contract.types) {
69
+ if (type.kind === "object" && type.properties) {
70
+ this.generateTypeValidation(type, w);
71
+ } else if (
72
+ type.kind === "array" &&
73
+ type.elementType?.kind === "object" &&
74
+ type.elementType.properties
75
+ ) {
76
+ // Generate validation function for the array element type
77
+ const elementTypeName = type.elementType.name
78
+ ? toPascalCase(type.elementType.name)
79
+ : `${toPascalCase(type.name)}Item`;
80
+ const elementType: TypeDefinition = {
81
+ name: elementTypeName,
82
+ kind: "object",
83
+ properties: type.elementType.properties,
84
+ };
85
+ this.generateTypeValidation(elementType, w);
86
+ }
87
+ }
88
+
89
+ // Generate validation functions for collected nested types
90
+ if (collectedTypes) {
91
+ for (const collected of collectedTypes) {
92
+ if (
93
+ collected.typeRef.kind === "object" &&
94
+ collected.typeRef.properties
95
+ ) {
96
+ const typeDefinition: TypeDefinition = {
97
+ name: collected.name,
98
+ kind: "object",
99
+ properties: collected.typeRef.properties,
100
+ };
101
+ this.generateTypeValidation(typeDefinition, w);
102
+ }
103
+ }
104
+ }
105
+
106
+ // Generate helper functions
107
+ this.generateHelperFunctions(w);
108
+
109
+ return w.toString();
110
+ }
111
+
112
+ private generateErrorTypes(w: GoBuilder): void {
113
+ w.struct("ValidationError", (b) => {
114
+ b.l('Field string `json:"field"`').l('Message string `json:"message"`');
115
+ }).n();
116
+
117
+ w.type("ValidationErrors", "[]*ValidationError").n();
118
+
119
+ w.method("e *ValidationError", "Error", "", "string", (b) => {
120
+ b.return('fmt.Sprintf("%s: %s", e.Field, e.Message)');
121
+ }).n();
122
+
123
+ w.method("e ValidationErrors", "Error", "", "string", (b) => {
124
+ b.var("msgs", "[]string");
125
+ b.l("for _, err := range e {")
126
+ .i()
127
+ .l("msgs = append(msgs, err.Error())")
128
+ .u()
129
+ .l("}");
130
+ b.return('strings.Join(msgs, "; ")');
131
+ }).n();
132
+ }
133
+
134
+ private generateTypeValidation(type: TypeDefinition, w: GoBuilder): void {
135
+ const typeName = toPascalCase(type.name);
136
+ const funcName = `Validate${typeName}`;
137
+
138
+ // Skip if already generated
139
+ if (this.generatedValidations.has(funcName)) {
140
+ return;
141
+ }
142
+ this.generatedValidations.add(funcName);
143
+
144
+ w.func(`${funcName}(input ${typeName}) error`, (b) => {
145
+ b.var("errs", "ValidationErrors");
146
+
147
+ if (type.properties) {
148
+ for (const prop of type.properties) {
149
+ this.generatePropertyValidation(prop, "input", b);
150
+ }
151
+ }
152
+
153
+ b.if("len(errs) > 0", (b) => {
154
+ b.return("errs");
155
+ });
156
+ b.return("nil");
157
+ }).n();
158
+ }
159
+
160
+ private generatePropertyValidation(
161
+ prop: Property,
162
+ prefix: string,
163
+ w: GoBuilder,
164
+ ): void {
165
+ const fieldName = toPascalCase(prop.name);
166
+ const fieldPath = `${prefix}.${fieldName}`;
167
+ const fieldPathStr = prop.name;
168
+ const unwrappedType = this.unwrapOptionalNullable(prop.type);
169
+ const isPointerType = this.isPointerType(prop.type);
170
+
171
+ if (isPointerType) {
172
+ w.comment(`Validate ${prop.name} when present`);
173
+ w.if(`${fieldPath} != nil`, (b) => {
174
+ this.generatePropertyValidationForValue(
175
+ prop,
176
+ `*${fieldPath}`,
177
+ fieldPathStr,
178
+ unwrappedType,
179
+ b,
180
+ false,
181
+ );
182
+ });
183
+ return;
184
+ }
185
+
186
+ this.generatePropertyValidationForValue(
187
+ prop,
188
+ fieldPath,
189
+ fieldPathStr,
190
+ unwrappedType,
191
+ w,
192
+ prop.required,
193
+ );
194
+ }
195
+
196
+ private generatePropertyValidationForValue(
197
+ prop: Property,
198
+ valuePath: string,
199
+ fieldPathStr: string,
200
+ typeRef: TypeReference,
201
+ w: GoBuilder,
202
+ isRequired: boolean,
203
+ ): void {
204
+ const actualType = this.getActualType(typeRef);
205
+ const isString = actualType === "string";
206
+ const isNumber = actualType === "number";
207
+ const isArray = typeRef.kind === "array";
208
+
209
+ const enumValues = this.getEnumValues(typeRef);
210
+ const isEnum = enumValues !== null;
211
+
212
+ if (isRequired) {
213
+ w.comment(`Validate ${prop.name}`);
214
+ if (isEnum || isString) {
215
+ w.if(`${valuePath} == ""`, (b) => {
216
+ b.l("errs = append(errs, &ValidationError{")
217
+ .i()
218
+ .l(`Field: "${fieldPathStr}",`)
219
+ .l(`Message: "is required",`)
220
+ .u()
221
+ .l("})");
222
+ });
223
+ } else if (isNumber) {
224
+ // For numbers, we can't easily check if zero is valid, so we skip required check
225
+ // The validation rules (min/max) will handle it
226
+ } else if (isArray) {
227
+ w.if(`${valuePath} == nil`, (b) => {
228
+ b.l("errs = append(errs, &ValidationError{")
229
+ .i()
230
+ .l(`Field: "${fieldPathStr}",`)
231
+ .l(`Message: "is required",`)
232
+ .u()
233
+ .l("})");
234
+ });
235
+ }
236
+ }
237
+
238
+ if (isEnum && enumValues) {
239
+ const enumValuesStr = enumValues.join(", ");
240
+ const enumConditions = enumValues
241
+ .map((v) => `${valuePath} != "${v}"`)
242
+ .join(" && ");
243
+ w.if(`${valuePath} != "" && ${enumConditions}`, (b) => {
244
+ b.l("errs = append(errs, &ValidationError{")
245
+ .i()
246
+ .l(`Field: "${fieldPathStr}",`)
247
+ .l(`Message: "must be one of: ${enumValuesStr}",`)
248
+ .u()
249
+ .l("})");
250
+ });
251
+ }
252
+
253
+ const validationRules = prop.validation || prop.type.validation;
254
+
255
+ if (validationRules) {
256
+ if (!isRequired) {
257
+ if (isString) {
258
+ w.if(`${valuePath} != ""`, (b) => {
259
+ const validationTypeRef: TypeReference = {
260
+ kind: "primitive",
261
+ baseType: actualType,
262
+ };
263
+ this.generateValidationRules(
264
+ validationRules,
265
+ valuePath,
266
+ fieldPathStr,
267
+ validationTypeRef,
268
+ b,
269
+ false,
270
+ );
271
+ });
272
+ } else if (isNumber) {
273
+ const validationTypeRef: TypeReference = {
274
+ kind: "primitive",
275
+ baseType: actualType,
276
+ };
277
+ this.generateValidationRules(
278
+ validationRules,
279
+ valuePath,
280
+ fieldPathStr,
281
+ validationTypeRef,
282
+ w,
283
+ false,
284
+ );
285
+ } else if (isArray) {
286
+ w.if(`${valuePath} != nil`, (b) => {
287
+ this.generateValidationRules(
288
+ validationRules,
289
+ valuePath,
290
+ fieldPathStr,
291
+ typeRef,
292
+ b,
293
+ false,
294
+ );
295
+ });
296
+ } else {
297
+ this.generateValidationRules(
298
+ validationRules,
299
+ valuePath,
300
+ fieldPathStr,
301
+ typeRef,
302
+ w,
303
+ false,
304
+ );
305
+ }
306
+ } else if (isArray) {
307
+ this.generateValidationRules(
308
+ validationRules,
309
+ valuePath,
310
+ fieldPathStr,
311
+ typeRef,
312
+ w,
313
+ true,
314
+ );
315
+ } else {
316
+ const validationTypeRef: TypeReference = {
317
+ kind: "primitive",
318
+ baseType: actualType,
319
+ };
320
+ this.generateValidationRules(
321
+ validationRules,
322
+ valuePath,
323
+ fieldPathStr,
324
+ validationTypeRef,
325
+ w,
326
+ true,
327
+ );
328
+ }
329
+ }
330
+
331
+ if (typeRef.kind === "object" && typeRef.name) {
332
+ const nestedTypeName = toPascalCase(typeRef.name);
333
+ const nestedFuncName = `Validate${nestedTypeName}`;
334
+ w.l(`if err := ${nestedFuncName}(${valuePath}); err != nil {`)
335
+ .i()
336
+ .l("if nestedErrs, ok := err.(ValidationErrors); ok {")
337
+ .i()
338
+ .l("errs = append(errs, nestedErrs...)")
339
+ .u()
340
+ .l("} else {")
341
+ .i()
342
+ .l("errs = append(errs, &ValidationError{")
343
+ .i()
344
+ .l(`Field: "${fieldPathStr}",`)
345
+ .l("Message: err.Error(),")
346
+ .u()
347
+ .l("})")
348
+ .u()
349
+ .l("}")
350
+ .u()
351
+ .l("}");
352
+ }
353
+
354
+ if (typeRef.kind === "array" && typeRef.elementType) {
355
+ const elementTypeRef = this.unwrapOptionalNullable(typeRef.elementType);
356
+ if (elementTypeRef.kind === "object" && elementTypeRef.name) {
357
+ const elementTypeName = toPascalCase(elementTypeRef.name);
358
+ const elementFuncName = `Validate${elementTypeName}`;
359
+ const isElementPointer = this.isPointerType(typeRef.elementType);
360
+
361
+ w.l(`for i, item := range ${valuePath} {`).i();
362
+ if (isElementPointer) {
363
+ w.l("if item == nil { continue }");
364
+ w.l(`if err := ${elementFuncName}(*item); err != nil {`);
365
+ } else {
366
+ w.l(`if err := ${elementFuncName}(item); err != nil {`);
367
+ }
368
+ w.i()
369
+ .l("if nestedErrs, ok := err.(ValidationErrors); ok {")
370
+ .i()
371
+ .l("for _, nestedErr := range nestedErrs {")
372
+ .i()
373
+ .l("errs = append(errs, &ValidationError{")
374
+ .i()
375
+ .l(
376
+ `Field: fmt.Sprintf("${fieldPathStr}[%%d].%%s", i, nestedErr.Field),`,
377
+ )
378
+ .l("Message: nestedErr.Message,")
379
+ .u()
380
+ .l("})")
381
+ .u()
382
+ .l("}")
383
+ .u()
384
+ .l("}")
385
+ .u()
386
+ .l("}")
387
+ .u()
388
+ .l("}");
389
+ w.u().l("}");
390
+ }
391
+ }
392
+ }
393
+
394
+ private generateValidationRules(
395
+ rules: ValidationRules,
396
+ fieldPath: string,
397
+ fieldPathStr: string,
398
+ typeRef: TypeReference,
399
+ w: GoBuilder,
400
+ isRequired = false,
401
+ ): void {
402
+ if (typeRef.baseType === "string") {
403
+ // For required fields, skip length checks if empty (already handled by required check)
404
+ // For optional fields, we only get here if field is not empty
405
+ if (rules.minLength !== undefined) {
406
+ // Only check length if string is not empty (for required fields)
407
+ const minLengthCondition = isRequired
408
+ ? `${fieldPath} != "" && len(${fieldPath}) < ${rules.minLength}`
409
+ : `len(${fieldPath}) < ${rules.minLength}`;
410
+ w.if(minLengthCondition, (b) => {
411
+ b.l("errs = append(errs, &ValidationError{")
412
+ .i()
413
+ .l(`Field: "${fieldPathStr}",`)
414
+ .l(
415
+ `Message: fmt.Sprintf("must be at least %d character(s)", ${rules.minLength}),`,
416
+ )
417
+ .u()
418
+ .l("})");
419
+ });
420
+ }
421
+ if (rules.maxLength !== undefined) {
422
+ w.if(`len(${fieldPath}) > ${rules.maxLength}`, (b) => {
423
+ b.l("errs = append(errs, &ValidationError{")
424
+ .i()
425
+ .l(`Field: "${fieldPathStr}",`)
426
+ .l(
427
+ `Message: fmt.Sprintf("must be at most %d character(s)", ${rules.maxLength}),`,
428
+ )
429
+ .u()
430
+ .l("})");
431
+ });
432
+ }
433
+ // Email validation - use mail.ParseAddress (more reliable than regex)
434
+ // Skip regex pattern if email is set (email() in Zod generates both format and regex)
435
+ if (rules.email) {
436
+ // For optional fields, we're already inside an "if fieldPath != "" block"
437
+ // For required fields, we need to check if not empty
438
+ if (isRequired) {
439
+ w.if(`${fieldPath} != ""`, (b) => {
440
+ b.l(`if _, err := mail.ParseAddress(${fieldPath}); err != nil {`)
441
+ .i()
442
+ .l("errs = append(errs, &ValidationError{")
443
+ .i()
444
+ .l(`Field: "${fieldPathStr}",`)
445
+ .l(`Message: "must be a valid email address",`)
446
+ .u()
447
+ .l("})")
448
+ .u()
449
+ .l("}");
450
+ });
451
+ } else {
452
+ // Already in "if fieldPath != "" block", so validate directly
453
+ w.l(`if _, err := mail.ParseAddress(${fieldPath}); err != nil {`)
454
+ .i()
455
+ .l("errs = append(errs, &ValidationError{")
456
+ .i()
457
+ .l(`Field: "${fieldPathStr}",`)
458
+ .l(`Message: "must be a valid email address",`)
459
+ .u()
460
+ .l("})")
461
+ .u()
462
+ .l("}");
463
+ }
464
+ }
465
+ // URL validation
466
+ if (rules.url) {
467
+ if (isRequired) {
468
+ w.if(`${fieldPath} != ""`, (b) => {
469
+ b.l(
470
+ `if u, err := url.Parse(${fieldPath}); err != nil || u.Scheme == "" || u.Host == "" {`,
471
+ )
472
+ .i()
473
+ .l("errs = append(errs, &ValidationError{")
474
+ .i()
475
+ .l(`Field: "${fieldPathStr}",`)
476
+ .l(`Message: "must be a valid URL",`)
477
+ .u()
478
+ .l("})")
479
+ .u()
480
+ .l("}");
481
+ });
482
+ } else {
483
+ w.l(
484
+ `if u, err := url.Parse(${fieldPath}); err != nil || u.Scheme == "" || u.Host == "" {`,
485
+ )
486
+ .i()
487
+ .l("errs = append(errs, &ValidationError{")
488
+ .i()
489
+ .l(`Field: "${fieldPathStr}",`)
490
+ .l(`Message: "must be a valid URL",`)
491
+ .u()
492
+ .l("})")
493
+ .u()
494
+ .l("}");
495
+ }
496
+ }
497
+ // UUID validation
498
+ if (rules.uuid) {
499
+ if (isRequired) {
500
+ w.if(`${fieldPath} != ""`, (b) => {
501
+ b.l(
502
+ `matched, _ := regexp.MatchString("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", ${fieldPath})`,
503
+ )
504
+ .n()
505
+ .l("if !matched {")
506
+ .i()
507
+ .l("errs = append(errs, &ValidationError{")
508
+ .i()
509
+ .l(`Field: "${fieldPathStr}",`)
510
+ .l(`Message: "must be a valid UUID",`)
511
+ .u()
512
+ .l("})")
513
+ .u()
514
+ .l("}");
515
+ });
516
+ } else {
517
+ w.l(
518
+ `matched, _ := regexp.MatchString("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", ${fieldPath})`,
519
+ )
520
+ .n()
521
+ .l("if !matched {")
522
+ .i()
523
+ .l("errs = append(errs, &ValidationError{")
524
+ .i()
525
+ .l(`Field: "${fieldPathStr}",`)
526
+ .l(`Message: "must be a valid UUID",`)
527
+ .u()
528
+ .l("})")
529
+ .u()
530
+ .l("}");
531
+ }
532
+ }
533
+ // Custom regex validation (only if not email/url/uuid which have dedicated validators)
534
+ if (rules.regex && !rules.email && !rules.url && !rules.uuid) {
535
+ if (isRequired) {
536
+ w.if(`${fieldPath} != ""`, (b) => {
537
+ // Escape the regex pattern for Go
538
+ const escapedRegex = rules.regex
539
+ ?.replace(/\\/g, "\\\\")
540
+ .replace(/"/g, '\\"');
541
+ b.l(
542
+ `matched, _ := regexp.MatchString("${escapedRegex}", ${fieldPath})`,
543
+ )
544
+ .n()
545
+ .l("if !matched {")
546
+ .i()
547
+ .l("errs = append(errs, &ValidationError{")
548
+ .i()
549
+ .l(`Field: "${fieldPathStr}",`)
550
+ .l(`Message: "must match the required pattern",`)
551
+ .u()
552
+ .l("})")
553
+ .u()
554
+ .l("}");
555
+ });
556
+ } else {
557
+ // Escape the regex pattern for Go
558
+ const escapedRegex = rules.regex
559
+ ?.replace(/\\/g, "\\\\")
560
+ .replace(/"/g, '\\"');
561
+ w.l(
562
+ `matched, _ := regexp.MatchString("${escapedRegex}", ${fieldPath})`,
563
+ )
564
+ .n()
565
+ .l("if !matched {")
566
+ .i()
567
+ .l("errs = append(errs, &ValidationError{")
568
+ .i()
569
+ .l(`Field: "${fieldPathStr}",`)
570
+ .l(`Message: "must match the required pattern",`)
571
+ .u()
572
+ .l("})")
573
+ .u()
574
+ .l("}");
575
+ }
576
+ }
577
+ } else if (typeRef.baseType === "number") {
578
+ if (rules.min !== undefined) {
579
+ w.if(`${fieldPath} < ${rules.min}`, (b) => {
580
+ b.l("errs = append(errs, &ValidationError{")
581
+ .i()
582
+ .l(`Field: "${fieldPathStr}",`)
583
+ .l(`Message: fmt.Sprintf("must be at least %v", ${rules.min}),`)
584
+ .u()
585
+ .l("})");
586
+ });
587
+ }
588
+ if (rules.max !== undefined) {
589
+ w.if(`${fieldPath} > ${rules.max}`, (b) => {
590
+ b.l("errs = append(errs, &ValidationError{")
591
+ .i()
592
+ .l(`Field: "${fieldPathStr}",`)
593
+ .l(`Message: fmt.Sprintf("must be at most %v", ${rules.max}),`)
594
+ .u()
595
+ .l("})");
596
+ });
597
+ }
598
+ if (rules.int) {
599
+ w.if(`float64(${fieldPath}) != float64(int64(${fieldPath}))`, (b) => {
600
+ b.l("errs = append(errs, &ValidationError{")
601
+ .i()
602
+ .l(`Field: "${fieldPathStr}",`)
603
+ .l(`Message: "must be an integer",`)
604
+ .u()
605
+ .l("})");
606
+ });
607
+ }
608
+ if (rules.positive) {
609
+ w.if(`${fieldPath} <= 0`, (b) => {
610
+ b.l("errs = append(errs, &ValidationError{")
611
+ .i()
612
+ .l(`Field: "${fieldPathStr}",`)
613
+ .l(`Message: "must be positive",`)
614
+ .u()
615
+ .l("})");
616
+ });
617
+ }
618
+ if (rules.negative) {
619
+ w.if(`${fieldPath} >= 0`, (b) => {
620
+ b.l("errs = append(errs, &ValidationError{")
621
+ .i()
622
+ .l(`Field: "${fieldPathStr}",`)
623
+ .l(`Message: "must be negative",`)
624
+ .u()
625
+ .l("})");
626
+ });
627
+ }
628
+ } else if (typeRef.kind === "array") {
629
+ // Only validate array length if array is not nil
630
+ const arrayCheckCondition = isRequired
631
+ ? `${fieldPath} != nil`
632
+ : `${fieldPath} != nil`;
633
+ if (rules.minItems !== undefined) {
634
+ w.if(
635
+ `${arrayCheckCondition} && len(${fieldPath}) < ${rules.minItems}`,
636
+ (b) => {
637
+ b.l("errs = append(errs, &ValidationError{")
638
+ .i()
639
+ .l(`Field: "${fieldPathStr}",`)
640
+ .l(
641
+ `Message: fmt.Sprintf("must have at least %d item(s)", ${rules.minItems}),`,
642
+ )
643
+ .u()
644
+ .l("})");
645
+ },
646
+ );
647
+ }
648
+ if (rules.maxItems !== undefined) {
649
+ w.if(
650
+ `${arrayCheckCondition} && len(${fieldPath}) > ${rules.maxItems}`,
651
+ (b) => {
652
+ b.l("errs = append(errs, &ValidationError{")
653
+ .i()
654
+ .l(`Field: "${fieldPathStr}",`)
655
+ .l(
656
+ `Message: fmt.Sprintf("must have at most %d item(s)", ${rules.maxItems}),`,
657
+ )
658
+ .u()
659
+ .l("})");
660
+ },
661
+ );
662
+ }
663
+ }
664
+ }
665
+
666
+ private hasValidationRule(
667
+ contract: ContractDefinition,
668
+ collectedTypes: CollectedType[] | undefined,
669
+ check: (rules: ValidationRules) => boolean,
670
+ ): boolean {
671
+ // Check contract types
672
+ for (const type of contract.types) {
673
+ if (this.checkTypeForValidationRule(type.properties, check)) {
674
+ return true;
675
+ }
676
+ // Check array validation
677
+ if (type.elementType?.validation && check(type.elementType.validation)) {
678
+ return true;
679
+ }
680
+ if (
681
+ type.elementType?.properties &&
682
+ this.checkTypeForValidationRule(type.elementType.properties, check)
683
+ ) {
684
+ return true;
685
+ }
686
+ }
687
+
688
+ // Check collected nested types
689
+ if (collectedTypes) {
690
+ for (const collected of collectedTypes) {
691
+ if (
692
+ collected.typeRef.properties &&
693
+ this.checkTypeForValidationRule(collected.typeRef.properties, check)
694
+ ) {
695
+ return true;
696
+ }
697
+ }
698
+ }
699
+
700
+ return false;
701
+ }
702
+
703
+ private checkTypeForValidationRule(
704
+ properties: Property[] | undefined,
705
+ check: (rules: ValidationRules) => boolean,
706
+ ): boolean {
707
+ if (!properties) return false;
708
+
709
+ for (const prop of properties) {
710
+ if (prop.validation && check(prop.validation)) {
711
+ return true;
712
+ }
713
+ // Check nested types recursively
714
+ if (this.checkTypeRefForValidationRule(prop.type, check)) {
715
+ return true;
716
+ }
717
+ }
718
+ return false;
719
+ }
720
+
721
+ private checkTypeRefForValidationRule(
722
+ typeRef: TypeReference,
723
+ check: (rules: ValidationRules) => boolean,
724
+ ): boolean {
725
+ // Check direct validation on type reference
726
+ if (typeRef.validation && check(typeRef.validation)) {
727
+ return true;
728
+ }
729
+
730
+ // Check nested properties
731
+ if (
732
+ typeRef.properties &&
733
+ this.checkTypeForValidationRule(typeRef.properties, check)
734
+ ) {
735
+ return true;
736
+ }
737
+
738
+ // Check array element types
739
+ if (typeRef.elementType) {
740
+ if (
741
+ typeRef.elementType.validation &&
742
+ check(typeRef.elementType.validation)
743
+ ) {
744
+ return true;
745
+ }
746
+ if (
747
+ typeRef.elementType.properties &&
748
+ this.checkTypeForValidationRule(typeRef.elementType.properties, check)
749
+ ) {
750
+ return true;
751
+ }
752
+ }
753
+
754
+ // Unwrap optional/nullable
755
+ if (
756
+ (typeRef.kind === "optional" || typeRef.kind === "nullable") &&
757
+ typeof typeRef.baseType === "object"
758
+ ) {
759
+ if (this.checkTypeRefForValidationRule(typeRef.baseType, check)) {
760
+ return true;
761
+ }
762
+ }
763
+
764
+ return false;
765
+ }
766
+
767
+ private generateHelperFunctions(_w: GoBuilder): void {
768
+ // Helper functions can be added here if needed
769
+ // For now, we use standard library functions directly
770
+ }
771
+
772
+ private unwrapOptionalNullable(typeRef: TypeReference): TypeReference {
773
+ if (typeRef.kind === "optional" || typeRef.kind === "nullable") {
774
+ if (typeRef.baseType) {
775
+ if (typeof typeRef.baseType === "string") {
776
+ return { kind: "primitive", baseType: typeRef.baseType };
777
+ }
778
+ return this.unwrapOptionalNullable(typeRef.baseType);
779
+ }
780
+ }
781
+
782
+ if (typeRef.kind === "union" && typeRef.unionTypes) {
783
+ const nonNullVariants = typeRef.unionTypes.filter(
784
+ (variant) =>
785
+ !(variant.kind === "literal" && variant.literalValue === null),
786
+ );
787
+ if (nonNullVariants.length === 1) {
788
+ return this.unwrapOptionalNullable(nonNullVariants[0]);
789
+ }
790
+ }
791
+
792
+ return typeRef;
793
+ }
794
+
795
+ private getActualType(typeRef: TypeReference): string {
796
+ const unwrapped = this.unwrapOptionalNullable(typeRef);
797
+ if (
798
+ unwrapped.kind === "primitive" &&
799
+ typeof unwrapped.baseType === "string"
800
+ ) {
801
+ return unwrapped.baseType;
802
+ }
803
+ if (typeof unwrapped.baseType === "string") {
804
+ return unwrapped.baseType;
805
+ }
806
+ if (unwrapped.baseType) {
807
+ return this.getActualType(unwrapped.baseType);
808
+ }
809
+ return "unknown";
810
+ }
811
+
812
+ private getEnumValues(typeRef: TypeReference): string[] | null {
813
+ const unwrapped = this.unwrapOptionalNullable(typeRef);
814
+ if (unwrapped.kind === "enum" && unwrapped.enumValues) {
815
+ return unwrapped.enumValues.filter(
816
+ (v): v is string => typeof v === "string",
817
+ );
818
+ }
819
+
820
+ return null;
821
+ }
822
+
823
+ private isPointerType(typeRef: TypeReference): boolean {
824
+ // A type becomes a pointer in Go when it's:
825
+ // - nullable (wrapping any type)
826
+ // - optional wrapping nullable
827
+ // - nullable wrapping optional
828
+
829
+ if (typeRef.kind === "nullable") {
830
+ return true;
831
+ }
832
+
833
+ if (typeRef.kind === "optional") {
834
+ if (typeRef.baseType && typeof typeRef.baseType !== "string") {
835
+ return this.isPointerType(typeRef.baseType);
836
+ }
837
+ }
838
+
839
+ if (typeRef.kind === "union" && typeRef.unionTypes) {
840
+ const nonNullVariants = typeRef.unionTypes.filter(
841
+ (variant) =>
842
+ !(variant.kind === "literal" && variant.literalValue === null),
843
+ );
844
+ if (nonNullVariants.length === 1) {
845
+ return true;
846
+ }
847
+ }
848
+
849
+ return false;
850
+ }
851
+ }