@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
|
@@ -47,16 +47,10 @@ function splitKey(key) {
|
|
|
47
47
|
]);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
function
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
TAG: "Error",
|
|
55
|
-
_0: `malformed capability key ` + JSON.stringify(entry.key) + ` — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`
|
|
56
|
-
};
|
|
57
|
-
}
|
|
58
|
-
let comments = entry.declaredBy.map(site => {
|
|
59
|
-
let site_ = ` // ` + site.pluginName + `: ` + site.component + `.` + site.field;
|
|
50
|
+
function renderComments(entry) {
|
|
51
|
+
return entry.declaredBy.map(site => {
|
|
52
|
+
let field = site.field;
|
|
53
|
+
let site_ = field !== undefined ? ` // ` + site.pluginName + `: ` + site.component + `.` + field : ` // ` + site.pluginName + `: ` + site.component;
|
|
60
54
|
let annotation = site.annotation;
|
|
61
55
|
if (annotation !== undefined) {
|
|
62
56
|
return site_ + ` → ` + annotation;
|
|
@@ -64,11 +58,35 @@ function renderEntry(entry) {
|
|
|
64
58
|
return site_;
|
|
65
59
|
}
|
|
66
60
|
});
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function renderEntry(entry) {
|
|
64
|
+
let match = entry.kind;
|
|
65
|
+
switch (match) {
|
|
66
|
+
case "ObjectStore" :
|
|
67
|
+
let match$1 = splitKey(entry.key);
|
|
68
|
+
if (match$1 !== undefined) {
|
|
69
|
+
return {
|
|
70
|
+
TAG: "Ok",
|
|
71
|
+
_0: renderComments(entry).concat([` ObjectStore({plugin: ` + JSON.stringify(match$1[0]) + `, store: ` + JSON.stringify(match$1[1]) + `}),`])
|
|
72
|
+
};
|
|
73
|
+
} else {
|
|
74
|
+
return {
|
|
75
|
+
TAG: "Error",
|
|
76
|
+
_0: `malformed capability key ` + JSON.stringify(entry.key) + ` — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
case "Geocoding" :
|
|
80
|
+
return {
|
|
81
|
+
TAG: "Ok",
|
|
82
|
+
_0: renderComments(entry).concat([" Geocoding,"])
|
|
83
|
+
};
|
|
84
|
+
case "Messaging" :
|
|
85
|
+
return {
|
|
86
|
+
TAG: "Ok",
|
|
87
|
+
_0: renderComments(entry).concat([" Messaging,"])
|
|
88
|
+
};
|
|
89
|
+
}
|
|
72
90
|
}
|
|
73
91
|
|
|
74
92
|
function duplicatePluginOwners(entries) {
|
|
@@ -106,8 +124,8 @@ let header = [
|
|
|
106
124
|
"//",
|
|
107
125
|
"// The platform's capability list, unioned from the committed",
|
|
108
126
|
"// `capabilities.json` manifests of the plugins deploy-manifest.yaml names.",
|
|
109
|
-
"// Each entry keeps its declaring
|
|
110
|
-
"// disappears from this list, the diff says which
|
|
127
|
+
"// Each entry keeps its declaring sites as provenance, so when a capability",
|
|
128
|
+
"// disappears from this list, the diff says which change removed it.",
|
|
111
129
|
""
|
|
112
130
|
];
|
|
113
131
|
|
|
@@ -145,6 +163,7 @@ export {
|
|
|
145
163
|
union,
|
|
146
164
|
quote,
|
|
147
165
|
splitKey,
|
|
166
|
+
renderComments,
|
|
148
167
|
renderEntry,
|
|
149
168
|
duplicatePluginOwners,
|
|
150
169
|
duplicatePluginMessage,
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Derive a trait's listing metadata from the trait itself.
|
|
2
|
+
//
|
|
3
|
+
// Usage: trait-manifest <trait-package> --out <file>
|
|
4
|
+
//
|
|
5
|
+
// The old design had a hand-written `trait.yaml`. Everything in it is already a
|
|
6
|
+
// fact somewhere typed, so this reads those facts instead: identity from the
|
|
7
|
+
// package, needs from the value the trait exports, and the config surface from
|
|
8
|
+
// the emitter's own sury schema.
|
|
9
|
+
//
|
|
10
|
+
// Nothing here knows a trait's vocabulary. Both modules are resolved by the name
|
|
11
|
+
// the trait is given and dynamically imported, exactly as `graft-trait` reaches a
|
|
12
|
+
// scaffold — so `reventless-spec` still depends on no trait.
|
|
13
|
+
|
|
14
|
+
@val external dynImport: string => promise<'a> = "import"
|
|
15
|
+
|
|
16
|
+
type scaffoldExports = {configSchema: S.t<unknown>}
|
|
17
|
+
type traitExports = {capabilityNeeds: array<CapabilityNeed.t>}
|
|
18
|
+
|
|
19
|
+
let fail = (message: string) => {
|
|
20
|
+
Console.error("trait-manifest: " ++ message)
|
|
21
|
+
NodeProcess.exit(1)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
let usage = `Usage: trait-manifest <trait-package> --out <file>
|
|
25
|
+
|
|
26
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
27
|
+
--out where to write the manifest
|
|
28
|
+
|
|
29
|
+
Everything is read from the trait: identity from its package.json, capability
|
|
30
|
+
needs from the value it exports, the config surface from its emitter's schema.
|
|
31
|
+
There is nothing to hand-write and nothing to keep in step.`
|
|
32
|
+
|
|
33
|
+
/** `@scope/trait-address-geocoding` → `AddressGeocoding`. The same derivation
|
|
34
|
+
`graft-trait` and `certify-trait` do, for the same reason: a trait's module
|
|
35
|
+
names follow from its package name, so nothing has to be configured. */
|
|
36
|
+
let moduleBase = (traitPackage: string) =>
|
|
37
|
+
traitPackage
|
|
38
|
+
->String.split("/")
|
|
39
|
+
->Array.last
|
|
40
|
+
->Option.getOr("")
|
|
41
|
+
->String.replace("trait-", "")
|
|
42
|
+
->String.split("-")
|
|
43
|
+
->Array.map(part =>
|
|
44
|
+
part->String.charAt(0)->String.toUpperCase ++
|
|
45
|
+
part->String.slice(~start=1, ~end=part->String.length)
|
|
46
|
+
)
|
|
47
|
+
->Array.join("")
|
|
48
|
+
|
|
49
|
+
let resolveFrom = (specifier: string) =>
|
|
50
|
+
try Some(
|
|
51
|
+
NodeModule.createRequire(NodeProcess.cwd() ++ "/index.js")->NodeModule.requireResolve(specifier),
|
|
52
|
+
) catch {
|
|
53
|
+
| _ => None
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let readPackageField = (packageJson: JSON.t, field: string, fallback: string) =>
|
|
57
|
+
packageJson
|
|
58
|
+
->JSON.Decode.object
|
|
59
|
+
->Option.flatMap(o => o->Dict.get(field))
|
|
60
|
+
->Option.flatMap(JSON.Decode.string)
|
|
61
|
+
->Option.getOr(fallback)
|
|
62
|
+
|
|
63
|
+
let main = async () => {
|
|
64
|
+
let argv = NodeProcess.argv->Array.slice(~start=2, ~end=NodeProcess.argv->Array.length)
|
|
65
|
+
let flag = key =>
|
|
66
|
+
switch argv->Array.indexOf("--" ++ key) {
|
|
67
|
+
| -1 => None
|
|
68
|
+
| i => argv->Array.get(i + 1)
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
switch (argv->Array.get(0), flag("out")) {
|
|
72
|
+
| (None, _) | (Some(""), _) | (Some("--help"), _) | (Some("-h"), _) => {
|
|
73
|
+
Console.log(usage)
|
|
74
|
+
NodeProcess.exit(argv->Array.length == 0 ? 1 : 0)
|
|
75
|
+
}
|
|
76
|
+
| (Some(_), None) => fail("--out is required.\n\n" ++ usage)
|
|
77
|
+
| (Some(traitPackage), Some(out)) => {
|
|
78
|
+
let base = moduleBase(traitPackage)
|
|
79
|
+
|
|
80
|
+
let packageJson = switch resolveFrom(`${traitPackage}/package.json`) {
|
|
81
|
+
| None =>
|
|
82
|
+
fail(
|
|
83
|
+
`${traitPackage} is not installed here. A manifest is derived from the trait, so ` ++
|
|
84
|
+
`the trait has to be resolvable.`,
|
|
85
|
+
)
|
|
86
|
+
JSON.Encode.null
|
|
87
|
+
| Some(path) => NodeFs.readFileSync(path)->JSON.parseOrThrow
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// The trait's entry module, for what it needs. Absent is refused rather
|
|
91
|
+
// than defaulted to `[]`: "needs nothing" and "nobody said" are different
|
|
92
|
+
// claims, and a listing that could not tell them apart would quietly
|
|
93
|
+
// publish the second as the first.
|
|
94
|
+
let traitModule: traitExports = switch resolveFrom(`${traitPackage}/src/${base}.res.mjs`) {
|
|
95
|
+
| None =>
|
|
96
|
+
fail(
|
|
97
|
+
`${traitPackage} exports no ${base} module, so its capability needs cannot be read.\n` ++
|
|
98
|
+
` A trait states them as a value — an empty array if it brokers nothing — because ` ++
|
|
99
|
+
`an unstated need fails silently at run time.`,
|
|
100
|
+
)
|
|
101
|
+
%raw(`undefined`)
|
|
102
|
+
| Some(path) => await dynImport(NodeUrl.pathToFileURL(path)["href"])
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// The emitter is optional: a trait whose graft is all patches has nothing
|
|
106
|
+
// to write, and says so here rather than by failing when someone tries.
|
|
107
|
+
let scaffold: option<scaffoldExports> = switch resolveFrom(
|
|
108
|
+
`${traitPackage}/src/${base}_Scaffold.res.mjs`,
|
|
109
|
+
) {
|
|
110
|
+
| None => None
|
|
111
|
+
| Some(path) => Some(await dynImport(NodeUrl.pathToFileURL(path)["href"]))
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
let manifest: TraitManifest.t = {
|
|
115
|
+
trait: readPackageField(packageJson, "name", traitPackage),
|
|
116
|
+
version: readPackageField(packageJson, "version", "0.0.0"),
|
|
117
|
+
description: readPackageField(packageJson, "description", ""),
|
|
118
|
+
license: readPackageField(packageJson, "license", ""),
|
|
119
|
+
capabilities: traitModule.capabilityNeeds->Array.map(CapabilityNeed.toString),
|
|
120
|
+
config: switch scaffold {
|
|
121
|
+
| Some({configSchema}) => TraitManifest.configFieldsOf(configSchema)
|
|
122
|
+
| None => []
|
|
123
|
+
},
|
|
124
|
+
scaffolded: scaffold->Option.isSome,
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
NodeFs.writeFileSync(out, manifest->TraitManifest.render)
|
|
128
|
+
Console.log(
|
|
129
|
+
`trait-manifest: ${manifest.trait}@${manifest.version} — ` ++
|
|
130
|
+
`${(manifest.capabilities->Array.length)->Int.toString} capabilit(ies), ` ++
|
|
131
|
+
`${(manifest.config->Array.length)->Int.toString} config field(s)`,
|
|
132
|
+
)
|
|
133
|
+
Console.log(`Wrote: ${out}`)
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
main()->Promise.ignore
|
|
@@ -0,0 +1,105 @@
|
|
|
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 Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
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 TraitManifest$Reventless from "../components/TraitManifest.res.mjs";
|
|
10
|
+
import * as CapabilityNeed$Reventless from "../semantic/CapabilityNeed.res.mjs";
|
|
11
|
+
|
|
12
|
+
function fail(message) {
|
|
13
|
+
console.error("trait-manifest: " + message);
|
|
14
|
+
process.exit(1);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
let usage = `Usage: trait-manifest <trait-package> --out <file>
|
|
18
|
+
|
|
19
|
+
<trait-package> the installed trait, e.g. @reventlessdev/trait-attachments
|
|
20
|
+
--out where to write the manifest
|
|
21
|
+
|
|
22
|
+
Everything is read from the trait: identity from its package.json, capability
|
|
23
|
+
needs from the value it exports, the config surface from its emitter's schema.
|
|
24
|
+
There is nothing to hand-write and nothing to keep in step.`;
|
|
25
|
+
|
|
26
|
+
function moduleBase(traitPackage) {
|
|
27
|
+
return Stdlib_Option.getOr(Stdlib_Array.last(traitPackage.split("/")), "").replace("trait-", "").split("-").map(part => part.charAt(0).toUpperCase() + part.slice(1, part.length)).join("");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function resolveFrom(specifier) {
|
|
31
|
+
try {
|
|
32
|
+
return Nodemodule.createRequire(process.cwd() + "/index.js").resolve(specifier);
|
|
33
|
+
} catch (exn) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readPackageField(packageJson, field, fallback) {
|
|
39
|
+
return Stdlib_Option.getOr(Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(packageJson), o => o[field]), Stdlib_JSON.Decode.string), fallback);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
async function main() {
|
|
43
|
+
let argv = process.argv.slice(2, process.argv.length);
|
|
44
|
+
let flag = key => {
|
|
45
|
+
let i = argv.indexOf("--" + key);
|
|
46
|
+
if (i !== -1) {
|
|
47
|
+
return argv[i + 1 | 0];
|
|
48
|
+
}
|
|
49
|
+
};
|
|
50
|
+
let match = argv[0];
|
|
51
|
+
let match$1 = flag("out");
|
|
52
|
+
if (match !== undefined) {
|
|
53
|
+
switch (match) {
|
|
54
|
+
case "" :
|
|
55
|
+
case "--help" :
|
|
56
|
+
case "-h" :
|
|
57
|
+
break;
|
|
58
|
+
default:
|
|
59
|
+
if (match$1 === undefined) {
|
|
60
|
+
return fail("--out is required.\n\n" + usage);
|
|
61
|
+
}
|
|
62
|
+
let base = moduleBase(match);
|
|
63
|
+
let path = resolveFrom(match + `/package.json`);
|
|
64
|
+
let packageJson = path !== undefined ? JSON.parse(Nodefs.readFileSync(path, "utf8")) : (fail(match + ` is not installed here. A manifest is derived from the trait, so the trait has to be resolvable.`), null);
|
|
65
|
+
let path$1 = resolveFrom(match + `/src/` + base + `.res.mjs`);
|
|
66
|
+
let traitModule = path$1 !== undefined ? await import(Nodeurl.pathToFileURL(path$1).href) : (fail(match + ` exports no ` + base + ` module, so its capability needs cannot be read.\n A trait states them as a value — an empty array if it brokers nothing — because an unstated need fails silently at run time.`), undefined);
|
|
67
|
+
let path$2 = resolveFrom(match + `/src/` + base + `_Scaffold.res.mjs`);
|
|
68
|
+
let scaffold = path$2 !== undefined ? await import(Nodeurl.pathToFileURL(path$2).href) : undefined;
|
|
69
|
+
let manifest_trait = readPackageField(packageJson, "name", match);
|
|
70
|
+
let manifest_version = readPackageField(packageJson, "version", "0.0.0");
|
|
71
|
+
let manifest_description = readPackageField(packageJson, "description", "");
|
|
72
|
+
let manifest_license = readPackageField(packageJson, "license", "");
|
|
73
|
+
let manifest_capabilities = traitModule.capabilityNeeds.map(CapabilityNeed$Reventless.toString);
|
|
74
|
+
let manifest_config = scaffold !== undefined ? TraitManifest$Reventless.configFieldsOf(scaffold.configSchema) : [];
|
|
75
|
+
let manifest_scaffolded = Stdlib_Option.isSome(scaffold);
|
|
76
|
+
let manifest = {
|
|
77
|
+
trait: manifest_trait,
|
|
78
|
+
version: manifest_version,
|
|
79
|
+
description: manifest_description,
|
|
80
|
+
license: manifest_license,
|
|
81
|
+
capabilities: manifest_capabilities,
|
|
82
|
+
config: manifest_config,
|
|
83
|
+
scaffolded: manifest_scaffolded
|
|
84
|
+
};
|
|
85
|
+
Nodefs.writeFileSync(match$1, TraitManifest$Reventless.render(manifest), "utf8");
|
|
86
|
+
console.log(`trait-manifest: ` + manifest_trait + `@` + manifest_version + ` — ` + (manifest_capabilities.length.toString() + ` capabilit(ies), `) + (manifest_config.length.toString() + ` config field(s)`));
|
|
87
|
+
console.log(`Wrote: ` + match$1);
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
console.log(usage);
|
|
92
|
+
process.exit(argv.length === 0 ? 1 : 0);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
main();
|
|
96
|
+
|
|
97
|
+
export {
|
|
98
|
+
fail,
|
|
99
|
+
usage,
|
|
100
|
+
moduleBase,
|
|
101
|
+
resolveFrom,
|
|
102
|
+
readPackageField,
|
|
103
|
+
main,
|
|
104
|
+
}
|
|
105
|
+
/* Not a pure module */
|
|
@@ -5,9 +5,9 @@ A plugin is provider-agnostic: it depends on `reventless-spec`, never on
|
|
|
5
5
|
`reventless-aws`, so it cannot name Amazon Location — and it should not want to.
|
|
6
6
|
What it can do is receive a function that geocodes, and let whoever assembled the
|
|
7
7
|
runtime decide what is on the other end. The same capability a client reaches
|
|
8
|
-
through a GraphQL field, plugin code reaches through here
|
|
9
|
-
|
|
10
|
-
|
|
8
|
+
through a GraphQL field, plugin code reaches through here. (The object store has
|
|
9
|
+
only the client half so far — `Upload_Presign`; `Offload.resolve(~fetch)` has no
|
|
10
|
+
injected caller yet, and the accessor for it belongs in this record.)
|
|
11
11
|
|
|
12
12
|
Injected rather than looked up, and required rather than optional, because the
|
|
13
13
|
alternative is a slot filled at cold start that nothing enforces: ES modules
|
|
@@ -24,6 +24,9 @@ leaves every `translate` reading `capabilities.geocode` untouched.
|
|
|
24
24
|
type t = {
|
|
25
25
|
/** Turn an address into ranked candidates. See `Geocoding.search`. */
|
|
26
26
|
geocode: Geocoding.search,
|
|
27
|
+
/** Send a message to a person, and say which channels this deployment can
|
|
28
|
+
attempt at all. See `Messaging.provider`. */
|
|
29
|
+
messaging: Messaging.provider,
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
/**
|
|
@@ -40,4 +43,17 @@ a blank.
|
|
|
40
43
|
*/
|
|
41
44
|
let none: t = {
|
|
42
45
|
geocode: async (~text as _) => Error(Unavailable("no geocoder is configured for this platform")),
|
|
46
|
+
// `channels: []` and a retryable `send` say two different true things, and both
|
|
47
|
+
// are needed. The empty list is what a deploy gate reads and what a preference
|
|
48
|
+
// centre renders — offering a channel nothing can deliver on would collect a
|
|
49
|
+
// subscription that never arrives. The send stays `Unavailable` rather than
|
|
50
|
+
// `UnsupportedChannel` because a caller that got this far is looking at a
|
|
51
|
+
// deployment gap, not at a fact about the recipient, and abandoning the message
|
|
52
|
+
// would record the second.
|
|
53
|
+
messaging: {
|
|
54
|
+
channels: [],
|
|
55
|
+
send: async (~recipient as _, ~message as _) => Error(
|
|
56
|
+
Unavailable("no messaging provider is configured for this platform"),
|
|
57
|
+
),
|
|
58
|
+
},
|
|
43
59
|
}
|
|
@@ -1,16 +1,32 @@
|
|
|
1
1
|
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
2
|
|
|
3
3
|
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
async function none_geocode(param) {
|
|
5
|
+
return {
|
|
6
6
|
TAG: "Error",
|
|
7
7
|
_0: {
|
|
8
8
|
TAG: "Unavailable",
|
|
9
9
|
_0: "no geocoder is configured for this platform"
|
|
10
10
|
}
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let none_messaging = {
|
|
15
|
+
channels: [],
|
|
16
|
+
send: async (param, param$1) => ({
|
|
17
|
+
TAG: "Error",
|
|
18
|
+
_0: {
|
|
19
|
+
TAG: "Unavailable",
|
|
20
|
+
_0: "no messaging provider is configured for this platform"
|
|
21
|
+
}
|
|
11
22
|
})
|
|
12
23
|
};
|
|
13
24
|
|
|
25
|
+
let none = {
|
|
26
|
+
geocode: none_geocode,
|
|
27
|
+
messaging: none_messaging
|
|
28
|
+
};
|
|
29
|
+
|
|
14
30
|
export {
|
|
15
31
|
none,
|
|
16
32
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A platform capability a component declares it needs, in the plugin's own
|
|
3
|
+
vocabulary.
|
|
4
|
+
|
|
5
|
+
`Capabilities.t` is what a slice is *handed*; this is what it says it will
|
|
6
|
+
reach for. The two are separate because they are read at different times: the
|
|
7
|
+
record is injected at run time, the declaration is read at build time by
|
|
8
|
+
`emit-capabilities` and at deploy time by the coverage gate, neither of which
|
|
9
|
+
can run a `translate`.
|
|
10
|
+
|
|
11
|
+
**Object stores are not here.** A store need is already declared by the field
|
|
12
|
+
that carries `@storageRef`, travels as `pluginStructure.requiredStores`, and is
|
|
13
|
+
identified by `(plugin, store)` rather than by a capability alone. This type
|
|
14
|
+
carries the needs that no field can express.
|
|
15
|
+
|
|
16
|
+
A real variant, so a trait exports its need as a value the compiler checks
|
|
17
|
+
rather than a string a README repeats.
|
|
18
|
+
*/
|
|
19
|
+
type t =
|
|
20
|
+
| Geocoding
|
|
21
|
+
| /** Sending a message to a person, reached through `Capabilities.messaging`.
|
|
22
|
+
One need however many channels the platform provisions — which channels
|
|
23
|
+
those are is a runtime answer the provider publishes, not a second
|
|
24
|
+
declaration, because a plugin that named `Sms` would fail a deploy it
|
|
25
|
+
could have run on email. */
|
|
26
|
+
Messaging
|
|
27
|
+
|
|
28
|
+
/** The spelling carried in `pluginStructure` and in `capabilities.json`.
|
|
29
|
+
Persisted structures hold strings, not enum members, so a plugin built
|
|
30
|
+
against a newer framework still decodes in an older reader. */
|
|
31
|
+
let toString = (need: t): string =>
|
|
32
|
+
switch need {
|
|
33
|
+
| Geocoding => "Geocoding"
|
|
34
|
+
| Messaging => "Messaging"
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** The inverse, for readers of a persisted structure or a committed manifest.
|
|
38
|
+
`None` for a capability this build does not know — a newer plugin's need is
|
|
39
|
+
reported as unrecognised, never silently dropped into a known arm. */
|
|
40
|
+
let fromString = (name: string): option<t> =>
|
|
41
|
+
switch name {
|
|
42
|
+
| "Geocoding" => Some(Geocoding)
|
|
43
|
+
| "Messaging" => Some(Messaging)
|
|
44
|
+
| _ => None
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** One declared need the deployment does not provision, with the component that
|
|
48
|
+
declared it — a deploy refusal has to name what to go and look at. */
|
|
49
|
+
type unmet = {need: t, component: string}
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
Declared capability needs a platform does not provision.
|
|
53
|
+
|
|
54
|
+
`declared` is `pluginStructure.requiredCapabilities` as persisted: `(capability,
|
|
55
|
+
component)` pairs whose capability is a string, because a structure is replayed
|
|
56
|
+
from an event log. A name this build does not recognise is **skipped** — it
|
|
57
|
+
belongs to a newer framework, and refusing a deploy over a capability this code
|
|
58
|
+
cannot evaluate would be a guess dressed as a check.
|
|
59
|
+
|
|
60
|
+
Only what was declared. A plugin that declares nothing is unaffected: an
|
|
61
|
+
unprovisioned capability it never named degrades exactly as it always has, which
|
|
62
|
+
is the modelled outcome for a deployment that simply does not have one.
|
|
63
|
+
*/
|
|
64
|
+
let unmet = (~declared: array<(string, string)>, ~provisioned: array<t>): array<unmet> =>
|
|
65
|
+
declared->Array.filterMap((((capability, component))) =>
|
|
66
|
+
switch fromString(capability) {
|
|
67
|
+
| Some(need) if !(provisioned->Array.includes(need)) => Some({need, component})
|
|
68
|
+
| _ => None
|
|
69
|
+
}
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
/** The refusal text: what is missing, who asked for it, and what silently happens
|
|
73
|
+
if it is not provisioned — the symptom is a wrong verdict, never an error. */
|
|
74
|
+
let unmetMessage = (unmet: array<unmet>): string =>
|
|
75
|
+
`Plugin declares capabilit(ies) the platform does not provision: ` ++
|
|
76
|
+
`${unmet
|
|
77
|
+
->Array.map(u => `${toString(u.need)} (${u.component})`)
|
|
78
|
+
->Array.join(", ")}.\n` ++
|
|
79
|
+
` Provision them in the platform stack and redeploy the platform first.\n` ++
|
|
80
|
+
` Without it every call answers Unavailable, the slice exhausts its retries, and a ` ++
|
|
81
|
+
`permanent verdict is recorded against data that is fine — no error anywhere.`
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
|
|
5
|
+
function toString(need) {
|
|
6
|
+
if (need === "Geocoding") {
|
|
7
|
+
return "Geocoding";
|
|
8
|
+
} else {
|
|
9
|
+
return "Messaging";
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function fromString(name) {
|
|
14
|
+
switch (name) {
|
|
15
|
+
case "Geocoding" :
|
|
16
|
+
return "Geocoding";
|
|
17
|
+
case "Messaging" :
|
|
18
|
+
return "Messaging";
|
|
19
|
+
default:
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function unmet(declared, provisioned) {
|
|
25
|
+
return Stdlib_Array.filterMap(declared, param => {
|
|
26
|
+
let need = fromString(param[0]);
|
|
27
|
+
if (need !== undefined && !provisioned.includes(need)) {
|
|
28
|
+
return {
|
|
29
|
+
need: need,
|
|
30
|
+
component: param[1]
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function unmetMessage(unmet) {
|
|
37
|
+
return `Plugin declares capabilit(ies) the platform does not provision: ` + (unmet.map(u => toString(u.need) + ` (` + u.component + `)`).join(", ") + `.\n`) + ` Provision them in the platform stack and redeploy the platform first.\n Without it every call answers Unavailable, the slice exhausts its retries, and a permanent verdict is recorded against data that is fine — no error anywhere.`;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export {
|
|
41
|
+
toString,
|
|
42
|
+
fromString,
|
|
43
|
+
unmet,
|
|
44
|
+
unmetMessage,
|
|
45
|
+
}
|
|
46
|
+
/* No side effect */
|