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 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 { code, warnings } = Codegen.generateModuleWithDiagnostics(schemas);
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
- // ─── Entry point ─────────────────────────────────────────────────────────────
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
- const options = parseArgs(process.argv.slice(2));
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
- err(` ${sym.error} ${c.boldRed("No input file specified")}`);
439
+ log(` ${c.dim("Mode:")} domain module generation`);
356
440
  blank();
357
- err(` ${c.dim("Usage:")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
358
- err(` ${c.dim("Help:")} osury ${c.cyan("--help")}`);
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
- generate(options.input, options.output);
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.17.3",
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 */
@@ -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 warnings = CodegenTransforms.collectUnionWarnings(schemas);
11
- let extractedUnions = schemas.flatMap(s => CodegenTransforms.extractUnions(s.name, s.schema).map(extracted => ({
12
- name: extracted.name,
13
- schema: extracted.schema,
14
- discriminatorTag: undefined
15
- })));
16
- let seen = {};
17
- let uniqueUnions = extractedUnions.filter(u => {
18
- if (Core__Option.isSome(seen[u.name])) {
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
- code: code,
47
- warnings: warnings
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.warnings.forEach(w => {
54
- console.log(w);
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 result.code;
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 */