@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.
- package/dist/index.d.ts +267 -36
- package/dist/index.js +1511 -310
- package/package.json +14 -5
- package/src/generator.ts +204 -0
- package/src/go-builder.ts +148 -0
- package/src/index.ts +15 -0
- package/src/patterns.test.ts +114 -0
- package/src/patterns.ts +284 -0
- package/src/server-generator.ts +233 -0
- package/src/type-collector.ts +222 -0
- package/src/type-generator.ts +353 -0
- package/src/type-mapper.ts +305 -0
- package/src/validation-generator.ts +851 -0
- package/src/validation-mapper.ts +260 -0
|
@@ -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
|
+
}
|