osury 0.17.3 → 0.20.1
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/bin/osury.mjs +189 -9
- package/package.json +8 -1
- package/src/BackendReScript.res.mjs +144 -0
- package/src/Codegen.res.mjs +30 -41
- package/src/CodegenHelpers.res.mjs +26 -0
- package/src/CodegenTransforms.res.mjs +93 -1
- package/src/CodegenTypes.res.mjs +36 -14
- package/src/DomainBackend.res.mjs +41 -0
- package/src/DomainConfig.res.mjs +206 -0
- package/src/DomainGen.res.mjs +53 -0
- package/src/DomainIR.res.mjs +2 -0
- package/src/Errors.res.mjs +3 -0
- package/src/IR.res.mjs +2 -0
- package/src/IRGen.res.mjs +330 -0
- package/src/OpenAPIParser.res.mjs +92 -2
- package/src/Schema.res.mjs +219 -158
package/bin/osury.mjs
CHANGED
|
@@ -2,6 +2,9 @@
|
|
|
2
2
|
|
|
3
3
|
import * as OpenAPIParser from "../src/OpenAPIParser.res.mjs";
|
|
4
4
|
import * as Codegen from "../src/Codegen.res.mjs";
|
|
5
|
+
import * as DomainConfig from "../src/DomainConfig.res.mjs";
|
|
6
|
+
import * as DomainGen from "../src/DomainGen.res.mjs";
|
|
7
|
+
import * as DomainBackend from "../src/DomainBackend.res.mjs";
|
|
5
8
|
import fs from "fs";
|
|
6
9
|
import path from "path";
|
|
7
10
|
import { createRequire } from "module";
|
|
@@ -78,6 +81,7 @@ function printHelp() {
|
|
|
78
81
|
log(` ${c.bold("Usage")}`);
|
|
79
82
|
log(` ${c.cyan("$")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
|
|
80
83
|
log(` ${c.cyan("$")} osury generate ${c.cyan("<input.json>")} -o ${c.cyan("<output.res>")}`);
|
|
84
|
+
log(` ${c.cyan("$")} osury domain ${c.dim("[options]")}`);
|
|
81
85
|
blank();
|
|
82
86
|
log(` ${c.bold("Options")}`);
|
|
83
87
|
log(` ${c.cyan("-o")}, ${c.cyan("--output")} Output file path ${c.dim("(default: ./Generated.res)")}`);
|
|
@@ -85,10 +89,16 @@ function printHelp() {
|
|
|
85
89
|
log(` ${c.cyan("-v")}, ${c.cyan("--version")} Show version`);
|
|
86
90
|
log(` ${c.cyan("--no-color")} Disable colored output`);
|
|
87
91
|
blank();
|
|
92
|
+
log(` ${c.bold("Domain options")}`);
|
|
93
|
+
log(` ${c.cyan("--config")} Config file path ${c.dim("(default: domain.config.json)")}`);
|
|
94
|
+
log(` ${c.cyan("--api-module")} API module name ${c.dim("(default: Api)")}`);
|
|
95
|
+
log(` ${c.cyan("-o")} Output directory ${c.dim("(default: src/domains/)")}`);
|
|
96
|
+
blank();
|
|
88
97
|
log(` ${c.bold("Examples")}`);
|
|
89
98
|
log(` ${c.cyan("$")} osury openapi.json`);
|
|
90
99
|
log(` ${c.cyan("$")} osury openapi.json src/API.res`);
|
|
91
100
|
log(` ${c.cyan("$")} osury generate schema.json -o src/Schema.res`);
|
|
101
|
+
log(` ${c.cyan("$")} osury domain --config domain.config.json --api-module Api`);
|
|
92
102
|
blank();
|
|
93
103
|
}
|
|
94
104
|
|
|
@@ -193,6 +203,8 @@ function formatErrorKind(kind) {
|
|
|
193
203
|
return `Circular reference ${c.bold(`"${kind._0}"`)}`;
|
|
194
204
|
case "AmbiguousUnion":
|
|
195
205
|
return `Ambiguous union (anyOf/oneOf cannot be distinguished)`;
|
|
206
|
+
case "MissingDiscriminator":
|
|
207
|
+
return `Missing discriminator for union ${c.bold(`"${kind._0}"`)}`;
|
|
196
208
|
case "InvalidJson":
|
|
197
209
|
return `Invalid JSON: ${kind._0}`;
|
|
198
210
|
default:
|
|
@@ -301,7 +313,27 @@ function generate(inputPath, outputPath) {
|
|
|
301
313
|
log(` ${sym.success} Parsed ${c.bold(String(schemaCount))} schema${schemaCount !== 1 ? "s" : ""} from ${c.cyan(inputPath)}`);
|
|
302
314
|
|
|
303
315
|
// ── Generate code ──
|
|
304
|
-
const
|
|
316
|
+
const genResult = Codegen.generateModuleWithDiagnostics(schemas);
|
|
317
|
+
|
|
318
|
+
if (genResult.TAG !== "Ok") {
|
|
319
|
+
const errors = genResult._0;
|
|
320
|
+
const count = errors.length;
|
|
321
|
+
|
|
322
|
+
err(
|
|
323
|
+
` ${sym.error} ${c.boldRed(`${count} codegen error${count !== 1 ? "s" : ""}`)} in ${c.cyan(inputPath)}`
|
|
324
|
+
);
|
|
325
|
+
blank();
|
|
326
|
+
|
|
327
|
+
errors.forEach((error, i) => {
|
|
328
|
+
err(formatParseError(error, i));
|
|
329
|
+
if (i < errors.length - 1) blank();
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
blank();
|
|
333
|
+
process.exit(1);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
const { code, warnings } = genResult._0;
|
|
305
337
|
|
|
306
338
|
// ── Print warnings ──
|
|
307
339
|
if (warnings.length > 0) {
|
|
@@ -346,18 +378,166 @@ function generate(inputPath, outputPath) {
|
|
|
346
378
|
blank();
|
|
347
379
|
}
|
|
348
380
|
|
|
349
|
-
// ───
|
|
381
|
+
// ─── Domain arg parsing ─────────────────────────────────────────────────────
|
|
382
|
+
|
|
383
|
+
function parseDomainArgs(args) {
|
|
384
|
+
const options = {
|
|
385
|
+
config: "domain.config.json",
|
|
386
|
+
apiModule: "Api",
|
|
387
|
+
outputDir: "src/domains/",
|
|
388
|
+
};
|
|
389
|
+
|
|
390
|
+
let i = 0;
|
|
391
|
+
while (i < args.length) {
|
|
392
|
+
const arg = args[i];
|
|
393
|
+
|
|
394
|
+
if (arg === "-h" || arg === "--help") {
|
|
395
|
+
printHelp();
|
|
396
|
+
process.exit(0);
|
|
397
|
+
} else if (arg === "--config") {
|
|
398
|
+
i++;
|
|
399
|
+
if (i >= args.length) {
|
|
400
|
+
header();
|
|
401
|
+
err(` ${sym.error} ${c.boldRed("Missing value for --config")}`);
|
|
402
|
+
blank();
|
|
403
|
+
process.exit(1);
|
|
404
|
+
}
|
|
405
|
+
options.config = args[i];
|
|
406
|
+
} else if (arg === "--api-module") {
|
|
407
|
+
i++;
|
|
408
|
+
if (i >= args.length) {
|
|
409
|
+
header();
|
|
410
|
+
err(` ${sym.error} ${c.boldRed("Missing value for --api-module")}`);
|
|
411
|
+
blank();
|
|
412
|
+
process.exit(1);
|
|
413
|
+
}
|
|
414
|
+
options.apiModule = args[i];
|
|
415
|
+
} else if (arg === "-o" || arg === "--output") {
|
|
416
|
+
i++;
|
|
417
|
+
if (i >= args.length) {
|
|
418
|
+
header();
|
|
419
|
+
err(` ${sym.error} ${c.boldRed("Missing value for --output")}`);
|
|
420
|
+
blank();
|
|
421
|
+
process.exit(1);
|
|
422
|
+
}
|
|
423
|
+
options.outputDir = args[i];
|
|
424
|
+
} else if (arg === "--no-color") {
|
|
425
|
+
// already handled
|
|
426
|
+
}
|
|
427
|
+
i++;
|
|
428
|
+
}
|
|
350
429
|
|
|
351
|
-
|
|
430
|
+
return options;
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
// ─── Domain generate ────────────────────────────────────────────────────────
|
|
434
|
+
|
|
435
|
+
function generateDomain(options) {
|
|
436
|
+
const start = performance.now();
|
|
352
437
|
|
|
353
|
-
if (!options.input) {
|
|
354
438
|
header();
|
|
355
|
-
|
|
439
|
+
log(` ${c.dim("Mode:")} domain module generation`);
|
|
356
440
|
blank();
|
|
357
|
-
|
|
358
|
-
|
|
441
|
+
|
|
442
|
+
// ── Check config file exists ──
|
|
443
|
+
if (!fs.existsSync(options.config)) {
|
|
444
|
+
err(` ${sym.error} ${c.boldRed("Config not found:")} ${c.cyan(options.config)}`);
|
|
445
|
+
blank();
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// ── Read & parse config JSON ──
|
|
450
|
+
let configJson;
|
|
451
|
+
try {
|
|
452
|
+
const raw = fs.readFileSync(options.config, "utf8");
|
|
453
|
+
configJson = JSON.parse(raw);
|
|
454
|
+
} catch (e) {
|
|
455
|
+
err(` ${sym.error} ${c.boldRed("Invalid JSON")} in ${c.cyan(options.config)}`);
|
|
456
|
+
blank();
|
|
457
|
+
if (e instanceof SyntaxError) {
|
|
458
|
+
err(` ${c.red(e.message)}`);
|
|
459
|
+
} else {
|
|
460
|
+
err(` ${c.red(e.message)}`);
|
|
461
|
+
}
|
|
462
|
+
blank();
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// ── Parse domain config ──
|
|
467
|
+
const parseResult = DomainConfig.parse(configJson);
|
|
468
|
+
|
|
469
|
+
if (parseResult.TAG !== "Ok") {
|
|
470
|
+
const errors = parseResult._0;
|
|
471
|
+
const count = errors.length;
|
|
472
|
+
|
|
473
|
+
err(
|
|
474
|
+
` ${sym.error} ${c.boldRed(`${count} config error${count !== 1 ? "s" : ""}`)} in ${c.cyan(options.config)}`
|
|
475
|
+
);
|
|
476
|
+
blank();
|
|
477
|
+
|
|
478
|
+
errors.forEach((error, i) => {
|
|
479
|
+
err(formatParseError(error, i));
|
|
480
|
+
if (i < errors.length - 1) blank();
|
|
481
|
+
});
|
|
482
|
+
|
|
483
|
+
blank();
|
|
484
|
+
process.exit(1);
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
const config = parseResult._0;
|
|
488
|
+
const moduleCount = config.modules.length;
|
|
489
|
+
log(` ${sym.success} Parsed ${c.bold(String(moduleCount))} domain module${moduleCount !== 1 ? "s" : ""} from ${c.cyan(options.config)}`);
|
|
490
|
+
|
|
491
|
+
// ── Generate domain modules ──
|
|
492
|
+
const modules = DomainGen.generate(config, options.apiModule);
|
|
493
|
+
|
|
494
|
+
// ── Ensure output directory exists ──
|
|
495
|
+
if (!fs.existsSync(options.outputDir)) {
|
|
496
|
+
fs.mkdirSync(options.outputDir, { recursive: true });
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
// ── Write each module ──
|
|
500
|
+
const writtenFiles = [];
|
|
501
|
+
modules.forEach((mod) => {
|
|
502
|
+
const code = DomainBackend.printModule(mod);
|
|
503
|
+
const outputPath = path.join(options.outputDir, mod.output);
|
|
504
|
+
fs.writeFileSync(outputPath, code);
|
|
505
|
+
writtenFiles.push(outputPath);
|
|
506
|
+
});
|
|
507
|
+
|
|
508
|
+
// ── Success output ──
|
|
509
|
+
blank();
|
|
510
|
+
log(` ${sym.success} Generated ${c.bold(String(writtenFiles.length))} domain module${writtenFiles.length !== 1 ? "s" : ""}`);
|
|
511
|
+
blank();
|
|
512
|
+
log(` ${c.dim("Files written:")}`);
|
|
513
|
+
writtenFiles.forEach((f) => {
|
|
514
|
+
log(` ${sym.bullet} ${c.cyan(f)}`);
|
|
515
|
+
});
|
|
516
|
+
blank();
|
|
517
|
+
log(` ${c.dim(`Done in ${elapsed(start)}`)}`);
|
|
359
518
|
blank();
|
|
360
|
-
process.exit(1);
|
|
361
519
|
}
|
|
362
520
|
|
|
363
|
-
|
|
521
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
522
|
+
|
|
523
|
+
const rawArgs = process.argv.slice(2);
|
|
524
|
+
|
|
525
|
+
// Check for "domain" subcommand
|
|
526
|
+
if (rawArgs[0] === "domain") {
|
|
527
|
+
const domainOptions = parseDomainArgs(rawArgs.slice(1));
|
|
528
|
+
generateDomain(domainOptions);
|
|
529
|
+
} else {
|
|
530
|
+
const options = parseArgs(rawArgs);
|
|
531
|
+
|
|
532
|
+
if (!options.input) {
|
|
533
|
+
header();
|
|
534
|
+
err(` ${sym.error} ${c.boldRed("No input file specified")}`);
|
|
535
|
+
blank();
|
|
536
|
+
err(` ${c.dim("Usage:")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
|
|
537
|
+
err(` ${c.dim("Help:")} osury ${c.cyan("--help")}`);
|
|
538
|
+
blank();
|
|
539
|
+
process.exit(1);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
generate(options.input, options.output);
|
|
543
|
+
}
|
package/package.json
CHANGED
|
@@ -2,22 +2,29 @@
|
|
|
2
2
|
"name": "osury",
|
|
3
3
|
"type": "module",
|
|
4
4
|
"description": "Generate ReScript types with Sury schemas from OpenAPI specifications",
|
|
5
|
-
"version": "0.
|
|
5
|
+
"version": "0.20.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
8
|
"osury": "bin/osury.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
12
|
+
"src/BackendReScript.res.mjs",
|
|
12
13
|
"src/Codegen.res.mjs",
|
|
13
14
|
"src/CodegenHelpers.res.mjs",
|
|
14
15
|
"src/CodegenShims.res.mjs",
|
|
15
16
|
"src/CodegenTransforms.res.mjs",
|
|
16
17
|
"src/CodegenTypes.res.mjs",
|
|
17
18
|
"src/Errors.res.mjs",
|
|
19
|
+
"src/IR.res.mjs",
|
|
20
|
+
"src/IRGen.res.mjs",
|
|
18
21
|
"src/OpenAPIParser.res.mjs",
|
|
19
22
|
"src/Schema.res.mjs",
|
|
20
23
|
"src/Schema.gen.tsx",
|
|
24
|
+
"src/DomainConfig.res.mjs",
|
|
25
|
+
"src/DomainIR.res.mjs",
|
|
26
|
+
"src/DomainGen.res.mjs",
|
|
27
|
+
"src/DomainBackend.res.mjs",
|
|
21
28
|
"README.md"
|
|
22
29
|
],
|
|
23
30
|
"scripts": {
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Core__Array from "@rescript/core/src/Core__Array.res.mjs";
|
|
4
|
+
import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
|
|
5
|
+
|
|
6
|
+
function printPrimitive(p) {
|
|
7
|
+
switch (p) {
|
|
8
|
+
case "PString" :
|
|
9
|
+
return "string";
|
|
10
|
+
case "PFloat" :
|
|
11
|
+
return "float";
|
|
12
|
+
case "PInt" :
|
|
13
|
+
return "int";
|
|
14
|
+
case "PBool" :
|
|
15
|
+
return "bool";
|
|
16
|
+
case "PUnit" :
|
|
17
|
+
return "unit";
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function printType(t) {
|
|
22
|
+
if (typeof t !== "object") {
|
|
23
|
+
return "JSON.t";
|
|
24
|
+
}
|
|
25
|
+
switch (t.TAG) {
|
|
26
|
+
case "Primitive" :
|
|
27
|
+
return printPrimitive(t._0);
|
|
28
|
+
case "Option" :
|
|
29
|
+
return `option<` + printType(t._0) + `>`;
|
|
30
|
+
case "Nullable" :
|
|
31
|
+
return `Nullable.t<` + printType(t._0) + `>`;
|
|
32
|
+
case "Array" :
|
|
33
|
+
return `array<` + printType(t._0) + `>`;
|
|
34
|
+
case "Dict" :
|
|
35
|
+
return `Dict.t<` + printType(t._0) + `>`;
|
|
36
|
+
case "Named" :
|
|
37
|
+
return t._0;
|
|
38
|
+
case "Enum" :
|
|
39
|
+
let variants = t._0.map(v => `#` + v).join(" | ");
|
|
40
|
+
return `[` + variants + `]`;
|
|
41
|
+
case "InlineRecord" :
|
|
42
|
+
return printRecord(t._0);
|
|
43
|
+
case "InlineVariant" :
|
|
44
|
+
return printVariantCases(t._0);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function printField(field) {
|
|
49
|
+
let typeStr = printType(field.type_);
|
|
50
|
+
let fieldType = Core__Array.reduce(field.annotations, typeStr, (acc, ann) => {
|
|
51
|
+
if (typeof ann !== "object" && ann === "SNull") {
|
|
52
|
+
return `@s.null ` + acc;
|
|
53
|
+
} else {
|
|
54
|
+
return acc;
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
let asAttr = Core__Option.getOr(Core__Array.findMap(field.annotations, ann => {
|
|
58
|
+
if (typeof ann !== "object" || ann.TAG !== "As") {
|
|
59
|
+
return;
|
|
60
|
+
} else {
|
|
61
|
+
return `@as("` + ann._0 + `") `;
|
|
62
|
+
}
|
|
63
|
+
}), "");
|
|
64
|
+
return asAttr + field.name + `: ` + fieldType;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function printRecord(fields) {
|
|
68
|
+
if (fields.length === 0) {
|
|
69
|
+
return "{}";
|
|
70
|
+
}
|
|
71
|
+
let fieldStrs = fields.map(printField);
|
|
72
|
+
return `{\n ` + fieldStrs.join(",\n ") + `\n}`;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function printVariantCase(c) {
|
|
76
|
+
let payloadStr = printType(c.payload);
|
|
77
|
+
return c.tag + `(` + payloadStr + `)`;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function printVariantCases(cases) {
|
|
81
|
+
let caseStrs = cases.map(printVariantCase);
|
|
82
|
+
return `[` + caseStrs.map(c => `#` + c).join(" | ") + `]`;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
function printAnnotation(ann) {
|
|
86
|
+
if (typeof ann === "object") {
|
|
87
|
+
if (ann.TAG === "Tag") {
|
|
88
|
+
return `@tag("` + ann._0 + `")`;
|
|
89
|
+
} else {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
switch (ann) {
|
|
94
|
+
case "GenType" :
|
|
95
|
+
return "@genType";
|
|
96
|
+
case "Schema" :
|
|
97
|
+
return "@schema";
|
|
98
|
+
case "Unboxed" :
|
|
99
|
+
return "@unboxed";
|
|
100
|
+
case "SNull" :
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function printAnnotations(annotations) {
|
|
106
|
+
return Core__Array.filterMap(annotations, printAnnotation).join("\n");
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
function printTypeDef(typeDef) {
|
|
110
|
+
let annotations = printAnnotations(typeDef.annotations);
|
|
111
|
+
let fields = typeDef.kind;
|
|
112
|
+
let body;
|
|
113
|
+
switch (fields.TAG) {
|
|
114
|
+
case "RecordDef" :
|
|
115
|
+
body = printRecord(fields._0);
|
|
116
|
+
break;
|
|
117
|
+
case "VariantDef" :
|
|
118
|
+
body = fields._0.map(printVariantCase).join(" | ");
|
|
119
|
+
break;
|
|
120
|
+
case "AliasDef" :
|
|
121
|
+
body = printType(fields._0);
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
return annotations + `\ntype ` + typeDef.name + ` = ` + body;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
function print(module_) {
|
|
128
|
+
let typeDefs = module_.types.map(printTypeDef).join("\n\n");
|
|
129
|
+
return module_.preamble + "\n\n" + typeDefs;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
export {
|
|
133
|
+
printPrimitive,
|
|
134
|
+
printType,
|
|
135
|
+
printField,
|
|
136
|
+
printRecord,
|
|
137
|
+
printVariantCase,
|
|
138
|
+
printVariantCases,
|
|
139
|
+
printAnnotation,
|
|
140
|
+
printAnnotations,
|
|
141
|
+
printTypeDef,
|
|
142
|
+
print,
|
|
143
|
+
}
|
|
144
|
+
/* No side effect */
|
package/src/Codegen.res.mjs
CHANGED
|
@@ -1,59 +1,45 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as IRGen from "./IRGen.res.mjs";
|
|
4
|
+
import * as Errors from "./Errors.res.mjs";
|
|
3
5
|
import * as CodegenShims from "./CodegenShims.res.mjs";
|
|
4
6
|
import * as CodegenTypes from "./CodegenTypes.res.mjs";
|
|
5
|
-
import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
|
|
6
7
|
import * as CodegenHelpers from "./CodegenHelpers.res.mjs";
|
|
8
|
+
import * as BackendReScript from "./BackendReScript.res.mjs";
|
|
7
9
|
import * as CodegenTransforms from "./CodegenTransforms.res.mjs";
|
|
8
10
|
|
|
9
11
|
function generateModuleWithDiagnostics(schemas) {
|
|
10
|
-
let
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
let
|
|
18
|
-
|
|
19
|
-
return false;
|
|
20
|
-
} else {
|
|
21
|
-
seen[u.name] = true;
|
|
22
|
-
return true;
|
|
23
|
-
}
|
|
24
|
-
});
|
|
25
|
-
let modifiedSchemas = schemas.map(s => ({
|
|
26
|
-
name: s.name,
|
|
27
|
-
schema: CodegenTransforms.replaceUnions(s.name, s.schema),
|
|
28
|
-
discriminatorTag: s.discriminatorTag
|
|
29
|
-
}));
|
|
30
|
-
let allSchemas = uniqueUnions.concat(modifiedSchemas);
|
|
31
|
-
let schemasDict = {};
|
|
32
|
-
let tagsDict = {};
|
|
33
|
-
allSchemas.forEach(s => {
|
|
34
|
-
schemasDict[s.name] = s.schema;
|
|
35
|
-
let tag = s.discriminatorTag;
|
|
36
|
-
if (tag !== undefined) {
|
|
37
|
-
tagsDict[s.name] = tag;
|
|
38
|
-
return;
|
|
39
|
-
}
|
|
40
|
-
});
|
|
41
|
-
let sorted = CodegenTransforms.topologicalSort(allSchemas);
|
|
42
|
-
let skipSet = {};
|
|
43
|
-
let typeDefs = sorted.map(s => CodegenTypes.generateTypeDefWithSkipSet(s, skipSet, schemasDict, tagsDict)).join("\n\n");
|
|
44
|
-
let code = "module S = Sury\n\n" + typeDefs;
|
|
12
|
+
let irModule = IRGen.generate(schemas);
|
|
13
|
+
if (irModule.TAG !== "Ok") {
|
|
14
|
+
return {
|
|
15
|
+
TAG: "Error",
|
|
16
|
+
_0: irModule._0
|
|
17
|
+
};
|
|
18
|
+
}
|
|
19
|
+
let irModule$1 = irModule._0;
|
|
20
|
+
let code = BackendReScript.print(irModule$1);
|
|
45
21
|
return {
|
|
46
|
-
|
|
47
|
-
|
|
22
|
+
TAG: "Ok",
|
|
23
|
+
_0: {
|
|
24
|
+
code: code,
|
|
25
|
+
warnings: irModule$1.warnings
|
|
26
|
+
}
|
|
48
27
|
};
|
|
49
28
|
}
|
|
50
29
|
|
|
51
30
|
function generateModule(schemas) {
|
|
52
31
|
let result = generateModuleWithDiagnostics(schemas);
|
|
53
|
-
result.
|
|
54
|
-
|
|
32
|
+
if (result.TAG === "Ok") {
|
|
33
|
+
let result$1 = result._0;
|
|
34
|
+
result$1.warnings.forEach(w => {
|
|
35
|
+
console.log(w);
|
|
36
|
+
});
|
|
37
|
+
return result$1.code;
|
|
38
|
+
}
|
|
39
|
+
result._0.forEach(e => {
|
|
40
|
+
console.error(Errors.formatError(e));
|
|
55
41
|
});
|
|
56
|
-
return
|
|
42
|
+
return "";
|
|
57
43
|
}
|
|
58
44
|
|
|
59
45
|
let lcFirst = CodegenHelpers.lcFirst;
|
|
@@ -96,6 +82,8 @@ let buildSkipSchemaSet = CodegenTransforms.buildSkipSchemaSet;
|
|
|
96
82
|
|
|
97
83
|
let collectUnionWarnings = CodegenTransforms.collectUnionWarnings;
|
|
98
84
|
|
|
85
|
+
let validateUnionDiscriminators = CodegenTransforms.validateUnionDiscriminators;
|
|
86
|
+
|
|
99
87
|
let generateType = CodegenTypes.generateType;
|
|
100
88
|
|
|
101
89
|
let generateTypeDef = CodegenTypes.generateTypeDef;
|
|
@@ -135,6 +123,7 @@ export {
|
|
|
135
123
|
topologicalSort,
|
|
136
124
|
buildSkipSchemaSet,
|
|
137
125
|
collectUnionWarnings,
|
|
126
|
+
validateUnionDiscriminators,
|
|
138
127
|
generateType,
|
|
139
128
|
generateTypeDef,
|
|
140
129
|
generateTypeDefWithSkipSet,
|
|
@@ -120,6 +120,8 @@ function getTagForType(t) {
|
|
|
120
120
|
return "Bool";
|
|
121
121
|
case "Null" :
|
|
122
122
|
return "Null";
|
|
123
|
+
case "Unknown" :
|
|
124
|
+
return "Unknown";
|
|
123
125
|
}
|
|
124
126
|
} else {
|
|
125
127
|
switch (t._tag) {
|
|
@@ -170,6 +172,29 @@ function hasUnion(_schema) {
|
|
|
170
172
|
};
|
|
171
173
|
}
|
|
172
174
|
|
|
175
|
+
function hasUnknown(_schema) {
|
|
176
|
+
while (true) {
|
|
177
|
+
let schema = _schema;
|
|
178
|
+
if (typeof schema !== "object") {
|
|
179
|
+
return schema === "Unknown";
|
|
180
|
+
}
|
|
181
|
+
switch (schema._tag) {
|
|
182
|
+
case "Object" :
|
|
183
|
+
return schema._0.some(f => hasUnknown(f.type));
|
|
184
|
+
case "PolyVariant" :
|
|
185
|
+
return schema._0.some(c => hasUnknown(c.payload));
|
|
186
|
+
case "Optional" :
|
|
187
|
+
case "Nullable" :
|
|
188
|
+
case "Array" :
|
|
189
|
+
case "Dict" :
|
|
190
|
+
_schema = schema._0;
|
|
191
|
+
continue;
|
|
192
|
+
default:
|
|
193
|
+
return false;
|
|
194
|
+
}
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
173
198
|
function isPrimitiveOnlyUnion(types) {
|
|
174
199
|
return types.every(t => {
|
|
175
200
|
if (typeof t === "object") {
|
|
@@ -196,6 +221,7 @@ export {
|
|
|
196
221
|
isNullableType,
|
|
197
222
|
getTagForType,
|
|
198
223
|
hasUnion,
|
|
224
|
+
hasUnknown,
|
|
199
225
|
isPrimitiveOnlyUnion,
|
|
200
226
|
}
|
|
201
227
|
/* No side effect */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
+
import * as Errors from "./Errors.res.mjs";
|
|
3
4
|
import * as Core__Array from "@rescript/core/src/Core__Array.res.mjs";
|
|
4
5
|
import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
|
|
5
6
|
import * as CodegenHelpers from "./CodegenHelpers.res.mjs";
|
|
@@ -88,6 +89,8 @@ function getUnionName(types) {
|
|
|
88
89
|
return "bool";
|
|
89
90
|
case "Null" :
|
|
90
91
|
return "null";
|
|
92
|
+
default:
|
|
93
|
+
return "unknown";
|
|
91
94
|
}
|
|
92
95
|
} else {
|
|
93
96
|
switch (t._tag) {
|
|
@@ -330,7 +333,7 @@ function topologicalSort(schemas) {
|
|
|
330
333
|
function buildSkipSchemaSet(schemas) {
|
|
331
334
|
let skipSet = {};
|
|
332
335
|
schemas.forEach(s => {
|
|
333
|
-
if (CodegenHelpers.hasUnion(s.schema)) {
|
|
336
|
+
if (CodegenHelpers.hasUnion(s.schema) || CodegenHelpers.hasUnknown(s.schema)) {
|
|
334
337
|
skipSet[s.name] = true;
|
|
335
338
|
return;
|
|
336
339
|
}
|
|
@@ -404,6 +407,94 @@ function collectUnionWarnings(schemas) {
|
|
|
404
407
|
return warnings;
|
|
405
408
|
}
|
|
406
409
|
|
|
410
|
+
function validateUnionDiscriminators(schemas) {
|
|
411
|
+
let seen = {};
|
|
412
|
+
let errors = [];
|
|
413
|
+
let schemasDict = {};
|
|
414
|
+
schemas.forEach(s => {
|
|
415
|
+
schemasDict[s.name] = s.schema;
|
|
416
|
+
});
|
|
417
|
+
let tagsDict = {};
|
|
418
|
+
schemas.forEach(s => {
|
|
419
|
+
let tag = s.discriminatorTag;
|
|
420
|
+
if (tag !== undefined) {
|
|
421
|
+
tagsDict[s.name] = tag;
|
|
422
|
+
return;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
let fieldDiscsDict = {};
|
|
426
|
+
schemas.forEach(s => {
|
|
427
|
+
let dict = s.fieldDiscriminators;
|
|
428
|
+
if (dict !== undefined) {
|
|
429
|
+
Object.entries(dict).forEach(param => {
|
|
430
|
+
fieldDiscsDict[param[0]] = param[1];
|
|
431
|
+
});
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
let findUnions = _schema => {
|
|
436
|
+
while (true) {
|
|
437
|
+
let schema = _schema;
|
|
438
|
+
if (typeof schema !== "object") {
|
|
439
|
+
return [];
|
|
440
|
+
}
|
|
441
|
+
switch (schema._tag) {
|
|
442
|
+
case "Object" :
|
|
443
|
+
return schema._0.flatMap(f => findUnions(f.type));
|
|
444
|
+
case "Optional" :
|
|
445
|
+
case "Nullable" :
|
|
446
|
+
case "Array" :
|
|
447
|
+
case "Dict" :
|
|
448
|
+
_schema = schema._0;
|
|
449
|
+
continue;
|
|
450
|
+
case "Union" :
|
|
451
|
+
return [schema._0];
|
|
452
|
+
default:
|
|
453
|
+
return [];
|
|
454
|
+
}
|
|
455
|
+
};
|
|
456
|
+
};
|
|
457
|
+
schemas.forEach(s => {
|
|
458
|
+
let unions = findUnions(s.schema);
|
|
459
|
+
unions.forEach(types => {
|
|
460
|
+
let unionName = getUnionName(types);
|
|
461
|
+
if (!Core__Option.isNone(seen[unionName])) {
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
seen[unionName] = true;
|
|
465
|
+
if (CodegenHelpers.isPrimitiveOnlyUnion(types)) {
|
|
466
|
+
return;
|
|
467
|
+
}
|
|
468
|
+
let match = isRefPlusDictUnion(types);
|
|
469
|
+
if (match !== undefined) {
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
let match$1 = isPrimitivePlusDictUnion(types);
|
|
473
|
+
if (match$1 !== undefined) {
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
if (!Core__Option.isNone(fieldDiscsDict[unionName])) {
|
|
477
|
+
return;
|
|
478
|
+
}
|
|
479
|
+
let allRefsHaveTags = types.every(t => {
|
|
480
|
+
if (typeof t !== "object" || t._tag !== "Ref") {
|
|
481
|
+
return true;
|
|
482
|
+
} else {
|
|
483
|
+
return Core__Option.isSome(tagsDict[t._0]);
|
|
484
|
+
}
|
|
485
|
+
});
|
|
486
|
+
if (!allRefsHaveTags) {
|
|
487
|
+
errors.push(Errors.makeError({
|
|
488
|
+
TAG: "MissingDiscriminator",
|
|
489
|
+
_0: unionName
|
|
490
|
+
}, undefined, "Add discriminator: { propertyName: \"type\" } to the anyOf/oneOf schema, or use the _tag convention with const values", undefined));
|
|
491
|
+
return;
|
|
492
|
+
}
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
return errors;
|
|
496
|
+
}
|
|
497
|
+
|
|
407
498
|
export {
|
|
408
499
|
isRefPlusDictUnion,
|
|
409
500
|
isPrimitivePlusDictUnion,
|
|
@@ -416,5 +507,6 @@ export {
|
|
|
416
507
|
topologicalSort,
|
|
417
508
|
buildSkipSchemaSet,
|
|
418
509
|
collectUnionWarnings,
|
|
510
|
+
validateUnionDiscriminators,
|
|
419
511
|
}
|
|
420
512
|
/* No side effect */
|