@reventlessdev/reventless-gwt 1.0.0-alpha.74
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 +698 -0
- package/LICENSE +202 -0
- package/bin/reventless-dev.mjs +21 -0
- package/package.json +54 -0
- package/rescript.json +36 -0
- package/src/.gwtignore +10 -0
- package/src/Automation_GWT.res +207 -0
- package/src/Automation_GWT.res.mjs +160 -0
- package/src/Behavior_GWT.res +603 -0
- package/src/Behavior_GWT.res.mjs +426 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +137 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +31 -0
- package/src/Cancellation.res.mjs +45 -0
- package/src/ChildProcess.res +84 -0
- package/src/ChildProcess.res.mjs +92 -0
- package/src/Cli.res +671 -0
- package/src/Cli.res.mjs +754 -0
- package/src/Collector.res +201 -0
- package/src/Collector.res.mjs +215 -0
- package/src/ComponentMeta.res +104 -0
- package/src/ComponentMeta.res.mjs +120 -0
- package/src/ComponentScan.res +103 -0
- package/src/ComponentScan.res.mjs +105 -0
- package/src/Delegate_GWT.res +303 -0
- package/src/Delegate_GWT.res.mjs +372 -0
- package/src/Diff.res +91 -0
- package/src/Diff.res.mjs +98 -0
- package/src/Discovery.res +105 -0
- package/src/Discovery.res.mjs +103 -0
- package/src/DomainDeadCode.res +93 -0
- package/src/DomainDeadCode.res.mjs +69 -0
- package/src/DomainGraph.res +0 -0
- package/src/DomainGraph.res.mjs +0 -0
- package/src/EventMapping_GWT.res +29 -0
- package/src/EventMapping_GWT.res.mjs +44 -0
- package/src/Filter.res +36 -0
- package/src/Filter.res.mjs +40 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +138 -0
- package/src/FormatterHuman.res.mjs +122 -0
- package/src/FormatterJson.res +257 -0
- package/src/FormatterJson.res.mjs +320 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +139 -0
- package/src/FormatterTap.res.mjs +149 -0
- package/src/FormatterVsCode.res +362 -0
- package/src/FormatterVsCode.res.mjs +564 -0
- package/src/Hint.res +119 -0
- package/src/Hint.res.mjs +124 -0
- package/src/InboundTranslation_GWT.res +83 -0
- package/src/InboundTranslation_GWT.res.mjs +94 -0
- package/src/JestBind.res +70 -0
- package/src/JestBind.res.mjs +63 -0
- package/src/Loader.res +24 -0
- package/src/Loader.res.mjs +23 -0
- package/src/LocalHost.res +135 -0
- package/src/LocalHost.res.mjs +127 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MultiSourceProjection_GWT.res +316 -0
- package/src/MultiSourceProjection_GWT.res.mjs +354 -0
- package/src/OutboundTranslation_GWT.res +238 -0
- package/src/OutboundTranslation_GWT.res.mjs +186 -0
- package/src/Outcome.res +161 -0
- package/src/Outcome.res.mjs +140 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +160 -0
- package/src/PlatformRunner.res.mjs +155 -0
- package/src/PlatformScan.res +138 -0
- package/src/PlatformScan.res.mjs +131 -0
- package/src/ProcessManager.res +91 -0
- package/src/ProcessManager.res.mjs +80 -0
- package/src/Projection_GWT.res +312 -0
- package/src/Projection_GWT.res.mjs +332 -0
- package/src/Query_GWT.res +379 -0
- package/src/Query_GWT.res.mjs +367 -0
- package/src/RenderRescript.res +125 -0
- package/src/RenderRescript.res.mjs +141 -0
- package/src/RunnerTypes.res +68 -0
- package/src/RunnerTypes.res.mjs +60 -0
- package/src/SideEffect_GWT.res +157 -0
- package/src/SideEffect_GWT.res.mjs +113 -0
- package/src/StateChange/ExternalAddCategorySlice.res +25 -0
- package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
- package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
- package/src/StateChange/WithFixtures.res +22 -0
- package/src/StateChange/WithFixtures.res.mjs +31 -0
- package/src/StateChange/WithFixtures_Behavior.res +17 -0
- package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
- package/src/StateChange/WithManualOpen.res +23 -0
- package/src/StateChange/WithManualOpen.res.mjs +31 -0
- package/src/StateChange/WithManualOpen_Behavior.res +17 -0
- package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
- package/src/StubRuntime.res +53 -0
- package/src/StubRuntime.res.mjs +54 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +92 -0
- package/src/Watch.res.mjs +83 -0
- package/src/WatcherProbe.res +48 -0
- package/src/WatcherProbe.res.mjs +46 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +94 -0
- package/tests/BuildClassifierTest.res.mjs +132 -0
- package/tests/ComponentMetaTest.res +52 -0
- package/tests/ComponentMetaTest.res.mjs +57 -0
- package/tests/DelegateGwtTest.res +487 -0
- package/tests/DelegateGwtTest.res.mjs +891 -0
- package/tests/DiscoveryTest.res +42 -0
- package/tests/DiscoveryTest.res.mjs +43 -0
- package/tests/DomainDeadCodeTest.res +116 -0
- package/tests/DomainDeadCodeTest.res.mjs +155 -0
- package/tests/DomainGraphTest.res +365 -0
- package/tests/DomainGraphTest.res.mjs +413 -0
- package/tests/FlowAggregateGwtTest.res +169 -0
- package/tests/FlowAggregateGwtTest.res.mjs +269 -0
- package/tests/FlowCrossPluginGwtTest.res +340 -0
- package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
- package/tests/FlowGwtTest.res +222 -0
- package/tests/FlowGwtTest.res.mjs +432 -0
- package/tests/FormatterVsCodeMessageTest.res +41 -0
- package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
- package/tests/InboundTranslationGwtTest.res +34 -0
- package/tests/InboundTranslationGwtTest.res.mjs +98 -0
- package/tests/LocalHostIntegration.res +70 -0
- package/tests/LocalHostIntegration.res.mjs +64 -0
- package/tests/LocalHostTest.res +71 -0
- package/tests/LocalHostTest.res.mjs +63 -0
- package/tests/MappingGwtTest.res +348 -0
- package/tests/MappingGwtTest.res.mjs +592 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +138 -0
- package/tests/PlatformRunnerTest.res +66 -0
- package/tests/PlatformRunnerTest.res.mjs +85 -0
- package/tests/PlatformScanTest.res +61 -0
- package/tests/PlatformScanTest.res.mjs +56 -0
- package/tests/QueryGwtTest.res +161 -0
- package/tests/QueryGwtTest.res.mjs +314 -0
- package/tests/QueryResolverGwtTest.res +71 -0
- package/tests/QueryResolverGwtTest.res.mjs +136 -0
- package/tests/RunnerUnitTest.res +219 -0
- package/tests/RunnerUnitTest.res.mjs +244 -0
- package/tests/SideEffect/FakeOrderNotification.res +23 -0
- package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
- package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
- package/tests/SideEffect/MockEmail.res +18 -0
- package/tests/SideEffect/MockEmail.res.mjs +47 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
- package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
- package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
- package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithFixtures_GWT.res +23 -0
- package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
- package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
- package/tests/StateChange/WithManualOpen_GWT.res +17 -0
- package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
- package/tests/StateChangeSliceGwtTest.res +160 -0
- package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
- package/tests/StateViewSliceGwtTest.res +64 -0
- package/tests/StateViewSliceGwtTest.res.mjs +161 -0
- package/tests/WatchDebounceTest.res +78 -0
- package/tests/WatchDebounceTest.res.mjs +97 -0
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
// Walks each owning package's src/ tree and enumerates its Reventless
|
|
2
|
+
// components by folder convention — including components with no GWT tests.
|
|
3
|
+
// This is the inventory the `components` NDJSON event carries: the basis for
|
|
4
|
+
// the activity-bar Plugin→kind→component tree and "untested slice" coverage.
|
|
5
|
+
// Computed fresh per discovery so it never drifts as components are added.
|
|
6
|
+
|
|
7
|
+
type component = {
|
|
8
|
+
// Absolute directory of the owning package (matches the `dir` in `packages`).
|
|
9
|
+
dir: string,
|
|
10
|
+
kind: string,
|
|
11
|
+
name: string,
|
|
12
|
+
// Absolute paths of the component's `src/` files (spec + body files like
|
|
13
|
+
// `*_Behavior.res` / `*_Mappings.res`), in walk order. Lets the client list
|
|
14
|
+
// spec / implementation files under each component node.
|
|
15
|
+
files: array<string>,
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
type dirent = {
|
|
19
|
+
name: string,
|
|
20
|
+
@as("isDirectory") _isDirectory: unit => bool,
|
|
21
|
+
@as("isFile") _isFile: unit => bool,
|
|
22
|
+
}
|
|
23
|
+
type readdirOpts = {withFileTypes: bool}
|
|
24
|
+
@module("node:fs/promises")
|
|
25
|
+
external _readdir: (string, readdirOpts) => promise<array<dirent>> = "readdir"
|
|
26
|
+
@module("node:fs") external _existsSync: string => bool = "existsSync"
|
|
27
|
+
@module("node:path") external join: (string, string) => string = "join"
|
|
28
|
+
|
|
29
|
+
let ignoreNames = ["node_modules", ".git", "dist", "lib", ".history"]
|
|
30
|
+
let shouldIgnore = (name: string) => Array.includes(ignoreNames, name)
|
|
31
|
+
|
|
32
|
+
// A directory carrying this sentinel file — and its whole subtree — is pruned
|
|
33
|
+
// from the component scan, matching `Discovery`'s `.gwtignore` convention so
|
|
34
|
+
// the domain tree and the test tree exclude the same generated/vendored trees.
|
|
35
|
+
let gwtIgnoreFile = ".gwtignore"
|
|
36
|
+
let isPruned = (entries: array<dirent>) => entries->Array.some(e => e.name == gwtIgnoreFile)
|
|
37
|
+
|
|
38
|
+
// Source `.res` only — `.res.mjs` / `.res.js` end in `.mjs` / `.js`, and `.resi`
|
|
39
|
+
// in `.resi`, so a plain `.res` suffix test excludes compiled output + interfaces.
|
|
40
|
+
let isSrcResFile = (name: string) => String.endsWith(name, ".res")
|
|
41
|
+
|
|
42
|
+
let rec walk = async (dir: string, acc: array<string>): array<string> => {
|
|
43
|
+
let entries = try {
|
|
44
|
+
await _readdir(dir, {withFileTypes: true})
|
|
45
|
+
} catch {
|
|
46
|
+
| _ => []
|
|
47
|
+
}
|
|
48
|
+
if isPruned(entries) {
|
|
49
|
+
acc
|
|
50
|
+
} else {
|
|
51
|
+
let found = ref(acc)
|
|
52
|
+
for i in 0 to entries->Array.length - 1 {
|
|
53
|
+
let entry = entries->Array.getUnsafe(i)
|
|
54
|
+
if !shouldIgnore(entry.name) {
|
|
55
|
+
let full = join(dir, entry.name)
|
|
56
|
+
if entry._isDirectory() {
|
|
57
|
+
let nested = await walk(full, [])
|
|
58
|
+
found := Array.concat(found.contents, nested)
|
|
59
|
+
} else if entry._isFile() && isSrcResFile(entry.name) {
|
|
60
|
+
found := Array.concat(found.contents, [full])
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
found.contents
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Enumerate components across the given package directories (deduplicated per
|
|
69
|
+
// dir+kind+name, declaration order preserved). Each component accumulates the
|
|
70
|
+
// source files that map to it (spec + body files).
|
|
71
|
+
type orderEntry = {dir: string, kind: string, name: string, key: string}
|
|
72
|
+
|
|
73
|
+
let scan = async (pkgDirs: array<string>): array<component> => {
|
|
74
|
+
let order = []
|
|
75
|
+
let filesByKey = Dict.make()
|
|
76
|
+
for i in 0 to pkgDirs->Array.length - 1 {
|
|
77
|
+
let dir = pkgDirs->Array.getUnsafe(i)
|
|
78
|
+
let srcDir = join(dir, "src")
|
|
79
|
+
if _existsSync(srcDir) {
|
|
80
|
+
let files = await walk(srcDir, [])
|
|
81
|
+
files->Array.forEach(file =>
|
|
82
|
+
switch ComponentMeta.componentOfSrcFile(file) {
|
|
83
|
+
| Some(c) =>
|
|
84
|
+
let key = dir ++ "::" ++ c.kind ++ "::" ++ c.name
|
|
85
|
+
switch filesByKey->Dict.get(key) {
|
|
86
|
+
| Some(existing) => filesByKey->Dict.set(key, Array.concat(existing, [file]))
|
|
87
|
+
| None => {
|
|
88
|
+
filesByKey->Dict.set(key, [file])
|
|
89
|
+
order->Array.push({dir, kind: c.kind, name: c.name, key})
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
| None => ()
|
|
93
|
+
}
|
|
94
|
+
)
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
order->Array.map(o => {
|
|
98
|
+
dir: o.dir,
|
|
99
|
+
kind: o.kind,
|
|
100
|
+
name: o.name,
|
|
101
|
+
files: filesByKey->Dict.get(o.key)->Option.getOr([]),
|
|
102
|
+
})
|
|
103
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
import * as Promises from "node:fs/promises";
|
|
7
|
+
import * as ComponentMeta$ReventlessGwt from "./ComponentMeta.res.mjs";
|
|
8
|
+
|
|
9
|
+
let ignoreNames = [
|
|
10
|
+
"node_modules",
|
|
11
|
+
".git",
|
|
12
|
+
"dist",
|
|
13
|
+
"lib",
|
|
14
|
+
".history"
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
function shouldIgnore(name) {
|
|
18
|
+
return ignoreNames.includes(name);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
let gwtIgnoreFile = ".gwtignore";
|
|
22
|
+
|
|
23
|
+
function isPruned(entries) {
|
|
24
|
+
return entries.some(e => e.name === gwtIgnoreFile);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function isSrcResFile(name) {
|
|
28
|
+
return name.endsWith(".res");
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
async function walk(dir, acc) {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = await Promises.readdir(dir, {
|
|
35
|
+
withFileTypes: true
|
|
36
|
+
});
|
|
37
|
+
} catch (exn) {
|
|
38
|
+
entries = [];
|
|
39
|
+
}
|
|
40
|
+
if (isPruned(entries)) {
|
|
41
|
+
return acc;
|
|
42
|
+
}
|
|
43
|
+
let found = acc;
|
|
44
|
+
for (let i = 0, i_finish = entries.length; i < i_finish; ++i) {
|
|
45
|
+
let entry = entries[i];
|
|
46
|
+
if (!ignoreNames.includes(entry.name)) {
|
|
47
|
+
let full = Nodepath.join(dir, entry.name);
|
|
48
|
+
if (entry.isDirectory()) {
|
|
49
|
+
let nested = await walk(full, []);
|
|
50
|
+
found = found.concat(nested);
|
|
51
|
+
} else if (entry.isFile() && entry.name.endsWith(".res")) {
|
|
52
|
+
found = found.concat([full]);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
return found;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function scan(pkgDirs) {
|
|
60
|
+
let order = [];
|
|
61
|
+
let filesByKey = {};
|
|
62
|
+
for (let i = 0, i_finish = pkgDirs.length; i < i_finish; ++i) {
|
|
63
|
+
let dir = pkgDirs[i];
|
|
64
|
+
let srcDir = Nodepath.join(dir, "src");
|
|
65
|
+
if (Nodefs.existsSync(srcDir)) {
|
|
66
|
+
let files = await walk(srcDir, []);
|
|
67
|
+
files.forEach(file => {
|
|
68
|
+
let c = ComponentMeta$ReventlessGwt.componentOfSrcFile(file);
|
|
69
|
+
if (c === undefined) {
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
let key = dir + "::" + c.kind + "::" + c.name;
|
|
73
|
+
let existing = filesByKey[key];
|
|
74
|
+
if (existing !== undefined) {
|
|
75
|
+
filesByKey[key] = existing.concat([file]);
|
|
76
|
+
} else {
|
|
77
|
+
filesByKey[key] = [file];
|
|
78
|
+
order.push({
|
|
79
|
+
dir: dir,
|
|
80
|
+
kind: c.kind,
|
|
81
|
+
name: c.name,
|
|
82
|
+
key: key
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
return order.map(o => ({
|
|
89
|
+
dir: o.dir,
|
|
90
|
+
kind: o.kind,
|
|
91
|
+
name: o.name,
|
|
92
|
+
files: Stdlib_Option.getOr(filesByKey[o.key], [])
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
ignoreNames,
|
|
98
|
+
shouldIgnore,
|
|
99
|
+
gwtIgnoreFile,
|
|
100
|
+
isPruned,
|
|
101
|
+
isSrcResFile,
|
|
102
|
+
walk,
|
|
103
|
+
scan,
|
|
104
|
+
}
|
|
105
|
+
/* node:fs Not a pure module */
|
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
open ReventlessCore
|
|
2
|
+
|
|
3
|
+
// Delegate_GWT — GWT for ExtensionPoint mappings and Extension delegates.
|
|
4
|
+
//
|
|
5
|
+
// Both are pure input → published-actions translations and form the one
|
|
6
|
+
// cross-plugin behavioural seam (per `.claude/rules/app-developer.md`). Each
|
|
7
|
+
// mapping has TWO directions, and each adapter drives both:
|
|
8
|
+
//
|
|
9
|
+
// * an ExtensionPoint mapping turns an internal `Delegate` event into public
|
|
10
|
+
// extension-point EVENTS (`mapOutgoingEvent`), and an incoming protocol
|
|
11
|
+
// COMMAND into a wrapped-aggregate command (`mapIncomingCommand`);
|
|
12
|
+
// * an Extension delegate turns an incoming extension-point EVENT into delegate
|
|
13
|
+
// COMMANDS (`mapIncomingEvent`), and an internal `Delegate` event into an
|
|
14
|
+
// outgoing protocol COMMAND (`mapOutgoingEvent`).
|
|
15
|
+
//
|
|
16
|
+
// The stimulus you inject names what arrives at the mapping, NOT what it emits:
|
|
17
|
+
// * `whenDelegateEvent` — an internal delegate event occurs (both adapters'
|
|
18
|
+
// `mapOutgoingEvent`);
|
|
19
|
+
// * `whenIncomingEvent` — a protocol event arrives (Extension `mapIncomingEvent`);
|
|
20
|
+
// * `whenIncomingCommand`— a protocol command arrives (EP `mapIncomingCommand`).
|
|
21
|
+
//
|
|
22
|
+
// This DSL drives the user-level mapping function with the inert `StubRuntime`
|
|
23
|
+
// handles and asserts the SET of published actions — including one-to-many
|
|
24
|
+
// fan-out (e.g. `OrderPlaced{productIds:[…]}` → N × `ItemOrdered`).
|
|
25
|
+
//
|
|
26
|
+
// `FromExtensionPoint(M)` / `FromExtension(M)` mirror the From* adapter pattern
|
|
27
|
+
// of `Mapping_GWT` / `Query_GWT`: one comparison core per direction.
|
|
28
|
+
//
|
|
29
|
+
// See `docs/plans/done/gwt-flow-and-extension-test-kinds.md` Phase 1.
|
|
30
|
+
|
|
31
|
+
module EPMapping = ReventlessInfra.ExtensionPointMapping
|
|
32
|
+
module ExtMapping = ReventlessInfra.ExtensionMapping
|
|
33
|
+
|
|
34
|
+
// A published action normalised for comparison: a routing target plus the
|
|
35
|
+
// encoded payload. EP events are keyed by entity id; delegate commands by the
|
|
36
|
+
// component (slice/aggregate) or extension-point they route to.
|
|
37
|
+
let encPublished = (~target: string, payload: JSON.t): JSON.t => {
|
|
38
|
+
let d = Dict.make()
|
|
39
|
+
d->Dict.set("target", JSON.Encode.string(target))
|
|
40
|
+
d->Dict.set("payload", payload)
|
|
41
|
+
JSON.Encode.object(d)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// -- Comparison core ---------------------------------------------------------
|
|
45
|
+
|
|
46
|
+
module type Delegate = {
|
|
47
|
+
let name: string
|
|
48
|
+
type inbound
|
|
49
|
+
// Drive the underlying mapping with StubRuntime, returning the published
|
|
50
|
+
// actions already normalised to JSON.
|
|
51
|
+
let run: inbound => promise<array<JSON.t>>
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module type T = {
|
|
55
|
+
type inbound
|
|
56
|
+
|
|
57
|
+
let describe: (string, unit => unit) => unit
|
|
58
|
+
let test: (string, ~timeout: int=?, unit => promise<Outcome.outcome>) => unit
|
|
59
|
+
|
|
60
|
+
// Feed the stimulus into the mapping; returns the normalised published actions.
|
|
61
|
+
// The adapters re-export this under direction-specific names (whenDelegateEvent
|
|
62
|
+
// / whenIncomingEvent / whenIncomingCommand).
|
|
63
|
+
let whenInput: inbound => promise<array<JSON.t>>
|
|
64
|
+
let thenPublishedJson: (promise<array<JSON.t>>, array<JSON.t>) => promise<Outcome.outcome>
|
|
65
|
+
let thenPublishesNothing: promise<array<JSON.t>> => promise<Outcome.outcome>
|
|
66
|
+
// Assert the SET of HANDLED DIRECTIVES (HandleDirective actions, encoded by the
|
|
67
|
+
// EP/Extension directive schema). Disjoint from thenPublishedJson — published
|
|
68
|
+
// events/commands and handled directives are separate channels.
|
|
69
|
+
let thenHandlesDirectivesJson: (promise<array<JSON.t>>, array<JSON.t>) => promise<Outcome.outcome>
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module Make = (D: Delegate): (T with type inbound = D.inbound) => {
|
|
73
|
+
type inbound = D.inbound
|
|
74
|
+
|
|
75
|
+
S.enableJson()
|
|
76
|
+
|
|
77
|
+
let describe = JestBind.describe
|
|
78
|
+
let test = (name, ~timeout=?, body) =>
|
|
79
|
+
JestBind.testPromise(~slice=D.name, name, ~timeout?, body)
|
|
80
|
+
|
|
81
|
+
let whenInput = ev => D.run(ev)
|
|
82
|
+
|
|
83
|
+
// Published actions form a SET — fan-out order is non-deterministic, so the
|
|
84
|
+
// comparison sorts by stringified JSON before comparing.
|
|
85
|
+
let sortJson = (arr: array<JSON.t>) =>
|
|
86
|
+
arr->Array.toSorted((a, b) => String.compare(JSON.stringify(a), JSON.stringify(b)))
|
|
87
|
+
|
|
88
|
+
// Handled directives ride the same normalised array tagged `target:"directive"`
|
|
89
|
+
// so a single mapping run carries both channels; the publish/directive verbs
|
|
90
|
+
// each project to their own. (Published actions never use that target — EP/Ext
|
|
91
|
+
// events and commands route to an id, a component name, or `<ep>:<id>`.)
|
|
92
|
+
let targetOf = (j: JSON.t): string =>
|
|
93
|
+
j
|
|
94
|
+
->JSON.Decode.object
|
|
95
|
+
->Option.flatMap(d => d->Dict.get("target"))
|
|
96
|
+
->Option.flatMap(JSON.Decode.string)
|
|
97
|
+
->Option.getOr("")
|
|
98
|
+
let dropDirectives = arr => arr->Array.filter(j => targetOf(j) != "directive")
|
|
99
|
+
let onlyDirectives = arr => arr->Array.filter(j => targetOf(j) == "directive")
|
|
100
|
+
|
|
101
|
+
let thenPublishedJson = async (actionsP, expected) => {
|
|
102
|
+
let actual = (await actionsP)->dropDirectives
|
|
103
|
+
if sortJson(actual) == sortJson(expected) {
|
|
104
|
+
Outcome.pass
|
|
105
|
+
} else {
|
|
106
|
+
Outcome.fail(PublishedActionsMismatch({expected, actual}))
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
let thenPublishesNothing = actionsP => thenPublishedJson(actionsP, [])
|
|
111
|
+
|
|
112
|
+
let thenHandlesDirectivesJson = async (actionsP, expected) => {
|
|
113
|
+
let actual = (await actionsP)->onlyDirectives
|
|
114
|
+
if sortJson(actual) == sortJson(expected) {
|
|
115
|
+
Outcome.pass
|
|
116
|
+
} else {
|
|
117
|
+
Outcome.fail(PublishedActionsMismatch({expected, actual}))
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// -- FromExtensionPoint: an EP mapping (both directions) ---------------------
|
|
123
|
+
|
|
124
|
+
module FromExtensionPoint = (M: EPMapping.Mapping) => {
|
|
125
|
+
let encEvent = (e: M.ExtensionPoint.event) => e->Message.encode(M.ExtensionPoint.eventSchema)
|
|
126
|
+
let encCommand = (c: M.Delegate.command) => c->Message.encode(M.Delegate.commandSchema)
|
|
127
|
+
let encDirective = (d: M.ExtensionPoint.directive) =>
|
|
128
|
+
encPublished(~target="directive", d->Message.encode(M.ExtensionPoint.directiveSchema))
|
|
129
|
+
|
|
130
|
+
// Internal delegate event → outgoing extension-point events (`mapOutgoingEvent`).
|
|
131
|
+
module EvCore = Make({
|
|
132
|
+
let name = M.ExtensionPoint.name
|
|
133
|
+
type inbound = M.Delegate.event
|
|
134
|
+
let run = async (event: M.Delegate.event) =>
|
|
135
|
+
switch M.mapOutgoingEvent {
|
|
136
|
+
| None => []
|
|
137
|
+
| Some(f) =>
|
|
138
|
+
let nested =
|
|
139
|
+
await f("gwt-id", event, StubRuntime.meta, StubRuntime.queryEngine)
|
|
140
|
+
->Array.map(async action =>
|
|
141
|
+
switch action {
|
|
142
|
+
| EPMapping.PublishEvent(id, ev) => [encPublished(~target=id, encEvent(ev))]
|
|
143
|
+
| EPMapping.PublishEventAsync(p) =>
|
|
144
|
+
let (id, ev) = await p
|
|
145
|
+
[encPublished(~target=id, encEvent(ev))]
|
|
146
|
+
| EPMapping.HandleDirective(_, dir) => [encDirective(dir)]
|
|
147
|
+
}
|
|
148
|
+
)
|
|
149
|
+
->Promise.all
|
|
150
|
+
nested->Array.flat
|
|
151
|
+
}
|
|
152
|
+
})
|
|
153
|
+
|
|
154
|
+
// Incoming extension-point command → wrapped-aggregate commands (`mapIncomingCommand`).
|
|
155
|
+
module CmdCore = Make({
|
|
156
|
+
let name = M.ExtensionPoint.name
|
|
157
|
+
type inbound = M.ExtensionPoint.command
|
|
158
|
+
let run = async (command: M.ExtensionPoint.command) =>
|
|
159
|
+
M.mapIncomingCommand("gwt-id", command, StubRuntime.meta)->Array.filterMap(action =>
|
|
160
|
+
switch action {
|
|
161
|
+
| EPMapping.PublishCommand(id, cmd) => Some(encPublished(~target=id, encCommand(cmd)))
|
|
162
|
+
| EPMapping.HandleDirective(_, dir) => Some(encDirective(dir))
|
|
163
|
+
}
|
|
164
|
+
)
|
|
165
|
+
})
|
|
166
|
+
|
|
167
|
+
type inbound = M.Delegate.event
|
|
168
|
+
let describe = EvCore.describe
|
|
169
|
+
let test = EvCore.test
|
|
170
|
+
// `thenPublishesNothing` is direction-agnostic (compares to []), usable after
|
|
171
|
+
// either `whenDelegateEvent` or `whenIncomingCommand`.
|
|
172
|
+
let thenPublishesNothing = EvCore.thenPublishesNothing
|
|
173
|
+
|
|
174
|
+
// Delegate event → outgoing protocol events.
|
|
175
|
+
let whenDelegateEvent = EvCore.whenInput
|
|
176
|
+
let thenPublishesEvent = (actionsP, id, event: M.ExtensionPoint.event) =>
|
|
177
|
+
EvCore.thenPublishedJson(actionsP, [encPublished(~target=id, encEvent(event))])
|
|
178
|
+
let thenPublishesEvents = (actionsP, pairs: array<(string, M.ExtensionPoint.event)>) =>
|
|
179
|
+
EvCore.thenPublishedJson(
|
|
180
|
+
actionsP,
|
|
181
|
+
pairs->Array.map(((id, ev)) => encPublished(~target=id, encEvent(ev))),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
// Incoming protocol command → wrapped-aggregate commands.
|
|
185
|
+
let whenIncomingCommand = CmdCore.whenInput
|
|
186
|
+
let thenPublishesCommand = (actionsP, id, cmd: M.Delegate.command) =>
|
|
187
|
+
CmdCore.thenPublishedJson(actionsP, [encPublished(~target=id, encCommand(cmd))])
|
|
188
|
+
let thenPublishesCommands = (actionsP, pairs: array<(string, M.Delegate.command)>) =>
|
|
189
|
+
CmdCore.thenPublishedJson(
|
|
190
|
+
actionsP,
|
|
191
|
+
pairs->Array.map(((id, cmd)) => encPublished(~target=id, encCommand(cmd))),
|
|
192
|
+
)
|
|
193
|
+
|
|
194
|
+
// Handled directives — usable after either `when`; both cores tag directives on
|
|
195
|
+
// the same channel, so this projects them out regardless of direction.
|
|
196
|
+
let thenHandlesDirective = (actionsP, directive: M.ExtensionPoint.directive) =>
|
|
197
|
+
EvCore.thenHandlesDirectivesJson(actionsP, [encDirective(directive)])
|
|
198
|
+
let thenHandlesDirectives = (actionsP, directives: array<M.ExtensionPoint.directive>) =>
|
|
199
|
+
EvCore.thenHandlesDirectivesJson(actionsP, directives->Array.map(encDirective))
|
|
200
|
+
let thenHandlesNoDirective = actionsP => EvCore.thenHandlesDirectivesJson(actionsP, [])
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
// -- FromExtension: an Extension delegate (both directions) ------------------
|
|
204
|
+
|
|
205
|
+
module FromExtension = (M: ExtMapping.Mapping) => {
|
|
206
|
+
let encCmd = (c: M.Delegate.command) => c->Message.encode(M.Delegate.commandSchema)
|
|
207
|
+
let encEpCmd = (c: M.ExtensionPoint.command) => c->Message.encode(M.ExtensionPoint.commandSchema)
|
|
208
|
+
let encDirective = (d: M.ExtensionPoint.directive) =>
|
|
209
|
+
encPublished(~target="directive", d->Message.encode(M.ExtensionPoint.directiveSchema))
|
|
210
|
+
|
|
211
|
+
// Incoming extension-point event → delegate commands (`mapIncomingEvent`).
|
|
212
|
+
module EvCore = Make({
|
|
213
|
+
let name = M.Delegate.name
|
|
214
|
+
type inbound = M.ExtensionPoint.event
|
|
215
|
+
let run = async (event: M.ExtensionPoint.event) => {
|
|
216
|
+
let nested =
|
|
217
|
+
await M.mapIncomingEvent(
|
|
218
|
+
"gwt-id",
|
|
219
|
+
event,
|
|
220
|
+
StubRuntime.meta,
|
|
221
|
+
StubRuntime.pluginDefinition,
|
|
222
|
+
StubRuntime.queryEngine,
|
|
223
|
+
)
|
|
224
|
+
->Array.map(async action =>
|
|
225
|
+
switch action {
|
|
226
|
+
| ExtMapping.PublishStateChangeSliceCommand(cmd) => [
|
|
227
|
+
encPublished(~target=M.Delegate.name, encCmd(cmd)),
|
|
228
|
+
]
|
|
229
|
+
| ExtMapping.PublishStateChangeSliceCommandAsync(p) =>
|
|
230
|
+
let cmd = await p
|
|
231
|
+
[encPublished(~target=M.Delegate.name, encCmd(cmd))]
|
|
232
|
+
| ExtMapping.PublishStateChangeSliceCommandsAsync(p) =>
|
|
233
|
+
(await p)->Array.map(cmd => encPublished(~target=M.Delegate.name, encCmd(cmd)))
|
|
234
|
+
| ExtMapping.PublishAggregateCommand(id, cmd) => [encPublished(~target=id, encCmd(cmd))]
|
|
235
|
+
| ExtMapping.PublishAggregateCommandAsync(p) =>
|
|
236
|
+
let (id, cmd) = await p
|
|
237
|
+
[encPublished(~target=id, encCmd(cmd))]
|
|
238
|
+
| ExtMapping.PublishAggregateCommandsAsync(p) =>
|
|
239
|
+
(await p)->Array.map(((id, cmd)) => encPublished(~target=id, encCmd(cmd)))
|
|
240
|
+
| ExtMapping.PublishExtensionPointCommand(id, cmd) => [
|
|
241
|
+
encPublished(~target=id, encEpCmd(cmd)),
|
|
242
|
+
]
|
|
243
|
+
| ExtMapping.ForwardCommand({extensionPointName, id, commandJson}) => [
|
|
244
|
+
encPublished(~target=`${extensionPointName}:${id}`, commandJson),
|
|
245
|
+
]
|
|
246
|
+
| ExtMapping.HandleDirective(_, dir) => [encDirective(dir)]
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
->Promise.all
|
|
250
|
+
nested->Array.flat
|
|
251
|
+
}
|
|
252
|
+
})
|
|
253
|
+
|
|
254
|
+
// Internal delegate event → outgoing protocol commands (`mapOutgoingEvent`, optional).
|
|
255
|
+
module OutCore = Make({
|
|
256
|
+
let name = M.Delegate.name
|
|
257
|
+
type inbound = M.Delegate.event
|
|
258
|
+
let run = async (event: M.Delegate.event) =>
|
|
259
|
+
switch M.mapOutgoingEvent {
|
|
260
|
+
| None => []
|
|
261
|
+
| Some(f) =>
|
|
262
|
+
f("gwt-id", event, StubRuntime.meta, StubRuntime.pluginDefinition)->Array.filterMap(action =>
|
|
263
|
+
switch action {
|
|
264
|
+
| ExtMapping.PublishExtensionPointCommand(id, cmd) =>
|
|
265
|
+
Some(encPublished(~target=id, encEpCmd(cmd)))
|
|
266
|
+
| ExtMapping.ForwardCommand({extensionPointName, id, commandJson}) =>
|
|
267
|
+
Some(encPublished(~target=`${extensionPointName}:${id}`, commandJson))
|
|
268
|
+
| ExtMapping.HandleDirective(_, dir) => Some(encDirective(dir))
|
|
269
|
+
}
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
})
|
|
273
|
+
|
|
274
|
+
type inbound = M.ExtensionPoint.event
|
|
275
|
+
let describe = EvCore.describe
|
|
276
|
+
let test = EvCore.test
|
|
277
|
+
let thenPublishesNothing = EvCore.thenPublishesNothing
|
|
278
|
+
|
|
279
|
+
// Incoming protocol event → delegate commands.
|
|
280
|
+
let whenIncomingEvent = EvCore.whenInput
|
|
281
|
+
let thenPublishesCommand = (actionsP, cmd: M.Delegate.command) =>
|
|
282
|
+
EvCore.thenPublishedJson(actionsP, [encPublished(~target=M.Delegate.name, encCmd(cmd))])
|
|
283
|
+
let thenPublishesCommands = (actionsP, cmds: array<M.Delegate.command>) =>
|
|
284
|
+
EvCore.thenPublishedJson(
|
|
285
|
+
actionsP,
|
|
286
|
+
cmds->Array.map(cmd => encPublished(~target=M.Delegate.name, encCmd(cmd))),
|
|
287
|
+
)
|
|
288
|
+
let thenPublishesAggregateCommand = (actionsP, id, cmd: M.Delegate.command) =>
|
|
289
|
+
EvCore.thenPublishedJson(actionsP, [encPublished(~target=id, encCmd(cmd))])
|
|
290
|
+
|
|
291
|
+
// Internal delegate event → outgoing protocol commands.
|
|
292
|
+
let whenDelegateEvent = OutCore.whenInput
|
|
293
|
+
let thenPublishesExtensionPointCommand = (actionsP, id, cmd: M.ExtensionPoint.command) =>
|
|
294
|
+
OutCore.thenPublishedJson(actionsP, [encPublished(~target=id, encEpCmd(cmd))])
|
|
295
|
+
|
|
296
|
+
// Handled directives — usable after either `when` (both cores tag directives on
|
|
297
|
+
// the same channel).
|
|
298
|
+
let thenHandlesDirective = (actionsP, directive: M.ExtensionPoint.directive) =>
|
|
299
|
+
EvCore.thenHandlesDirectivesJson(actionsP, [encDirective(directive)])
|
|
300
|
+
let thenHandlesDirectives = (actionsP, directives: array<M.ExtensionPoint.directive>) =>
|
|
301
|
+
EvCore.thenHandlesDirectivesJson(actionsP, directives->Array.map(encDirective))
|
|
302
|
+
let thenHandlesNoDirective = actionsP => EvCore.thenHandlesDirectivesJson(actionsP, [])
|
|
303
|
+
}
|