@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
package/dist/index.js
CHANGED
|
@@ -1,8 +1,18 @@
|
|
|
1
1
|
// src/generator.ts
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
TYPE_KINDS,
|
|
4
|
+
toPascalCase as toPascalCase7,
|
|
5
|
+
VALIDATION_KINDS,
|
|
6
|
+
validateSupport
|
|
7
|
+
} from "@xrpckit/sdk";
|
|
8
|
+
|
|
9
|
+
// src/server-generator.ts
|
|
10
|
+
import {
|
|
11
|
+
toPascalCase
|
|
12
|
+
} from "@xrpckit/sdk";
|
|
3
13
|
|
|
4
14
|
// src/go-builder.ts
|
|
5
|
-
import { CodeWriter } from "@xrpckit/
|
|
15
|
+
import { CodeWriter } from "@xrpckit/sdk";
|
|
6
16
|
var GoBuilder = class extends CodeWriter {
|
|
7
17
|
// Short aliases
|
|
8
18
|
l(text) {
|
|
@@ -78,9 +88,11 @@ var GoBuilder = class extends CodeWriter {
|
|
|
78
88
|
var(name, type, value) {
|
|
79
89
|
if (type && value) {
|
|
80
90
|
return this.l(`var ${name} ${type} = ${value}`);
|
|
81
|
-
}
|
|
91
|
+
}
|
|
92
|
+
if (type) {
|
|
82
93
|
return this.l(`var ${name} ${type}`);
|
|
83
|
-
}
|
|
94
|
+
}
|
|
95
|
+
if (value) {
|
|
84
96
|
return this.l(`${name} := ${value}`);
|
|
85
97
|
}
|
|
86
98
|
return this.l(`var ${name}`);
|
|
@@ -115,63 +127,532 @@ var GoBuilder = class extends CodeWriter {
|
|
|
115
127
|
}
|
|
116
128
|
};
|
|
117
129
|
|
|
118
|
-
// src/
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
130
|
+
// src/server-generator.ts
|
|
131
|
+
function toMethodName(fullName) {
|
|
132
|
+
return fullName.split(".").map((part) => toPascalCase(part)).join("");
|
|
133
|
+
}
|
|
134
|
+
function toFieldName(fullName) {
|
|
135
|
+
return fullName.split(".").map((part, i) => i === 0 ? part : toPascalCase(part)).join("");
|
|
136
|
+
}
|
|
137
|
+
var GoServerGenerator = class {
|
|
138
|
+
w;
|
|
139
|
+
packageName;
|
|
140
|
+
constructor(packageName = "server") {
|
|
141
|
+
this.w = new GoBuilder();
|
|
142
|
+
this.packageName = packageName;
|
|
143
|
+
}
|
|
144
|
+
generateServer(contract) {
|
|
145
|
+
const w = this.w.reset();
|
|
146
|
+
w.package(this.packageName).import("encoding/json", "net/http", "fmt");
|
|
147
|
+
w.struct("Router", (b) => {
|
|
148
|
+
b.l("middleware []MiddlewareFunc");
|
|
149
|
+
for (const endpoint of contract.endpoints) {
|
|
150
|
+
const fieldName = toFieldName(endpoint.fullName);
|
|
151
|
+
const handlerType = `${toMethodName(endpoint.fullName)}Handler`;
|
|
152
|
+
b.l(`${fieldName} ${handlerType}`);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
w.func("NewRouter() *Router", (b) => {
|
|
156
|
+
b.l("return &Router{").i().l("middleware: make([]MiddlewareFunc, 0),").u().l("}");
|
|
157
|
+
});
|
|
158
|
+
for (const endpoint of contract.endpoints) {
|
|
159
|
+
const methodName = toMethodName(endpoint.fullName);
|
|
160
|
+
const fieldName = toFieldName(endpoint.fullName);
|
|
161
|
+
const handlerType = `${methodName}Handler`;
|
|
162
|
+
w.method(
|
|
163
|
+
"r *Router",
|
|
164
|
+
methodName,
|
|
165
|
+
`handler ${handlerType}`,
|
|
166
|
+
"*Router",
|
|
167
|
+
(b) => {
|
|
168
|
+
b.l(`r.${fieldName} = handler`).return("r");
|
|
169
|
+
}
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
w.method(
|
|
173
|
+
"r *Router",
|
|
174
|
+
"Use",
|
|
175
|
+
"middleware MiddlewareFunc",
|
|
176
|
+
"*Router",
|
|
177
|
+
(b) => {
|
|
178
|
+
b.l("r.middleware = append(r.middleware, middleware)").return("r");
|
|
179
|
+
}
|
|
180
|
+
);
|
|
181
|
+
this.generateServeHTTP(contract.endpoints, w);
|
|
182
|
+
return w.toString();
|
|
183
|
+
}
|
|
184
|
+
generateServeHTTP(endpoints, w) {
|
|
185
|
+
w.method(
|
|
186
|
+
"r *Router",
|
|
187
|
+
"ServeHTTP",
|
|
188
|
+
"w http.ResponseWriter, req *http.Request",
|
|
189
|
+
"",
|
|
190
|
+
(b) => {
|
|
191
|
+
b.if("req.Method != http.MethodPost", (b2) => {
|
|
192
|
+
b2.l(
|
|
193
|
+
'http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)'
|
|
194
|
+
).return();
|
|
195
|
+
}).n();
|
|
196
|
+
b.var("request", "struct {").i().l('Method string `json:"method"`').l('Params json.RawMessage `json:"params"`').u().l("}").n();
|
|
197
|
+
b.if(
|
|
198
|
+
"err := json.NewDecoder(req.Body).Decode(&request); err != nil",
|
|
199
|
+
(b2) => {
|
|
200
|
+
b2.l(
|
|
201
|
+
'http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)'
|
|
202
|
+
).return();
|
|
203
|
+
}
|
|
204
|
+
).n();
|
|
205
|
+
b.decl("ctx", "&Context{").i().l("Request: req,").l("ResponseWriter: w,").l("Data: make(map[string]interface{}),").u().l("}").n();
|
|
206
|
+
b.comment("Execute middleware chain").l("for _, middleware := range r.middleware {").i().decl("result", "middleware(ctx)").if("result.Error != nil", (b2) => {
|
|
207
|
+
b2.l(
|
|
208
|
+
'http.Error(w, fmt.Sprintf("Middleware error: %v", result.Error), http.StatusInternalServerError)'
|
|
209
|
+
).return();
|
|
210
|
+
}).if("result.Response != nil", (b2) => {
|
|
211
|
+
b2.comment("Middleware short-circuited with response").return();
|
|
212
|
+
}).l("ctx = result.Context").u().l("}").n();
|
|
213
|
+
const cases = endpoints.map((endpoint) => ({
|
|
214
|
+
value: `"${endpoint.fullName}"`,
|
|
215
|
+
fn: (b2) => {
|
|
216
|
+
const fieldName = toFieldName(endpoint.fullName);
|
|
217
|
+
b2.if(`r.${fieldName} == nil`, (b3) => {
|
|
218
|
+
b3.l(
|
|
219
|
+
'http.Error(w, "Handler not registered", http.StatusNotFound)'
|
|
220
|
+
).return();
|
|
221
|
+
}).n();
|
|
222
|
+
const inputTypeName = toPascalCase(endpoint.input.name);
|
|
223
|
+
b2.var("input", inputTypeName);
|
|
224
|
+
b2.if(
|
|
225
|
+
"err := json.Unmarshal(request.Params, &input); err != nil",
|
|
226
|
+
(b3) => {
|
|
227
|
+
b3.l(
|
|
228
|
+
'http.Error(w, fmt.Sprintf("Invalid params: %v", err), http.StatusBadRequest)'
|
|
229
|
+
).return();
|
|
230
|
+
}
|
|
231
|
+
).n();
|
|
232
|
+
const validationFuncName = `Validate${inputTypeName}`;
|
|
233
|
+
b2.if(`err := ${validationFuncName}(input); err != nil`, (b3) => {
|
|
234
|
+
b3.l('w.Header().Set("Content-Type", "application/json")').l(
|
|
235
|
+
"w.WriteHeader(http.StatusBadRequest)"
|
|
236
|
+
);
|
|
237
|
+
b3.l("if validationErrs, ok := err.(ValidationErrors); ok {").i().l("json.NewEncoder(w).Encode(map[string]interface{}{").i().l('"error": "Validation failed",').l('"errors": validationErrs,').u().l("})").u().l("} else {").i().l("json.NewEncoder(w).Encode(map[string]interface{}{").i().l('"error": err.Error(),').u().l("})").u().l("}");
|
|
238
|
+
b3.return();
|
|
239
|
+
}).n();
|
|
240
|
+
b2.decl("result, err", `r.${fieldName}(ctx, input)`);
|
|
241
|
+
b2.ifErr((b3) => {
|
|
242
|
+
b3.l('w.Header().Set("Content-Type", "application/json")').l(
|
|
243
|
+
'json.NewEncoder(w).Encode(map[string]interface{}{"error": err.Error()})'
|
|
244
|
+
).return();
|
|
245
|
+
}).n();
|
|
246
|
+
b2.l('w.Header().Set("Content-Type", "application/json")').l(
|
|
247
|
+
'json.NewEncoder(w).Encode(map[string]interface{}{"result": result})'
|
|
248
|
+
).return();
|
|
249
|
+
}
|
|
250
|
+
}));
|
|
251
|
+
w.switch("request.Method", cases, (b2) => {
|
|
252
|
+
b2.l(
|
|
253
|
+
'http.Error(w, "Method not found", http.StatusNotFound)'
|
|
254
|
+
).return();
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
);
|
|
258
|
+
}
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
// src/type-collector.ts
|
|
262
|
+
import {
|
|
263
|
+
toPascalCase as toPascalCase2
|
|
264
|
+
} from "@xrpckit/sdk";
|
|
265
|
+
var GoTypeCollector = class {
|
|
266
|
+
collectedTypes = /* @__PURE__ */ new Map();
|
|
267
|
+
usedNames = /* @__PURE__ */ new Set();
|
|
268
|
+
/**
|
|
269
|
+
* Collect all types from a contract that need Go struct generation.
|
|
270
|
+
* This includes:
|
|
271
|
+
* - Named types from contract.types
|
|
272
|
+
* - Anonymous inline objects nested in properties
|
|
273
|
+
* - Array element types that are inline objects
|
|
274
|
+
* - Optional/nullable wrapped inline objects
|
|
275
|
+
*/
|
|
276
|
+
collectTypes(contract) {
|
|
277
|
+
this.collectedTypes.clear();
|
|
278
|
+
this.usedNames.clear();
|
|
279
|
+
for (const type of contract.types) {
|
|
280
|
+
const pascalName = toPascalCase2(type.name);
|
|
281
|
+
this.usedNames.add(pascalName);
|
|
282
|
+
}
|
|
283
|
+
for (const type of contract.types) {
|
|
284
|
+
this.processTypeDefinition(type, type.name);
|
|
285
|
+
}
|
|
286
|
+
return Array.from(this.collectedTypes.values());
|
|
287
|
+
}
|
|
288
|
+
/**
|
|
289
|
+
* Process a type definition and extract any nested inline types
|
|
290
|
+
*/
|
|
291
|
+
processTypeDefinition(type, contextName) {
|
|
292
|
+
if (type.properties) {
|
|
293
|
+
for (const prop of type.properties) {
|
|
294
|
+
this.processProperty(prop, contextName);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (type.kind === "array" && type.elementType) {
|
|
298
|
+
this.processTypeReference(
|
|
299
|
+
type.elementType,
|
|
300
|
+
`${contextName}Item`,
|
|
301
|
+
`${contextName}.elementType`
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
}
|
|
305
|
+
/**
|
|
306
|
+
* Process a property and extract any nested inline types
|
|
307
|
+
*/
|
|
308
|
+
processProperty(prop, parentContext) {
|
|
309
|
+
const propContext = `${parentContext}${toPascalCase2(prop.name)}`;
|
|
310
|
+
this.processTypeReference(
|
|
311
|
+
prop.type,
|
|
312
|
+
propContext,
|
|
313
|
+
`${parentContext}.${prop.name}`
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Process a type reference and extract any nested inline types.
|
|
318
|
+
* This is the core function that handles all the different type kinds.
|
|
319
|
+
*/
|
|
320
|
+
processTypeReference(typeRef, suggestedName, source) {
|
|
122
321
|
if (typeRef.kind === "optional") {
|
|
123
322
|
if (typeof typeRef.baseType === "object") {
|
|
124
|
-
|
|
323
|
+
this.processTypeReference(
|
|
324
|
+
typeRef.baseType,
|
|
325
|
+
suggestedName,
|
|
326
|
+
`${source}.optional`
|
|
327
|
+
);
|
|
125
328
|
}
|
|
126
|
-
return
|
|
329
|
+
return;
|
|
127
330
|
}
|
|
128
331
|
if (typeRef.kind === "nullable") {
|
|
129
332
|
if (typeof typeRef.baseType === "object") {
|
|
130
|
-
|
|
333
|
+
this.processTypeReference(
|
|
334
|
+
typeRef.baseType,
|
|
335
|
+
suggestedName,
|
|
336
|
+
`${source}.nullable`
|
|
337
|
+
);
|
|
131
338
|
}
|
|
132
|
-
return
|
|
339
|
+
return;
|
|
133
340
|
}
|
|
134
|
-
if (typeRef.kind === "array") {
|
|
135
|
-
|
|
341
|
+
if (typeRef.kind === "array" && typeRef.elementType) {
|
|
342
|
+
const elementName = suggestedName.endsWith("Item") ? suggestedName : `${suggestedName}Item`;
|
|
343
|
+
this.processTypeReference(
|
|
344
|
+
typeRef.elementType,
|
|
345
|
+
elementName,
|
|
346
|
+
`${source}.array`
|
|
347
|
+
);
|
|
348
|
+
return;
|
|
136
349
|
}
|
|
137
|
-
if (typeRef.kind === "object") {
|
|
138
|
-
|
|
139
|
-
|
|
350
|
+
if (typeRef.kind === "object" && typeRef.properties && !typeRef.name) {
|
|
351
|
+
const assignedName = this.assignUniqueName(suggestedName);
|
|
352
|
+
typeRef.name = assignedName;
|
|
353
|
+
this.collectedTypes.set(assignedName, {
|
|
354
|
+
name: assignedName,
|
|
355
|
+
typeRef,
|
|
356
|
+
source
|
|
357
|
+
});
|
|
358
|
+
for (const prop of typeRef.properties) {
|
|
359
|
+
this.processProperty(prop, assignedName);
|
|
140
360
|
}
|
|
141
|
-
return
|
|
142
|
-
}
|
|
143
|
-
if (typeRef.kind === "record") {
|
|
144
|
-
const valueType = typeRef.valueType ? this.mapType(typeRef.valueType) : "interface{}";
|
|
145
|
-
return `map[string]${valueType}`;
|
|
361
|
+
return;
|
|
146
362
|
}
|
|
147
|
-
if (typeRef.kind === "
|
|
148
|
-
|
|
363
|
+
if (typeRef.kind === "record" && typeRef.valueType) {
|
|
364
|
+
this.processTypeReference(
|
|
365
|
+
typeRef.valueType,
|
|
366
|
+
`${suggestedName}Value`,
|
|
367
|
+
`${source}.record`
|
|
368
|
+
);
|
|
369
|
+
return;
|
|
149
370
|
}
|
|
150
|
-
if (typeRef.kind === "
|
|
151
|
-
|
|
371
|
+
if (typeRef.kind === "tuple" && typeRef.tupleElements) {
|
|
372
|
+
typeRef.tupleElements.forEach((elem, index) => {
|
|
373
|
+
this.processTypeReference(
|
|
374
|
+
elem,
|
|
375
|
+
`${suggestedName}V${index}`,
|
|
376
|
+
`${source}.tuple[${index}]`
|
|
377
|
+
);
|
|
378
|
+
});
|
|
379
|
+
return;
|
|
152
380
|
}
|
|
153
|
-
if (typeRef.kind === "
|
|
154
|
-
|
|
381
|
+
if (typeRef.kind === "union" && typeRef.unionTypes) {
|
|
382
|
+
typeRef.unionTypes.forEach((variant, index) => {
|
|
383
|
+
this.processTypeReference(
|
|
384
|
+
variant,
|
|
385
|
+
`${suggestedName}Variant${index}`,
|
|
386
|
+
`${source}.union[${index}]`
|
|
387
|
+
);
|
|
388
|
+
});
|
|
389
|
+
return;
|
|
155
390
|
}
|
|
156
|
-
if (typeRef.kind === "
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
} else if (typeof typeRef.literalValue === "number") {
|
|
160
|
-
return "float64";
|
|
161
|
-
} else if (typeof typeRef.literalValue === "boolean") {
|
|
162
|
-
return "bool";
|
|
391
|
+
if (typeRef.kind === "object" && typeRef.properties && typeRef.name) {
|
|
392
|
+
for (const prop of typeRef.properties) {
|
|
393
|
+
this.processProperty(prop, toPascalCase2(typeRef.name));
|
|
163
394
|
}
|
|
164
|
-
return "interface{}";
|
|
165
|
-
}
|
|
166
|
-
if (typeRef.kind === "date") {
|
|
167
|
-
return "time.Time";
|
|
168
395
|
}
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
396
|
+
}
|
|
397
|
+
/**
|
|
398
|
+
* Assign a unique name, adding numeric suffix if there's a collision
|
|
399
|
+
*/
|
|
400
|
+
assignUniqueName(baseName) {
|
|
401
|
+
let name = baseName;
|
|
402
|
+
let counter = 1;
|
|
403
|
+
while (this.usedNames.has(name)) {
|
|
404
|
+
name = `${baseName}${counter}`;
|
|
405
|
+
counter++;
|
|
172
406
|
}
|
|
173
|
-
|
|
407
|
+
this.usedNames.add(name);
|
|
408
|
+
return name;
|
|
409
|
+
}
|
|
410
|
+
/**
|
|
411
|
+
* Get all collected nested types (not including the main contract types)
|
|
412
|
+
*/
|
|
413
|
+
getCollectedTypes() {
|
|
414
|
+
return Array.from(this.collectedTypes.values());
|
|
174
415
|
}
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
// src/type-generator.ts
|
|
419
|
+
import {
|
|
420
|
+
toPascalCase as toPascalCase5
|
|
421
|
+
} from "@xrpckit/sdk";
|
|
422
|
+
|
|
423
|
+
// src/type-mapper.ts
|
|
424
|
+
import {
|
|
425
|
+
TypeMapperBase,
|
|
426
|
+
toPascalCase as toPascalCase4
|
|
427
|
+
} from "@xrpckit/sdk";
|
|
428
|
+
|
|
429
|
+
// src/patterns.ts
|
|
430
|
+
import { toPascalCase as toPascalCase3 } from "@xrpckit/sdk";
|
|
431
|
+
function createGoEnumPattern(name, values) {
|
|
432
|
+
const stringValues = values.filter((v) => typeof v === "string");
|
|
433
|
+
const constants = stringValues.map((v) => {
|
|
434
|
+
const constName = `${name}${toPascalCase3(v)}`;
|
|
435
|
+
return { constName, value: v };
|
|
436
|
+
});
|
|
437
|
+
const code = `// ${name} enum type
|
|
438
|
+
type ${name} string
|
|
439
|
+
|
|
440
|
+
const (
|
|
441
|
+
${constants.map((c) => ` ${c.constName} ${name} = "${c.value}"`).join("\n")}
|
|
442
|
+
)
|
|
443
|
+
|
|
444
|
+
// IsValid checks if the ${name} value is valid
|
|
445
|
+
func (e ${name}) IsValid() bool {
|
|
446
|
+
switch e {
|
|
447
|
+
case ${constants.map((c) => c.constName).join(", ")}:
|
|
448
|
+
return true
|
|
449
|
+
}
|
|
450
|
+
return false
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
// Parse${name} parses a string into a ${name} value
|
|
454
|
+
func Parse${name}(s string) (${name}, error) {
|
|
455
|
+
e := ${name}(s)
|
|
456
|
+
if !e.IsValid() {
|
|
457
|
+
return "", fmt.Errorf("invalid ${name}: %s", s)
|
|
458
|
+
}
|
|
459
|
+
return e, nil
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
// All${name}Values returns all valid ${name} values
|
|
463
|
+
func All${name}Values() []${name} {
|
|
464
|
+
return []${name}{${constants.map((c) => c.constName).join(", ")}}
|
|
465
|
+
}`;
|
|
466
|
+
return {
|
|
467
|
+
id: `enum_${name}`,
|
|
468
|
+
code,
|
|
469
|
+
imports: ["fmt"],
|
|
470
|
+
includeOnce: true,
|
|
471
|
+
priority: 100
|
|
472
|
+
// Enums should come early
|
|
473
|
+
};
|
|
474
|
+
}
|
|
475
|
+
function createGoBigIntPattern() {
|
|
476
|
+
const code = `// BigInt is a wrapper around big.Int for JSON serialization
|
|
477
|
+
type BigInt struct {
|
|
478
|
+
*big.Int
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
// MarshalJSON implements json.Marshaler for BigInt
|
|
482
|
+
func (b BigInt) MarshalJSON() ([]byte, error) {
|
|
483
|
+
if b.Int == nil {
|
|
484
|
+
return []byte("null"), nil
|
|
485
|
+
}
|
|
486
|
+
return []byte(b.String()), nil
|
|
487
|
+
}
|
|
488
|
+
|
|
489
|
+
// UnmarshalJSON implements json.Unmarshaler for BigInt
|
|
490
|
+
func (b *BigInt) UnmarshalJSON(data []byte) error {
|
|
491
|
+
str := string(data)
|
|
492
|
+
if str == "null" {
|
|
493
|
+
b.Int = nil
|
|
494
|
+
return nil
|
|
495
|
+
}
|
|
496
|
+
// Remove quotes if present
|
|
497
|
+
if len(str) >= 2 && str[0] == '"' && str[len(str)-1] == '"' {
|
|
498
|
+
str = str[1 : len(str)-1]
|
|
499
|
+
}
|
|
500
|
+
b.Int = new(big.Int)
|
|
501
|
+
_, ok := b.Int.SetString(str, 10)
|
|
502
|
+
if !ok {
|
|
503
|
+
return fmt.Errorf("invalid BigInt: %s", str)
|
|
504
|
+
}
|
|
505
|
+
return nil
|
|
506
|
+
}`;
|
|
507
|
+
return {
|
|
508
|
+
id: "bigint",
|
|
509
|
+
code,
|
|
510
|
+
imports: ["math/big", "fmt"],
|
|
511
|
+
includeOnce: true,
|
|
512
|
+
priority: 90
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
function createGoUnionPattern(name, variants) {
|
|
516
|
+
const assertions = variants.map((v) => {
|
|
517
|
+
const methodName = `As${toPascalCase3(v.replace(/[*[\]]/g, ""))}`;
|
|
518
|
+
return `// ${methodName} returns the value as ${v}, or ok=false if not that type
|
|
519
|
+
func (u ${name}) ${methodName}() (${v}, bool) {
|
|
520
|
+
val, ok := u.Value.(${v})
|
|
521
|
+
return val, ok
|
|
522
|
+
}`;
|
|
523
|
+
}).join("\n\n");
|
|
524
|
+
const code = `// ${name} represents a union type that can hold one of several types
|
|
525
|
+
type ${name} struct {
|
|
526
|
+
Value interface{}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// MarshalJSON implements json.Marshaler for ${name}
|
|
530
|
+
func (u ${name}) MarshalJSON() ([]byte, error) {
|
|
531
|
+
return json.Marshal(u.Value)
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
// UnmarshalJSON implements json.Unmarshaler for ${name}
|
|
535
|
+
func (u *${name}) UnmarshalJSON(data []byte) error {
|
|
536
|
+
return json.Unmarshal(data, &u.Value)
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
${assertions}`;
|
|
540
|
+
return {
|
|
541
|
+
id: `union_${name}`,
|
|
542
|
+
code,
|
|
543
|
+
imports: ["encoding/json"],
|
|
544
|
+
includeOnce: true,
|
|
545
|
+
priority: 80
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
function createGoTuplePattern(name, elements) {
|
|
549
|
+
const fields = elements.map((el, i) => ` V${i} ${el} \`json:"${i}"\``);
|
|
550
|
+
const code = `// ${name} represents a tuple type with ${elements.length} elements
|
|
551
|
+
type ${name} struct {
|
|
552
|
+
${fields.join("\n")}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
// MarshalJSON implements json.Marshaler for ${name} to serialize as JSON array
|
|
556
|
+
func (t ${name}) MarshalJSON() ([]byte, error) {
|
|
557
|
+
return json.Marshal([]interface{}{${elements.map((_, i) => `t.V${i}`).join(", ")}})
|
|
558
|
+
}
|
|
559
|
+
|
|
560
|
+
// UnmarshalJSON implements json.Unmarshaler for ${name} from JSON array
|
|
561
|
+
func (t *${name}) UnmarshalJSON(data []byte) error {
|
|
562
|
+
var arr []json.RawMessage
|
|
563
|
+
if err := json.Unmarshal(data, &arr); err != nil {
|
|
564
|
+
return err
|
|
565
|
+
}
|
|
566
|
+
if len(arr) != ${elements.length} {
|
|
567
|
+
return fmt.Errorf("expected ${elements.length} elements, got %d", len(arr))
|
|
568
|
+
}
|
|
569
|
+
${elements.map((_, i) => ` if err := json.Unmarshal(arr[${i}], &t.V${i}); err != nil {
|
|
570
|
+
return fmt.Errorf("element ${i}: %w", err)
|
|
571
|
+
}`).join("\n")}
|
|
572
|
+
return nil
|
|
573
|
+
}`;
|
|
574
|
+
return {
|
|
575
|
+
id: `tuple_${name}`,
|
|
576
|
+
code,
|
|
577
|
+
imports: ["encoding/json", "fmt"],
|
|
578
|
+
includeOnce: true,
|
|
579
|
+
priority: 70
|
|
580
|
+
};
|
|
581
|
+
}
|
|
582
|
+
function createGoDatePattern() {
|
|
583
|
+
const code = `// DateTime is a wrapper around time.Time with flexible JSON parsing
|
|
584
|
+
type DateTime struct {
|
|
585
|
+
time.Time
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Common date/time formats to try when parsing
|
|
589
|
+
var dateTimeFormats = []string{
|
|
590
|
+
time.RFC3339,
|
|
591
|
+
time.RFC3339Nano,
|
|
592
|
+
"2006-01-02T15:04:05Z07:00",
|
|
593
|
+
"2006-01-02T15:04:05",
|
|
594
|
+
"2006-01-02",
|
|
595
|
+
}
|
|
596
|
+
|
|
597
|
+
// UnmarshalJSON implements json.Unmarshaler for DateTime
|
|
598
|
+
func (d *DateTime) UnmarshalJSON(data []byte) error {
|
|
599
|
+
str := string(data)
|
|
600
|
+
if str == "null" {
|
|
601
|
+
return nil
|
|
602
|
+
}
|
|
603
|
+
// Remove quotes
|
|
604
|
+
if len(str) >= 2 && str[0] == '"' && str[len(str)-1] == '"' {
|
|
605
|
+
str = str[1 : len(str)-1]
|
|
606
|
+
}
|
|
607
|
+
// Try each format
|
|
608
|
+
for _, format := range dateTimeFormats {
|
|
609
|
+
if t, err := time.Parse(format, str); err == nil {
|
|
610
|
+
d.Time = t
|
|
611
|
+
return nil
|
|
612
|
+
}
|
|
613
|
+
}
|
|
614
|
+
return fmt.Errorf("cannot parse date: %s", str)
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
// MarshalJSON implements json.Marshaler for DateTime
|
|
618
|
+
func (d DateTime) MarshalJSON() ([]byte, error) {
|
|
619
|
+
return json.Marshal(d.Time.Format(time.RFC3339))
|
|
620
|
+
}`;
|
|
621
|
+
return {
|
|
622
|
+
id: "datetime",
|
|
623
|
+
code,
|
|
624
|
+
imports: ["time", "encoding/json", "fmt"],
|
|
625
|
+
includeOnce: true,
|
|
626
|
+
priority: 85
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
// src/type-mapper.ts
|
|
631
|
+
var GoTypeMapper = class extends TypeMapperBase {
|
|
632
|
+
// Registry of tuple type names for reference during validation
|
|
633
|
+
tupleTypes = /* @__PURE__ */ new Map();
|
|
634
|
+
// Registry of union type names
|
|
635
|
+
unionTypes = /* @__PURE__ */ new Map();
|
|
636
|
+
/**
|
|
637
|
+
* Complete mapping of all type kinds to Go types.
|
|
638
|
+
* TypeScript enforces exhaustiveness at compile time.
|
|
639
|
+
*/
|
|
640
|
+
typeMapping = {
|
|
641
|
+
object: (ctx) => this.handleObject(ctx),
|
|
642
|
+
array: (ctx) => this.handleArray(ctx),
|
|
643
|
+
primitive: (ctx) => this.handlePrimitive(ctx),
|
|
644
|
+
optional: (ctx) => this.handleOptional(ctx),
|
|
645
|
+
nullable: (ctx) => this.handleNullable(ctx),
|
|
646
|
+
union: (ctx) => this.handleUnion(ctx),
|
|
647
|
+
enum: (ctx) => this.handleEnum(ctx),
|
|
648
|
+
literal: (ctx) => this.handleLiteral(ctx),
|
|
649
|
+
record: (ctx) => this.handleRecord(ctx),
|
|
650
|
+
tuple: (ctx) => this.handleTuple(ctx),
|
|
651
|
+
date: () => this.handleDate()
|
|
652
|
+
};
|
|
653
|
+
/**
|
|
654
|
+
* Map a primitive base type to Go type.
|
|
655
|
+
*/
|
|
175
656
|
mapPrimitive(type) {
|
|
176
657
|
const mapping = {
|
|
177
658
|
string: "string",
|
|
@@ -180,34 +661,240 @@ var GoTypeMapper = class {
|
|
|
180
661
|
boolean: "bool",
|
|
181
662
|
date: "time.Time",
|
|
182
663
|
uuid: "string",
|
|
183
|
-
email: "string"
|
|
664
|
+
email: "string",
|
|
665
|
+
any: "interface{}",
|
|
666
|
+
unknown: "interface{}"
|
|
667
|
+
};
|
|
668
|
+
const result = mapping[type];
|
|
669
|
+
if (!result) {
|
|
670
|
+
console.warn("[GoTypeMapper] Unknown primitive type:", type);
|
|
671
|
+
return "interface{}";
|
|
672
|
+
}
|
|
673
|
+
return result;
|
|
674
|
+
}
|
|
675
|
+
/**
|
|
676
|
+
* Get all registered tuple types that need struct generation
|
|
677
|
+
*/
|
|
678
|
+
getTupleTypes() {
|
|
679
|
+
return this.tupleTypes;
|
|
680
|
+
}
|
|
681
|
+
/**
|
|
682
|
+
* Get all registered union types that need wrapper struct generation
|
|
683
|
+
*/
|
|
684
|
+
getUnionTypes() {
|
|
685
|
+
return this.unionTypes;
|
|
686
|
+
}
|
|
687
|
+
/**
|
|
688
|
+
* Reset the type registries (call between generation runs)
|
|
689
|
+
*/
|
|
690
|
+
reset() {
|
|
691
|
+
super.reset();
|
|
692
|
+
this.tupleTypes.clear();
|
|
693
|
+
this.unionTypes.clear();
|
|
694
|
+
}
|
|
695
|
+
// --- Private handler methods ---
|
|
696
|
+
handleObject(ctx) {
|
|
697
|
+
const { typeRef, name } = ctx;
|
|
698
|
+
if (name || typeRef.name) {
|
|
699
|
+
return { type: toPascalCase4(name || typeRef.name) };
|
|
700
|
+
}
|
|
701
|
+
console.warn(
|
|
702
|
+
"[GoTypeMapper] Object without name - falling back to map[string]interface{}:",
|
|
703
|
+
typeRef
|
|
704
|
+
);
|
|
705
|
+
return { type: "map[string]interface{}" };
|
|
706
|
+
}
|
|
707
|
+
handleArray(ctx) {
|
|
708
|
+
const { typeRef } = ctx;
|
|
709
|
+
if (!typeRef.elementType) {
|
|
710
|
+
return { type: "[]interface{}" };
|
|
711
|
+
}
|
|
712
|
+
const element = this.mapType(typeRef.elementType);
|
|
713
|
+
return {
|
|
714
|
+
type: `[]${element.type}`,
|
|
715
|
+
imports: element.imports
|
|
716
|
+
};
|
|
717
|
+
}
|
|
718
|
+
handlePrimitive(ctx) {
|
|
719
|
+
const { typeRef } = ctx;
|
|
720
|
+
const baseType = typeof typeRef.baseType === "string" ? typeRef.baseType : "unknown";
|
|
721
|
+
const goType = this.mapPrimitive(baseType);
|
|
722
|
+
if (goType === "time.Time") {
|
|
723
|
+
return { type: goType, imports: ["time"] };
|
|
724
|
+
}
|
|
725
|
+
return { type: goType };
|
|
726
|
+
}
|
|
727
|
+
handleOptional(ctx) {
|
|
728
|
+
const { typeRef } = ctx;
|
|
729
|
+
if (typeof typeRef.baseType === "object") {
|
|
730
|
+
return this.mapType(typeRef.baseType);
|
|
731
|
+
}
|
|
732
|
+
if (typeof typeRef.baseType === "string") {
|
|
733
|
+
return { type: this.mapPrimitive(typeRef.baseType) };
|
|
734
|
+
}
|
|
735
|
+
console.warn("[GoTypeMapper] Optional with unknown baseType:", typeRef);
|
|
736
|
+
return { type: "interface{}" };
|
|
737
|
+
}
|
|
738
|
+
handleNullable(ctx) {
|
|
739
|
+
const { typeRef } = ctx;
|
|
740
|
+
if (typeof typeRef.baseType === "object") {
|
|
741
|
+
const base = this.mapType(typeRef.baseType);
|
|
742
|
+
return {
|
|
743
|
+
type: `*${base.type}`,
|
|
744
|
+
imports: base.imports
|
|
745
|
+
};
|
|
746
|
+
}
|
|
747
|
+
if (typeof typeRef.baseType === "string") {
|
|
748
|
+
return { type: `*${this.mapPrimitive(typeRef.baseType)}` };
|
|
749
|
+
}
|
|
750
|
+
console.warn("[GoTypeMapper] Nullable with unknown baseType:", typeRef);
|
|
751
|
+
return { type: "*interface{}" };
|
|
752
|
+
}
|
|
753
|
+
handleUnion(ctx) {
|
|
754
|
+
const { typeRef, name } = ctx;
|
|
755
|
+
const unionName = name || typeRef.name;
|
|
756
|
+
if (unionName) {
|
|
757
|
+
const goName = toPascalCase4(unionName);
|
|
758
|
+
this.unionTypes.set(goName, typeRef);
|
|
759
|
+
const variants = typeRef.unionTypes?.map((v) => this.mapType(v).type) ?? [];
|
|
760
|
+
const utility = createGoUnionPattern(goName, variants);
|
|
761
|
+
return {
|
|
762
|
+
type: goName,
|
|
763
|
+
utilities: [utility],
|
|
764
|
+
imports: utility.imports
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
if (typeRef.unionTypes && typeRef.unionTypes.length > 0) {
|
|
768
|
+
const variantTypes = typeRef.unionTypes.map((v) => this.mapType(v));
|
|
769
|
+
const allSameType = variantTypes.every(
|
|
770
|
+
(v) => v.type === variantTypes[0].type
|
|
771
|
+
);
|
|
772
|
+
if (allSameType) {
|
|
773
|
+
return variantTypes[0];
|
|
774
|
+
}
|
|
775
|
+
const nonNullVariants = typeRef.unionTypes.filter(
|
|
776
|
+
(v) => !(v.kind === "literal" && v.literalValue === null)
|
|
777
|
+
);
|
|
778
|
+
if (nonNullVariants.length === 1) {
|
|
779
|
+
const base = this.mapType(nonNullVariants[0]);
|
|
780
|
+
return {
|
|
781
|
+
type: `*${base.type}`,
|
|
782
|
+
imports: base.imports
|
|
783
|
+
};
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
console.warn(
|
|
787
|
+
"[GoTypeMapper] Anonymous heterogeneous union - using interface{}:",
|
|
788
|
+
typeRef
|
|
789
|
+
);
|
|
790
|
+
return { type: "interface{}" };
|
|
791
|
+
}
|
|
792
|
+
handleEnum(_ctx) {
|
|
793
|
+
return { type: "string" };
|
|
794
|
+
}
|
|
795
|
+
handleLiteral(ctx) {
|
|
796
|
+
const { typeRef } = ctx;
|
|
797
|
+
if (typeRef.literalValue === null) {
|
|
798
|
+
return { type: "interface{}" };
|
|
799
|
+
}
|
|
800
|
+
if (typeof typeRef.literalValue === "string") {
|
|
801
|
+
return { type: "string" };
|
|
802
|
+
}
|
|
803
|
+
if (typeof typeRef.literalValue === "number") {
|
|
804
|
+
return { type: "float64" };
|
|
805
|
+
}
|
|
806
|
+
if (typeof typeRef.literalValue === "boolean") {
|
|
807
|
+
return { type: "bool" };
|
|
808
|
+
}
|
|
809
|
+
console.warn("[GoTypeMapper] Unknown literal type:", typeRef);
|
|
810
|
+
return { type: "interface{}" };
|
|
811
|
+
}
|
|
812
|
+
handleRecord(ctx) {
|
|
813
|
+
const { typeRef } = ctx;
|
|
814
|
+
if (!typeRef.valueType) {
|
|
815
|
+
return { type: "map[string]interface{}" };
|
|
816
|
+
}
|
|
817
|
+
const value = this.mapType(typeRef.valueType);
|
|
818
|
+
return {
|
|
819
|
+
type: `map[string]${value.type}`,
|
|
820
|
+
imports: value.imports
|
|
821
|
+
};
|
|
822
|
+
}
|
|
823
|
+
handleTuple(ctx) {
|
|
824
|
+
const { typeRef, name } = ctx;
|
|
825
|
+
const tupleName = name || typeRef.name;
|
|
826
|
+
if (tupleName) {
|
|
827
|
+
const goName = toPascalCase4(tupleName);
|
|
828
|
+
this.tupleTypes.set(goName, typeRef);
|
|
829
|
+
const elements = typeRef.tupleElements?.map((e) => this.mapType(e).type) ?? [];
|
|
830
|
+
const utility = createGoTuplePattern(goName, elements);
|
|
831
|
+
return {
|
|
832
|
+
type: goName,
|
|
833
|
+
utilities: [utility],
|
|
834
|
+
imports: utility.imports
|
|
835
|
+
};
|
|
836
|
+
}
|
|
837
|
+
if (typeRef.tupleElements && typeRef.tupleElements.length > 0) {
|
|
838
|
+
const elementTypes = typeRef.tupleElements.map((e) => this.mapType(e));
|
|
839
|
+
const allSameType = elementTypes.every(
|
|
840
|
+
(e) => e.type === elementTypes[0].type
|
|
841
|
+
);
|
|
842
|
+
if (allSameType) {
|
|
843
|
+
return { type: `[]${elementTypes[0].type}` };
|
|
844
|
+
}
|
|
845
|
+
}
|
|
846
|
+
console.warn(
|
|
847
|
+
"[GoTypeMapper] Anonymous heterogeneous tuple - using []interface{}:",
|
|
848
|
+
typeRef
|
|
849
|
+
);
|
|
850
|
+
return { type: "[]interface{}" };
|
|
851
|
+
}
|
|
852
|
+
handleDate() {
|
|
853
|
+
return {
|
|
854
|
+
type: "time.Time",
|
|
855
|
+
imports: ["time"]
|
|
184
856
|
};
|
|
185
|
-
return mapping[type] || "interface{}";
|
|
186
857
|
}
|
|
187
858
|
};
|
|
188
859
|
|
|
189
860
|
// src/type-generator.ts
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
return fullName.split(".").map((part) => toPascalCase2(part)).join("");
|
|
861
|
+
function toMethodName2(fullName) {
|
|
862
|
+
return fullName.split(".").map((part) => toPascalCase5(part)).join("");
|
|
193
863
|
}
|
|
194
864
|
var GoTypeGenerator = class {
|
|
195
865
|
w;
|
|
196
866
|
typeMapper;
|
|
197
867
|
packageName;
|
|
868
|
+
generatedTypes = /* @__PURE__ */ new Set();
|
|
198
869
|
constructor(packageName = "server") {
|
|
199
870
|
this.w = new GoBuilder();
|
|
200
871
|
this.typeMapper = new GoTypeMapper();
|
|
201
872
|
this.packageName = packageName;
|
|
202
873
|
}
|
|
203
|
-
|
|
874
|
+
/**
|
|
875
|
+
* Generate Go types from a contract definition.
|
|
876
|
+
* @param contract - The contract definition (should have names assigned to inline types)
|
|
877
|
+
* @param collectedTypes - Optional pre-collected nested types from GoTypeCollector
|
|
878
|
+
*/
|
|
879
|
+
generateTypes(contract, collectedTypes) {
|
|
204
880
|
const w = this.w.reset();
|
|
881
|
+
this.typeMapper.reset();
|
|
882
|
+
this.generatedTypes.clear();
|
|
205
883
|
w.package(this.packageName).import("net/http");
|
|
206
884
|
this.generateContextType();
|
|
207
885
|
this.generateMiddlewareTypes();
|
|
208
886
|
for (const type of contract.types) {
|
|
209
887
|
this.generateType(type);
|
|
210
888
|
}
|
|
889
|
+
if (collectedTypes) {
|
|
890
|
+
for (const collected of collectedTypes) {
|
|
891
|
+
if (!this.generatedTypes.has(collected.name)) {
|
|
892
|
+
this.generateTypeFromReference(collected.name, collected.typeRef);
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
this.generateTupleTypes();
|
|
897
|
+
this.generateUnionTypes();
|
|
211
898
|
this.generateTypedHandlers(contract);
|
|
212
899
|
return w.toString();
|
|
213
900
|
}
|
|
@@ -215,21 +902,11 @@ var GoTypeGenerator = class {
|
|
|
215
902
|
this.w.struct("Context", (b) => {
|
|
216
903
|
b.l("Request *http.Request").l("ResponseWriter http.ResponseWriter").l("Data map[string]interface{}");
|
|
217
904
|
}).n();
|
|
218
|
-
this.w.comment("GetUserId retrieves userId from context if set by middleware").n().func("GetUserId(ctx *Context) (string, bool)", (b) => {
|
|
219
|
-
b.if('val, ok := ctx.Data["userId"].(string); ok', (b2) => {
|
|
220
|
-
b2.return("val, true");
|
|
221
|
-
});
|
|
222
|
-
b.return('"", false');
|
|
223
|
-
}).n();
|
|
224
|
-
this.w.comment("GetSessionId retrieves sessionId from context if set by middleware").n().func("GetSessionId(ctx *Context) (string, bool)", (b) => {
|
|
225
|
-
b.if('val, ok := ctx.Data["sessionId"].(string); ok', (b2) => {
|
|
226
|
-
b2.return("val, true");
|
|
227
|
-
});
|
|
228
|
-
b.return('"", false');
|
|
229
|
-
}).n();
|
|
230
905
|
}
|
|
231
906
|
generateMiddlewareTypes() {
|
|
232
|
-
this.w.comment(
|
|
907
|
+
this.w.comment(
|
|
908
|
+
"MiddlewareFunc is a function that processes a request and extends context"
|
|
909
|
+
).n().type("MiddlewareFunc", "func(ctx *Context) *MiddlewareResult").n();
|
|
233
910
|
this.w.struct("MiddlewareResult", (b) => {
|
|
234
911
|
b.l("Context *Context").l("Error error").l("Response *http.Response");
|
|
235
912
|
}).n();
|
|
@@ -239,38 +916,157 @@ var GoTypeGenerator = class {
|
|
|
239
916
|
this.w.comment("NewMiddlewareError creates a middleware result with an error").n().func("NewMiddlewareError(err error) *MiddlewareResult", (b) => {
|
|
240
917
|
b.return("&MiddlewareResult{Error: err}");
|
|
241
918
|
}).n();
|
|
242
|
-
this.w.comment(
|
|
243
|
-
|
|
244
|
-
|
|
919
|
+
this.w.comment(
|
|
920
|
+
"NewMiddlewareResponse creates a middleware result that short-circuits with a response"
|
|
921
|
+
).n().func(
|
|
922
|
+
"NewMiddlewareResponse(resp *http.Response) *MiddlewareResult",
|
|
923
|
+
(b) => {
|
|
924
|
+
b.return("&MiddlewareResult{Response: resp}");
|
|
925
|
+
}
|
|
926
|
+
).n();
|
|
245
927
|
}
|
|
246
928
|
generateType(type) {
|
|
247
|
-
const typeName =
|
|
929
|
+
const typeName = toPascalCase5(type.name);
|
|
930
|
+
if (this.generatedTypes.has(typeName)) {
|
|
931
|
+
return;
|
|
932
|
+
}
|
|
248
933
|
if (type.kind === "array" && type.elementType) {
|
|
249
934
|
if (type.elementType.kind === "object" && type.elementType.properties) {
|
|
250
|
-
const elementTypeName =
|
|
251
|
-
this.
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
b.l(`${toPascalCase2(prop.name)} ${goType} \`${jsonTag}\``);
|
|
256
|
-
}
|
|
257
|
-
});
|
|
935
|
+
const elementTypeName = type.elementType.name ? toPascalCase5(type.elementType.name) : `${typeName}Item`;
|
|
936
|
+
if (!this.generatedTypes.has(elementTypeName)) {
|
|
937
|
+
this.generateTypeFromReference(elementTypeName, type.elementType);
|
|
938
|
+
}
|
|
939
|
+
this.generatedTypes.add(typeName);
|
|
258
940
|
this.w.type(typeName, `[]${elementTypeName}`);
|
|
259
941
|
} else {
|
|
260
|
-
const elementGoType = this.typeMapper.mapType(type.elementType);
|
|
942
|
+
const elementGoType = this.typeMapper.mapType(type.elementType).type;
|
|
943
|
+
this.generatedTypes.add(typeName);
|
|
261
944
|
this.w.type(typeName, `[]${elementGoType}`);
|
|
262
945
|
}
|
|
263
946
|
return;
|
|
264
947
|
}
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
948
|
+
if (type.kind === "object") {
|
|
949
|
+
this.generatedTypes.add(typeName);
|
|
950
|
+
this.w.struct(typeName, (b) => {
|
|
951
|
+
if (type.properties) {
|
|
952
|
+
for (const prop of type.properties) {
|
|
953
|
+
const goType2 = this.typeMapper.mapType(prop.type).type;
|
|
954
|
+
const jsonTag = this.generateJSONTag(prop);
|
|
955
|
+
b.l(`${toPascalCase5(prop.name)} ${goType2} \`${jsonTag}\``);
|
|
956
|
+
}
|
|
957
|
+
}
|
|
958
|
+
});
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
if (type.kind === "union" || type.kind === "tuple") {
|
|
962
|
+
this.typeMapper.mapType(type);
|
|
963
|
+
return;
|
|
964
|
+
}
|
|
965
|
+
const goType = this.typeMapper.mapType(type).type;
|
|
966
|
+
this.generatedTypes.add(typeName);
|
|
967
|
+
this.w.type(typeName, goType);
|
|
968
|
+
}
|
|
969
|
+
/**
|
|
970
|
+
* Generate a type from a TypeReference (used for nested inline types)
|
|
971
|
+
*/
|
|
972
|
+
generateTypeFromReference(typeName, typeRef) {
|
|
973
|
+
if (this.generatedTypes.has(typeName)) {
|
|
974
|
+
return;
|
|
975
|
+
}
|
|
976
|
+
if (typeRef.kind === "object" && typeRef.properties) {
|
|
977
|
+
this.generatedTypes.add(typeName);
|
|
978
|
+
this.w.struct(typeName, (b) => {
|
|
979
|
+
for (const prop of typeRef.properties) {
|
|
980
|
+
const goType2 = this.typeMapper.mapType(prop.type).type;
|
|
269
981
|
const jsonTag = this.generateJSONTag(prop);
|
|
270
|
-
b.l(`${
|
|
982
|
+
b.l(`${toPascalCase5(prop.name)} ${goType2} \`${jsonTag}\``);
|
|
271
983
|
}
|
|
984
|
+
});
|
|
985
|
+
return;
|
|
986
|
+
}
|
|
987
|
+
if (typeRef.kind === "array" && typeRef.elementType) {
|
|
988
|
+
if (typeRef.elementType.kind === "object" && typeRef.elementType.properties) {
|
|
989
|
+
const elementTypeName = typeRef.elementType.name ? toPascalCase5(typeRef.elementType.name) : `${typeName}Item`;
|
|
990
|
+
if (!this.generatedTypes.has(elementTypeName)) {
|
|
991
|
+
this.generateTypeFromReference(elementTypeName, typeRef.elementType);
|
|
992
|
+
}
|
|
993
|
+
this.generatedTypes.add(typeName);
|
|
994
|
+
this.w.type(typeName, `[]${elementTypeName}`);
|
|
995
|
+
} else {
|
|
996
|
+
const elementGoType = this.typeMapper.mapType(typeRef.elementType).type;
|
|
997
|
+
this.generatedTypes.add(typeName);
|
|
998
|
+
this.w.type(typeName, `[]${elementGoType}`);
|
|
272
999
|
}
|
|
273
|
-
|
|
1000
|
+
return;
|
|
1001
|
+
}
|
|
1002
|
+
if (typeRef.kind === "union" || typeRef.kind === "tuple") {
|
|
1003
|
+
this.typeMapper.mapType(typeRef, { name: typeName });
|
|
1004
|
+
return;
|
|
1005
|
+
}
|
|
1006
|
+
const goType = this.typeMapper.mapType(typeRef).type;
|
|
1007
|
+
this.generatedTypes.add(typeName);
|
|
1008
|
+
this.w.type(typeName, goType);
|
|
1009
|
+
}
|
|
1010
|
+
/**
|
|
1011
|
+
* Generate struct types for tuples
|
|
1012
|
+
*/
|
|
1013
|
+
generateTupleTypes() {
|
|
1014
|
+
const tupleTypes = this.typeMapper.getTupleTypes();
|
|
1015
|
+
for (const [name, typeRef] of tupleTypes) {
|
|
1016
|
+
if (this.generatedTypes.has(name)) continue;
|
|
1017
|
+
this.generatedTypes.add(name);
|
|
1018
|
+
if (typeRef.tupleElements && typeRef.tupleElements.length > 0) {
|
|
1019
|
+
this.w.struct(name, (b) => {
|
|
1020
|
+
typeRef.tupleElements?.forEach((elem, index) => {
|
|
1021
|
+
const goType = this.typeMapper.mapType(elem).type;
|
|
1022
|
+
b.l(`V${index} ${goType} \`json:"v${index}"\``);
|
|
1023
|
+
});
|
|
1024
|
+
});
|
|
1025
|
+
}
|
|
1026
|
+
}
|
|
1027
|
+
}
|
|
1028
|
+
/**
|
|
1029
|
+
* Generate wrapper struct types for unions
|
|
1030
|
+
*/
|
|
1031
|
+
generateUnionTypes() {
|
|
1032
|
+
const unionTypes = this.typeMapper.getUnionTypes();
|
|
1033
|
+
for (const [name, typeRef] of unionTypes) {
|
|
1034
|
+
if (this.generatedTypes.has(name)) continue;
|
|
1035
|
+
this.generatedTypes.add(name);
|
|
1036
|
+
if (typeRef.unionTypes && typeRef.unionTypes.length > 0) {
|
|
1037
|
+
this.w.struct(name, (b) => {
|
|
1038
|
+
b.l('Type string `json:"type,omitempty"`');
|
|
1039
|
+
const seenTypes = /* @__PURE__ */ new Set();
|
|
1040
|
+
typeRef.unionTypes?.forEach((variant, index) => {
|
|
1041
|
+
const goType = this.typeMapper.mapType(variant).type;
|
|
1042
|
+
if (seenTypes.has(goType)) return;
|
|
1043
|
+
seenTypes.add(goType);
|
|
1044
|
+
const fieldName = this.getUnionFieldName(variant, index);
|
|
1045
|
+
b.l(
|
|
1046
|
+
`${fieldName} *${goType} \`json:"${fieldName.toLowerCase()},omitempty"\``
|
|
1047
|
+
);
|
|
1048
|
+
});
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
/**
|
|
1054
|
+
* Get a descriptive field name for a union variant
|
|
1055
|
+
*/
|
|
1056
|
+
getUnionFieldName(typeRef, index) {
|
|
1057
|
+
if (typeRef.name) {
|
|
1058
|
+
return toPascalCase5(typeRef.name);
|
|
1059
|
+
}
|
|
1060
|
+
if (typeRef.kind === "primitive" && typeof typeRef.baseType === "string") {
|
|
1061
|
+
return toPascalCase5(typeRef.baseType);
|
|
1062
|
+
}
|
|
1063
|
+
if (typeRef.kind === "literal") {
|
|
1064
|
+
const val = typeRef.literalValue;
|
|
1065
|
+
if (typeof val === "string") return `String${index}`;
|
|
1066
|
+
if (typeof val === "number") return `Number${index}`;
|
|
1067
|
+
if (typeof val === "boolean") return `Bool${index}`;
|
|
1068
|
+
}
|
|
1069
|
+
return `Variant${index}`;
|
|
274
1070
|
}
|
|
275
1071
|
generateJSONTag(prop) {
|
|
276
1072
|
if (prop.required) {
|
|
@@ -281,128 +1077,53 @@ var GoTypeGenerator = class {
|
|
|
281
1077
|
generateTypedHandlers(contract) {
|
|
282
1078
|
this.w.comment("Typed handler types for each endpoint").n();
|
|
283
1079
|
for (const endpoint of contract.endpoints) {
|
|
284
|
-
const handlerName =
|
|
285
|
-
const inputType =
|
|
286
|
-
const outputType =
|
|
287
|
-
this.w.comment(`Handler type for ${endpoint.fullName}`).type(
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
// src/server-generator.ts
|
|
293
|
-
import { toPascalCase as toPascalCase3 } from "@xrpckit/codegen";
|
|
294
|
-
function toMethodName2(fullName) {
|
|
295
|
-
return fullName.split(".").map((part) => toPascalCase3(part)).join("");
|
|
296
|
-
}
|
|
297
|
-
function toFieldName(fullName) {
|
|
298
|
-
return fullName.split(".").map((part, i) => i === 0 ? part : toPascalCase3(part)).join("");
|
|
299
|
-
}
|
|
300
|
-
var GoServerGenerator = class {
|
|
301
|
-
w;
|
|
302
|
-
packageName;
|
|
303
|
-
constructor(packageName = "server") {
|
|
304
|
-
this.w = new GoBuilder();
|
|
305
|
-
this.packageName = packageName;
|
|
306
|
-
}
|
|
307
|
-
generateServer(contract) {
|
|
308
|
-
const w = this.w.reset();
|
|
309
|
-
w.package(this.packageName).import("encoding/json", "net/http", "fmt");
|
|
310
|
-
w.struct("Router", (b) => {
|
|
311
|
-
b.l("middleware []MiddlewareFunc");
|
|
312
|
-
for (const endpoint of contract.endpoints) {
|
|
313
|
-
const fieldName = toFieldName(endpoint.fullName);
|
|
314
|
-
const handlerType = toMethodName2(endpoint.fullName) + "Handler";
|
|
315
|
-
b.l(`${fieldName} ${handlerType}`);
|
|
316
|
-
}
|
|
317
|
-
});
|
|
318
|
-
w.func("NewRouter() *Router", (b) => {
|
|
319
|
-
b.l("return &Router{").i().l("middleware: make([]MiddlewareFunc, 0),").u().l("}");
|
|
320
|
-
});
|
|
321
|
-
for (const endpoint of contract.endpoints) {
|
|
322
|
-
const methodName = toMethodName2(endpoint.fullName);
|
|
323
|
-
const fieldName = toFieldName(endpoint.fullName);
|
|
324
|
-
const handlerType = methodName + "Handler";
|
|
325
|
-
w.method("r *Router", methodName, `handler ${handlerType}`, "*Router", (b) => {
|
|
326
|
-
b.l(`r.${fieldName} = handler`).return("r");
|
|
327
|
-
});
|
|
1080
|
+
const handlerName = `${toMethodName2(endpoint.fullName)}Handler`;
|
|
1081
|
+
const inputType = toPascalCase5(endpoint.input.name);
|
|
1082
|
+
const outputType = toPascalCase5(endpoint.output.name);
|
|
1083
|
+
this.w.comment(`Handler type for ${endpoint.fullName}`).type(
|
|
1084
|
+
handlerName,
|
|
1085
|
+
`func(ctx *Context, input ${inputType}) (${outputType}, error)`
|
|
1086
|
+
).n();
|
|
328
1087
|
}
|
|
329
|
-
w.method("r *Router", "Use", "middleware MiddlewareFunc", "*Router", (b) => {
|
|
330
|
-
b.l("r.middleware = append(r.middleware, middleware)").return("r");
|
|
331
|
-
});
|
|
332
|
-
this.generateServeHTTP(contract.endpoints, w);
|
|
333
|
-
return w.toString();
|
|
334
|
-
}
|
|
335
|
-
generateServeHTTP(endpoints, w) {
|
|
336
|
-
w.method("r *Router", "ServeHTTP", "w http.ResponseWriter, req *http.Request", "", (b) => {
|
|
337
|
-
b.if("req.Method != http.MethodPost", (b2) => {
|
|
338
|
-
b2.l('http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)').return();
|
|
339
|
-
}).n();
|
|
340
|
-
b.var("request", "struct {").i().l('Method string `json:"method"`').l('Params json.RawMessage `json:"params"`').u().l("}").n();
|
|
341
|
-
b.if("err := json.NewDecoder(req.Body).Decode(&request); err != nil", (b2) => {
|
|
342
|
-
b2.l('http.Error(w, fmt.Sprintf("Invalid request: %v", err), http.StatusBadRequest)').return();
|
|
343
|
-
}).n();
|
|
344
|
-
b.decl("ctx", "&Context{").i().l("Request: req,").l("ResponseWriter: w,").l("Data: make(map[string]interface{}),").u().l("}").n();
|
|
345
|
-
b.comment("Execute middleware chain").l("for _, middleware := range r.middleware {").i().decl("result", "middleware(ctx)").if("result.Error != nil", (b2) => {
|
|
346
|
-
b2.l('http.Error(w, fmt.Sprintf("Middleware error: %v", result.Error), http.StatusInternalServerError)').return();
|
|
347
|
-
}).if("result.Response != nil", (b2) => {
|
|
348
|
-
b2.comment("Middleware short-circuited with response").return();
|
|
349
|
-
}).l("ctx = result.Context").u().l("}").n();
|
|
350
|
-
const cases = endpoints.map((endpoint) => ({
|
|
351
|
-
value: `"${endpoint.fullName}"`,
|
|
352
|
-
fn: (b2) => {
|
|
353
|
-
const fieldName = toFieldName(endpoint.fullName);
|
|
354
|
-
b2.if(`r.${fieldName} == nil`, (b3) => {
|
|
355
|
-
b3.l('http.Error(w, "Handler not registered", http.StatusNotFound)').return();
|
|
356
|
-
}).n();
|
|
357
|
-
const inputTypeName = toPascalCase3(endpoint.input.name);
|
|
358
|
-
b2.var("input", inputTypeName);
|
|
359
|
-
b2.if("err := json.Unmarshal(request.Params, &input); err != nil", (b3) => {
|
|
360
|
-
b3.l('http.Error(w, fmt.Sprintf("Invalid params: %v", err), http.StatusBadRequest)').return();
|
|
361
|
-
}).n();
|
|
362
|
-
const validationFuncName = `Validate${inputTypeName}`;
|
|
363
|
-
b2.if(`err := ${validationFuncName}(input); err != nil`, (b3) => {
|
|
364
|
-
b3.l('w.Header().Set("Content-Type", "application/json")').l("w.WriteHeader(http.StatusBadRequest)");
|
|
365
|
-
b3.l("if validationErrs, ok := err.(ValidationErrors); ok {").i().l("json.NewEncoder(w).Encode(map[string]interface{}{").i().l('"error": "Validation failed",').l('"errors": validationErrs,').u().l("})").u().l("} else {").i().l("json.NewEncoder(w).Encode(map[string]interface{}{").i().l('"error": err.Error(),').u().l("})").u().l("}");
|
|
366
|
-
b3.return();
|
|
367
|
-
}).n();
|
|
368
|
-
b2.decl("result, err", `r.${fieldName}(ctx, input)`);
|
|
369
|
-
b2.ifErr((b3) => {
|
|
370
|
-
b3.l('w.Header().Set("Content-Type", "application/json")').l('json.NewEncoder(w).Encode(map[string]interface{}{"error": err.Error()})').return();
|
|
371
|
-
}).n();
|
|
372
|
-
b2.l('w.Header().Set("Content-Type", "application/json")').l('json.NewEncoder(w).Encode(map[string]interface{}{"result": result})').return();
|
|
373
|
-
}
|
|
374
|
-
}));
|
|
375
|
-
w.switch("request.Method", cases, (b2) => {
|
|
376
|
-
b2.l('http.Error(w, "Method not found", http.StatusNotFound)').return();
|
|
377
|
-
});
|
|
378
|
-
});
|
|
379
1088
|
}
|
|
380
1089
|
};
|
|
381
1090
|
|
|
382
1091
|
// src/validation-generator.ts
|
|
383
|
-
import {
|
|
1092
|
+
import {
|
|
1093
|
+
toPascalCase as toPascalCase6
|
|
1094
|
+
} from "@xrpckit/sdk";
|
|
384
1095
|
var GoValidationGenerator = class {
|
|
385
1096
|
w;
|
|
386
1097
|
packageName;
|
|
1098
|
+
generatedValidations = /* @__PURE__ */ new Set();
|
|
387
1099
|
constructor(packageName = "server") {
|
|
388
1100
|
this.w = new GoBuilder();
|
|
389
1101
|
this.packageName = packageName;
|
|
390
1102
|
}
|
|
391
|
-
|
|
1103
|
+
/**
|
|
1104
|
+
* Generate Go validation functions from a contract definition.
|
|
1105
|
+
* @param contract - The contract definition (should have names assigned to inline types)
|
|
1106
|
+
* @param collectedTypes - Optional pre-collected nested types from GoTypeCollector
|
|
1107
|
+
*/
|
|
1108
|
+
generateValidation(contract, collectedTypes) {
|
|
392
1109
|
const w = this.w.reset();
|
|
1110
|
+
this.generatedValidations.clear();
|
|
393
1111
|
const imports = /* @__PURE__ */ new Set(["fmt", "strings"]);
|
|
394
1112
|
const needsRegex = this.hasValidationRule(
|
|
395
1113
|
contract,
|
|
396
|
-
|
|
1114
|
+
collectedTypes,
|
|
1115
|
+
(rules) => !!(rules.uuid || rules.regex && !rules.email)
|
|
397
1116
|
// regex but not email (email uses mail)
|
|
398
1117
|
);
|
|
399
1118
|
const needsMail = this.hasValidationRule(
|
|
400
1119
|
contract,
|
|
401
|
-
|
|
1120
|
+
collectedTypes,
|
|
1121
|
+
(rules) => !!rules.email
|
|
402
1122
|
);
|
|
403
1123
|
const needsURL = this.hasValidationRule(
|
|
404
1124
|
contract,
|
|
405
|
-
|
|
1125
|
+
collectedTypes,
|
|
1126
|
+
(rules) => !!rules.url
|
|
406
1127
|
);
|
|
407
1128
|
if (needsRegex) imports.add("regexp");
|
|
408
1129
|
if (needsMail) imports.add("net/mail");
|
|
@@ -416,7 +1137,7 @@ var GoValidationGenerator = class {
|
|
|
416
1137
|
if (type.kind === "object" && type.properties) {
|
|
417
1138
|
this.generateTypeValidation(type, w);
|
|
418
1139
|
} else if (type.kind === "array" && type.elementType?.kind === "object" && type.elementType.properties) {
|
|
419
|
-
const elementTypeName =
|
|
1140
|
+
const elementTypeName = type.elementType.name ? toPascalCase6(type.elementType.name) : `${toPascalCase6(type.name)}Item`;
|
|
420
1141
|
const elementType = {
|
|
421
1142
|
name: elementTypeName,
|
|
422
1143
|
kind: "object",
|
|
@@ -425,6 +1146,18 @@ var GoValidationGenerator = class {
|
|
|
425
1146
|
this.generateTypeValidation(elementType, w);
|
|
426
1147
|
}
|
|
427
1148
|
}
|
|
1149
|
+
if (collectedTypes) {
|
|
1150
|
+
for (const collected of collectedTypes) {
|
|
1151
|
+
if (collected.typeRef.kind === "object" && collected.typeRef.properties) {
|
|
1152
|
+
const typeDefinition = {
|
|
1153
|
+
name: collected.name,
|
|
1154
|
+
kind: "object",
|
|
1155
|
+
properties: collected.typeRef.properties
|
|
1156
|
+
};
|
|
1157
|
+
this.generateTypeValidation(typeDefinition, w);
|
|
1158
|
+
}
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
428
1161
|
this.generateHelperFunctions(w);
|
|
429
1162
|
return w.toString();
|
|
430
1163
|
}
|
|
@@ -443,13 +1176,17 @@ var GoValidationGenerator = class {
|
|
|
443
1176
|
}).n();
|
|
444
1177
|
}
|
|
445
1178
|
generateTypeValidation(type, w) {
|
|
446
|
-
const typeName =
|
|
1179
|
+
const typeName = toPascalCase6(type.name);
|
|
447
1180
|
const funcName = `Validate${typeName}`;
|
|
1181
|
+
if (this.generatedValidations.has(funcName)) {
|
|
1182
|
+
return;
|
|
1183
|
+
}
|
|
1184
|
+
this.generatedValidations.add(funcName);
|
|
448
1185
|
w.func(`${funcName}(input ${typeName}) error`, (b) => {
|
|
449
1186
|
b.var("errs", "ValidationErrors");
|
|
450
1187
|
if (type.properties) {
|
|
451
1188
|
for (const prop of type.properties) {
|
|
452
|
-
this.generatePropertyValidation(prop, "input",
|
|
1189
|
+
this.generatePropertyValidation(prop, "input", b);
|
|
453
1190
|
}
|
|
454
1191
|
}
|
|
455
1192
|
b.if("len(errs) > 0", (b2) => {
|
|
@@ -458,97 +1195,160 @@ var GoValidationGenerator = class {
|
|
|
458
1195
|
b.return("nil");
|
|
459
1196
|
}).n();
|
|
460
1197
|
}
|
|
461
|
-
generatePropertyValidation(prop, prefix,
|
|
462
|
-
const fieldName =
|
|
1198
|
+
generatePropertyValidation(prop, prefix, w) {
|
|
1199
|
+
const fieldName = toPascalCase6(prop.name);
|
|
463
1200
|
const fieldPath = `${prefix}.${fieldName}`;
|
|
464
1201
|
const fieldPathStr = prop.name;
|
|
465
|
-
const
|
|
1202
|
+
const unwrappedType = this.unwrapOptionalNullable(prop.type);
|
|
1203
|
+
const isPointerType = this.isPointerType(prop.type);
|
|
1204
|
+
if (isPointerType) {
|
|
1205
|
+
w.comment(`Validate ${prop.name} when present`);
|
|
1206
|
+
w.if(`${fieldPath} != nil`, (b) => {
|
|
1207
|
+
this.generatePropertyValidationForValue(
|
|
1208
|
+
prop,
|
|
1209
|
+
`*${fieldPath}`,
|
|
1210
|
+
fieldPathStr,
|
|
1211
|
+
unwrappedType,
|
|
1212
|
+
b,
|
|
1213
|
+
false
|
|
1214
|
+
);
|
|
1215
|
+
});
|
|
1216
|
+
return;
|
|
1217
|
+
}
|
|
1218
|
+
this.generatePropertyValidationForValue(
|
|
1219
|
+
prop,
|
|
1220
|
+
fieldPath,
|
|
1221
|
+
fieldPathStr,
|
|
1222
|
+
unwrappedType,
|
|
1223
|
+
w,
|
|
1224
|
+
prop.required
|
|
1225
|
+
);
|
|
1226
|
+
}
|
|
1227
|
+
generatePropertyValidationForValue(prop, valuePath, fieldPathStr, typeRef, w, isRequired) {
|
|
1228
|
+
const actualType = this.getActualType(typeRef);
|
|
466
1229
|
const isString = actualType === "string";
|
|
467
1230
|
const isNumber = actualType === "number";
|
|
468
|
-
const isArray =
|
|
469
|
-
|
|
1231
|
+
const isArray = typeRef.kind === "array";
|
|
1232
|
+
const enumValues = this.getEnumValues(typeRef);
|
|
1233
|
+
const isEnum = enumValues !== null;
|
|
1234
|
+
if (isRequired) {
|
|
470
1235
|
w.comment(`Validate ${prop.name}`);
|
|
471
|
-
if (isString) {
|
|
472
|
-
w.if(`${
|
|
473
|
-
b.l(
|
|
1236
|
+
if (isEnum || isString) {
|
|
1237
|
+
w.if(`${valuePath} == ""`, (b) => {
|
|
1238
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "is required",`).u().l("})");
|
|
474
1239
|
});
|
|
475
1240
|
} else if (isNumber) {
|
|
476
1241
|
} else if (isArray) {
|
|
477
|
-
w.if(`${
|
|
478
|
-
b.l(
|
|
1242
|
+
w.if(`${valuePath} == nil`, (b) => {
|
|
1243
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "is required",`).u().l("})");
|
|
479
1244
|
});
|
|
480
1245
|
}
|
|
481
1246
|
}
|
|
1247
|
+
if (isEnum && enumValues) {
|
|
1248
|
+
const enumValuesStr = enumValues.join(", ");
|
|
1249
|
+
const enumConditions = enumValues.map((v) => `${valuePath} != "${v}"`).join(" && ");
|
|
1250
|
+
w.if(`${valuePath} != "" && ${enumConditions}`, (b) => {
|
|
1251
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be one of: ${enumValuesStr}",`).u().l("})");
|
|
1252
|
+
});
|
|
1253
|
+
}
|
|
482
1254
|
const validationRules = prop.validation || prop.type.validation;
|
|
483
1255
|
if (validationRules) {
|
|
484
|
-
if (!
|
|
1256
|
+
if (!isRequired) {
|
|
485
1257
|
if (isString) {
|
|
486
|
-
w.if(`${
|
|
1258
|
+
w.if(`${valuePath} != ""`, (b) => {
|
|
487
1259
|
const validationTypeRef = {
|
|
488
1260
|
kind: "primitive",
|
|
489
1261
|
baseType: actualType
|
|
490
1262
|
};
|
|
491
|
-
this.generateValidationRules(
|
|
1263
|
+
this.generateValidationRules(
|
|
1264
|
+
validationRules,
|
|
1265
|
+
valuePath,
|
|
1266
|
+
fieldPathStr,
|
|
1267
|
+
validationTypeRef,
|
|
1268
|
+
b,
|
|
1269
|
+
false
|
|
1270
|
+
);
|
|
492
1271
|
});
|
|
493
1272
|
} else if (isNumber) {
|
|
494
1273
|
const validationTypeRef = {
|
|
495
1274
|
kind: "primitive",
|
|
496
1275
|
baseType: actualType
|
|
497
1276
|
};
|
|
498
|
-
this.generateValidationRules(
|
|
1277
|
+
this.generateValidationRules(
|
|
1278
|
+
validationRules,
|
|
1279
|
+
valuePath,
|
|
1280
|
+
fieldPathStr,
|
|
1281
|
+
validationTypeRef,
|
|
1282
|
+
w,
|
|
1283
|
+
false
|
|
1284
|
+
);
|
|
499
1285
|
} else if (isArray) {
|
|
500
|
-
w.if(`${
|
|
501
|
-
this.generateValidationRules(
|
|
1286
|
+
w.if(`${valuePath} != nil`, (b) => {
|
|
1287
|
+
this.generateValidationRules(
|
|
1288
|
+
validationRules,
|
|
1289
|
+
valuePath,
|
|
1290
|
+
fieldPathStr,
|
|
1291
|
+
typeRef,
|
|
1292
|
+
b,
|
|
1293
|
+
false
|
|
1294
|
+
);
|
|
502
1295
|
});
|
|
503
1296
|
} else {
|
|
504
|
-
this.generateValidationRules(
|
|
1297
|
+
this.generateValidationRules(
|
|
1298
|
+
validationRules,
|
|
1299
|
+
valuePath,
|
|
1300
|
+
fieldPathStr,
|
|
1301
|
+
typeRef,
|
|
1302
|
+
w,
|
|
1303
|
+
false
|
|
1304
|
+
);
|
|
505
1305
|
}
|
|
1306
|
+
} else if (isArray) {
|
|
1307
|
+
this.generateValidationRules(
|
|
1308
|
+
validationRules,
|
|
1309
|
+
valuePath,
|
|
1310
|
+
fieldPathStr,
|
|
1311
|
+
typeRef,
|
|
1312
|
+
w,
|
|
1313
|
+
true
|
|
1314
|
+
);
|
|
506
1315
|
} else {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
1316
|
+
const validationTypeRef = {
|
|
1317
|
+
kind: "primitive",
|
|
1318
|
+
baseType: actualType
|
|
1319
|
+
};
|
|
1320
|
+
this.generateValidationRules(
|
|
1321
|
+
validationRules,
|
|
1322
|
+
valuePath,
|
|
1323
|
+
fieldPathStr,
|
|
1324
|
+
validationTypeRef,
|
|
1325
|
+
w,
|
|
1326
|
+
true
|
|
1327
|
+
);
|
|
516
1328
|
}
|
|
517
1329
|
}
|
|
518
|
-
if (
|
|
519
|
-
const nestedTypeName =
|
|
1330
|
+
if (typeRef.kind === "object" && typeRef.name) {
|
|
1331
|
+
const nestedTypeName = toPascalCase6(typeRef.name);
|
|
520
1332
|
const nestedFuncName = `Validate${nestedTypeName}`;
|
|
521
|
-
w.if(
|
|
522
|
-
b.l(`if err := ${nestedFuncName}(${fieldPath}); err != nil {`).i().l(`if nestedErrs, ok := err.(ValidationErrors); ok {`).i().l(`errs = append(errs, nestedErrs...)`).u().l(`} else {`).i().l(`errs = append(errs, &ValidationError{`).i().l(`Field: "${fieldPathStr}",`).l(`Message: err.Error(),`).u().l(`})`).u().l(`}`).u().l(`}`);
|
|
523
|
-
});
|
|
524
|
-
} else if (prop.type.kind === "object" && prop.type.properties) {
|
|
525
|
-
w.if(`${fieldPath} != nil`, (b) => {
|
|
526
|
-
for (const nestedProp of prop.type.properties || []) {
|
|
527
|
-
const inlineType = {
|
|
528
|
-
name: "",
|
|
529
|
-
kind: "object",
|
|
530
|
-
properties: prop.type.properties
|
|
531
|
-
};
|
|
532
|
-
this.generatePropertyValidation(nestedProp, fieldPath, inlineType, b);
|
|
533
|
-
}
|
|
534
|
-
});
|
|
1333
|
+
w.l(`if err := ${nestedFuncName}(${valuePath}); err != nil {`).i().l("if nestedErrs, ok := err.(ValidationErrors); ok {").i().l("errs = append(errs, nestedErrs...)").u().l("} else {").i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l("Message: err.Error(),").u().l("})").u().l("}").u().l("}");
|
|
535
1334
|
}
|
|
536
|
-
if (
|
|
537
|
-
|
|
538
|
-
|
|
1335
|
+
if (typeRef.kind === "array" && typeRef.elementType) {
|
|
1336
|
+
const elementTypeRef = this.unwrapOptionalNullable(typeRef.elementType);
|
|
1337
|
+
if (elementTypeRef.kind === "object" && elementTypeRef.name) {
|
|
1338
|
+
const elementTypeName = toPascalCase6(elementTypeRef.name);
|
|
539
1339
|
const elementFuncName = `Validate${elementTypeName}`;
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
};
|
|
548
|
-
for (const nestedProp of prop.type.elementType.properties || []) {
|
|
549
|
-
this.generatePropertyValidation(nestedProp, "item", inlineType, w);
|
|
1340
|
+
const isElementPointer = this.isPointerType(typeRef.elementType);
|
|
1341
|
+
w.l(`for i, item := range ${valuePath} {`).i();
|
|
1342
|
+
if (isElementPointer) {
|
|
1343
|
+
w.l("if item == nil { continue }");
|
|
1344
|
+
w.l(`if err := ${elementFuncName}(*item); err != nil {`);
|
|
1345
|
+
} else {
|
|
1346
|
+
w.l(`if err := ${elementFuncName}(item); err != nil {`);
|
|
550
1347
|
}
|
|
551
|
-
w.
|
|
1348
|
+
w.i().l("if nestedErrs, ok := err.(ValidationErrors); ok {").i().l("for _, nestedErr := range nestedErrs {").i().l("errs = append(errs, &ValidationError{").i().l(
|
|
1349
|
+
`Field: fmt.Sprintf("${fieldPathStr}[%%d].%%s", i, nestedErr.Field),`
|
|
1350
|
+
).l("Message: nestedErr.Message,").u().l("})").u().l("}").u().l("}").u().l("}").u().l("}");
|
|
1351
|
+
w.u().l("}");
|
|
552
1352
|
}
|
|
553
1353
|
}
|
|
554
1354
|
}
|
|
@@ -557,169 +1357,570 @@ var GoValidationGenerator = class {
|
|
|
557
1357
|
if (rules.minLength !== void 0) {
|
|
558
1358
|
const minLengthCondition = isRequired ? `${fieldPath} != "" && len(${fieldPath}) < ${rules.minLength}` : `len(${fieldPath}) < ${rules.minLength}`;
|
|
559
1359
|
w.if(minLengthCondition, (b) => {
|
|
560
|
-
b.l(
|
|
1360
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(
|
|
1361
|
+
`Message: fmt.Sprintf("must be at least %d character(s)", ${rules.minLength}),`
|
|
1362
|
+
).u().l("})");
|
|
561
1363
|
});
|
|
562
1364
|
}
|
|
563
1365
|
if (rules.maxLength !== void 0) {
|
|
564
1366
|
w.if(`len(${fieldPath}) > ${rules.maxLength}`, (b) => {
|
|
565
|
-
b.l(
|
|
1367
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(
|
|
1368
|
+
`Message: fmt.Sprintf("must be at most %d character(s)", ${rules.maxLength}),`
|
|
1369
|
+
).u().l("})");
|
|
566
1370
|
});
|
|
567
1371
|
}
|
|
568
1372
|
if (rules.email) {
|
|
569
1373
|
if (isRequired) {
|
|
570
1374
|
w.if(`${fieldPath} != ""`, (b) => {
|
|
571
|
-
b.l(`if _, err := mail.ParseAddress(${fieldPath}); err != nil {`).i().l(
|
|
1375
|
+
b.l(`if _, err := mail.ParseAddress(${fieldPath}); err != nil {`).i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be a valid email address",`).u().l("})").u().l("}");
|
|
572
1376
|
});
|
|
573
1377
|
} else {
|
|
574
|
-
w.l(`if _, err := mail.ParseAddress(${fieldPath}); err != nil {`).i().l(
|
|
1378
|
+
w.l(`if _, err := mail.ParseAddress(${fieldPath}); err != nil {`).i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be a valid email address",`).u().l("})").u().l("}");
|
|
575
1379
|
}
|
|
576
1380
|
}
|
|
577
1381
|
if (rules.url) {
|
|
578
1382
|
if (isRequired) {
|
|
579
1383
|
w.if(`${fieldPath} != ""`, (b) => {
|
|
580
|
-
b.l(
|
|
1384
|
+
b.l(
|
|
1385
|
+
`if u, err := url.Parse(${fieldPath}); err != nil || u.Scheme == "" || u.Host == "" {`
|
|
1386
|
+
).i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be a valid URL",`).u().l("})").u().l("}");
|
|
581
1387
|
});
|
|
582
1388
|
} else {
|
|
583
|
-
w.l(
|
|
1389
|
+
w.l(
|
|
1390
|
+
`if u, err := url.Parse(${fieldPath}); err != nil || u.Scheme == "" || u.Host == "" {`
|
|
1391
|
+
).i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be a valid URL",`).u().l("})").u().l("}");
|
|
584
1392
|
}
|
|
585
1393
|
}
|
|
586
1394
|
if (rules.uuid) {
|
|
587
1395
|
if (isRequired) {
|
|
588
1396
|
w.if(`${fieldPath} != ""`, (b) => {
|
|
589
|
-
b.l(
|
|
1397
|
+
b.l(
|
|
1398
|
+
`matched, _ := regexp.MatchString("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", ${fieldPath})`
|
|
1399
|
+
).n().l("if !matched {").i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be a valid UUID",`).u().l("})").u().l("}");
|
|
590
1400
|
});
|
|
591
1401
|
} else {
|
|
592
|
-
w.l(
|
|
1402
|
+
w.l(
|
|
1403
|
+
`matched, _ := regexp.MatchString("^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$", ${fieldPath})`
|
|
1404
|
+
).n().l("if !matched {").i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be a valid UUID",`).u().l("})").u().l("}");
|
|
593
1405
|
}
|
|
594
1406
|
}
|
|
595
1407
|
if (rules.regex && !rules.email && !rules.url && !rules.uuid) {
|
|
596
1408
|
if (isRequired) {
|
|
597
1409
|
w.if(`${fieldPath} != ""`, (b) => {
|
|
598
|
-
const escapedRegex = rules.regex
|
|
599
|
-
b.l(
|
|
1410
|
+
const escapedRegex = rules.regex?.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1411
|
+
b.l(
|
|
1412
|
+
`matched, _ := regexp.MatchString("${escapedRegex}", ${fieldPath})`
|
|
1413
|
+
).n().l("if !matched {").i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must match the required pattern",`).u().l("})").u().l("}");
|
|
600
1414
|
});
|
|
601
1415
|
} else {
|
|
602
|
-
const escapedRegex = rules.regex
|
|
603
|
-
w.l(
|
|
1416
|
+
const escapedRegex = rules.regex?.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1417
|
+
w.l(
|
|
1418
|
+
`matched, _ := regexp.MatchString("${escapedRegex}", ${fieldPath})`
|
|
1419
|
+
).n().l("if !matched {").i().l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must match the required pattern",`).u().l("})").u().l("}");
|
|
604
1420
|
}
|
|
605
1421
|
}
|
|
606
1422
|
} else if (typeRef.baseType === "number") {
|
|
607
1423
|
if (rules.min !== void 0) {
|
|
608
1424
|
w.if(`${fieldPath} < ${rules.min}`, (b) => {
|
|
609
|
-
b.l(
|
|
1425
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: fmt.Sprintf("must be at least %v", ${rules.min}),`).u().l("})");
|
|
610
1426
|
});
|
|
611
1427
|
}
|
|
612
1428
|
if (rules.max !== void 0) {
|
|
613
1429
|
w.if(`${fieldPath} > ${rules.max}`, (b) => {
|
|
614
|
-
b.l(
|
|
1430
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: fmt.Sprintf("must be at most %v", ${rules.max}),`).u().l("})");
|
|
615
1431
|
});
|
|
616
1432
|
}
|
|
617
1433
|
if (rules.int) {
|
|
618
1434
|
w.if(`float64(${fieldPath}) != float64(int64(${fieldPath}))`, (b) => {
|
|
619
|
-
b.l(
|
|
1435
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be an integer",`).u().l("})");
|
|
620
1436
|
});
|
|
621
1437
|
}
|
|
622
1438
|
if (rules.positive) {
|
|
623
1439
|
w.if(`${fieldPath} <= 0`, (b) => {
|
|
624
|
-
b.l(
|
|
1440
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be positive",`).u().l("})");
|
|
625
1441
|
});
|
|
626
1442
|
}
|
|
627
1443
|
if (rules.negative) {
|
|
628
1444
|
w.if(`${fieldPath} >= 0`, (b) => {
|
|
629
|
-
b.l(
|
|
1445
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(`Message: "must be negative",`).u().l("})");
|
|
630
1446
|
});
|
|
631
1447
|
}
|
|
632
1448
|
} else if (typeRef.kind === "array") {
|
|
633
1449
|
const arrayCheckCondition = isRequired ? `${fieldPath} != nil` : `${fieldPath} != nil`;
|
|
634
1450
|
if (rules.minItems !== void 0) {
|
|
635
|
-
w.if(
|
|
636
|
-
|
|
637
|
-
|
|
1451
|
+
w.if(
|
|
1452
|
+
`${arrayCheckCondition} && len(${fieldPath}) < ${rules.minItems}`,
|
|
1453
|
+
(b) => {
|
|
1454
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(
|
|
1455
|
+
`Message: fmt.Sprintf("must have at least %d item(s)", ${rules.minItems}),`
|
|
1456
|
+
).u().l("})");
|
|
1457
|
+
}
|
|
1458
|
+
);
|
|
638
1459
|
}
|
|
639
1460
|
if (rules.maxItems !== void 0) {
|
|
640
|
-
w.if(
|
|
641
|
-
|
|
642
|
-
|
|
1461
|
+
w.if(
|
|
1462
|
+
`${arrayCheckCondition} && len(${fieldPath}) > ${rules.maxItems}`,
|
|
1463
|
+
(b) => {
|
|
1464
|
+
b.l("errs = append(errs, &ValidationError{").i().l(`Field: "${fieldPathStr}",`).l(
|
|
1465
|
+
`Message: fmt.Sprintf("must have at most %d item(s)", ${rules.maxItems}),`
|
|
1466
|
+
).u().l("})");
|
|
1467
|
+
}
|
|
1468
|
+
);
|
|
643
1469
|
}
|
|
644
1470
|
}
|
|
645
1471
|
}
|
|
646
|
-
hasValidationRule(contract, check) {
|
|
1472
|
+
hasValidationRule(contract, collectedTypes, check) {
|
|
647
1473
|
for (const type of contract.types) {
|
|
648
|
-
if (type.properties) {
|
|
649
|
-
|
|
650
|
-
if (prop.validation && check(prop.validation)) {
|
|
651
|
-
return true;
|
|
652
|
-
}
|
|
653
|
-
if (prop.type.properties) {
|
|
654
|
-
for (const nestedProp of prop.type.properties) {
|
|
655
|
-
if (nestedProp.validation && check(nestedProp.validation)) {
|
|
656
|
-
return true;
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
if (prop.type.elementType?.properties) {
|
|
661
|
-
for (const elemProp of prop.type.elementType.properties) {
|
|
662
|
-
if (elemProp.validation && check(elemProp.validation)) {
|
|
663
|
-
return true;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
}
|
|
667
|
-
}
|
|
1474
|
+
if (this.checkTypeForValidationRule(type.properties, check)) {
|
|
1475
|
+
return true;
|
|
668
1476
|
}
|
|
669
1477
|
if (type.elementType?.validation && check(type.elementType.validation)) {
|
|
670
1478
|
return true;
|
|
671
1479
|
}
|
|
1480
|
+
if (type.elementType?.properties && this.checkTypeForValidationRule(type.elementType.properties, check)) {
|
|
1481
|
+
return true;
|
|
1482
|
+
}
|
|
1483
|
+
}
|
|
1484
|
+
if (collectedTypes) {
|
|
1485
|
+
for (const collected of collectedTypes) {
|
|
1486
|
+
if (collected.typeRef.properties && this.checkTypeForValidationRule(collected.typeRef.properties, check)) {
|
|
1487
|
+
return true;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
672
1490
|
}
|
|
673
1491
|
return false;
|
|
674
1492
|
}
|
|
675
|
-
|
|
1493
|
+
checkTypeForValidationRule(properties, check) {
|
|
1494
|
+
if (!properties) return false;
|
|
1495
|
+
for (const prop of properties) {
|
|
1496
|
+
if (prop.validation && check(prop.validation)) {
|
|
1497
|
+
return true;
|
|
1498
|
+
}
|
|
1499
|
+
if (this.checkTypeRefForValidationRule(prop.type, check)) {
|
|
1500
|
+
return true;
|
|
1501
|
+
}
|
|
1502
|
+
}
|
|
1503
|
+
return false;
|
|
676
1504
|
}
|
|
677
|
-
|
|
1505
|
+
checkTypeRefForValidationRule(typeRef, check) {
|
|
1506
|
+
if (typeRef.validation && check(typeRef.validation)) {
|
|
1507
|
+
return true;
|
|
1508
|
+
}
|
|
1509
|
+
if (typeRef.properties && this.checkTypeForValidationRule(typeRef.properties, check)) {
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
if (typeRef.elementType) {
|
|
1513
|
+
if (typeRef.elementType.validation && check(typeRef.elementType.validation)) {
|
|
1514
|
+
return true;
|
|
1515
|
+
}
|
|
1516
|
+
if (typeRef.elementType.properties && this.checkTypeForValidationRule(typeRef.elementType.properties, check)) {
|
|
1517
|
+
return true;
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
if ((typeRef.kind === "optional" || typeRef.kind === "nullable") && typeof typeRef.baseType === "object") {
|
|
1521
|
+
if (this.checkTypeRefForValidationRule(typeRef.baseType, check)) {
|
|
1522
|
+
return true;
|
|
1523
|
+
}
|
|
1524
|
+
}
|
|
1525
|
+
return false;
|
|
1526
|
+
}
|
|
1527
|
+
generateHelperFunctions(_w) {
|
|
1528
|
+
}
|
|
1529
|
+
unwrapOptionalNullable(typeRef) {
|
|
678
1530
|
if (typeRef.kind === "optional" || typeRef.kind === "nullable") {
|
|
679
1531
|
if (typeRef.baseType) {
|
|
680
1532
|
if (typeof typeRef.baseType === "string") {
|
|
681
|
-
return typeRef.baseType;
|
|
1533
|
+
return { kind: "primitive", baseType: typeRef.baseType };
|
|
682
1534
|
}
|
|
683
|
-
return this.
|
|
1535
|
+
return this.unwrapOptionalNullable(typeRef.baseType);
|
|
684
1536
|
}
|
|
685
1537
|
}
|
|
686
|
-
if (
|
|
687
|
-
|
|
1538
|
+
if (typeRef.kind === "union" && typeRef.unionTypes) {
|
|
1539
|
+
const nonNullVariants = typeRef.unionTypes.filter(
|
|
1540
|
+
(variant) => !(variant.kind === "literal" && variant.literalValue === null)
|
|
1541
|
+
);
|
|
1542
|
+
if (nonNullVariants.length === 1) {
|
|
1543
|
+
return this.unwrapOptionalNullable(nonNullVariants[0]);
|
|
1544
|
+
}
|
|
688
1545
|
}
|
|
689
|
-
|
|
690
|
-
|
|
1546
|
+
return typeRef;
|
|
1547
|
+
}
|
|
1548
|
+
getActualType(typeRef) {
|
|
1549
|
+
const unwrapped = this.unwrapOptionalNullable(typeRef);
|
|
1550
|
+
if (unwrapped.kind === "primitive" && typeof unwrapped.baseType === "string") {
|
|
1551
|
+
return unwrapped.baseType;
|
|
1552
|
+
}
|
|
1553
|
+
if (typeof unwrapped.baseType === "string") {
|
|
1554
|
+
return unwrapped.baseType;
|
|
1555
|
+
}
|
|
1556
|
+
if (unwrapped.baseType) {
|
|
1557
|
+
return this.getActualType(unwrapped.baseType);
|
|
691
1558
|
}
|
|
692
1559
|
return "unknown";
|
|
693
1560
|
}
|
|
694
|
-
|
|
695
|
-
|
|
1561
|
+
getEnumValues(typeRef) {
|
|
1562
|
+
const unwrapped = this.unwrapOptionalNullable(typeRef);
|
|
1563
|
+
if (unwrapped.kind === "enum" && unwrapped.enumValues) {
|
|
1564
|
+
return unwrapped.enumValues.filter(
|
|
1565
|
+
(v) => typeof v === "string"
|
|
1566
|
+
);
|
|
1567
|
+
}
|
|
1568
|
+
return null;
|
|
1569
|
+
}
|
|
1570
|
+
isPointerType(typeRef) {
|
|
1571
|
+
if (typeRef.kind === "nullable") {
|
|
1572
|
+
return true;
|
|
1573
|
+
}
|
|
1574
|
+
if (typeRef.kind === "optional") {
|
|
1575
|
+
if (typeRef.baseType && typeof typeRef.baseType !== "string") {
|
|
1576
|
+
return this.isPointerType(typeRef.baseType);
|
|
1577
|
+
}
|
|
1578
|
+
}
|
|
1579
|
+
if (typeRef.kind === "union" && typeRef.unionTypes) {
|
|
1580
|
+
const nonNullVariants = typeRef.unionTypes.filter(
|
|
1581
|
+
(variant) => !(variant.kind === "literal" && variant.literalValue === null)
|
|
1582
|
+
);
|
|
1583
|
+
if (nonNullVariants.length === 1) {
|
|
1584
|
+
return true;
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
return false;
|
|
696
1588
|
}
|
|
697
1589
|
};
|
|
698
1590
|
|
|
699
1591
|
// src/generator.ts
|
|
700
|
-
var
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
1592
|
+
var support = {
|
|
1593
|
+
supportedTypes: [...TYPE_KINDS],
|
|
1594
|
+
supportedValidations: [...VALIDATION_KINDS],
|
|
1595
|
+
notes: [
|
|
1596
|
+
"Generates idiomatic Go code using standard library only",
|
|
1597
|
+
"Uses net/http for HTTP handling",
|
|
1598
|
+
"Uses encoding/json for JSON marshaling",
|
|
1599
|
+
"Validation uses net/mail for email, net/url for URLs, regexp for patterns"
|
|
1600
|
+
]
|
|
1601
|
+
};
|
|
1602
|
+
function getPackageName(options) {
|
|
1603
|
+
if (options && typeof options.packageName === "string" && options.packageName) {
|
|
1604
|
+
return options.packageName;
|
|
1605
|
+
}
|
|
1606
|
+
return "server";
|
|
1607
|
+
}
|
|
1608
|
+
function isNullableType(typeRef) {
|
|
1609
|
+
if (typeRef.kind === "nullable") {
|
|
1610
|
+
return true;
|
|
1611
|
+
}
|
|
1612
|
+
if (typeRef.kind === "optional" && typeof typeRef.baseType === "object") {
|
|
1613
|
+
return isNullableType(typeRef.baseType);
|
|
1614
|
+
}
|
|
1615
|
+
if (typeRef.kind === "union" && typeRef.unionTypes) {
|
|
1616
|
+
const nonNullVariants = typeRef.unionTypes.filter(
|
|
1617
|
+
(variant) => !(variant.kind === "literal" && variant.literalValue === null)
|
|
1618
|
+
);
|
|
1619
|
+
return nonNullVariants.length === 1;
|
|
1620
|
+
}
|
|
1621
|
+
return false;
|
|
1622
|
+
}
|
|
1623
|
+
function collectRequiredNullableFields(contract) {
|
|
1624
|
+
const results = /* @__PURE__ */ new Set();
|
|
1625
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1626
|
+
const collectFromProperties = (properties, parentName) => {
|
|
1627
|
+
for (const prop of properties) {
|
|
1628
|
+
const propPath = `${parentName}.${prop.name}`;
|
|
1629
|
+
if (prop.required && isNullableType(prop.type)) {
|
|
1630
|
+
results.add(propPath);
|
|
1631
|
+
}
|
|
1632
|
+
collectFromTypeRef(prop.type, `${parentName}${toPascalCase7(prop.name)}`);
|
|
1633
|
+
}
|
|
1634
|
+
};
|
|
1635
|
+
const collectFromTypeRef = (typeRef, contextName) => {
|
|
1636
|
+
if (typeRef.kind === "optional" || typeRef.kind === "nullable") {
|
|
1637
|
+
if (typeof typeRef.baseType === "object") {
|
|
1638
|
+
collectFromTypeRef(typeRef.baseType, contextName);
|
|
1639
|
+
}
|
|
1640
|
+
return;
|
|
1641
|
+
}
|
|
1642
|
+
if (typeRef.kind === "object" && typeRef.properties) {
|
|
1643
|
+
const typeName = typeRef.name ? toPascalCase7(typeRef.name) : contextName;
|
|
1644
|
+
collectFromProperties(typeRef.properties, typeName);
|
|
1645
|
+
return;
|
|
1646
|
+
}
|
|
1647
|
+
if (typeRef.kind === "array" && typeRef.elementType) {
|
|
1648
|
+
collectFromTypeRef(typeRef.elementType, `${contextName}Item`);
|
|
1649
|
+
return;
|
|
1650
|
+
}
|
|
1651
|
+
if (typeRef.kind === "record" && typeRef.valueType) {
|
|
1652
|
+
collectFromTypeRef(typeRef.valueType, `${contextName}Value`);
|
|
1653
|
+
return;
|
|
1654
|
+
}
|
|
1655
|
+
if (typeRef.kind === "tuple" && typeRef.tupleElements) {
|
|
1656
|
+
typeRef.tupleElements.forEach((elem, index) => {
|
|
1657
|
+
collectFromTypeRef(elem, `${contextName}V${index}`);
|
|
1658
|
+
});
|
|
1659
|
+
return;
|
|
1660
|
+
}
|
|
1661
|
+
if (typeRef.kind === "union" && typeRef.unionTypes) {
|
|
1662
|
+
typeRef.unionTypes.forEach((variant, index) => {
|
|
1663
|
+
collectFromTypeRef(variant, `${contextName}Variant${index}`);
|
|
1664
|
+
});
|
|
1665
|
+
}
|
|
1666
|
+
};
|
|
1667
|
+
const collectFromTypeDefinition = (typeDef, typeName) => {
|
|
1668
|
+
if (visited.has(typeName)) return;
|
|
1669
|
+
visited.add(typeName);
|
|
1670
|
+
if (typeDef.properties) {
|
|
1671
|
+
collectFromProperties(typeDef.properties, typeName);
|
|
1672
|
+
}
|
|
1673
|
+
if (typeDef.kind === "array" && typeDef.elementType) {
|
|
1674
|
+
collectFromTypeRef(typeDef.elementType, `${typeName}Item`);
|
|
1675
|
+
}
|
|
1676
|
+
};
|
|
1677
|
+
for (const type of contract.types) {
|
|
1678
|
+
collectFromTypeDefinition(type, toPascalCase7(type.name));
|
|
1679
|
+
}
|
|
1680
|
+
for (const endpoint of contract.endpoints) {
|
|
1681
|
+
collectFromTypeRef(endpoint.input, `${endpoint.fullName}.input`);
|
|
1682
|
+
collectFromTypeRef(endpoint.output, `${endpoint.fullName}.output`);
|
|
1683
|
+
}
|
|
1684
|
+
return Array.from(results);
|
|
1685
|
+
}
|
|
1686
|
+
function generateGoServer(input) {
|
|
1687
|
+
const { contract } = input;
|
|
1688
|
+
const diagnostics = validateSupport(contract, support, "go-server");
|
|
1689
|
+
const requiredNullableFields = collectRequiredNullableFields(contract);
|
|
1690
|
+
for (const field of requiredNullableFields) {
|
|
1691
|
+
diagnostics.push({
|
|
1692
|
+
severity: "warning",
|
|
1693
|
+
message: `Field "${field}" is required and nullable. Go cannot distinguish missing values from null, so validation only runs when the value is present.`
|
|
1694
|
+
});
|
|
1695
|
+
}
|
|
1696
|
+
const hasErrors = diagnostics.some((issue) => issue.severity === "error");
|
|
1697
|
+
if (hasErrors) {
|
|
1698
|
+
return { files: [], diagnostics };
|
|
1699
|
+
}
|
|
1700
|
+
const packageName = getPackageName(input.options);
|
|
1701
|
+
const typeCollector = new GoTypeCollector();
|
|
1702
|
+
const collectedTypes = typeCollector.collectTypes(contract);
|
|
1703
|
+
const typeGenerator = new GoTypeGenerator(packageName);
|
|
1704
|
+
const serverGenerator = new GoServerGenerator(packageName);
|
|
1705
|
+
const validationGenerator = new GoValidationGenerator(packageName);
|
|
1706
|
+
return {
|
|
1707
|
+
files: [
|
|
1708
|
+
{
|
|
1709
|
+
path: "types.go",
|
|
1710
|
+
content: typeGenerator.generateTypes(contract, collectedTypes)
|
|
1711
|
+
},
|
|
1712
|
+
{
|
|
1713
|
+
path: "router.go",
|
|
1714
|
+
content: serverGenerator.generateServer(contract)
|
|
1715
|
+
},
|
|
1716
|
+
{
|
|
1717
|
+
path: "validation.go",
|
|
1718
|
+
content: validationGenerator.generateValidation(
|
|
1719
|
+
contract,
|
|
1720
|
+
collectedTypes
|
|
1721
|
+
)
|
|
1722
|
+
}
|
|
1723
|
+
],
|
|
1724
|
+
diagnostics
|
|
1725
|
+
};
|
|
1726
|
+
}
|
|
1727
|
+
var goTarget = {
|
|
1728
|
+
name: "go-server",
|
|
1729
|
+
generate: generateGoServer
|
|
1730
|
+
};
|
|
1731
|
+
|
|
1732
|
+
// src/validation-mapper.ts
|
|
1733
|
+
import {
|
|
1734
|
+
ValidationMapperBase
|
|
1735
|
+
} from "@xrpckit/sdk";
|
|
1736
|
+
var GoValidationMapper = class extends ValidationMapperBase {
|
|
1737
|
+
/**
|
|
1738
|
+
* Complete mapping of all validation kinds to Go validation code.
|
|
1739
|
+
* TypeScript enforces exhaustiveness at compile time.
|
|
1740
|
+
*/
|
|
1741
|
+
validationMapping = {
|
|
1742
|
+
// String validations
|
|
1743
|
+
minLength: (ctx) => this.handleMinLength(ctx),
|
|
1744
|
+
maxLength: (ctx) => this.handleMaxLength(ctx),
|
|
1745
|
+
email: (ctx) => this.handleEmail(ctx),
|
|
1746
|
+
url: (ctx) => this.handleUrl(ctx),
|
|
1747
|
+
uuid: (ctx) => this.handleUuid(ctx),
|
|
1748
|
+
regex: (ctx) => this.handleRegex(ctx),
|
|
1749
|
+
// Number validations
|
|
1750
|
+
min: (ctx) => this.handleMin(ctx),
|
|
1751
|
+
max: (ctx) => this.handleMax(ctx),
|
|
1752
|
+
int: (ctx) => this.handleInt(ctx),
|
|
1753
|
+
positive: (ctx) => this.handlePositive(ctx),
|
|
1754
|
+
negative: (ctx) => this.handleNegative(ctx),
|
|
1755
|
+
// Array validations
|
|
1756
|
+
minItems: (ctx) => this.handleMinItems(ctx),
|
|
1757
|
+
maxItems: (ctx) => this.handleMaxItems(ctx)
|
|
1758
|
+
};
|
|
1759
|
+
// --- String validation handlers ---
|
|
1760
|
+
handleMinLength(ctx) {
|
|
1761
|
+
const { fieldPath, value, isRequired } = ctx;
|
|
1762
|
+
const condition = isRequired ? `${fieldPath} != "" && len(${fieldPath}) < ${value}` : `len(${fieldPath}) < ${value}`;
|
|
1763
|
+
return {
|
|
1764
|
+
validation: {
|
|
1765
|
+
condition,
|
|
1766
|
+
message: `fmt.Sprintf("must be at least %d character(s)", ${value})`,
|
|
1767
|
+
skipIfEmpty: !isRequired
|
|
1768
|
+
},
|
|
1769
|
+
imports: ["fmt"]
|
|
1770
|
+
};
|
|
1771
|
+
}
|
|
1772
|
+
handleMaxLength(ctx) {
|
|
1773
|
+
const { fieldPath, value } = ctx;
|
|
1774
|
+
return {
|
|
1775
|
+
validation: {
|
|
1776
|
+
condition: `len(${fieldPath}) > ${value}`,
|
|
1777
|
+
message: `fmt.Sprintf("must be at most %d character(s)", ${value})`
|
|
1778
|
+
},
|
|
1779
|
+
imports: ["fmt"]
|
|
1780
|
+
};
|
|
1781
|
+
}
|
|
1782
|
+
handleEmail(ctx) {
|
|
1783
|
+
const { fieldPath, isRequired } = ctx;
|
|
1784
|
+
const condition = `func() bool { _, err := mail.ParseAddress(${fieldPath}); return err != nil }()`;
|
|
1785
|
+
return {
|
|
1786
|
+
validation: {
|
|
1787
|
+
condition,
|
|
1788
|
+
message: `"must be a valid email address"`,
|
|
1789
|
+
skipIfEmpty: !isRequired
|
|
1790
|
+
},
|
|
1791
|
+
imports: ["net/mail"]
|
|
1792
|
+
};
|
|
1793
|
+
}
|
|
1794
|
+
handleUrl(ctx) {
|
|
1795
|
+
const { fieldPath, isRequired } = ctx;
|
|
1796
|
+
const condition = `func() bool { u, err := url.Parse(${fieldPath}); return err != nil || u.Scheme == "" || u.Host == "" }()`;
|
|
1797
|
+
return {
|
|
1798
|
+
validation: {
|
|
1799
|
+
condition,
|
|
1800
|
+
message: `"must be a valid URL"`,
|
|
1801
|
+
skipIfEmpty: !isRequired
|
|
1802
|
+
},
|
|
1803
|
+
imports: ["net/url"]
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
handleUuid(ctx) {
|
|
1807
|
+
const { fieldPath, isRequired } = ctx;
|
|
1808
|
+
const uuidRegex = "^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$";
|
|
1809
|
+
const condition = `func() bool { matched, _ := regexp.MatchString("${uuidRegex}", ${fieldPath}); return !matched }()`;
|
|
1810
|
+
return {
|
|
1811
|
+
validation: {
|
|
1812
|
+
condition,
|
|
1813
|
+
message: `"must be a valid UUID"`,
|
|
1814
|
+
skipIfEmpty: !isRequired
|
|
1815
|
+
},
|
|
1816
|
+
imports: ["regexp"]
|
|
1817
|
+
};
|
|
1818
|
+
}
|
|
1819
|
+
handleRegex(ctx) {
|
|
1820
|
+
const { fieldPath, value, isRequired, allRules } = ctx;
|
|
1821
|
+
if (allRules.email || allRules.url || allRules.uuid) {
|
|
1822
|
+
return {
|
|
1823
|
+
validation: {
|
|
1824
|
+
condition: "false",
|
|
1825
|
+
// Never triggers
|
|
1826
|
+
message: `""`
|
|
1827
|
+
// No message
|
|
1828
|
+
}
|
|
1829
|
+
};
|
|
1830
|
+
}
|
|
1831
|
+
const escapedRegex = String(value).replace(/\\/g, "\\\\").replace(/"/g, '\\"');
|
|
1832
|
+
const condition = `func() bool { matched, _ := regexp.MatchString("${escapedRegex}", ${fieldPath}); return !matched }()`;
|
|
1833
|
+
return {
|
|
1834
|
+
validation: {
|
|
1835
|
+
condition,
|
|
1836
|
+
message: `"must match the required pattern"`,
|
|
1837
|
+
skipIfEmpty: !isRequired
|
|
1838
|
+
},
|
|
1839
|
+
imports: ["regexp"]
|
|
1840
|
+
};
|
|
1841
|
+
}
|
|
1842
|
+
// --- Number validation handlers ---
|
|
1843
|
+
handleMin(ctx) {
|
|
1844
|
+
const { fieldPath, value } = ctx;
|
|
1845
|
+
return {
|
|
1846
|
+
validation: {
|
|
1847
|
+
condition: `${fieldPath} < ${value}`,
|
|
1848
|
+
message: `fmt.Sprintf("must be at least %v", ${value})`
|
|
1849
|
+
},
|
|
1850
|
+
imports: ["fmt"]
|
|
1851
|
+
};
|
|
1852
|
+
}
|
|
1853
|
+
handleMax(ctx) {
|
|
1854
|
+
const { fieldPath, value } = ctx;
|
|
1855
|
+
return {
|
|
1856
|
+
validation: {
|
|
1857
|
+
condition: `${fieldPath} > ${value}`,
|
|
1858
|
+
message: `fmt.Sprintf("must be at most %v", ${value})`
|
|
1859
|
+
},
|
|
1860
|
+
imports: ["fmt"]
|
|
1861
|
+
};
|
|
1862
|
+
}
|
|
1863
|
+
handleInt(ctx) {
|
|
1864
|
+
const { fieldPath } = ctx;
|
|
1865
|
+
return {
|
|
1866
|
+
validation: {
|
|
1867
|
+
condition: `float64(${fieldPath}) != float64(int64(${fieldPath}))`,
|
|
1868
|
+
message: `"must be an integer"`
|
|
1869
|
+
}
|
|
1870
|
+
};
|
|
1871
|
+
}
|
|
1872
|
+
handlePositive(ctx) {
|
|
1873
|
+
const { fieldPath } = ctx;
|
|
1874
|
+
return {
|
|
1875
|
+
validation: {
|
|
1876
|
+
condition: `${fieldPath} <= 0`,
|
|
1877
|
+
message: `"must be positive"`
|
|
1878
|
+
}
|
|
1879
|
+
};
|
|
1880
|
+
}
|
|
1881
|
+
handleNegative(ctx) {
|
|
1882
|
+
const { fieldPath } = ctx;
|
|
1883
|
+
return {
|
|
1884
|
+
validation: {
|
|
1885
|
+
condition: `${fieldPath} >= 0`,
|
|
1886
|
+
message: `"must be negative"`
|
|
1887
|
+
}
|
|
1888
|
+
};
|
|
1889
|
+
}
|
|
1890
|
+
// --- Array validation handlers ---
|
|
1891
|
+
handleMinItems(ctx) {
|
|
1892
|
+
const { fieldPath, value } = ctx;
|
|
1893
|
+
return {
|
|
1894
|
+
validation: {
|
|
1895
|
+
condition: `${fieldPath} != nil && len(${fieldPath}) < ${value}`,
|
|
1896
|
+
message: `fmt.Sprintf("must have at least %d item(s)", ${value})`
|
|
1897
|
+
},
|
|
1898
|
+
imports: ["fmt"]
|
|
1899
|
+
};
|
|
1900
|
+
}
|
|
1901
|
+
handleMaxItems(ctx) {
|
|
1902
|
+
const { fieldPath, value } = ctx;
|
|
712
1903
|
return {
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
1904
|
+
validation: {
|
|
1905
|
+
condition: `${fieldPath} != nil && len(${fieldPath}) > ${value}`,
|
|
1906
|
+
message: `fmt.Sprintf("must have at most %d item(s)", ${value})`
|
|
1907
|
+
},
|
|
1908
|
+
imports: ["fmt"]
|
|
716
1909
|
};
|
|
717
1910
|
}
|
|
718
1911
|
};
|
|
719
1912
|
export {
|
|
720
1913
|
GoBuilder,
|
|
721
|
-
GoCodeGenerator,
|
|
722
1914
|
GoServerGenerator,
|
|
1915
|
+
GoTypeCollector,
|
|
723
1916
|
GoTypeGenerator,
|
|
724
|
-
GoTypeMapper
|
|
1917
|
+
GoTypeMapper,
|
|
1918
|
+
GoValidationGenerator,
|
|
1919
|
+
GoValidationMapper,
|
|
1920
|
+
createGoBigIntPattern,
|
|
1921
|
+
createGoDatePattern,
|
|
1922
|
+
createGoEnumPattern,
|
|
1923
|
+
createGoTuplePattern,
|
|
1924
|
+
createGoUnionPattern,
|
|
1925
|
+
goTarget
|
|
725
1926
|
};
|