osury 0.16.0 → 0.17.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/README.md +19 -0
- package/bin/osury.mjs +306 -61
- package/package.json +4 -3
- package/src/Codegen.res.mjs +87 -782
package/README.md
CHANGED
|
@@ -156,6 +156,25 @@ export type user = {
|
|
|
156
156
|
|
|
157
157
|
The library uses **sury-ppx** for code-first approach — `@schema` annotation automatically generates runtime validators from type definitions.
|
|
158
158
|
|
|
159
|
+
## Demo Playground
|
|
160
|
+
|
|
161
|
+
This repository includes a local demo website for codegen exploration.
|
|
162
|
+
|
|
163
|
+
### Run demo
|
|
164
|
+
|
|
165
|
+
```bash
|
|
166
|
+
npm run demo
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Open [http://localhost:4173/demo/](http://localhost:4173/demo/).
|
|
170
|
+
|
|
171
|
+
### What it supports
|
|
172
|
+
|
|
173
|
+
- Upload OpenAPI JSON as a file
|
|
174
|
+
- Paste OpenAPI JSON into a text area
|
|
175
|
+
- Formatted ReScript output
|
|
176
|
+
- Formatted TypeScript output (derived from osury AST and matching generated ReScript structures)
|
|
177
|
+
|
|
159
178
|
### Helper Files
|
|
160
179
|
|
|
161
180
|
Also generates helper files:
|
package/bin/osury.mjs
CHANGED
|
@@ -4,37 +4,98 @@ import * as OpenAPIParser from "../src/OpenAPIParser.res.mjs";
|
|
|
4
4
|
import * as Codegen from "../src/Codegen.res.mjs";
|
|
5
5
|
import fs from "fs";
|
|
6
6
|
import path from "path";
|
|
7
|
+
import { createRequire } from "module";
|
|
8
|
+
import { performance } from "perf_hooks";
|
|
7
9
|
|
|
8
|
-
|
|
10
|
+
// ─── Package info ────────────────────────────────────────────────────────────
|
|
11
|
+
|
|
12
|
+
const require = createRequire(import.meta.url);
|
|
13
|
+
const pkg = require("../package.json");
|
|
14
|
+
const VERSION = pkg.version;
|
|
15
|
+
|
|
16
|
+
// ─── Color support ───────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
const noColor =
|
|
19
|
+
process.env.NO_COLOR != null ||
|
|
20
|
+
process.argv.includes("--no-color") ||
|
|
21
|
+
!process.stderr.isTTY;
|
|
22
|
+
|
|
23
|
+
const fmt = (code, text) =>
|
|
24
|
+
noColor ? text : `\x1b[${code}m${text}\x1b[0m`;
|
|
25
|
+
|
|
26
|
+
const c = {
|
|
27
|
+
bold: (t) => fmt("1", t),
|
|
28
|
+
dim: (t) => fmt("2", t),
|
|
29
|
+
italic: (t) => fmt("3", t),
|
|
30
|
+
red: (t) => fmt("31", t),
|
|
31
|
+
green: (t) => fmt("32", t),
|
|
32
|
+
yellow: (t) => fmt("33", t),
|
|
33
|
+
blue: (t) => fmt("34", t),
|
|
34
|
+
magenta: (t) => fmt("35", t),
|
|
35
|
+
cyan: (t) => fmt("36", t),
|
|
36
|
+
white: (t) => fmt("37", t),
|
|
37
|
+
gray: (t) => fmt("90", t),
|
|
38
|
+
boldRed: (t) => fmt("1;31", t),
|
|
39
|
+
boldGreen: (t) => fmt("1;32", t),
|
|
40
|
+
boldYellow: (t) => fmt("1;33", t),
|
|
41
|
+
boldCyan: (t) => fmt("1;36", t),
|
|
42
|
+
};
|
|
43
|
+
|
|
44
|
+
// ─── Symbols ─────────────────────────────────────────────────────────────────
|
|
45
|
+
|
|
46
|
+
const sym = {
|
|
47
|
+
success: c.green("✓"),
|
|
48
|
+
error: c.red("✗"),
|
|
49
|
+
warning: c.yellow("⚠"),
|
|
50
|
+
arrow: c.dim("→"),
|
|
51
|
+
bullet: c.dim("·"),
|
|
52
|
+
bar: c.dim("│"),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// ─── Output helpers ──────────────────────────────────────────────────────────
|
|
56
|
+
|
|
57
|
+
const log = (...args) => console.log(...args);
|
|
58
|
+
const err = (...args) => console.error(...args);
|
|
59
|
+
const blank = () => log();
|
|
60
|
+
|
|
61
|
+
function header() {
|
|
62
|
+
blank();
|
|
63
|
+
log(` ${c.bold("osury")} ${c.dim(`v${VERSION}`)}`);
|
|
64
|
+
log(` ${c.dim("OpenAPI 3.x → ReScript + Sury")}`);
|
|
65
|
+
blank();
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function elapsed(startMs) {
|
|
69
|
+
const ms = performance.now() - startMs;
|
|
70
|
+
if (ms < 1000) return `${Math.round(ms)}ms`;
|
|
71
|
+
return `${(ms / 1000).toFixed(2)}s`;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// ─── Help ────────────────────────────────────────────────────────────────────
|
|
9
75
|
|
|
10
76
|
function printHelp() {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
osury openapi.json
|
|
28
|
-
osury openapi.json src/API.res
|
|
29
|
-
osury generate ./schema.json -o ./src/Schema.res
|
|
30
|
-
`);
|
|
77
|
+
header();
|
|
78
|
+
log(` ${c.bold("Usage")}`);
|
|
79
|
+
log(` ${c.cyan("$")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
|
|
80
|
+
log(` ${c.cyan("$")} osury generate ${c.cyan("<input.json>")} -o ${c.cyan("<output.res>")}`);
|
|
81
|
+
blank();
|
|
82
|
+
log(` ${c.bold("Options")}`);
|
|
83
|
+
log(` ${c.cyan("-o")}, ${c.cyan("--output")} Output file path ${c.dim("(default: ./Generated.res)")}`);
|
|
84
|
+
log(` ${c.cyan("-h")}, ${c.cyan("--help")} Show this help`);
|
|
85
|
+
log(` ${c.cyan("-v")}, ${c.cyan("--version")} Show version`);
|
|
86
|
+
log(` ${c.cyan("--no-color")} Disable colored output`);
|
|
87
|
+
blank();
|
|
88
|
+
log(` ${c.bold("Examples")}`);
|
|
89
|
+
log(` ${c.cyan("$")} osury openapi.json`);
|
|
90
|
+
log(` ${c.cyan("$")} osury openapi.json src/API.res`);
|
|
91
|
+
log(` ${c.cyan("$")} osury generate schema.json -o src/Schema.res`);
|
|
92
|
+
blank();
|
|
31
93
|
}
|
|
32
94
|
|
|
95
|
+
// ─── Arg parsing ─────────────────────────────────────────────────────────────
|
|
96
|
+
|
|
33
97
|
function parseArgs(args) {
|
|
34
|
-
const options = {
|
|
35
|
-
input: null,
|
|
36
|
-
output: "./Generated.res",
|
|
37
|
-
};
|
|
98
|
+
const options = { input: null, output: "./Generated.res" };
|
|
38
99
|
|
|
39
100
|
let i = 0;
|
|
40
101
|
while (i < args.length) {
|
|
@@ -43,10 +104,24 @@ function parseArgs(args) {
|
|
|
43
104
|
if (arg === "-h" || arg === "--help") {
|
|
44
105
|
printHelp();
|
|
45
106
|
process.exit(0);
|
|
107
|
+
} else if (arg === "-v" || arg === "--version") {
|
|
108
|
+
log(`osury v${VERSION}`);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
} else if (arg === "--no-color") {
|
|
111
|
+
// already handled above
|
|
46
112
|
} else if (arg === "-o" || arg === "--output") {
|
|
47
|
-
|
|
113
|
+
i++;
|
|
114
|
+
if (i >= args.length) {
|
|
115
|
+
header();
|
|
116
|
+
err(` ${sym.error} ${c.boldRed("Missing value for --output")}`);
|
|
117
|
+
blank();
|
|
118
|
+
err(` Expected: osury input.json ${c.cyan("-o <path>")}`);
|
|
119
|
+
blank();
|
|
120
|
+
process.exit(1);
|
|
121
|
+
}
|
|
122
|
+
options.output = args[i];
|
|
48
123
|
} else if (arg === "generate") {
|
|
49
|
-
//
|
|
124
|
+
// skip command word
|
|
50
125
|
} else if (!options.input) {
|
|
51
126
|
options.input = arg;
|
|
52
127
|
} else if (options.output === "./Generated.res") {
|
|
@@ -58,60 +133,230 @@ function parseArgs(args) {
|
|
|
58
133
|
return options;
|
|
59
134
|
}
|
|
60
135
|
|
|
136
|
+
// ─── "Did you mean?" ─────────────────────────────────────────────────────────
|
|
137
|
+
|
|
138
|
+
function levenshtein(a, b) {
|
|
139
|
+
const m = a.length, n = b.length;
|
|
140
|
+
const dp = Array.from({ length: m + 1 }, () => Array(n + 1).fill(0));
|
|
141
|
+
for (let i = 0; i <= m; i++) dp[i][0] = i;
|
|
142
|
+
for (let j = 0; j <= n; j++) dp[0][j] = j;
|
|
143
|
+
for (let i = 1; i <= m; i++)
|
|
144
|
+
for (let j = 1; j <= n; j++)
|
|
145
|
+
dp[i][j] = a[i - 1] === b[j - 1]
|
|
146
|
+
? dp[i - 1][j - 1]
|
|
147
|
+
: 1 + Math.min(dp[i - 1][j], dp[i][j - 1], dp[i - 1][j - 1]);
|
|
148
|
+
return dp[m][n];
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function findSimilarFiles(target) {
|
|
152
|
+
const dir = path.dirname(target);
|
|
153
|
+
const resolvedDir = dir === "." ? process.cwd() : dir;
|
|
154
|
+
const base = path.basename(target).toLowerCase();
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const files = fs.readdirSync(resolvedDir);
|
|
158
|
+
return files
|
|
159
|
+
.filter((f) => {
|
|
160
|
+
const fl = f.toLowerCase();
|
|
161
|
+
if (
|
|
162
|
+
!fl.endsWith(".json") &&
|
|
163
|
+
!fl.endsWith(".yaml") &&
|
|
164
|
+
!fl.endsWith(".yml")
|
|
165
|
+
)
|
|
166
|
+
return false;
|
|
167
|
+
// Levenshtein distance ≤ 40% of target name length
|
|
168
|
+
return levenshtein(base, fl) <= Math.ceil(base.length * 0.4);
|
|
169
|
+
})
|
|
170
|
+
.sort((a, b) => levenshtein(a.toLowerCase(), base) - levenshtein(b.toLowerCase(), base))
|
|
171
|
+
.slice(0, 3);
|
|
172
|
+
} catch {
|
|
173
|
+
return [];
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ─── Error formatting ────────────────────────────────────────────────────────
|
|
178
|
+
|
|
179
|
+
function formatErrorKind(kind) {
|
|
180
|
+
if (!kind || !kind.TAG) return "Unknown error";
|
|
181
|
+
switch (kind.TAG) {
|
|
182
|
+
case "UnknownType":
|
|
183
|
+
return `Unknown type ${c.bold(`"${kind._0}"`)}`;
|
|
184
|
+
case "MissingRequiredField":
|
|
185
|
+
return `Missing required field ${c.bold(`"${kind._0}"`)}`;
|
|
186
|
+
case "InvalidRef":
|
|
187
|
+
return `Invalid reference ${c.bold(`"${kind._0}"`)}`;
|
|
188
|
+
case "UnsupportedFeature":
|
|
189
|
+
return `Unsupported feature ${c.bold(`"${kind._0}"`)}`;
|
|
190
|
+
case "InvalidFormat":
|
|
191
|
+
return `Invalid format ${c.bold(`"${kind._0}"`)}`;
|
|
192
|
+
case "CircularReference":
|
|
193
|
+
return `Circular reference ${c.bold(`"${kind._0}"`)}`;
|
|
194
|
+
case "AmbiguousUnion":
|
|
195
|
+
return `Ambiguous union (anyOf/oneOf cannot be distinguished)`;
|
|
196
|
+
case "InvalidJson":
|
|
197
|
+
return `Invalid JSON: ${kind._0}`;
|
|
198
|
+
default:
|
|
199
|
+
return kind.TAG + (kind._0 ? `: ${kind._0}` : "");
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatParseError(error, index) {
|
|
204
|
+
const pathStr =
|
|
205
|
+
error.location?.path?.length > 0
|
|
206
|
+
? c.cyan("#/" + error.location.path.join("/"))
|
|
207
|
+
: c.cyan("#");
|
|
208
|
+
|
|
209
|
+
const lines = [];
|
|
210
|
+
lines.push(` ${c.dim(`${index + 1}.`)} ${pathStr}`);
|
|
211
|
+
lines.push(` ${formatErrorKind(error.kind)}`);
|
|
212
|
+
|
|
213
|
+
if (error.hint) {
|
|
214
|
+
lines.push(` ${c.dim("Hint:")} ${c.italic(error.hint)}`);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return lines.join("\n");
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// ─── Warning formatting ─────────────────────────────────────────────────────
|
|
221
|
+
|
|
222
|
+
function formatWarning(warning) {
|
|
223
|
+
// Warnings from collectUnionWarnings look like:
|
|
224
|
+
// "⚠ floatOrDict: anyOf [float, Dict] without discriminator, @tag("_tag") may not work at runtime"
|
|
225
|
+
// "⚠ modelInfoOrDict: anyOf without discriminator, simplified to modelInfo"
|
|
226
|
+
const cleaned = warning.replace(/^⚠\s*/, "");
|
|
227
|
+
return ` ${sym.warning} ${c.yellow(cleaned)}`;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
// ─── Main generate ───────────────────────────────────────────────────────────
|
|
231
|
+
|
|
61
232
|
function generate(inputPath, outputPath) {
|
|
62
|
-
|
|
233
|
+
const start = performance.now();
|
|
234
|
+
|
|
235
|
+
header();
|
|
236
|
+
|
|
237
|
+
// ── Check input file exists ──
|
|
63
238
|
if (!fs.existsSync(inputPath)) {
|
|
64
|
-
|
|
239
|
+
err(` ${sym.error} ${c.boldRed("File not found:")} ${c.cyan(inputPath)}`);
|
|
240
|
+
|
|
241
|
+
const similar = findSimilarFiles(inputPath);
|
|
242
|
+
if (similar.length > 0) {
|
|
243
|
+
blank();
|
|
244
|
+
err(` ${c.dim("Did you mean?")}`);
|
|
245
|
+
similar.forEach((f) => {
|
|
246
|
+
err(` ${sym.bullet} ${c.cyan(f)}`);
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
blank();
|
|
65
251
|
process.exit(1);
|
|
66
252
|
}
|
|
67
253
|
|
|
68
|
-
|
|
254
|
+
// ── Read & parse JSON ──
|
|
255
|
+
let doc;
|
|
256
|
+
try {
|
|
257
|
+
const raw = fs.readFileSync(inputPath, "utf8");
|
|
258
|
+
doc = JSON.parse(raw);
|
|
259
|
+
} catch (e) {
|
|
260
|
+
err(` ${sym.error} ${c.boldRed("Invalid JSON")} in ${c.cyan(inputPath)}`);
|
|
261
|
+
blank();
|
|
262
|
+
|
|
263
|
+
if (e instanceof SyntaxError) {
|
|
264
|
+
// Extract useful part of the message
|
|
265
|
+
const msg = e.message.replace(/^Unexpected/, "Unexpected");
|
|
266
|
+
err(` ${c.red(msg)}`);
|
|
267
|
+
} else {
|
|
268
|
+
err(` ${c.red(e.message)}`);
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
blank();
|
|
272
|
+
err(` ${c.dim("Tip:")} Validate your JSON at ${c.cyan("https://jsonlint.com")}`);
|
|
273
|
+
blank();
|
|
274
|
+
process.exit(1);
|
|
275
|
+
}
|
|
69
276
|
|
|
70
|
-
// Parse
|
|
277
|
+
// ── Parse OpenAPI document ──
|
|
71
278
|
const result = OpenAPIParser.parseDocument(doc);
|
|
72
279
|
|
|
73
|
-
if (result.TAG
|
|
74
|
-
const
|
|
280
|
+
if (result.TAG !== "Ok") {
|
|
281
|
+
const errors = result._0;
|
|
282
|
+
const count = errors.length;
|
|
75
283
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
284
|
+
err(
|
|
285
|
+
` ${sym.error} ${c.boldRed(`${count} parse error${count !== 1 ? "s" : ""}`)} in ${c.cyan(inputPath)}`
|
|
286
|
+
);
|
|
287
|
+
blank();
|
|
288
|
+
|
|
289
|
+
errors.forEach((error, i) => {
|
|
290
|
+
err(formatParseError(error, i));
|
|
291
|
+
if (i < errors.length - 1) blank();
|
|
292
|
+
});
|
|
81
293
|
|
|
82
|
-
|
|
83
|
-
|
|
294
|
+
blank();
|
|
295
|
+
process.exit(1);
|
|
296
|
+
}
|
|
84
297
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
fs.writeFileSync(dictShimPath, Codegen.generateDictShim());
|
|
298
|
+
const schemas = result._0;
|
|
299
|
+
const schemaCount = schemas.length;
|
|
88
300
|
|
|
89
|
-
|
|
90
|
-
const nullableResPath = path.join(outputDir || ".", "Nullable.res");
|
|
91
|
-
fs.writeFileSync(nullableResPath, Codegen.generateNullableModule());
|
|
301
|
+
log(` ${sym.success} Parsed ${c.bold(String(schemaCount))} schema${schemaCount !== 1 ? "s" : ""} from ${c.cyan(inputPath)}`);
|
|
92
302
|
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
fs.writeFileSync(nullableShimPath, Codegen.generateNullableShim());
|
|
303
|
+
// ── Generate code ──
|
|
304
|
+
const { code, warnings } = Codegen.generateModuleWithDiagnostics(schemas);
|
|
96
305
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
const location = err.location?.path?.join(".") || "root";
|
|
103
|
-
console.error(` [${location}] ${err.kind.TAG}: ${err.kind._0 || ""}`);
|
|
306
|
+
// ── Print warnings ──
|
|
307
|
+
if (warnings.length > 0) {
|
|
308
|
+
blank();
|
|
309
|
+
warnings.forEach((w) => {
|
|
310
|
+
log(formatWarning(w));
|
|
104
311
|
});
|
|
105
|
-
process.exit(1);
|
|
106
312
|
}
|
|
313
|
+
|
|
314
|
+
// ── Ensure output directory exists ──
|
|
315
|
+
const outputDir = path.dirname(outputPath);
|
|
316
|
+
if (outputDir && outputDir !== "." && !fs.existsSync(outputDir)) {
|
|
317
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
// ── Count generated types ──
|
|
321
|
+
const typeCount = (code.match(/^type\s/gm) || []).length;
|
|
322
|
+
|
|
323
|
+
// ── Write files ──
|
|
324
|
+
fs.writeFileSync(outputPath, code);
|
|
325
|
+
|
|
326
|
+
const dictShimPath = path.join(outputDir || ".", "Dict.gen.ts");
|
|
327
|
+
fs.writeFileSync(dictShimPath, Codegen.generateDictShim());
|
|
328
|
+
|
|
329
|
+
const nullableResPath = path.join(outputDir || ".", "Nullable.res");
|
|
330
|
+
fs.writeFileSync(nullableResPath, Codegen.generateNullableModule());
|
|
331
|
+
|
|
332
|
+
const nullableShimPath = path.join(outputDir || ".", "Nullable.shim.ts");
|
|
333
|
+
fs.writeFileSync(nullableShimPath, Codegen.generateNullableShim());
|
|
334
|
+
|
|
335
|
+
// ── Success output ──
|
|
336
|
+
blank();
|
|
337
|
+
log(` ${sym.success} Generated ${c.bold(String(typeCount))} type${typeCount !== 1 ? "s" : ""}`);
|
|
338
|
+
blank();
|
|
339
|
+
log(` ${c.dim("Files written:")}`);
|
|
340
|
+
log(` ${sym.bullet} ${c.cyan(outputPath)} ${c.dim("(main module)")}`);
|
|
341
|
+
log(` ${sym.bullet} ${c.cyan(dictShimPath)} ${c.dim("(TS shim)")}`);
|
|
342
|
+
log(` ${sym.bullet} ${c.cyan(nullableResPath)} ${c.dim("(ReScript module)")}`);
|
|
343
|
+
log(` ${sym.bullet} ${c.cyan(nullableShimPath)} ${c.dim("(TS shim)")}`);
|
|
344
|
+
blank();
|
|
345
|
+
log(` ${c.dim(`Done in ${elapsed(start)}`)}`);
|
|
346
|
+
blank();
|
|
107
347
|
}
|
|
108
348
|
|
|
109
|
-
//
|
|
110
|
-
|
|
349
|
+
// ─── Entry point ─────────────────────────────────────────────────────────────
|
|
350
|
+
|
|
351
|
+
const options = parseArgs(process.argv.slice(2));
|
|
111
352
|
|
|
112
353
|
if (!options.input) {
|
|
113
|
-
|
|
114
|
-
|
|
354
|
+
header();
|
|
355
|
+
err(` ${sym.error} ${c.boldRed("No input file specified")}`);
|
|
356
|
+
blank();
|
|
357
|
+
err(` ${c.dim("Usage:")} osury ${c.cyan("<input.json>")} ${c.dim("[output.res]")}`);
|
|
358
|
+
err(` ${c.dim("Help:")} osury ${c.cyan("--help")}`);
|
|
359
|
+
blank();
|
|
115
360
|
process.exit(1);
|
|
116
361
|
}
|
|
117
362
|
|
package/package.json
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
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.17.1",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"bin": {
|
|
8
|
-
"osury": "
|
|
8
|
+
"osury": "bin/osury.mjs"
|
|
9
9
|
},
|
|
10
10
|
"files": [
|
|
11
11
|
"bin/",
|
|
@@ -21,6 +21,7 @@
|
|
|
21
21
|
"res:build": "rescript",
|
|
22
22
|
"res:dev": "rescript watch",
|
|
23
23
|
"codegen": "node scripts/codegen.mjs",
|
|
24
|
+
"demo": "npm --prefix demo run dev",
|
|
24
25
|
"test": "node --experimental-vm-modules node_modules/jest/bin/jest.js",
|
|
25
26
|
"prepublishOnly": "npm run res:build && npm test"
|
|
26
27
|
},
|
|
@@ -55,6 +56,6 @@
|
|
|
55
56
|
],
|
|
56
57
|
"repository": {
|
|
57
58
|
"type": "git",
|
|
58
|
-
"url": "https://github.com/greenteamer/osury"
|
|
59
|
+
"url": "git+https://github.com/greenteamer/osury.git"
|
|
59
60
|
}
|
|
60
61
|
}
|
package/src/Codegen.res.mjs
CHANGED
|
@@ -1,749 +1,14 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
|
-
import * as
|
|
3
|
+
import * as CodegenShims from "./CodegenShims.res.mjs";
|
|
4
|
+
import * as CodegenTypes from "./CodegenTypes.res.mjs";
|
|
4
5
|
import * as Core__Option from "@rescript/core/src/Core__Option.res.mjs";
|
|
6
|
+
import * as CodegenHelpers from "./CodegenHelpers.res.mjs";
|
|
7
|
+
import * as CodegenTransforms from "./CodegenTransforms.res.mjs";
|
|
5
8
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
"rec",
|
|
10
|
-
"and",
|
|
11
|
-
"as",
|
|
12
|
-
"open",
|
|
13
|
-
"include",
|
|
14
|
-
"module",
|
|
15
|
-
"sig",
|
|
16
|
-
"struct",
|
|
17
|
-
"exception",
|
|
18
|
-
"external",
|
|
19
|
-
"if",
|
|
20
|
-
"else",
|
|
21
|
-
"switch",
|
|
22
|
-
"while",
|
|
23
|
-
"for",
|
|
24
|
-
"try",
|
|
25
|
-
"catch",
|
|
26
|
-
"when",
|
|
27
|
-
"true",
|
|
28
|
-
"false",
|
|
29
|
-
"assert",
|
|
30
|
-
"lazy",
|
|
31
|
-
"constraint",
|
|
32
|
-
"functor",
|
|
33
|
-
"class",
|
|
34
|
-
"method",
|
|
35
|
-
"object",
|
|
36
|
-
"private",
|
|
37
|
-
"public",
|
|
38
|
-
"virtual",
|
|
39
|
-
"mutable",
|
|
40
|
-
"new",
|
|
41
|
-
"inherit",
|
|
42
|
-
"initializer",
|
|
43
|
-
"val",
|
|
44
|
-
"with",
|
|
45
|
-
"match",
|
|
46
|
-
"of",
|
|
47
|
-
"fun",
|
|
48
|
-
"function",
|
|
49
|
-
"in",
|
|
50
|
-
"do",
|
|
51
|
-
"done",
|
|
52
|
-
"begin",
|
|
53
|
-
"end",
|
|
54
|
-
"then",
|
|
55
|
-
"to",
|
|
56
|
-
"downto",
|
|
57
|
-
"or",
|
|
58
|
-
"land",
|
|
59
|
-
"lor",
|
|
60
|
-
"lxor",
|
|
61
|
-
"lsl",
|
|
62
|
-
"lsr",
|
|
63
|
-
"asr",
|
|
64
|
-
"mod",
|
|
65
|
-
"await",
|
|
66
|
-
"async"
|
|
67
|
-
];
|
|
68
|
-
|
|
69
|
-
function isReservedKeyword(name) {
|
|
70
|
-
return reservedKeywords.includes(name);
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function lcFirst(s) {
|
|
74
|
-
if (s.length === 0) {
|
|
75
|
-
return s;
|
|
76
|
-
}
|
|
77
|
-
let first = s.charAt(0).toLowerCase();
|
|
78
|
-
let rest = s.slice(1);
|
|
79
|
-
return first + rest;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
function generateType(schema) {
|
|
83
|
-
if (typeof schema !== "object") {
|
|
84
|
-
switch (schema) {
|
|
85
|
-
case "String" :
|
|
86
|
-
return "string";
|
|
87
|
-
case "Number" :
|
|
88
|
-
return "float";
|
|
89
|
-
case "Integer" :
|
|
90
|
-
return "int";
|
|
91
|
-
case "Boolean" :
|
|
92
|
-
return "bool";
|
|
93
|
-
case "Null" :
|
|
94
|
-
return "unit";
|
|
95
|
-
}
|
|
96
|
-
} else {
|
|
97
|
-
switch (schema._tag) {
|
|
98
|
-
case "Optional" :
|
|
99
|
-
return `option<` + generateType(schema._0) + `>`;
|
|
100
|
-
case "Nullable" :
|
|
101
|
-
return `Nullable.t<` + generateType(schema._0) + `>`;
|
|
102
|
-
case "Object" :
|
|
103
|
-
return generateRecord(schema._0);
|
|
104
|
-
case "Array" :
|
|
105
|
-
return `array<` + generateType(schema._0) + `>`;
|
|
106
|
-
case "Ref" :
|
|
107
|
-
return lcFirst(schema._0);
|
|
108
|
-
case "Enum" :
|
|
109
|
-
let variants = schema._0.map(v => `#` + v).join(" | ");
|
|
110
|
-
return `[` + variants + `]`;
|
|
111
|
-
case "PolyVariant" :
|
|
112
|
-
return generatePolyVariant(schema._0);
|
|
113
|
-
case "Dict" :
|
|
114
|
-
return `Dict.t<` + generateType(schema._0) + `>`;
|
|
115
|
-
case "Union" :
|
|
116
|
-
return generateUnion(schema._0);
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
function isOptionalType(schema) {
|
|
122
|
-
if (typeof schema !== "object") {
|
|
123
|
-
return false;
|
|
124
|
-
}
|
|
125
|
-
switch (schema._tag) {
|
|
126
|
-
case "Optional" :
|
|
127
|
-
case "Nullable" :
|
|
128
|
-
return true;
|
|
129
|
-
default:
|
|
130
|
-
return false;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
function isNullableType(schema) {
|
|
135
|
-
if (typeof schema !== "object") {
|
|
136
|
-
return false;
|
|
137
|
-
} else {
|
|
138
|
-
return schema._tag === "Nullable";
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
function generateRecord(fields) {
|
|
143
|
-
if (fields.length === 0) {
|
|
144
|
-
return "{}";
|
|
145
|
-
}
|
|
146
|
-
let fieldStrs = fields.map(field => {
|
|
147
|
-
let typeStr = generateType(field.type);
|
|
148
|
-
let optionalType = field.required || isOptionalType(field.type) ? typeStr : `option<` + typeStr + `>`;
|
|
149
|
-
let finalType = isNullableType(field.type) ? `@s.null ` + optionalType : optionalType;
|
|
150
|
-
let asAttr = reservedKeywords.includes(field.name) ? `@as("` + field.name + `") ` : "";
|
|
151
|
-
let fieldName = reservedKeywords.includes(field.name) ? field.name + `_` : field.name;
|
|
152
|
-
return asAttr + fieldName + `: ` + finalType;
|
|
153
|
-
});
|
|
154
|
-
return `{\n ` + fieldStrs.join(",\n ") + `\n}`;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function generatePolyVariant(cases) {
|
|
158
|
-
let caseStrs = cases.map(c => {
|
|
159
|
-
let payloadStr = generateType(c.payload);
|
|
160
|
-
return `#` + c._tag + `(` + payloadStr + `)`;
|
|
161
|
-
});
|
|
162
|
-
return `[` + caseStrs.join(" | ") + `]`;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
function ucFirst(s) {
|
|
166
|
-
if (s.length === 0) {
|
|
167
|
-
return s;
|
|
168
|
-
}
|
|
169
|
-
let first = s.charAt(0).toUpperCase();
|
|
170
|
-
let rest = s.slice(1);
|
|
171
|
-
return first + rest;
|
|
172
|
-
}
|
|
173
|
-
|
|
174
|
-
function getTagForType(t) {
|
|
175
|
-
if (typeof t !== "object") {
|
|
176
|
-
switch (t) {
|
|
177
|
-
case "String" :
|
|
178
|
-
return "String";
|
|
179
|
-
case "Number" :
|
|
180
|
-
return "Float";
|
|
181
|
-
case "Integer" :
|
|
182
|
-
return "Int";
|
|
183
|
-
case "Boolean" :
|
|
184
|
-
return "Bool";
|
|
185
|
-
case "Null" :
|
|
186
|
-
return "Null";
|
|
187
|
-
}
|
|
188
|
-
} else {
|
|
189
|
-
switch (t._tag) {
|
|
190
|
-
case "Optional" :
|
|
191
|
-
return `Option` + getTagForType(t._0);
|
|
192
|
-
case "Nullable" :
|
|
193
|
-
return `Null` + getTagForType(t._0);
|
|
194
|
-
case "Object" :
|
|
195
|
-
return "Object";
|
|
196
|
-
case "Array" :
|
|
197
|
-
return `Array` + getTagForType(t._0);
|
|
198
|
-
case "Ref" :
|
|
199
|
-
return ucFirst(t._0);
|
|
200
|
-
case "Enum" :
|
|
201
|
-
return "Enum";
|
|
202
|
-
case "PolyVariant" :
|
|
203
|
-
return "Variant";
|
|
204
|
-
case "Dict" :
|
|
205
|
-
return "Dict";
|
|
206
|
-
case "Union" :
|
|
207
|
-
return "Union";
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
}
|
|
211
|
-
|
|
212
|
-
function generateUnion(types) {
|
|
213
|
-
let caseStrs = types.map(t => {
|
|
214
|
-
let tag = getTagForType(t);
|
|
215
|
-
let payload = generateType(t);
|
|
216
|
-
return `#` + tag + `(` + payload + `)`;
|
|
217
|
-
});
|
|
218
|
-
return `[` + caseStrs.join(" | ") + `]`;
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function hasUnion(_schema) {
|
|
222
|
-
while (true) {
|
|
223
|
-
let schema = _schema;
|
|
224
|
-
if (typeof schema !== "object") {
|
|
225
|
-
return false;
|
|
226
|
-
}
|
|
227
|
-
switch (schema._tag) {
|
|
228
|
-
case "Object" :
|
|
229
|
-
return schema._0.some(f => hasUnion(f.type));
|
|
230
|
-
case "PolyVariant" :
|
|
231
|
-
return schema._0.some(c => hasUnion(c.payload));
|
|
232
|
-
case "Optional" :
|
|
233
|
-
case "Nullable" :
|
|
234
|
-
case "Array" :
|
|
235
|
-
case "Dict" :
|
|
236
|
-
_schema = schema._0;
|
|
237
|
-
continue;
|
|
238
|
-
case "Union" :
|
|
239
|
-
return true;
|
|
240
|
-
default:
|
|
241
|
-
return false;
|
|
242
|
-
}
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
function isPrimitiveOnlyUnion(types) {
|
|
247
|
-
return types.every(t => {
|
|
248
|
-
if (typeof t === "object") {
|
|
249
|
-
return false;
|
|
250
|
-
}
|
|
251
|
-
switch (t) {
|
|
252
|
-
case "String" :
|
|
253
|
-
case "Number" :
|
|
254
|
-
case "Integer" :
|
|
255
|
-
case "Boolean" :
|
|
256
|
-
return true;
|
|
257
|
-
default:
|
|
258
|
-
return false;
|
|
259
|
-
}
|
|
260
|
-
});
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
function generateInlineRecord(refName, schemasDict) {
|
|
264
|
-
let other = schemasDict[refName];
|
|
265
|
-
if (other !== undefined) {
|
|
266
|
-
if (typeof other !== "object" || other._tag !== "Object") {
|
|
267
|
-
return generateType(other);
|
|
268
|
-
} else {
|
|
269
|
-
return generateRecord(other._0);
|
|
270
|
-
}
|
|
271
|
-
} else {
|
|
272
|
-
return lcFirst(refName);
|
|
273
|
-
}
|
|
274
|
-
}
|
|
275
|
-
|
|
276
|
-
function generateInlineVariantBody(types, schemasDict, tagsDict) {
|
|
277
|
-
return types.map(t => {
|
|
278
|
-
if (typeof t === "object" && t._tag === "Ref") {
|
|
279
|
-
let name = t._0;
|
|
280
|
-
let tagValue = tagsDict[name];
|
|
281
|
-
let tag = tagValue !== undefined ? ucFirst(tagValue) : ucFirst(name);
|
|
282
|
-
let inlineRecord = generateInlineRecord(name, schemasDict);
|
|
283
|
-
return tag + `(` + inlineRecord + `)`;
|
|
284
|
-
}
|
|
285
|
-
let tag$1 = getTagForType(t);
|
|
286
|
-
let payload = generateType(t);
|
|
287
|
-
return tag$1 + `(` + payload + `)`;
|
|
288
|
-
}).join(" | ");
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
function isRefPlusDictUnion(types) {
|
|
292
|
-
if (types.length !== 2) {
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
let hasDict = types.some(t => {
|
|
296
|
-
if (typeof t !== "object") {
|
|
297
|
-
return false;
|
|
298
|
-
}
|
|
299
|
-
if (t._tag !== "Dict") {
|
|
300
|
-
return false;
|
|
301
|
-
}
|
|
302
|
-
let tmp = t._0;
|
|
303
|
-
if (typeof tmp !== "object") {
|
|
304
|
-
return tmp === "String";
|
|
305
|
-
} else {
|
|
306
|
-
return false;
|
|
307
|
-
}
|
|
308
|
-
});
|
|
309
|
-
let refName = Core__Array.findMap(types, t => {
|
|
310
|
-
if (typeof t !== "object" || t._tag !== "Ref") {
|
|
311
|
-
return;
|
|
312
|
-
} else {
|
|
313
|
-
return t._0;
|
|
314
|
-
}
|
|
315
|
-
});
|
|
316
|
-
if (hasDict) {
|
|
317
|
-
return refName;
|
|
318
|
-
}
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
function isPrimitivePlusDictUnion(types) {
|
|
322
|
-
if (types.length !== 2) {
|
|
323
|
-
return;
|
|
324
|
-
}
|
|
325
|
-
let hasDict = types.some(t => {
|
|
326
|
-
if (typeof t !== "object") {
|
|
327
|
-
return false;
|
|
328
|
-
}
|
|
329
|
-
if (t._tag !== "Dict") {
|
|
330
|
-
return false;
|
|
331
|
-
}
|
|
332
|
-
let tmp = t._0;
|
|
333
|
-
if (typeof tmp !== "object") {
|
|
334
|
-
return tmp === "String";
|
|
335
|
-
} else {
|
|
336
|
-
return false;
|
|
337
|
-
}
|
|
338
|
-
});
|
|
339
|
-
let primitiveName = Core__Array.findMap(types, t => {
|
|
340
|
-
if (typeof t === "object") {
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
switch (t) {
|
|
344
|
-
case "String" :
|
|
345
|
-
return "string";
|
|
346
|
-
case "Number" :
|
|
347
|
-
return "float";
|
|
348
|
-
case "Integer" :
|
|
349
|
-
return "int";
|
|
350
|
-
case "Boolean" :
|
|
351
|
-
return "bool";
|
|
352
|
-
default:
|
|
353
|
-
return;
|
|
354
|
-
}
|
|
355
|
-
});
|
|
356
|
-
if (hasDict) {
|
|
357
|
-
return primitiveName;
|
|
358
|
-
}
|
|
359
|
-
}
|
|
360
|
-
|
|
361
|
-
function getUnionName(types) {
|
|
362
|
-
let names = types.map(t => {
|
|
363
|
-
if (typeof t !== "object") {
|
|
364
|
-
switch (t) {
|
|
365
|
-
case "String" :
|
|
366
|
-
return "string";
|
|
367
|
-
case "Number" :
|
|
368
|
-
return "float";
|
|
369
|
-
case "Integer" :
|
|
370
|
-
return "int";
|
|
371
|
-
case "Boolean" :
|
|
372
|
-
return "bool";
|
|
373
|
-
case "Null" :
|
|
374
|
-
return "null";
|
|
375
|
-
}
|
|
376
|
-
} else {
|
|
377
|
-
switch (t._tag) {
|
|
378
|
-
case "Array" :
|
|
379
|
-
return "array";
|
|
380
|
-
case "Ref" :
|
|
381
|
-
return lcFirst(t._0);
|
|
382
|
-
case "Dict" :
|
|
383
|
-
return "dict";
|
|
384
|
-
default:
|
|
385
|
-
return "unknown";
|
|
386
|
-
}
|
|
387
|
-
}
|
|
388
|
-
});
|
|
389
|
-
if (names.length === 0) {
|
|
390
|
-
return "emptyUnion";
|
|
391
|
-
}
|
|
392
|
-
let first = Core__Option.getOr(names[0], "unknown");
|
|
393
|
-
let rest = names.slice(1);
|
|
394
|
-
return first + rest.map(n => "Or" + ucFirst(n)).join("");
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
function extractUnionsFromType(_schema) {
|
|
398
|
-
while (true) {
|
|
399
|
-
let schema = _schema;
|
|
400
|
-
if (typeof schema !== "object") {
|
|
401
|
-
return [];
|
|
402
|
-
}
|
|
403
|
-
switch (schema._tag) {
|
|
404
|
-
case "Object" :
|
|
405
|
-
return schema._0.flatMap(field => extractUnionsFromType(field.type));
|
|
406
|
-
case "Optional" :
|
|
407
|
-
case "Nullable" :
|
|
408
|
-
case "Array" :
|
|
409
|
-
case "Dict" :
|
|
410
|
-
_schema = schema._0;
|
|
411
|
-
continue;
|
|
412
|
-
case "Union" :
|
|
413
|
-
let types = schema._0;
|
|
414
|
-
let match = isRefPlusDictUnion(types);
|
|
415
|
-
if (match !== undefined) {
|
|
416
|
-
return [];
|
|
417
|
-
}
|
|
418
|
-
let name = getUnionName(types);
|
|
419
|
-
return [{
|
|
420
|
-
name: name,
|
|
421
|
-
schema: schema
|
|
422
|
-
}];
|
|
423
|
-
default:
|
|
424
|
-
return [];
|
|
425
|
-
}
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
function extractUnions(_parentName, schema) {
|
|
430
|
-
if (typeof schema !== "object") {
|
|
431
|
-
return [];
|
|
432
|
-
} else if (schema._tag === "Object") {
|
|
433
|
-
return schema._0.flatMap(field => extractUnionsFromType(field.type));
|
|
434
|
-
} else {
|
|
435
|
-
return [];
|
|
436
|
-
}
|
|
437
|
-
}
|
|
438
|
-
|
|
439
|
-
function replaceUnionInType(schema) {
|
|
440
|
-
if (typeof schema !== "object") {
|
|
441
|
-
return schema;
|
|
442
|
-
}
|
|
443
|
-
switch (schema._tag) {
|
|
444
|
-
case "Optional" :
|
|
445
|
-
return {
|
|
446
|
-
_tag: "Optional",
|
|
447
|
-
_0: replaceUnionInType(schema._0)
|
|
448
|
-
};
|
|
449
|
-
case "Nullable" :
|
|
450
|
-
return {
|
|
451
|
-
_tag: "Nullable",
|
|
452
|
-
_0: replaceUnionInType(schema._0)
|
|
453
|
-
};
|
|
454
|
-
case "Object" :
|
|
455
|
-
let newFields = schema._0.map(field => {
|
|
456
|
-
let newType = replaceUnionInType(field.type);
|
|
457
|
-
return {
|
|
458
|
-
name: field.name,
|
|
459
|
-
type: newType,
|
|
460
|
-
required: field.required
|
|
461
|
-
};
|
|
462
|
-
});
|
|
463
|
-
return {
|
|
464
|
-
_tag: "Object",
|
|
465
|
-
_0: newFields
|
|
466
|
-
};
|
|
467
|
-
case "Array" :
|
|
468
|
-
return {
|
|
469
|
-
_tag: "Array",
|
|
470
|
-
_0: replaceUnionInType(schema._0)
|
|
471
|
-
};
|
|
472
|
-
case "Dict" :
|
|
473
|
-
return {
|
|
474
|
-
_tag: "Dict",
|
|
475
|
-
_0: replaceUnionInType(schema._0)
|
|
476
|
-
};
|
|
477
|
-
case "Union" :
|
|
478
|
-
let types = schema._0;
|
|
479
|
-
let refName = isRefPlusDictUnion(types);
|
|
480
|
-
if (refName !== undefined) {
|
|
481
|
-
return {
|
|
482
|
-
_tag: "Ref",
|
|
483
|
-
_0: refName
|
|
484
|
-
};
|
|
485
|
-
} else {
|
|
486
|
-
return {
|
|
487
|
-
_tag: "Ref",
|
|
488
|
-
_0: getUnionName(types)
|
|
489
|
-
};
|
|
490
|
-
}
|
|
491
|
-
default:
|
|
492
|
-
return schema;
|
|
493
|
-
}
|
|
494
|
-
}
|
|
495
|
-
|
|
496
|
-
function replaceUnions(_parentName, schema) {
|
|
497
|
-
if (typeof schema !== "object") {
|
|
498
|
-
return schema;
|
|
499
|
-
}
|
|
500
|
-
if (schema._tag !== "Object") {
|
|
501
|
-
return schema;
|
|
502
|
-
}
|
|
503
|
-
let newFields = schema._0.map(field => {
|
|
504
|
-
let newType = replaceUnionInType(field.type);
|
|
505
|
-
return {
|
|
506
|
-
name: field.name,
|
|
507
|
-
type: newType,
|
|
508
|
-
required: field.required
|
|
509
|
-
};
|
|
510
|
-
});
|
|
511
|
-
return {
|
|
512
|
-
_tag: "Object",
|
|
513
|
-
_0: newFields
|
|
514
|
-
};
|
|
515
|
-
}
|
|
516
|
-
|
|
517
|
-
function getDependencies(_schema) {
|
|
518
|
-
while (true) {
|
|
519
|
-
let schema = _schema;
|
|
520
|
-
if (typeof schema !== "object") {
|
|
521
|
-
return [];
|
|
522
|
-
}
|
|
523
|
-
switch (schema._tag) {
|
|
524
|
-
case "Object" :
|
|
525
|
-
return schema._0.flatMap(f => getDependencies(f.type));
|
|
526
|
-
case "Ref" :
|
|
527
|
-
return [schema._0];
|
|
528
|
-
case "Enum" :
|
|
529
|
-
return [];
|
|
530
|
-
case "PolyVariant" :
|
|
531
|
-
return schema._0.flatMap(c => getDependencies(c.payload));
|
|
532
|
-
case "Optional" :
|
|
533
|
-
case "Nullable" :
|
|
534
|
-
case "Array" :
|
|
535
|
-
case "Dict" :
|
|
536
|
-
_schema = schema._0;
|
|
537
|
-
continue;
|
|
538
|
-
case "Union" :
|
|
539
|
-
return schema._0.flatMap(getDependencies);
|
|
540
|
-
}
|
|
541
|
-
};
|
|
542
|
-
}
|
|
543
|
-
|
|
544
|
-
function topologicalSort(schemas) {
|
|
545
|
-
let schemaMap = {};
|
|
546
|
-
schemas.forEach(s => {
|
|
547
|
-
schemaMap[s.name] = s;
|
|
548
|
-
});
|
|
549
|
-
let deps = {};
|
|
550
|
-
schemas.forEach(s => {
|
|
551
|
-
let refNames = getDependencies(s.schema);
|
|
552
|
-
let validRefs = refNames.filter(name => Core__Option.isSome(schemaMap[name]));
|
|
553
|
-
deps[s.name] = validRefs;
|
|
554
|
-
});
|
|
555
|
-
let outDegree = {};
|
|
556
|
-
schemas.forEach(s => {
|
|
557
|
-
let myDeps = Core__Option.getOr(deps[s.name], []);
|
|
558
|
-
outDegree[s.name] = myDeps.length;
|
|
559
|
-
});
|
|
560
|
-
let reverseDeps = {};
|
|
561
|
-
schemas.forEach(s => {
|
|
562
|
-
reverseDeps[s.name] = [];
|
|
563
|
-
});
|
|
564
|
-
Object.entries(deps).forEach(param => {
|
|
565
|
-
let name = param[0];
|
|
566
|
-
param[1].forEach(refName => {
|
|
567
|
-
let arr = reverseDeps[refName];
|
|
568
|
-
if (arr !== undefined) {
|
|
569
|
-
arr.push(name);
|
|
570
|
-
return;
|
|
571
|
-
}
|
|
572
|
-
});
|
|
573
|
-
});
|
|
574
|
-
let queue = schemas.filter(s => Core__Option.getOr(outDegree[s.name], 0) === 0).map(s => s.name);
|
|
575
|
-
let result = [];
|
|
576
|
-
let visited = {};
|
|
577
|
-
let process = () => {
|
|
578
|
-
while (true) {
|
|
579
|
-
let name = queue.shift();
|
|
580
|
-
if (name === undefined) {
|
|
581
|
-
return;
|
|
582
|
-
}
|
|
583
|
-
if (Core__Option.isNone(visited[name])) {
|
|
584
|
-
visited[name] = true;
|
|
585
|
-
let schema = schemaMap[name];
|
|
586
|
-
if (schema !== undefined) {
|
|
587
|
-
result.push(schema);
|
|
588
|
-
}
|
|
589
|
-
let dependents = reverseDeps[name];
|
|
590
|
-
if (dependents !== undefined) {
|
|
591
|
-
dependents.forEach(depName => {
|
|
592
|
-
let current = Core__Option.getOr(outDegree[depName], 0);
|
|
593
|
-
outDegree[depName] = current - 1 | 0;
|
|
594
|
-
if ((current - 1 | 0) === 0) {
|
|
595
|
-
queue.push(depName);
|
|
596
|
-
return;
|
|
597
|
-
}
|
|
598
|
-
});
|
|
599
|
-
}
|
|
600
|
-
}
|
|
601
|
-
continue;
|
|
602
|
-
};
|
|
603
|
-
};
|
|
604
|
-
process();
|
|
605
|
-
schemas.forEach(s => {
|
|
606
|
-
if (Core__Option.isNone(visited[s.name])) {
|
|
607
|
-
result.push(s);
|
|
608
|
-
return;
|
|
609
|
-
}
|
|
610
|
-
});
|
|
611
|
-
return result;
|
|
612
|
-
}
|
|
613
|
-
|
|
614
|
-
function buildSkipSchemaSet(schemas) {
|
|
615
|
-
let skipSet = {};
|
|
616
|
-
schemas.forEach(s => {
|
|
617
|
-
if (hasUnion(s.schema)) {
|
|
618
|
-
skipSet[s.name] = true;
|
|
619
|
-
return;
|
|
620
|
-
}
|
|
621
|
-
});
|
|
622
|
-
let changed = {
|
|
623
|
-
contents: true
|
|
624
|
-
};
|
|
625
|
-
while (changed.contents) {
|
|
626
|
-
changed.contents = false;
|
|
627
|
-
schemas.forEach(s => {
|
|
628
|
-
if (!Core__Option.isNone(skipSet[s.name])) {
|
|
629
|
-
return;
|
|
630
|
-
}
|
|
631
|
-
let refs = getDependencies(s.schema);
|
|
632
|
-
let refsSkipSchema = refs.some(refName => Core__Option.isSome(skipSet[refName]));
|
|
633
|
-
if (refsSkipSchema) {
|
|
634
|
-
skipSet[s.name] = true;
|
|
635
|
-
changed.contents = true;
|
|
636
|
-
return;
|
|
637
|
-
}
|
|
638
|
-
});
|
|
639
|
-
};
|
|
640
|
-
return skipSet;
|
|
641
|
-
}
|
|
642
|
-
|
|
643
|
-
function generateVariantBody(types) {
|
|
644
|
-
return types.map(t => {
|
|
645
|
-
let tag = getTagForType(t);
|
|
646
|
-
let payload = generateType(t);
|
|
647
|
-
return tag + `(` + payload + `)`;
|
|
648
|
-
}).join(" | ");
|
|
649
|
-
}
|
|
650
|
-
|
|
651
|
-
function generateTypeDefWithSkipSet(namedSchema, _skipSet, schemasDict, tagsDict) {
|
|
652
|
-
let typeName = lcFirst(namedSchema.name);
|
|
653
|
-
let types = namedSchema.schema;
|
|
654
|
-
if (typeof types === "object" && types._tag === "Union") {
|
|
655
|
-
let types$1 = types._0;
|
|
656
|
-
if (isPrimitiveOnlyUnion(types$1)) {
|
|
657
|
-
let variantBody = generateVariantBody(types$1);
|
|
658
|
-
return `@genType
|
|
659
|
-
@tag("_tag")
|
|
660
|
-
@unboxed
|
|
661
|
-
@schema
|
|
662
|
-
type ` + typeName + ` = ` + variantBody;
|
|
663
|
-
}
|
|
664
|
-
let variantBody$1 = generateInlineVariantBody(types$1, schemasDict, tagsDict);
|
|
665
|
-
return `@genType
|
|
666
|
-
@tag("_tag")
|
|
667
|
-
@schema
|
|
668
|
-
type ` + typeName + ` = ` + variantBody$1;
|
|
669
|
-
}
|
|
670
|
-
let typeBody = generateType(namedSchema.schema);
|
|
671
|
-
return `@genType
|
|
672
|
-
@schema
|
|
673
|
-
type ` + typeName + ` = ` + typeBody;
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
function generateTypeDef(namedSchema) {
|
|
677
|
-
let typeName = lcFirst(namedSchema.name);
|
|
678
|
-
let types = namedSchema.schema;
|
|
679
|
-
if (typeof types === "object" && types._tag === "Union") {
|
|
680
|
-
let variantBody = generateVariantBody(types._0);
|
|
681
|
-
return `@genType
|
|
682
|
-
@tag("_tag")
|
|
683
|
-
@unboxed
|
|
684
|
-
@schema
|
|
685
|
-
type ` + typeName + ` = ` + variantBody;
|
|
686
|
-
}
|
|
687
|
-
let annotations = hasUnion(namedSchema.schema) ? "@genType" : "@genType\n@schema";
|
|
688
|
-
let typeBody = generateType(namedSchema.schema);
|
|
689
|
-
return annotations + `
|
|
690
|
-
type ` + typeName + ` = ` + typeBody;
|
|
691
|
-
}
|
|
692
|
-
|
|
693
|
-
function collectUnionWarnings(schemas) {
|
|
694
|
-
let seen = {};
|
|
695
|
-
let warnings = [];
|
|
696
|
-
let findUnions = _schema => {
|
|
697
|
-
while (true) {
|
|
698
|
-
let schema = _schema;
|
|
699
|
-
if (typeof schema !== "object") {
|
|
700
|
-
return [];
|
|
701
|
-
}
|
|
702
|
-
switch (schema._tag) {
|
|
703
|
-
case "Object" :
|
|
704
|
-
return schema._0.flatMap(f => findUnions(f.type));
|
|
705
|
-
case "Optional" :
|
|
706
|
-
case "Nullable" :
|
|
707
|
-
case "Array" :
|
|
708
|
-
case "Dict" :
|
|
709
|
-
_schema = schema._0;
|
|
710
|
-
continue;
|
|
711
|
-
case "Union" :
|
|
712
|
-
return [schema._0];
|
|
713
|
-
default:
|
|
714
|
-
return [];
|
|
715
|
-
}
|
|
716
|
-
};
|
|
717
|
-
};
|
|
718
|
-
schemas.forEach(s => {
|
|
719
|
-
let unions = findUnions(s.schema);
|
|
720
|
-
unions.forEach(types => {
|
|
721
|
-
let unionName = getUnionName(types);
|
|
722
|
-
if (!Core__Option.isNone(seen[unionName])) {
|
|
723
|
-
return;
|
|
724
|
-
}
|
|
725
|
-
seen[unionName] = true;
|
|
726
|
-
let refName = isRefPlusDictUnion(types);
|
|
727
|
-
if (refName !== undefined) {
|
|
728
|
-
warnings.push(`⚠ ` + unionName + `: anyOf without discriminator, simplified to ` + lcFirst(refName));
|
|
729
|
-
return;
|
|
730
|
-
}
|
|
731
|
-
let primName = isPrimitivePlusDictUnion(types);
|
|
732
|
-
if (primName !== undefined) {
|
|
733
|
-
warnings.push(`⚠ ` + unionName + `: anyOf [` + primName + `, Dict] without discriminator, @tag("_tag") may not work at runtime`);
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
736
|
-
});
|
|
737
|
-
});
|
|
738
|
-
return warnings;
|
|
739
|
-
}
|
|
740
|
-
|
|
741
|
-
function generateModule(schemas) {
|
|
742
|
-
let warnings = collectUnionWarnings(schemas);
|
|
743
|
-
warnings.forEach(w => {
|
|
744
|
-
console.log(w);
|
|
745
|
-
});
|
|
746
|
-
let extractedUnions = schemas.flatMap(s => extractUnions(s.name, s.schema).map(extracted => ({
|
|
9
|
+
function generateModuleWithDiagnostics(schemas) {
|
|
10
|
+
let warnings = CodegenTransforms.collectUnionWarnings(schemas);
|
|
11
|
+
let extractedUnions = schemas.flatMap(s => CodegenTransforms.extractUnions(s.name, s.schema).map(extracted => ({
|
|
747
12
|
name: extracted.name,
|
|
748
13
|
schema: extracted.schema,
|
|
749
14
|
discriminatorTag: undefined
|
|
@@ -759,7 +24,7 @@ function generateModule(schemas) {
|
|
|
759
24
|
});
|
|
760
25
|
let modifiedSchemas = schemas.map(s => ({
|
|
761
26
|
name: s.name,
|
|
762
|
-
schema: replaceUnions(s.name, s.schema),
|
|
27
|
+
schema: CodegenTransforms.replaceUnions(s.name, s.schema),
|
|
763
28
|
discriminatorTag: s.discriminatorTag
|
|
764
29
|
}));
|
|
765
30
|
let allSchemas = uniqueUnions.concat(modifiedSchemas);
|
|
@@ -773,56 +38,92 @@ function generateModule(schemas) {
|
|
|
773
38
|
return;
|
|
774
39
|
}
|
|
775
40
|
});
|
|
776
|
-
let sorted = topologicalSort(allSchemas);
|
|
41
|
+
let sorted = CodegenTransforms.topologicalSort(allSchemas);
|
|
777
42
|
let skipSet = {};
|
|
778
|
-
let typeDefs = sorted.map(s => generateTypeDefWithSkipSet(s, skipSet, schemasDict, tagsDict)).join("\n\n");
|
|
779
|
-
|
|
43
|
+
let typeDefs = sorted.map(s => CodegenTypes.generateTypeDefWithSkipSet(s, skipSet, schemasDict, tagsDict)).join("\n\n");
|
|
44
|
+
let code = "module S = Sury\n\n" + typeDefs;
|
|
45
|
+
return {
|
|
46
|
+
code: code,
|
|
47
|
+
warnings: warnings
|
|
48
|
+
};
|
|
780
49
|
}
|
|
781
50
|
|
|
782
|
-
function
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
51
|
+
function generateModule(schemas) {
|
|
52
|
+
let result = generateModuleWithDiagnostics(schemas);
|
|
53
|
+
result.warnings.forEach(w => {
|
|
54
|
+
console.log(w);
|
|
55
|
+
});
|
|
56
|
+
return result.code;
|
|
786
57
|
}
|
|
787
58
|
|
|
788
|
-
|
|
789
|
-
return `/**
|
|
790
|
-
* Type shim for ReScript Nullable.t
|
|
791
|
-
*
|
|
792
|
-
* This file is generated by osury (not by ReScript/genType).
|
|
793
|
-
* It maps ReScript's option<T> to TypeScript's T | null for JSON nullable fields.
|
|
794
|
-
*
|
|
795
|
-
* ReScript side: Nullable.t<T> = option<T> (works with sury's S.null schema)
|
|
796
|
-
* TypeScript side: t<T> = T | null (correct JSON null representation)
|
|
797
|
-
*/
|
|
798
|
-
export type t<T> = T | null;
|
|
799
|
-
`;
|
|
800
|
-
}
|
|
59
|
+
let lcFirst = CodegenHelpers.lcFirst;
|
|
801
60
|
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
61
|
+
let ucFirst = CodegenHelpers.ucFirst;
|
|
62
|
+
|
|
63
|
+
let reservedKeywords = CodegenHelpers.reservedKeywords;
|
|
64
|
+
|
|
65
|
+
let isReservedKeyword = CodegenHelpers.isReservedKeyword;
|
|
66
|
+
|
|
67
|
+
let isOptionalType = CodegenHelpers.isOptionalType;
|
|
68
|
+
|
|
69
|
+
let isNullableType = CodegenHelpers.isNullableType;
|
|
70
|
+
|
|
71
|
+
let getTagForType = CodegenHelpers.getTagForType;
|
|
72
|
+
|
|
73
|
+
let hasUnion = CodegenHelpers.hasUnion;
|
|
74
|
+
|
|
75
|
+
let isPrimitiveOnlyUnion = CodegenHelpers.isPrimitiveOnlyUnion;
|
|
76
|
+
|
|
77
|
+
let isRefPlusDictUnion = CodegenTransforms.isRefPlusDictUnion;
|
|
78
|
+
|
|
79
|
+
let isPrimitivePlusDictUnion = CodegenTransforms.isPrimitivePlusDictUnion;
|
|
80
|
+
|
|
81
|
+
let getUnionName = CodegenTransforms.getUnionName;
|
|
82
|
+
|
|
83
|
+
let extractUnions = CodegenTransforms.extractUnions;
|
|
84
|
+
|
|
85
|
+
let extractUnionsFromType = CodegenTransforms.extractUnionsFromType;
|
|
86
|
+
|
|
87
|
+
let replaceUnions = CodegenTransforms.replaceUnions;
|
|
88
|
+
|
|
89
|
+
let replaceUnionInType = CodegenTransforms.replaceUnionInType;
|
|
90
|
+
|
|
91
|
+
let getDependencies = CodegenTransforms.getDependencies;
|
|
92
|
+
|
|
93
|
+
let topologicalSort = CodegenTransforms.topologicalSort;
|
|
94
|
+
|
|
95
|
+
let buildSkipSchemaSet = CodegenTransforms.buildSkipSchemaSet;
|
|
96
|
+
|
|
97
|
+
let collectUnionWarnings = CodegenTransforms.collectUnionWarnings;
|
|
98
|
+
|
|
99
|
+
let generateType = CodegenTypes.generateType;
|
|
100
|
+
|
|
101
|
+
let generateTypeDef = CodegenTypes.generateTypeDef;
|
|
102
|
+
|
|
103
|
+
let generateTypeDefWithSkipSet = CodegenTypes.generateTypeDefWithSkipSet;
|
|
104
|
+
|
|
105
|
+
let generateVariantBody = CodegenTypes.generateVariantBody;
|
|
106
|
+
|
|
107
|
+
let generateInlineVariantBody = CodegenTypes.generateInlineVariantBody;
|
|
108
|
+
|
|
109
|
+
let generateInlineRecord = CodegenTypes.generateInlineRecord;
|
|
110
|
+
|
|
111
|
+
let generateDictShim = CodegenShims.generateDictShim;
|
|
112
|
+
|
|
113
|
+
let generateNullableShim = CodegenShims.generateNullableShim;
|
|
114
|
+
|
|
115
|
+
let generateNullableModule = CodegenShims.generateNullableModule;
|
|
809
116
|
|
|
810
117
|
export {
|
|
118
|
+
lcFirst,
|
|
119
|
+
ucFirst,
|
|
811
120
|
reservedKeywords,
|
|
812
121
|
isReservedKeyword,
|
|
813
|
-
lcFirst,
|
|
814
|
-
generateType,
|
|
815
122
|
isOptionalType,
|
|
816
123
|
isNullableType,
|
|
817
|
-
generateRecord,
|
|
818
|
-
generatePolyVariant,
|
|
819
|
-
ucFirst,
|
|
820
124
|
getTagForType,
|
|
821
|
-
generateUnion,
|
|
822
125
|
hasUnion,
|
|
823
126
|
isPrimitiveOnlyUnion,
|
|
824
|
-
generateInlineRecord,
|
|
825
|
-
generateInlineVariantBody,
|
|
826
127
|
isRefPlusDictUnion,
|
|
827
128
|
isPrimitivePlusDictUnion,
|
|
828
129
|
getUnionName,
|
|
@@ -833,13 +134,17 @@ export {
|
|
|
833
134
|
getDependencies,
|
|
834
135
|
topologicalSort,
|
|
835
136
|
buildSkipSchemaSet,
|
|
836
|
-
generateVariantBody,
|
|
837
|
-
generateTypeDefWithSkipSet,
|
|
838
|
-
generateTypeDef,
|
|
839
137
|
collectUnionWarnings,
|
|
840
|
-
|
|
138
|
+
generateType,
|
|
139
|
+
generateTypeDef,
|
|
140
|
+
generateTypeDefWithSkipSet,
|
|
141
|
+
generateVariantBody,
|
|
142
|
+
generateInlineVariantBody,
|
|
143
|
+
generateInlineRecord,
|
|
841
144
|
generateDictShim,
|
|
842
145
|
generateNullableShim,
|
|
843
146
|
generateNullableModule,
|
|
147
|
+
generateModuleWithDiagnostics,
|
|
148
|
+
generateModule,
|
|
844
149
|
}
|
|
845
150
|
/* No side effect */
|