@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.
Files changed (43) hide show
  1. package/CHANGELOG.md +56 -0
  2. package/package.json +5 -3
  3. package/run-certify-trait.mjs +2 -0
  4. package/run-graft-trait.mjs +2 -0
  5. package/run-trait-manifest.mjs +2 -0
  6. package/schema/platform-api.graphql +41 -3
  7. package/src/components/Aggregate.res +22 -0
  8. package/src/components/AutomationSlice.res +29 -3
  9. package/src/components/CapabilityManifest.res +52 -24
  10. package/src/components/CapabilityManifest.res.mjs +35 -11
  11. package/src/components/InboundTranslationSlice.res +14 -0
  12. package/src/components/OutboundTranslationSlice.res +24 -0
  13. package/src/components/Plugin.res +202 -422
  14. package/src/components/Plugin.res.mjs +65 -3
  15. package/src/components/StateChangeSlice.res +22 -0
  16. package/src/components/TraitCertificate.res +105 -0
  17. package/src/components/TraitCertificate.res.mjs +65 -0
  18. package/src/components/TraitManifest.res +90 -0
  19. package/src/components/TraitManifest.res.mjs +48 -0
  20. package/src/generator/CertifyTrait.res +190 -0
  21. package/src/generator/CertifyTrait.res.mjs +154 -0
  22. package/src/generator/GraftTrait.res +230 -0
  23. package/src/generator/GraftTrait.res.mjs +193 -0
  24. package/src/generator/PlatformCodegen.res +44 -30
  25. package/src/generator/PlatformCodegen.res.mjs +36 -17
  26. package/src/generator/TraitManifestCli.res +138 -0
  27. package/src/generator/TraitManifestCli.res.mjs +105 -0
  28. package/src/semantic/Capabilities.res +19 -3
  29. package/src/semantic/Capabilities.res.mjs +18 -2
  30. package/src/semantic/CapabilityNeed.res +81 -0
  31. package/src/semantic/CapabilityNeed.res.mjs +46 -0
  32. package/src/semantic/Currency.res +519 -502
  33. package/src/semantic/Currency.res.mjs +7 -655
  34. package/src/semantic/Messaging.res +127 -0
  35. package/src/semantic/Messaging.res.mjs +57 -0
  36. package/src/semantic/Money.res +123 -21
  37. package/src/semantic/Money.res.mjs +49 -2
  38. package/src/types/Trait.res +62 -0
  39. package/src/types/Trait.res.mjs +18 -0
  40. package/src/types/Transition.res +71 -0
  41. package/src/types/Transition.res.mjs +36 -0
  42. package/scripts/generate-currency.mjs +0 -215
  43. package/scripts/iso-4217-list-one.xml +0 -1956
@@ -47,16 +47,10 @@ function splitKey(key) {
47
47
  ]);
48
48
  }
49
49
 
50
- function renderEntry(entry) {
51
- let match = splitKey(entry.key);
52
- if (match === undefined) {
53
- return {
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
- let line = ` ObjectStore({plugin: ` + JSON.stringify(match[0]) + `, store: ` + JSON.stringify(match[1]) + `}),`;
68
- return {
69
- TAG: "Ok",
70
- _0: comments.concat([line])
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 fields as provenance, so when a capability",
110
- "// disappears from this list, the diff says which field's change removed it.",
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; the object store
9
- already works this way, with `Upload_Presign` on one side and an injected
10
- `Offload.resolve(~fetch)` on the other.
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
- let none = {
5
- geocode: async param => ({
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 */