@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.
@@ -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
+ };
@@ -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
- /** No edge declared: legal in every state, moves the row nowhere. The honest
40
- answer for a report a slice publishes, which must not be refused because
41
- the row moved on while the report was in flight. */
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 */