@reventlessdev/reventless-spec 3.0.0-alpha.123 → 3.0.0-alpha.125
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/CHANGELOG.md +56 -0
- package/package.json +5 -3
- package/run-certify-trait.mjs +2 -0
- package/run-graft-trait.mjs +2 -0
- package/run-trait-manifest.mjs +2 -0
- package/schema/platform-api.graphql +41 -3
- package/src/components/Aggregate.res +22 -0
- package/src/components/AutomationSlice.res +29 -3
- package/src/components/CapabilityManifest.res +52 -24
- package/src/components/CapabilityManifest.res.mjs +35 -11
- package/src/components/InboundTranslationSlice.res +14 -0
- package/src/components/OutboundTranslationSlice.res +24 -0
- package/src/components/Plugin.res +202 -422
- package/src/components/Plugin.res.mjs +65 -3
- package/src/components/StateChangeSlice.res +22 -0
- package/src/components/TraitCertificate.res +105 -0
- package/src/components/TraitCertificate.res.mjs +65 -0
- package/src/components/TraitManifest.res +90 -0
- package/src/components/TraitManifest.res.mjs +48 -0
- package/src/generator/CertifyTrait.res +190 -0
- package/src/generator/CertifyTrait.res.mjs +154 -0
- package/src/generator/GraftTrait.res +230 -0
- package/src/generator/GraftTrait.res.mjs +193 -0
- package/src/generator/PlatformCodegen.res +44 -30
- package/src/generator/PlatformCodegen.res.mjs +36 -17
- package/src/generator/TraitManifestCli.res +138 -0
- package/src/generator/TraitManifestCli.res.mjs +105 -0
- package/src/semantic/Capabilities.res +19 -3
- package/src/semantic/Capabilities.res.mjs +18 -2
- package/src/semantic/CapabilityNeed.res +81 -0
- package/src/semantic/CapabilityNeed.res.mjs +46 -0
- package/src/semantic/Currency.res +519 -502
- package/src/semantic/Currency.res.mjs +7 -655
- package/src/semantic/Messaging.res +127 -0
- package/src/semantic/Messaging.res.mjs +57 -0
- package/src/semantic/Money.res +123 -21
- package/src/semantic/Money.res.mjs +49 -2
- package/src/types/Trait.res +62 -0
- package/src/types/Trait.res.mjs +18 -0
- package/src/types/Transition.res +71 -0
- package/src/types/Transition.res.mjs +36 -0
- package/scripts/generate-currency.mjs +0 -215
- package/scripts/iso-4217-list-one.xml +0 -1956
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Sury from "sury";
|
|
4
|
+
import * as Nodefs from "node:fs";
|
|
5
|
+
import * as Nodeurl from "node:url";
|
|
6
|
+
import * as Nodemodule from "node:module";
|
|
7
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
8
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
9
|
+
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
10
|
+
import * as PackageVersion$Reventless from "../PackageVersion.res.mjs";
|
|
11
|
+
import * as TraitCertificate$Reventless from "../components/TraitCertificate.res.mjs";
|
|
12
|
+
|
|
13
|
+
function fail(message) {
|
|
14
|
+
console.error("certify-trait: " + message);
|
|
15
|
+
process.exit(1);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
let usage = `Usage: certify-trait <trait-package> --host <componentName> --report <jest.json> --out <file>
|
|
19
|
+
|
|
20
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
21
|
+
--host the bound component's name, as its Spec declares it
|
|
22
|
+
--report a Jest JSON report (jest --json --outputFile=…)
|
|
23
|
+
--out where to write the certificate
|
|
24
|
+
|
|
25
|
+
Exits non-zero if the suite is absent from the report, or if it did not pass —
|
|
26
|
+
an absent suite is the failure worth catching, since a build that never ran the
|
|
27
|
+
conformance suite is indistinguishable from one that ran it green.`;
|
|
28
|
+
|
|
29
|
+
let reportAssertionSchema = Sury.$schema(s => ({
|
|
30
|
+
ancestorTitles: s.m(Sury.array(Sury.string)),
|
|
31
|
+
title: s.m(Sury.string),
|
|
32
|
+
status: s.m(Sury.string)
|
|
33
|
+
}));
|
|
34
|
+
|
|
35
|
+
let reportSuiteSchema = Sury.$schema(s => ({
|
|
36
|
+
assertionResults: s.m(Sury.array(reportAssertionSchema))
|
|
37
|
+
}));
|
|
38
|
+
|
|
39
|
+
let reportSchema = Sury.$schema(s => ({
|
|
40
|
+
testResults: s.m(Sury.array(reportSuiteSchema))
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
function assertionsFor(report, suite) {
|
|
44
|
+
return report.testResults.flatMap(s => Stdlib_Array.filterMap(s.assertionResults, a => {
|
|
45
|
+
if (a.ancestorTitles.includes(suite)) {
|
|
46
|
+
return [
|
|
47
|
+
a.title,
|
|
48
|
+
a.status === "passed"
|
|
49
|
+
];
|
|
50
|
+
}
|
|
51
|
+
}));
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function readJson(path) {
|
|
55
|
+
try {
|
|
56
|
+
return JSON.parse(Nodefs.readFileSync(path, "utf8"));
|
|
57
|
+
} catch (exn) {
|
|
58
|
+
fail(`could not read ` + path + ` as JSON.`);
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async function main() {
|
|
64
|
+
let argv = process.argv.slice(2, process.argv.length);
|
|
65
|
+
let flag = key => {
|
|
66
|
+
let i = argv.indexOf("--" + key);
|
|
67
|
+
if (i !== -1) {
|
|
68
|
+
return argv[i + 1 | 0];
|
|
69
|
+
}
|
|
70
|
+
};
|
|
71
|
+
let traitPackage = argv[0];
|
|
72
|
+
if (traitPackage !== undefined) {
|
|
73
|
+
switch (traitPackage) {
|
|
74
|
+
case "" :
|
|
75
|
+
case "--help" :
|
|
76
|
+
case "-h" :
|
|
77
|
+
break;
|
|
78
|
+
default:
|
|
79
|
+
let match = flag("host");
|
|
80
|
+
let match$1 = flag("report");
|
|
81
|
+
let match$2 = flag("out");
|
|
82
|
+
if (match === undefined) {
|
|
83
|
+
return fail("--host, --report and --out are all required.\n\n" + usage);
|
|
84
|
+
}
|
|
85
|
+
if (match$1 === undefined) {
|
|
86
|
+
return fail("--host, --report and --out are all required.\n\n" + usage);
|
|
87
|
+
}
|
|
88
|
+
if (match$2 === undefined) {
|
|
89
|
+
return fail("--host, --report and --out are all required.\n\n" + usage);
|
|
90
|
+
}
|
|
91
|
+
let conformanceModule = Stdlib_Option.getOr(Stdlib_Array.last(traitPackage.split("/")), "").replace("trait-", "").split("-").map(part => part.charAt(0).toUpperCase() + part.slice(1, part.length)).join("") + "_Conformance";
|
|
92
|
+
let specifier = traitPackage + `/src/` + conformanceModule + `.res.mjs`;
|
|
93
|
+
let modulePath;
|
|
94
|
+
try {
|
|
95
|
+
modulePath = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
|
|
96
|
+
} catch (exn) {
|
|
97
|
+
modulePath = specifier;
|
|
98
|
+
}
|
|
99
|
+
let conformance;
|
|
100
|
+
try {
|
|
101
|
+
conformance = await import(Nodeurl.pathToFileURL(modulePath).href);
|
|
102
|
+
} catch (exn$1) {
|
|
103
|
+
fail(traitPackage + ` ships no conformance suite (looked for ` + specifier + `).\n A trait without one cannot be certified — there is nothing to prove.`);
|
|
104
|
+
conformance = undefined;
|
|
105
|
+
}
|
|
106
|
+
let suite = conformance.suiteName(match);
|
|
107
|
+
let parsed;
|
|
108
|
+
try {
|
|
109
|
+
parsed = Util_Sury$Reventless.fromJson(readJson(match$1), reportSchema);
|
|
110
|
+
} catch (exn$2) {
|
|
111
|
+
fail(match$1 + ` is not a Jest JSON report (expected \`testResults\`).`);
|
|
112
|
+
parsed = undefined;
|
|
113
|
+
}
|
|
114
|
+
let results = assertionsFor(parsed, suite);
|
|
115
|
+
if (results.length === 0) {
|
|
116
|
+
return fail(`the report contains no suite titled "` + suite + `".\n` + (` Either the conformance binding was never registered, or ` + match + ` is not the `) + `name its Spec declares.`);
|
|
117
|
+
}
|
|
118
|
+
let resolveVersion = specifier => {
|
|
119
|
+
try {
|
|
120
|
+
let entry = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
|
|
121
|
+
return PackageVersion$Reventless.fromModuleUrl(Nodeurl.pathToFileURL(entry).href);
|
|
122
|
+
} catch (exn) {
|
|
123
|
+
return "0.0.0";
|
|
124
|
+
}
|
|
125
|
+
};
|
|
126
|
+
let certificate = TraitCertificate$Reventless.fromReport(traitPackage, resolveVersion(specifier), resolveVersion("@reventlessdev/reventless-spec/package.json"), match, suite, results);
|
|
127
|
+
Nodefs.writeFileSync(match$2, TraitCertificate$Reventless.render(certificate), "utf8");
|
|
128
|
+
console.log(`certify-trait: ` + TraitCertificate$Reventless.summarize(certificate));
|
|
129
|
+
console.log(`Wrote: ` + match$2);
|
|
130
|
+
if (!TraitCertificate$Reventless.verified(certificate)) {
|
|
131
|
+
process.exit(1);
|
|
132
|
+
return;
|
|
133
|
+
} else {
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
console.log(usage);
|
|
139
|
+
process.exit(argv.length === 0 ? 1 : 0);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
main();
|
|
143
|
+
|
|
144
|
+
export {
|
|
145
|
+
fail,
|
|
146
|
+
usage,
|
|
147
|
+
reportAssertionSchema,
|
|
148
|
+
reportSuiteSchema,
|
|
149
|
+
reportSchema,
|
|
150
|
+
assertionsFor,
|
|
151
|
+
readJson,
|
|
152
|
+
main,
|
|
153
|
+
}
|
|
154
|
+
/* reportAssertionSchema Not a pure module */
|
|
@@ -0,0 +1,230 @@
|
|
|
1
|
+
// Graft a domain trait into a plugin: write the spec surface it can own, print
|
|
2
|
+
// the arms it cannot.
|
|
3
|
+
//
|
|
4
|
+
// Usage: graft-trait <trait-package> --into <srcDir> --tests <testsDir> [--key value …]
|
|
5
|
+
//
|
|
6
|
+
// The trait is resolved **by the name it is given** and its scaffold module is
|
|
7
|
+
// dynamically imported. That is what keeps this file honest: `reventless-spec`
|
|
8
|
+
// ships the CLI and never depends on a trait, so the rule that framework
|
|
9
|
+
// packages never import one survives a generator that runs their code.
|
|
10
|
+
//
|
|
11
|
+
// Every remaining `--key value` becomes a config field, handed to the trait's
|
|
12
|
+
// own sury-validated schema. A misspelled key is a decode error naming the key,
|
|
13
|
+
// here, rather than a placeholder that survives into a file and fails at
|
|
14
|
+
// compile — which is the whole difference between this and a text template.
|
|
15
|
+
|
|
16
|
+
@val external dynImport: string => promise<'a> = "import"
|
|
17
|
+
|
|
18
|
+
// ── The dynamic-import boundary ──────────────────────────────────────────────
|
|
19
|
+
//
|
|
20
|
+
// Typed at the boundary: a trait's scaffold module exports exactly these two,
|
|
21
|
+
// and everything downstream is ordinary typed ReScript. Same shape as
|
|
22
|
+
// `EmitCapabilities`, which reaches a composition root the same way.
|
|
23
|
+
|
|
24
|
+
type file = {path: string, contents: string}
|
|
25
|
+
type patch = {into: string, at: string, contents: string}
|
|
26
|
+
type output = {files: array<file>, patches: array<patch>}
|
|
27
|
+
type scaffoldExports = {
|
|
28
|
+
configSchema: S.t<unknown>,
|
|
29
|
+
emit: (~config: unknown, ~into: string, ~tests: string) => output,
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let fail = (message: string) => {
|
|
33
|
+
Console.error("graft-trait: " ++ message)
|
|
34
|
+
NodeProcess.exit(1)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
let usage = `Usage: graft-trait <trait-package> --into <srcDir> --tests <testsDir> [--key value …]
|
|
38
|
+
|
|
39
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
40
|
+
--into where the graft's own components are written
|
|
41
|
+
--tests where its conformance binding is written
|
|
42
|
+
--dry-run print what would be written, write nothing
|
|
43
|
+
|
|
44
|
+
Every other --key value is a field of the trait's config. The trait validates
|
|
45
|
+
them, so run it once to be told what it wants.`
|
|
46
|
+
|
|
47
|
+
// ── Arguments ────────────────────────────────────────────────────────────────
|
|
48
|
+
|
|
49
|
+
/** `--key value` pairs, in order, with the leading positional taken off first.
|
|
50
|
+
A flag with no value (`--dry-run`) reads as `true`, so the config schema can
|
|
51
|
+
take it as a boolean without the CLI knowing which keys are flags. */
|
|
52
|
+
let parseFlags = (args: array<string>): dict<JSON.t> => {
|
|
53
|
+
let out = Dict.make()
|
|
54
|
+
let rec go = i =>
|
|
55
|
+
switch args->Array.get(i) {
|
|
56
|
+
| None => ()
|
|
57
|
+
| Some(arg) if arg->String.startsWith("--") =>
|
|
58
|
+
let key = arg->String.sliceToEnd(~start=2)
|
|
59
|
+
switch args->Array.get(i + 1) {
|
|
60
|
+
| Some(value) if !(value->String.startsWith("--")) =>
|
|
61
|
+
out->Dict.set(key, JSON.Encode.string(value))
|
|
62
|
+
go(i + 2)
|
|
63
|
+
| _ =>
|
|
64
|
+
out->Dict.set(key, JSON.Encode.bool(true))
|
|
65
|
+
go(i + 1)
|
|
66
|
+
}
|
|
67
|
+
| Some(_) => go(i + 1)
|
|
68
|
+
}
|
|
69
|
+
go(0)
|
|
70
|
+
out
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** A comma-separated value becomes an array. Applied only to keys the trait's
|
|
74
|
+
schema declares as arrays, so a name that happens to contain a comma is not
|
|
75
|
+
split behind the author's back. */
|
|
76
|
+
let splitList = (value: string): JSON.t =>
|
|
77
|
+
value
|
|
78
|
+
->String.split(",")
|
|
79
|
+
->Array.map(part => part->String.trim)
|
|
80
|
+
->Array.filter(part => part != "")
|
|
81
|
+
->Array.map(JSON.Encode.string)
|
|
82
|
+
->JSON.Encode.array
|
|
83
|
+
|
|
84
|
+
let arrayFieldNames = (schema: S.t<unknown>): array<string> =>
|
|
85
|
+
switch schema {
|
|
86
|
+
| Object({properties}) =>
|
|
87
|
+
properties
|
|
88
|
+
->Dict.toArray
|
|
89
|
+
->Array.filterMap(((name, field)) =>
|
|
90
|
+
switch field {
|
|
91
|
+
| Array(_) => Some(name)
|
|
92
|
+
| AnyOf({anyOf}) =>
|
|
93
|
+
anyOf->Array.some(m =>
|
|
94
|
+
switch m {
|
|
95
|
+
| Array(_) => true
|
|
96
|
+
| _ => false
|
|
97
|
+
}
|
|
98
|
+
)
|
|
99
|
+
? Some(name)
|
|
100
|
+
: None
|
|
101
|
+
| _ => None
|
|
102
|
+
}
|
|
103
|
+
)
|
|
104
|
+
| _ => []
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── Entry point ──────────────────────────────────────────────────────────────
|
|
108
|
+
|
|
109
|
+
let main = async () => {
|
|
110
|
+
let argv = NodeProcess.argv->Array.sliceToEnd(~start=2)
|
|
111
|
+
switch argv->Array.get(0) {
|
|
112
|
+
| None | Some("") | Some("--help") | Some("-h") => {
|
|
113
|
+
Console.log(usage)
|
|
114
|
+
NodeProcess.exit(argv->Array.length == 0 ? 1 : 0)
|
|
115
|
+
}
|
|
116
|
+
| Some(traitPackage) => {
|
|
117
|
+
let flags = parseFlags(argv->Array.sliceToEnd(~start=1))
|
|
118
|
+
let stringFlag = key => flags->Dict.get(key)->Option.flatMap(JSON.Decode.string)
|
|
119
|
+
let into = stringFlag("into")
|
|
120
|
+
let tests = stringFlag("tests")
|
|
121
|
+
let dryRun = flags->Dict.get("dry-run")->Option.isSome
|
|
122
|
+
|
|
123
|
+
switch (into, tests) {
|
|
124
|
+
| (None, _) | (_, None) => fail("--into and --tests are both required.\n\n" ++ usage)
|
|
125
|
+
| (Some(into), Some(tests)) => {
|
|
126
|
+
// Resolved by name, from the consumer's own `node_modules`. A trait
|
|
127
|
+
// that ships no scaffold fails here, saying so, rather than half-way
|
|
128
|
+
// through writing files.
|
|
129
|
+
// `@scope/trait-attachments` → `Attachments_Scaffold`;
|
|
130
|
+
// `@scope/trait-address-geocoding` → `AddressGeocoding_Scaffold`.
|
|
131
|
+
let scaffoldModule =
|
|
132
|
+
traitPackage
|
|
133
|
+
->String.split("/")
|
|
134
|
+
->Array.last
|
|
135
|
+
->Option.getOr("")
|
|
136
|
+
->String.replace("trait-", "")
|
|
137
|
+
->String.split("-")
|
|
138
|
+
->Array.map(part =>
|
|
139
|
+
part->String.charAt(0)->String.toUpperCase ++ part->String.sliceToEnd(~start=1)
|
|
140
|
+
)
|
|
141
|
+
->Array.join("") ++ "_Scaffold"
|
|
142
|
+
let specifier = `${traitPackage}/src/${scaffoldModule}.res.mjs`
|
|
143
|
+
// Resolved from the **caller's** directory, not this file's. The trait
|
|
144
|
+
// is a dependency of the plugin being grafted, and deliberately not one
|
|
145
|
+
// of this package — a bare `import()` here would look in
|
|
146
|
+
// `reventless-spec`'s own tree and never find it. Same reason
|
|
147
|
+
// `HostShellDist` reaches for the host shell this way.
|
|
148
|
+
let modulePath = try NodeModule.createRequire(
|
|
149
|
+
NodeProcess.cwd() ++ "/index.js",
|
|
150
|
+
)->NodeModule.requireResolve(specifier) catch {
|
|
151
|
+
| _ => specifier
|
|
152
|
+
}
|
|
153
|
+
let scaffold: scaffoldExports = try await dynImport(
|
|
154
|
+
NodeUrl.pathToFileURL(modulePath)["href"],
|
|
155
|
+
) catch {
|
|
156
|
+
| _ =>
|
|
157
|
+
fail(
|
|
158
|
+
`${traitPackage} ships no scaffold (looked for ${specifier}).\n` ++
|
|
159
|
+
` Not every trait has one — a trait whose graft is all patches has nothing to write.\n` ++
|
|
160
|
+
` Write the graft by hand instead; its README says what the host must declare.`,
|
|
161
|
+
)
|
|
162
|
+
%raw(`undefined`)
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// The trait validates its own config, so the CLI never learns a
|
|
166
|
+
// trait's vocabulary — and a misspelled key is named here.
|
|
167
|
+
let listFields = arrayFieldNames(scaffold.configSchema)
|
|
168
|
+
let raw = Dict.make()
|
|
169
|
+
flags
|
|
170
|
+
->Dict.toArray
|
|
171
|
+
->Array.forEach(((key, value)) =>
|
|
172
|
+
switch (listFields->Array.includes(key), value->JSON.Decode.string) {
|
|
173
|
+
| (true, Some(text)) => raw->Dict.set(key, splitList(text))
|
|
174
|
+
| _ => raw->Dict.set(key, value)
|
|
175
|
+
}
|
|
176
|
+
)
|
|
177
|
+
["into", "tests", "dry-run"]->Array.forEach(k => raw->Dict.delete(k))
|
|
178
|
+
|
|
179
|
+
let config = try raw->JSON.Encode.object->Util_Sury.fromJson(scaffold.configSchema) catch {
|
|
180
|
+
| exn => {
|
|
181
|
+
fail(
|
|
182
|
+
`${traitPackage} refused this config: ${Util_Sury.exnMessage(exn)}\n` ++
|
|
183
|
+
` Every --key is a field of the trait's own config; it decides what it needs.`,
|
|
184
|
+
)
|
|
185
|
+
%raw(`undefined`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
let {files, patches} = scaffold.emit(~config, ~into, ~tests)
|
|
190
|
+
|
|
191
|
+
files->Array.forEach(({path, contents}) =>
|
|
192
|
+
if dryRun {
|
|
193
|
+
Console.log(`Would write: ${path} (${contents->String.split("\n")->Array.length->Int.toString} lines)`)
|
|
194
|
+
} else if NodeFs.existsSync(path) {
|
|
195
|
+
// Never overwrite. From the moment a graft lands it is the host's
|
|
196
|
+
// source, edited with the policy the trait deliberately does not
|
|
197
|
+
// write — silently replacing it would delete exactly the part no
|
|
198
|
+
// generator can reproduce.
|
|
199
|
+
Console.log(`Skipped (exists): ${path}`)
|
|
200
|
+
} else {
|
|
201
|
+
NodeFs.mkdirSync(NodePath.dirname(path), {recursive: true})
|
|
202
|
+
NodeFs.writeFileSync(path, contents)
|
|
203
|
+
Console.log(`Wrote: ${path}`)
|
|
204
|
+
}
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if patches->Array.length > 0 {
|
|
208
|
+
Console.log("")
|
|
209
|
+
Console.log("── Paste these; they go into files you already own ──────────────")
|
|
210
|
+
patches->Array.forEach(({into, at, contents}) => {
|
|
211
|
+
Console.log("")
|
|
212
|
+
Console.log(`# ${into} — ${at}`)
|
|
213
|
+
Console.log(contents)
|
|
214
|
+
})
|
|
215
|
+
Console.log("")
|
|
216
|
+
Console.log(
|
|
217
|
+
"Printed rather than written: placing an arm in an existing ordered `switch`\n" ++
|
|
218
|
+
"is an AST operation, and a text splice into the wrong arm is a bug the\n" ++
|
|
219
|
+
"compiler cannot see.",
|
|
220
|
+
)
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
NodeProcess.exit(0)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
await main()
|
|
@@ -0,0 +1,193 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodeurl from "node:url";
|
|
5
|
+
import * as Nodepath from "node:path";
|
|
6
|
+
import * as Stdlib_Dict from "@rescript/runtime/lib/es6/Stdlib_Dict.js";
|
|
7
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
8
|
+
import * as Nodemodule from "node:module";
|
|
9
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
10
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
11
|
+
import * as Primitive_exceptions from "@rescript/runtime/lib/es6/Primitive_exceptions.js";
|
|
12
|
+
import * as Util_Sury$Reventless from "../util/Util_Sury.res.mjs";
|
|
13
|
+
|
|
14
|
+
function fail(message) {
|
|
15
|
+
console.error("graft-trait: " + message);
|
|
16
|
+
process.exit(1);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
let usage = `Usage: graft-trait <trait-package> --into <srcDir> --tests <testsDir> [--key value …]
|
|
20
|
+
|
|
21
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
22
|
+
--into where the graft's own components are written
|
|
23
|
+
--tests where its conformance binding is written
|
|
24
|
+
--dry-run print what would be written, write nothing
|
|
25
|
+
|
|
26
|
+
Every other --key value is a field of the trait's config. The trait validates
|
|
27
|
+
them, so run it once to be told what it wants.`;
|
|
28
|
+
|
|
29
|
+
function parseFlags(args) {
|
|
30
|
+
let out = {};
|
|
31
|
+
let go = _i => {
|
|
32
|
+
while (true) {
|
|
33
|
+
let i = _i;
|
|
34
|
+
let arg = args[i];
|
|
35
|
+
if (arg === undefined) {
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (arg.startsWith("--")) {
|
|
39
|
+
let key = arg.slice(2);
|
|
40
|
+
let value = args[i + 1 | 0];
|
|
41
|
+
if (value !== undefined && !value.startsWith("--")) {
|
|
42
|
+
out[key] = value;
|
|
43
|
+
_i = i + 2 | 0;
|
|
44
|
+
continue;
|
|
45
|
+
}
|
|
46
|
+
out[key] = true;
|
|
47
|
+
_i = i + 1 | 0;
|
|
48
|
+
continue;
|
|
49
|
+
}
|
|
50
|
+
_i = i + 1 | 0;
|
|
51
|
+
continue;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
go(0);
|
|
55
|
+
return out;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function splitList(value) {
|
|
59
|
+
return value.split(",").map(part => part.trim()).filter(part => part !== "").map(prim => prim);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function arrayFieldNames(schema) {
|
|
63
|
+
if (schema.type === "object") {
|
|
64
|
+
return Stdlib_Array.filterMap(Object.entries(schema.properties), param => {
|
|
65
|
+
let field = param[1];
|
|
66
|
+
let name = param[0];
|
|
67
|
+
switch (field.type) {
|
|
68
|
+
case "array" :
|
|
69
|
+
return name;
|
|
70
|
+
case "anyOf" :
|
|
71
|
+
if (field.anyOf.some(m => m.type === "array")) {
|
|
72
|
+
return name;
|
|
73
|
+
} else {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
default:
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
});
|
|
80
|
+
} else {
|
|
81
|
+
return [];
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
async function main() {
|
|
86
|
+
let argv = process.argv.slice(2);
|
|
87
|
+
let traitPackage = argv[0];
|
|
88
|
+
if (traitPackage !== undefined) {
|
|
89
|
+
switch (traitPackage) {
|
|
90
|
+
case "" :
|
|
91
|
+
case "--help" :
|
|
92
|
+
case "-h" :
|
|
93
|
+
break;
|
|
94
|
+
default:
|
|
95
|
+
let flags = parseFlags(argv.slice(1));
|
|
96
|
+
let stringFlag = key => Stdlib_Option.flatMap(flags[key], Stdlib_JSON.Decode.string);
|
|
97
|
+
let into = stringFlag("into");
|
|
98
|
+
let tests = stringFlag("tests");
|
|
99
|
+
let dryRun = Stdlib_Option.isSome(flags["dry-run"]);
|
|
100
|
+
if (into === undefined) {
|
|
101
|
+
return fail("--into and --tests are both required.\n\n" + usage);
|
|
102
|
+
}
|
|
103
|
+
if (tests === undefined) {
|
|
104
|
+
return fail("--into and --tests are both required.\n\n" + usage);
|
|
105
|
+
}
|
|
106
|
+
let scaffoldModule = Stdlib_Option.getOr(Stdlib_Array.last(traitPackage.split("/")), "").replace("trait-", "").split("-").map(part => part.charAt(0).toUpperCase() + part.slice(1)).join("") + "_Scaffold";
|
|
107
|
+
let specifier = traitPackage + `/src/` + scaffoldModule + `.res.mjs`;
|
|
108
|
+
let modulePath;
|
|
109
|
+
try {
|
|
110
|
+
modulePath = Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
|
|
111
|
+
} catch (exn) {
|
|
112
|
+
modulePath = specifier;
|
|
113
|
+
}
|
|
114
|
+
let scaffold;
|
|
115
|
+
try {
|
|
116
|
+
scaffold = await import(Nodeurl.pathToFileURL(modulePath).href);
|
|
117
|
+
} catch (exn$1) {
|
|
118
|
+
fail(traitPackage + ` ships no scaffold (looked for ` + specifier + `).\n Not every trait has one — a trait whose graft is all patches has nothing to write.\n Write the graft by hand instead; its README says what the host must declare.`);
|
|
119
|
+
scaffold = undefined;
|
|
120
|
+
}
|
|
121
|
+
let listFields = arrayFieldNames(scaffold.configSchema);
|
|
122
|
+
let raw = {};
|
|
123
|
+
Object.entries(flags).forEach(param => {
|
|
124
|
+
let value = param[1];
|
|
125
|
+
let key = param[0];
|
|
126
|
+
let match = listFields.includes(key);
|
|
127
|
+
let match$1 = Stdlib_JSON.Decode.string(value);
|
|
128
|
+
if (match && match$1 !== undefined) {
|
|
129
|
+
raw[key] = splitList(match$1);
|
|
130
|
+
} else {
|
|
131
|
+
raw[key] = value;
|
|
132
|
+
}
|
|
133
|
+
});
|
|
134
|
+
[
|
|
135
|
+
"into",
|
|
136
|
+
"tests",
|
|
137
|
+
"dry-run"
|
|
138
|
+
].forEach(k => Stdlib_Dict.$$delete(raw, k));
|
|
139
|
+
let config;
|
|
140
|
+
try {
|
|
141
|
+
config = Util_Sury$Reventless.fromJson(raw, scaffold.configSchema);
|
|
142
|
+
} catch (raw_exn) {
|
|
143
|
+
let exn$2 = Primitive_exceptions.internalToException(raw_exn);
|
|
144
|
+
fail(traitPackage + ` refused this config: ` + Util_Sury$Reventless.exnMessage(exn$2, undefined) + `\n Every --key is a field of the trait's own config; it decides what it needs.`);
|
|
145
|
+
config = undefined;
|
|
146
|
+
}
|
|
147
|
+
let match = scaffold.emit(config, into, tests);
|
|
148
|
+
let patches = match.patches;
|
|
149
|
+
match.files.forEach(param => {
|
|
150
|
+
let contents = param.contents;
|
|
151
|
+
let path = param.path;
|
|
152
|
+
if (dryRun) {
|
|
153
|
+
console.log(`Would write: ` + path + ` (` + contents.split("\n").length.toString() + ` lines)`);
|
|
154
|
+
} else if (Nodefs.existsSync(path)) {
|
|
155
|
+
console.log(`Skipped (exists): ` + path);
|
|
156
|
+
} else {
|
|
157
|
+
Nodefs.mkdirSync(Nodepath.dirname(path), {
|
|
158
|
+
recursive: true
|
|
159
|
+
});
|
|
160
|
+
Nodefs.writeFileSync(path, contents, "utf8");
|
|
161
|
+
console.log(`Wrote: ` + path);
|
|
162
|
+
}
|
|
163
|
+
});
|
|
164
|
+
if (patches.length !== 0) {
|
|
165
|
+
console.log("");
|
|
166
|
+
console.log("── Paste these; they go into files you already own ──────────────");
|
|
167
|
+
patches.forEach(param => {
|
|
168
|
+
console.log("");
|
|
169
|
+
console.log(`# ` + param.into + ` — ` + param.at);
|
|
170
|
+
console.log(param.contents);
|
|
171
|
+
});
|
|
172
|
+
console.log("");
|
|
173
|
+
console.log("Printed rather than written: placing an arm in an existing ordered `switch`\nis an AST operation, and a text splice into the wrong arm is a bug the\ncompiler cannot see.");
|
|
174
|
+
}
|
|
175
|
+
process.exit(0);
|
|
176
|
+
return;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
console.log(usage);
|
|
180
|
+
process.exit(argv.length === 0 ? 1 : 0);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
await main();
|
|
184
|
+
|
|
185
|
+
export {
|
|
186
|
+
fail,
|
|
187
|
+
usage,
|
|
188
|
+
parseFlags,
|
|
189
|
+
splitList,
|
|
190
|
+
arrayFieldNames,
|
|
191
|
+
main,
|
|
192
|
+
}
|
|
193
|
+
/* Not a pure module */
|
|
@@ -17,17 +17,20 @@ type pluginManifest = {
|
|
|
17
17
|
manifest: CapabilityManifest.t,
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
/** One declaring site, keeping which plugin's manifest carried it.
|
|
20
|
+
/** One declaring site, keeping which plugin's manifest carried it. `field` is
|
|
21
|
+
absent for a capability a slice declares — there is no field to name. */
|
|
21
22
|
type provenance = {
|
|
22
23
|
pluginName: string,
|
|
23
24
|
component: string,
|
|
24
|
-
field: string
|
|
25
|
+
field: option<string>,
|
|
25
26
|
annotation: option<string>,
|
|
26
27
|
}
|
|
27
28
|
|
|
28
|
-
/** One capability the platform must provision: the qualified
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
/** One capability the platform must provision: the key — a qualified
|
|
30
|
+
`{plugin}.{store}` for a store, the capability's own name otherwise — with
|
|
31
|
+
every declaring site across every plugin. Identity is the key: many fields,
|
|
32
|
+
in several plugins, legitimately name one store, and several slices
|
|
33
|
+
legitimately need one capability. */
|
|
31
34
|
type unionEntry = {
|
|
32
35
|
kind: CapabilityManifest.kind,
|
|
33
36
|
key: string,
|
|
@@ -83,31 +86,42 @@ let splitKey = (key: string): option<(string, string)> =>
|
|
|
83
86
|
//
|
|
84
87
|
// A site with no recorded store predates the recording. It still names the
|
|
85
88
|
// field, which is the comment's job; it just makes no claim it cannot support.
|
|
89
|
+
let renderComments = (entry: unionEntry): array<string> =>
|
|
90
|
+
entry.declaredBy->Array.map(site => {
|
|
91
|
+
let site_ = switch site.field {
|
|
92
|
+
| Some(field) => ` // ${site.pluginName}: ${site.component}.${field}`
|
|
93
|
+
| None => ` // ${site.pluginName}: ${site.component}`
|
|
94
|
+
}
|
|
95
|
+
// `→ store` rather than `@storageRef("store")`: a field may name its
|
|
96
|
+
// store by annotation *or* by being named, and the manifest carries
|
|
97
|
+
// the store either way without saying which. Naming the annotation
|
|
98
|
+
// would send a reader looking for source text a derived field has not
|
|
99
|
+
// got.
|
|
100
|
+
switch site.annotation {
|
|
101
|
+
| Some(annotation) => `${site_} → ${annotation}`
|
|
102
|
+
| None => site_
|
|
103
|
+
}
|
|
104
|
+
})
|
|
105
|
+
|
|
86
106
|
let renderEntry = (entry: unionEntry): result<array<string>, string> =>
|
|
87
|
-
switch
|
|
88
|
-
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
// got.
|
|
101
|
-
switch site.annotation {
|
|
102
|
-
| Some(annotation) => `${site_} → ${annotation}`
|
|
103
|
-
| None => site_
|
|
104
|
-
}
|
|
105
|
-
})
|
|
106
|
-
let line = switch entry.kind {
|
|
107
|
-
| ObjectStore => ` ObjectStore({plugin: ${quote(plugin)}, store: ${quote(store)}}),`
|
|
108
|
-
}
|
|
109
|
-
Ok(comments->Array.concat([line]))
|
|
107
|
+
switch entry.kind {
|
|
108
|
+
| ObjectStore =>
|
|
109
|
+
switch splitKey(entry.key) {
|
|
110
|
+
| None =>
|
|
111
|
+
Error(
|
|
112
|
+
`malformed capability key ${quote(entry.key)} — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`,
|
|
113
|
+
)
|
|
114
|
+
| Some((plugin, store)) =>
|
|
115
|
+
Ok(
|
|
116
|
+
renderComments(entry)->Array.concat([
|
|
117
|
+
` ObjectStore({plugin: ${quote(plugin)}, store: ${quote(store)}}),`,
|
|
118
|
+
]),
|
|
119
|
+
)
|
|
110
120
|
}
|
|
121
|
+
// A capability with no per-plugin identity: one arm however many slices
|
|
122
|
+
// declared it, with every declaring slice kept as provenance.
|
|
123
|
+
| Geocoding => Ok(renderComments(entry)->Array.concat([" Geocoding,"]))
|
|
124
|
+
| Messaging => Ok(renderComments(entry)->Array.concat([" Messaging,"]))
|
|
111
125
|
}
|
|
112
126
|
|
|
113
127
|
/**
|
|
@@ -170,8 +184,8 @@ let header = [
|
|
|
170
184
|
"//",
|
|
171
185
|
"// The platform's capability list, unioned from the committed",
|
|
172
186
|
"// `capabilities.json` manifests of the plugins deploy-manifest.yaml names.",
|
|
173
|
-
"// Each entry keeps its declaring
|
|
174
|
-
"// disappears from this list, the diff says which
|
|
187
|
+
"// Each entry keeps its declaring sites as provenance, so when a capability",
|
|
188
|
+
"// disappears from this list, the diff says which change removed it.",
|
|
175
189
|
"",
|
|
176
190
|
]
|
|
177
191
|
|