bend-schema 0.1.0
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/LICENSE +21 -0
- package/README.md +230 -0
- package/core/LAWS.bend +379 -0
- package/core/LICENSE +23 -0
- package/core/PROOF.bend +4527 -0
- package/core/core.bend +1585 -0
- package/dist-core/core.d.mts +188 -0
- package/dist-core/core.mjs +2974 -0
- package/package.json +41 -0
- package/src/codec.ts +177 -0
- package/src/gen-cli.ts +61 -0
- package/src/gen.ts +96 -0
- package/src/index.ts +350 -0
package/package.json
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "bend-schema",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"devDependencies": {
|
|
7
|
+
"@types/bun": "^1.2.0",
|
|
8
|
+
"bend-emit": "0.3.1",
|
|
9
|
+
"bend-falsify": "github:nohzafk/bend-falsify#a50873f",
|
|
10
|
+
"typescript": "^7.0.2"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": "./src/index.ts",
|
|
14
|
+
"./codec": "./src/codec.ts",
|
|
15
|
+
"./gen": "./src/gen.ts"
|
|
16
|
+
},
|
|
17
|
+
"bin": {
|
|
18
|
+
"bend-schema": "./src/gen-cli.ts"
|
|
19
|
+
},
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "git+https://github.com/nohzafk/bend-schema.git"
|
|
23
|
+
},
|
|
24
|
+
"homepage": "https://github.com/nohzafk/bend-schema#readme",
|
|
25
|
+
"bugs": {
|
|
26
|
+
"url": "https://github.com/nohzafk/bend-schema/issues"
|
|
27
|
+
},
|
|
28
|
+
"files": [
|
|
29
|
+
"src/index.ts",
|
|
30
|
+
"src/codec.ts",
|
|
31
|
+
"src/gen.ts",
|
|
32
|
+
"src/gen-cli.ts",
|
|
33
|
+
"core/core.bend",
|
|
34
|
+
"core/LAWS.bend",
|
|
35
|
+
"core/PROOF.bend",
|
|
36
|
+
"core/LICENSE",
|
|
37
|
+
"dist-core/",
|
|
38
|
+
"LICENSE",
|
|
39
|
+
"README.md"
|
|
40
|
+
]
|
|
41
|
+
}
|
package/src/codec.ts
ADDED
|
@@ -0,0 +1,177 @@
|
|
|
1
|
+
// The universal codec: any JS value, as the schema check takes it.
|
|
2
|
+
//
|
|
3
|
+
// It knows no schema, so it decides nothing about shapes: a whole number from 0
|
|
4
|
+
// to the runtime's Nat bound (2^48-1) is RNum, a boolean RBool, null RNull, a
|
|
5
|
+
// string RStr, an array a chain of RCons, a plain object a chain of RKey in its
|
|
6
|
+
// own key order, and anything else (a fraction, a negative, NaN, undefined, a
|
|
7
|
+
// Date or any other non-plain object) is RBad, for check to report where it
|
|
8
|
+
// sits. Every rule about shapes is the proved core's (LAWS.bend).
|
|
9
|
+
//
|
|
10
|
+
// Size is the one thing it decides, because it is the one thing the core
|
|
11
|
+
// cannot: the compiled JS walks a list or an object with one native JS frame
|
|
12
|
+
// per element or key, so a long value throws RangeError. So the codec counts
|
|
13
|
+
// what such a walk would cost -- see BUDGET -- and puts one RTooBig in place of
|
|
14
|
+
// the array or object that runs past the budget, which the core reports as
|
|
15
|
+
// TooLarge at that node's path. Nothing here is a rule about values: a size is
|
|
16
|
+
// not a shape, and a host cannot choose the sizes it is sent.
|
|
17
|
+
|
|
18
|
+
import type { BendList, BendMaybe, Err, Raw, Step, Why } from "../dist-core/core.mjs";
|
|
19
|
+
|
|
20
|
+
// The largest Nat the runtime holds: bend's own Nat.add(Nat.mul(65535,
|
|
21
|
+
// 4294967295 + 1), 4294967295). A number past it is not a Nat at all, so it
|
|
22
|
+
// is not a value the core can be given.
|
|
23
|
+
export const NAT_MAX = 2 ** 48 - 1;
|
|
24
|
+
|
|
25
|
+
// One rule, two ways out: the runtime holds v as a Nat, or it does not.
|
|
26
|
+
const natFit = (v: number): boolean => Number.isSafeInteger(v) && v >= 0 && v <= NAT_MAX;
|
|
27
|
+
|
|
28
|
+
// `nat(name, v)` is v when the runtime can hold it as a Nat, and an Error
|
|
29
|
+
// otherwise. It is toRaw's RNum rule, thrown instead of reported: a host that
|
|
30
|
+
// must refuse a number before building anything -- billing's `units`, which is
|
|
31
|
+
// not part of the plan it checks -- needs the refusal here, and the message
|
|
32
|
+
// names the field, the bound and the value.
|
|
33
|
+
export function nat(name: string, v: number): number {
|
|
34
|
+
if (!natFit(v)) {
|
|
35
|
+
throw new Error(`${name} must be a whole number from 0 to ${NAT_MAX} (${name}=${v})`);
|
|
36
|
+
}
|
|
37
|
+
return v;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// The most list elements and object keys one value may hold, counted over
|
|
41
|
+
// every level of nesting and summed: `[[1,2],[3]]` is 2 + 3 = 5. Past it, a
|
|
42
|
+
// node becomes RTooBig.
|
|
43
|
+
//
|
|
44
|
+
// Measured, not guessed (with bend 2.0.35 on a macOS arm64 Mac, each shape in
|
|
45
|
+
// its own process). Each shape below is the largest count where toRaw, check0,
|
|
46
|
+
// conforms0, enc and dec all return in bun, and what the next step throws:
|
|
47
|
+
//
|
|
48
|
+
// flat list 30000 check0: RangeError
|
|
49
|
+
// nested object 6257 check0: RangeError
|
|
50
|
+
// nested list 6257 check0: RangeError
|
|
51
|
+
// list of lists 29929 survived the largest count tried
|
|
52
|
+
// list of objects 29929 survived the largest count tried
|
|
53
|
+
// mixed 15224 survived the largest count tried
|
|
54
|
+
// flat object 6656 check0: RangeError (25 probes, 903 s)
|
|
55
|
+
//
|
|
56
|
+
// The binding shapes are the nested object and the nested list, both at 6257,
|
|
57
|
+
// and a value at the budget keeps about 2.0x of margin under them. (On bend
|
|
58
|
+
// 2.0.27 the nested object reached about 10,600 and a flat object bound first,
|
|
59
|
+
// at 6144, which is why KEYS_MAX exists; the budget was set to half of that,
|
|
60
|
+
// 3072, and is kept.) One budget covers lists, objects and nesting alike, and
|
|
61
|
+
// what makes it enough is the invariant: toRaw never builds a value whose count
|
|
62
|
+
// is past it, so the walk that follows is bounded by it.
|
|
63
|
+
|
|
64
|
+
export const BUDGET = 3072;
|
|
65
|
+
|
|
66
|
+
// The most keys one object may hold. A second limit, on time rather than
|
|
67
|
+
// stack: the core finds each field by scanning the object from the front, so
|
|
68
|
+
// walking an object costs about keys^2 lookups. At 3072 keys that was about
|
|
69
|
+
// 8 s for each of check0, conforms0 and dec -- a small request that holds a
|
|
70
|
+
// server for seconds. At 256 keys it is about 65,000 lookups. An object with
|
|
71
|
+
// more keys than this is RTooBig, like a value past the budget; a set that
|
|
72
|
+
// large belongs in a list.
|
|
73
|
+
export const KEYS_MAX = 256;
|
|
74
|
+
|
|
75
|
+
export function toRaw(v: unknown): Raw {
|
|
76
|
+
let left = BUDGET;
|
|
77
|
+
const build = (v: unknown): Raw => {
|
|
78
|
+
if (v === null) return { $: "RNull" };
|
|
79
|
+
if (typeof v === "number") return natFit(v) ? { $: "RNum", n: BigInt(v) } : { $: "RBad" };
|
|
80
|
+
if (typeof v === "boolean") return { $: "RBool", b: v };
|
|
81
|
+
if (typeof v === "string") return { $: "RStr", s: v };
|
|
82
|
+
if (Array.isArray(v)) {
|
|
83
|
+
if (v.length > left) return { $: "RTooBig" };
|
|
84
|
+
left -= v.length;
|
|
85
|
+
// Elements are built first to last, so the budget runs out where a
|
|
86
|
+
// reader would expect: at the first node past it, not at the start.
|
|
87
|
+
const heads = v.map(build);
|
|
88
|
+
return heads.reduceRight<Raw>((tail, head) => ({ $: "RCons", head, tail }), { $: "RNil" });
|
|
89
|
+
}
|
|
90
|
+
if (typeof v === "object") {
|
|
91
|
+
// A plain object, and nothing else. A Date, Map, Set or class instance is
|
|
92
|
+
// not JSON, and carries no own enumerable keys, so it would otherwise
|
|
93
|
+
// become an empty REnd -- a value the core would accept as conforming to
|
|
94
|
+
// any object schema with no required key.
|
|
95
|
+
const proto = Object.getPrototypeOf(v);
|
|
96
|
+
if (proto !== Object.prototype && proto !== null) return { $: "RBad" };
|
|
97
|
+
const entries = Object.entries(v as object);
|
|
98
|
+
if (entries.length > KEYS_MAX || entries.length > left) return { $: "RTooBig" };
|
|
99
|
+
left -= entries.length;
|
|
100
|
+
const vals = entries.map(([key, val]) => [key, build(val)] as const);
|
|
101
|
+
return vals.reduceRight<Raw>((rest, [key, val]) => ({ $: "RKey", key, val, rest }), { $: "REnd" });
|
|
102
|
+
}
|
|
103
|
+
return { $: "RBad" };
|
|
104
|
+
};
|
|
105
|
+
return build(v);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export const NONE: BendMaybe<bigint> = { $: "None" };
|
|
109
|
+
|
|
110
|
+
function steps(p: BendList<Step>): Step[] {
|
|
111
|
+
const out: Step[] = [];
|
|
112
|
+
for (let x = p; x.$ === "Con"; x = x.tail) out.push(x.head);
|
|
113
|
+
return out;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// A path as text: "[2].upTo". A field step counts the schema fields it
|
|
117
|
+
// skipped, for the proofs; the text shows only its name.
|
|
118
|
+
export function pathText(p: BendList<Step>): string {
|
|
119
|
+
return steps(p).map((s) => (s.$ === "AtIndex" ? `[${s.i}]` : s.$ === "AtField" ? `.${s.name}` : s.$ === "AtKey" ? `.${s.key}` : `[${s.i}].${s.key}`)).join("");
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// What is wrong, in words. tsc checks every reason is here.
|
|
123
|
+
export function whyText(w: Why): string {
|
|
124
|
+
switch (w.$) {
|
|
125
|
+
case "Missing":
|
|
126
|
+
return "missing";
|
|
127
|
+
case "NotNat":
|
|
128
|
+
return `must be a whole number from 0 to ${NAT_MAX}`;
|
|
129
|
+
case "NotString":
|
|
130
|
+
return "must be a string";
|
|
131
|
+
case "NotBool":
|
|
132
|
+
return "must be a boolean";
|
|
133
|
+
case "NotList":
|
|
134
|
+
return "must be a list";
|
|
135
|
+
case "NotObject":
|
|
136
|
+
return "must be an object";
|
|
137
|
+
case "NoElements":
|
|
138
|
+
return "must not be empty";
|
|
139
|
+
case "OpenNotLast":
|
|
140
|
+
return "only the last may be null";
|
|
141
|
+
case "LastNotOpen":
|
|
142
|
+
return "the last must be null";
|
|
143
|
+
case "NotIncreasing":
|
|
144
|
+
return `must be past ${w.prev} (got ${w.got})`;
|
|
145
|
+
case "NotTrue":
|
|
146
|
+
return "must be true";
|
|
147
|
+
case "NotOneOf":
|
|
148
|
+
return "is not one of the allowed names";
|
|
149
|
+
case "NoVariant":
|
|
150
|
+
return "must have one of its keys";
|
|
151
|
+
case "TwoVariants":
|
|
152
|
+
return "is a second key, where only one is allowed";
|
|
153
|
+
case "TooShort":
|
|
154
|
+
return "has too few elements";
|
|
155
|
+
case "TooLong":
|
|
156
|
+
return "has too many elements";
|
|
157
|
+
case "LengthNotIn":
|
|
158
|
+
return `must be ${w.lo} to ${w.hi} characters long`;
|
|
159
|
+
case "CountNotIn":
|
|
160
|
+
return `must have ${w.lo} to ${w.hi} elements`;
|
|
161
|
+
case "UnknownKey":
|
|
162
|
+
return "is not a key this object allows";
|
|
163
|
+
case "RepeatedKey":
|
|
164
|
+
return `is a second ${w.key}`;
|
|
165
|
+
case "NotIn":
|
|
166
|
+
return `must be from ${w.lo} to ${w.hi}`;
|
|
167
|
+
case "TooLarge":
|
|
168
|
+
return "too large";
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// An error in words, its path after `where` (the name the host gives the
|
|
173
|
+
// value): errText(e, "tiers") is "tiers[0].upTo: missing". With no `where`
|
|
174
|
+
// and an empty path, the value itself is named: "the value: must be a list".
|
|
175
|
+
export function errText(e: Err, where = ""): string {
|
|
176
|
+
return `${where + pathText(e.path) || "the value"}: ${whyText(e.why)}`;
|
|
177
|
+
}
|
package/src/gen-cli.ts
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
#!/usr/bin/env bun
|
|
2
|
+
// The command: `bend-schema gen <module.ts> <out.bend>`, run with bun (`bunx`)
|
|
3
|
+
// or node (`npx`). Bun does the work either way: the shebang is `env bun`,
|
|
4
|
+
// because the command executes the module and Node refuses to strip types from
|
|
5
|
+
// TypeScript in node_modules.
|
|
6
|
+
//
|
|
7
|
+
// The module must export `schemas` — an object whose keys name the Bend defs:
|
|
8
|
+
//
|
|
9
|
+
// export const schemas = { config: Config, workers: Workers };
|
|
10
|
+
//
|
|
11
|
+
// An explicit map rather than every exported Schema: the names are the author's,
|
|
12
|
+
// and a schema that is a step inside another is not printed by accident.
|
|
13
|
+
//
|
|
14
|
+
// The module is **executed** to be read, so it must be schemas and nothing
|
|
15
|
+
// else: a file that does something on import would do it at build time. (A
|
|
16
|
+
// `describe()` in it fails here — `Cannot use describe outside of the test
|
|
17
|
+
// runner` — which is how this was found.)
|
|
18
|
+
//
|
|
19
|
+
// The import path in the generated file is computed here: this file lives in
|
|
20
|
+
// the package, so the core sits at <pkg>/core/core.bend, and the output file's
|
|
21
|
+
// own directory gives the relative path to it. The caller passes no path.
|
|
22
|
+
//
|
|
23
|
+
// Nothing here needs bun's own APIs. `import.meta.dirname` is Node's name, and
|
|
24
|
+
// bun's; `import.meta.dir` is bun-only, which is why this file does not use it.
|
|
25
|
+
// Node can strip types from a `.ts` file, but not from one inside
|
|
26
|
+
// `node_modules`: a consumer's installed copy is always in there, so Node
|
|
27
|
+
// refuses to run it (ERR_UNSUPPORTED_NODE_MODULES_TYPE_STRIPPING) and the
|
|
28
|
+
// shebang above is what makes the command work.
|
|
29
|
+
|
|
30
|
+
import { mkdirSync, writeFileSync } from "node:fs";
|
|
31
|
+
import { dirname, relative, resolve } from "node:path";
|
|
32
|
+
import { emit } from "./gen";
|
|
33
|
+
import type { Schema } from "./index";
|
|
34
|
+
|
|
35
|
+
const pkgCore = resolve(import.meta.dirname, "../core/core.bend");
|
|
36
|
+
const args = process.argv.slice(2);
|
|
37
|
+
if (args[0] === "gen") args.shift();
|
|
38
|
+
const [modArg, outArg] = args;
|
|
39
|
+
if (!modArg || !outArg) {
|
|
40
|
+
console.error("usage: bunx bend-schema gen <module.ts> <out.bend>");
|
|
41
|
+
process.exit(2);
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
const mod: Record<string, unknown> = await import(resolve(modArg));
|
|
45
|
+
const schemas = mod.schemas as Record<string, Schema<unknown>> | undefined;
|
|
46
|
+
if (!schemas || typeof schemas !== "object" || Array.isArray(schemas)) {
|
|
47
|
+
console.error(
|
|
48
|
+
`gen: ${modArg} must export \`schemas\` — an object of Schema values, one per Bend def:\n` +
|
|
49
|
+
` export const schemas = { config: Config };`,
|
|
50
|
+
);
|
|
51
|
+
process.exit(1);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const out = resolve(outArg);
|
|
55
|
+
mkdirSync(dirname(out), { recursive: true });
|
|
56
|
+
const rel = relative(dirname(out), pkgCore);
|
|
57
|
+
writeFileSync(
|
|
58
|
+
out,
|
|
59
|
+
emit(schemas, { libPath: rel.startsWith(".") ? rel : `./${rel}`, from: modArg }),
|
|
60
|
+
);
|
|
61
|
+
console.error(`gen: ${Object.keys(schemas).length} schema(s) from ${modArg} -> ${outArg}`);
|
package/src/gen.ts
ADDED
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
// gen.ts — the same schema, on the Bend side.
|
|
2
|
+
//
|
|
3
|
+
// A project writes its schema once, with `s` (src/index.ts), and this prints
|
|
4
|
+
// the same schema as Bend defs, so a Bend core checks the host's values against
|
|
5
|
+
// the one definition. Nothing is written twice, and nothing can drift: the
|
|
6
|
+
// generated file is output, not a second source.
|
|
7
|
+
//
|
|
8
|
+
// What is printed is `Schema.node` — the core's own Schema value, which `toNode`
|
|
9
|
+
// builds and `wf` accepts, so an ill-formed schema throws before anything is
|
|
10
|
+
// written. This file interprets nothing; it writes that value out in Bend's
|
|
11
|
+
// syntax. A constructor it has no case for throws here, and a case it prints
|
|
12
|
+
// wrongly is a Bend compile error. Neither can be a silent difference.
|
|
13
|
+
//
|
|
14
|
+
// The constructor names and their field order are core/core.bend's own
|
|
15
|
+
// (`type Schema is Data:`), which is why this file goes with bend-schema rather
|
|
16
|
+
// than in a package of its own: a new constructor touches both.
|
|
17
|
+
|
|
18
|
+
import type { BendList, Schema as Node } from "../dist-core/core.mjs";
|
|
19
|
+
import type { Schema } from "./index";
|
|
20
|
+
|
|
21
|
+
const q = (s: string) => JSON.stringify(s);
|
|
22
|
+
const n = (x: bigint | number) => `${x}n`;
|
|
23
|
+
const ctor = (name: string, args: string[]) =>
|
|
24
|
+
args.length ? `S.${name}{${args.join(", ")}}` : `S.${name}{}`;
|
|
25
|
+
|
|
26
|
+
// A `List<&2, String>` as Bend source: "a" <> "b" <> Nil{}.
|
|
27
|
+
function strList(l: BendList<string>): string {
|
|
28
|
+
const out: string[] = [];
|
|
29
|
+
let cur: BendList<string> = l;
|
|
30
|
+
while (cur.$ === "Con") {
|
|
31
|
+
out.push(q(cur.head));
|
|
32
|
+
cur = cur.tail;
|
|
33
|
+
}
|
|
34
|
+
return out.length ? out.join(" <> ") + " <> Nil{}" : "Nil{}";
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** One schema as a Bend expression, e.g. `S.SNatIn{1n, 64n}`. */
|
|
38
|
+
export function printNode(x: Node): string {
|
|
39
|
+
switch (x.$) {
|
|
40
|
+
case "SNat":
|
|
41
|
+
case "SStr":
|
|
42
|
+
case "SBool":
|
|
43
|
+
case "STrue":
|
|
44
|
+
case "SEnd":
|
|
45
|
+
case "STEnd":
|
|
46
|
+
case "SVEnd":
|
|
47
|
+
return ctor(x.$, []);
|
|
48
|
+
case "SNatIn":
|
|
49
|
+
return ctor("SNatIn", [n(x.lo as bigint), n(x.hi as bigint)]);
|
|
50
|
+
case "SStrLen":
|
|
51
|
+
return ctor("SStrLen", [n(x.lo as bigint), n(x.hi as bigint), printNode(x.s as Node)]);
|
|
52
|
+
case "SOpt":
|
|
53
|
+
return ctor("SOpt", [printNode(x.inner as Node)]);
|
|
54
|
+
case "SOptional":
|
|
55
|
+
return ctor("SOptional", [printNode(x.inner as Node)]);
|
|
56
|
+
case "SList":
|
|
57
|
+
return ctor("SList", [printNode(x.elem as Node)]);
|
|
58
|
+
case "SListLen":
|
|
59
|
+
return ctor("SListLen", [n(x.lo as bigint), n(x.hi as bigint), printNode(x.s as Node)]);
|
|
60
|
+
case "SStrict":
|
|
61
|
+
return ctor("SStrict", [printNode(x.s as Node)]);
|
|
62
|
+
case "SField":
|
|
63
|
+
return ctor("SField", [q(x.name as string), printNode(x.s as Node), printNode(x.rest as Node)]);
|
|
64
|
+
case "SEnum":
|
|
65
|
+
return ctor("SEnum", [strList(x.names as BendList<string>)]);
|
|
66
|
+
case "STuple":
|
|
67
|
+
return ctor("STuple", [printNode(x.s as Node), printNode(x.rest as Node)]);
|
|
68
|
+
case "SVariant":
|
|
69
|
+
return ctor("SVariant", [q(x.name as string), printNode(x.s as Node), printNode(x.rest as Node)]);
|
|
70
|
+
case "STagged":
|
|
71
|
+
return ctor("STagged", [q(x.key as string), q(x.name as string), printNode(x.s as Node), printNode(x.rest as Node)]);
|
|
72
|
+
case "STagEnd":
|
|
73
|
+
return ctor("STagEnd", [q(x.key as string)]);
|
|
74
|
+
default:
|
|
75
|
+
throw new Error(`gen: no Bend form for ${x.$}`);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/** A whole Bend file: one `def <key>_schema() -> S.Schema:` per key. */
|
|
80
|
+
export function emit(
|
|
81
|
+
schemas: Record<string, Schema<unknown>>,
|
|
82
|
+
opts: { libPath: string; from: string },
|
|
83
|
+
): string {
|
|
84
|
+
const defs: string[] = [];
|
|
85
|
+
for (const [name, s] of Object.entries(schemas)) {
|
|
86
|
+
if (!/^[A-Za-z_]\w*$/.test(name)) throw new Error(`gen: "${name}" is not a Bend name`);
|
|
87
|
+
if (!s || typeof s.node !== "object") throw new Error(`gen: ${name} is not a Schema`);
|
|
88
|
+
defs.push(`def ${name}_schema() -> S.Schema:\n ${printNode(s.node as Node)}\n`);
|
|
89
|
+
}
|
|
90
|
+
if (defs.length === 0) throw new Error("gen: nothing to print");
|
|
91
|
+
return (
|
|
92
|
+
`import Base\nimport ${opts.libPath} as S\n\n` +
|
|
93
|
+
`# Generated by bend-schema from ${opts.from}. Do not edit; regenerate.\n\n` +
|
|
94
|
+
defs.join("\n")
|
|
95
|
+
);
|
|
96
|
+
}
|