@reventlessdev/reventless-spec 3.0.0-alpha.129 → 3.0.0-alpha.131
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 +23 -0
- package/package.json +3 -2
- package/run-check-lifecycle.mjs +2 -0
- package/schema/platform-api.graphql +1 -0
- package/src/components/DcbTag.res +229 -32
- package/src/components/DcbTag.res.mjs +232 -41
- package/src/components/Plugin.res +47 -4
- package/src/components/Plugin.res.mjs +1 -0
- package/src/components/Reference.res +48 -0
- package/src/components/Reference.res.mjs +28 -0
- package/src/generator/Codegen.res +14 -1
- package/src/generator/Codegen.res.mjs +9 -5
- package/src/generator/PluginGenerator.res +4 -1
- package/src/generator/PluginGenerator.res.mjs +2 -1
- package/src/lifecycle/CheckLifecycleModel.res +1337 -0
- package/src/lifecycle/CheckLifecycleModel.res.mjs +1107 -0
- package/src/lifecycle/loadPluginStructure.mjs +66 -0
- package/src/types/Transition.res +30 -3
- package/src/types/Transition.res.mjs +13 -4
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
// Reading a built plugin's declared structure.
|
|
2
|
+
//
|
|
3
|
+
// A plugin's generated definition module is a functor over the platform type
|
|
4
|
+
// that exposes `pluginStructure` as an ordinary value, and the local platform's
|
|
5
|
+
// `Make` takes no configuration — so applying one to the other is enough to hold
|
|
6
|
+
// a real structure. No deploy program, no database, no server, nothing left
|
|
7
|
+
// running.
|
|
8
|
+
//
|
|
9
|
+
// This lives in a companion `.mjs` because it is untyped reflection: `import` is
|
|
10
|
+
// syntax rather than a value, so a computed specifier cannot be bound as an
|
|
11
|
+
// external, and the value that comes back is shaped by the plugin rather than by
|
|
12
|
+
// anything this repository can name.
|
|
13
|
+
//
|
|
14
|
+
// The structure is handed back as plain JSON and read field by field on the
|
|
15
|
+
// ReScript side, rather than parsed through the published schema. A
|
|
16
|
+
// `pluginStructure` has two legitimate representations — an absent optional is
|
|
17
|
+
// `undefined` in memory and an explicit `null` on the wire — and the published
|
|
18
|
+
// schema describes the wire form, so validating an in-memory structure against
|
|
19
|
+
// it fails on every optional that happens to be empty.
|
|
20
|
+
import { existsSync } from "node:fs";
|
|
21
|
+
import { join } from "node:path";
|
|
22
|
+
|
|
23
|
+
const localPlatformPath = (pluginDir) =>
|
|
24
|
+
join(
|
|
25
|
+
pluginDir,
|
|
26
|
+
"node_modules/@reventlessdev/reventless-local/src/Platform.res.mjs",
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
const pluginEntryPath = (pluginDir) => join(pluginDir, "src/Plugin.res.mjs");
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* @returns {Promise<{ok: true, structure: unknown} | {ok: false, error: string}>}
|
|
33
|
+
*/
|
|
34
|
+
export const loadPluginStructure = async (pluginDir) => {
|
|
35
|
+
const entry = pluginEntryPath(pluginDir);
|
|
36
|
+
const local = localPlatformPath(pluginDir);
|
|
37
|
+
if (!existsSync(entry)) {
|
|
38
|
+
return {
|
|
39
|
+
ok: false,
|
|
40
|
+
error: `no compiled plugin at ${entry} — build the plugin first`,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
if (!existsSync(local)) {
|
|
44
|
+
return {
|
|
45
|
+
ok: false,
|
|
46
|
+
error: `no local platform installed at ${local}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
try {
|
|
50
|
+
const localMod = await import(local);
|
|
51
|
+
const pluginMod = await import(entry);
|
|
52
|
+
const structure = pluginMod.Make(localMod.Make()).pluginStructure;
|
|
53
|
+
// A structure whose component arrays are absent came from a release this
|
|
54
|
+
// reader cannot understand. Saying so beats reporting a model that is
|
|
55
|
+
// quietly half-empty.
|
|
56
|
+
const present = ["readModels", "stateViewSlices", "stateChangeSlices", "aggregates"].every(
|
|
57
|
+
(k) => Array.isArray(structure?.[k]),
|
|
58
|
+
);
|
|
59
|
+
return present
|
|
60
|
+
? { ok: true, structure }
|
|
61
|
+
: { ok: false, error: "the plugin structure declares no component arrays" };
|
|
62
|
+
} catch (e) {
|
|
63
|
+
const message = e && (e.message || e.RE_EXN_ID) ? String(e.message || e.RE_EXN_ID) : "unknown error";
|
|
64
|
+
return { ok: false, error: `reading the plugin structure: ${message}` };
|
|
65
|
+
}
|
|
66
|
+
};
|
package/src/types/Transition.res
CHANGED
|
@@ -36,10 +36,23 @@
|
|
|
36
36
|
// plugin structure is assembled, and what leaves is the pair of names the
|
|
37
37
|
// structure already carried.
|
|
38
38
|
type t<'state> =
|
|
39
|
-
/**
|
|
40
|
-
|
|
41
|
-
|
|
39
|
+
/** Legal in every state, and moves the row nowhere. The honest answer for a
|
|
40
|
+
report a slice publishes, which must not be refused because the row moved
|
|
41
|
+
on while the report was in flight. A claim, not an omission — see
|
|
42
|
+
`Undeclared`, which is the omission. */
|
|
42
43
|
| Unrestricted
|
|
44
|
+
/** The spec wrote no switch, and the ppx injected this. Never write it: say
|
|
45
|
+
`Unrestricted` if you mean the command is legal everywhere.
|
|
46
|
+
|
|
47
|
+
It reads exactly like `Unrestricted` — no from-set, no target — everywhere
|
|
48
|
+
but one place, and that place is why it exists. The harvested lifecycle
|
|
49
|
+
model may answer for silence; it may not narrow a claim. A corpus only
|
|
50
|
+
covers the states somebody wrote a scenario for, so letting it answer for
|
|
51
|
+
`Unrestricted` would shrink "legal in every state" down to an accident of
|
|
52
|
+
coverage, and the command would quietly stop being offered on the rows
|
|
53
|
+
nobody tested. With the two spelled apart, that shrinkage cannot happen and
|
|
54
|
+
a scenario that genuinely refutes the claim is a contradiction instead. */
|
|
55
|
+
| Undeclared
|
|
43
56
|
/** Brings the row into existence, so there is no state it could come from.
|
|
44
57
|
Distinct from `Unrestricted`, which draws no edge at all. */
|
|
45
58
|
| Creates('state)
|
|
@@ -54,6 +67,7 @@ type t<'state> =
|
|
|
54
67
|
tells apart. */
|
|
55
68
|
let allowedStates = (transition: t<'state>): option<array<'state>> =>
|
|
56
69
|
switch transition {
|
|
70
|
+
| Undeclared
|
|
57
71
|
| Unrestricted
|
|
58
72
|
| Creates(_) => None
|
|
59
73
|
| Guards(states)
|
|
@@ -63,8 +77,21 @@ let allowedStates = (transition: t<'state>): option<array<'state>> =>
|
|
|
63
77
|
/** The state the command's handler writes, or `None` for one that moves nothing. */
|
|
64
78
|
let targetState = (transition: t<'state>): option<'state> =>
|
|
65
79
|
switch transition {
|
|
80
|
+
| Undeclared
|
|
66
81
|
| Unrestricted
|
|
67
82
|
| Guards(_) => None
|
|
68
83
|
| Creates(state)
|
|
69
84
|
| Moves(_, state) => Some(state)
|
|
70
85
|
}
|
|
86
|
+
|
|
87
|
+
/** Whether the command is claimed legal in every state, as opposed to nothing
|
|
88
|
+
being claimed at all. The two erase to the same pair of `None`s above, so this
|
|
89
|
+
is the only thing that can tell a reader of the erased form which it holds. */
|
|
90
|
+
let isUnrestricted = (transition: t<'state>): bool =>
|
|
91
|
+
switch transition {
|
|
92
|
+
| Unrestricted => true
|
|
93
|
+
| Undeclared
|
|
94
|
+
| Creates(_)
|
|
95
|
+
| Guards(_)
|
|
96
|
+
| Moves(_, _) => false
|
|
97
|
+
}
|
|
@@ -7,11 +7,11 @@ function allowedStates(transition) {
|
|
|
7
7
|
return;
|
|
8
8
|
}
|
|
9
9
|
switch (transition.TAG) {
|
|
10
|
-
case "Creates" :
|
|
11
|
-
return;
|
|
12
10
|
case "Guards" :
|
|
13
11
|
case "Moves" :
|
|
14
12
|
return transition._0;
|
|
13
|
+
default:
|
|
14
|
+
return;
|
|
15
15
|
}
|
|
16
16
|
}
|
|
17
17
|
|
|
@@ -22,15 +22,24 @@ function targetState(transition) {
|
|
|
22
22
|
switch (transition.TAG) {
|
|
23
23
|
case "Creates" :
|
|
24
24
|
return Primitive_option.some(transition._0);
|
|
25
|
-
case "Guards" :
|
|
26
|
-
return;
|
|
27
25
|
case "Moves" :
|
|
28
26
|
return Primitive_option.some(transition._1);
|
|
27
|
+
default:
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isUnrestricted(transition) {
|
|
33
|
+
if (typeof transition !== "object") {
|
|
34
|
+
return transition === "Unrestricted";
|
|
35
|
+
} else {
|
|
36
|
+
return false;
|
|
29
37
|
}
|
|
30
38
|
}
|
|
31
39
|
|
|
32
40
|
export {
|
|
33
41
|
allowedStates,
|
|
34
42
|
targetState,
|
|
43
|
+
isUnrestricted,
|
|
35
44
|
}
|
|
36
45
|
/* No side effect */
|