@reventlessdev/reventless-gwt 1.0.0-alpha.100
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 +916 -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 +569 -0
- package/src/Behavior_GWT.res.mjs +523 -0
- package/src/Bind.res +29 -0
- package/src/Bind.res.mjs +22 -0
- package/src/BuildClassifier.res +147 -0
- package/src/BuildClassifier.res.mjs +136 -0
- package/src/Cancellation.res +37 -0
- package/src/Cancellation.res.mjs +46 -0
- package/src/ChildProcess.res +102 -0
- package/src/ChildProcess.res.mjs +107 -0
- package/src/Cli.res +905 -0
- package/src/Cli.res.mjs +952 -0
- package/src/Collector.res +212 -0
- package/src/Collector.res.mjs +218 -0
- package/src/ComponentMeta.res +77 -0
- package/src/ComponentMeta.res.mjs +91 -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 +99 -0
- package/src/DomainDeadCode.res +98 -0
- package/src/DomainDeadCode.res.mjs +72 -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 +45 -0
- package/src/Filter.res.mjs +47 -0
- package/src/Flow_GWT.res +664 -0
- package/src/Flow_GWT.res.mjs +604 -0
- package/src/FormatterHuman.res +109 -0
- package/src/FormatterHuman.res.mjs +115 -0
- package/src/FormatterJson.res +264 -0
- package/src/FormatterJson.res.mjs +322 -0
- package/src/FormatterJunit.res +93 -0
- package/src/FormatterJunit.res.mjs +90 -0
- package/src/FormatterTap.res +100 -0
- package/src/FormatterTap.res.mjs +131 -0
- package/src/FormatterVsCode.res +352 -0
- package/src/FormatterVsCode.res.mjs +546 -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 +119 -0
- package/src/LocalHost.res.mjs +99 -0
- package/src/Mapping_GWT.res +474 -0
- package/src/Mapping_GWT.res.mjs +322 -0
- package/src/MismatchRender.res +72 -0
- package/src/MismatchRender.res.mjs +127 -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 +97 -0
- package/src/Outcome.res.mjs +93 -0
- package/src/PackageScan.res +80 -0
- package/src/PackageScan.res.mjs +81 -0
- package/src/PlatformRunner.res +181 -0
- package/src/PlatformRunner.res.mjs +176 -0
- package/src/PlatformScan.res +142 -0
- package/src/PlatformScan.res.mjs +132 -0
- package/src/ProcessManager.res +135 -0
- package/src/ProcessManager.res.mjs +119 -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/RunWorker.res +24 -0
- package/src/RunWorker.res.mjs +16 -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 +54 -0
- package/src/StubRuntime.res.mjs +55 -0
- package/src/TestFixtures.res +16 -0
- package/src/TestFixtures.res.mjs +34 -0
- package/src/Watch.res +108 -0
- package/src/Watch.res.mjs +85 -0
- package/src/WatcherProbe.res +56 -0
- package/src/WatcherProbe.res.mjs +59 -0
- package/src/Worker.res +24 -0
- package/src/Worker.res.mjs +9 -0
- package/tests/AutomationGwtTest.res +71 -0
- package/tests/AutomationGwtTest.res.mjs +189 -0
- package/tests/BuildClassifierTest.res +133 -0
- package/tests/BuildClassifierTest.res.mjs +176 -0
- package/tests/CliRunEntryTest.res +57 -0
- package/tests/CliRunEntryTest.res.mjs +65 -0
- package/tests/CliWatchScopeTest.res +62 -0
- package/tests/CliWatchScopeTest.res.mjs +95 -0
- package/tests/ComponentMetaTest.res +67 -0
- package/tests/ComponentMetaTest.res.mjs +71 -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 +119 -0
- package/tests/DomainDeadCodeTest.res.mjs +158 -0
- package/tests/DomainGraphTest.res +448 -0
- package/tests/DomainGraphTest.res.mjs +505 -0
- package/tests/FlowAggregateGwtTest.res +171 -0
- package/tests/FlowAggregateGwtTest.res.mjs +273 -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 +433 -0
- package/tests/FormatterGoldenTest.res +125 -0
- package/tests/FormatterGoldenTest.res.mjs +162 -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 +99 -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 +350 -0
- package/tests/MappingGwtTest.res.mjs +596 -0
- package/tests/OutboundTranslationGwtTest.res +59 -0
- package/tests/OutboundTranslationGwtTest.res.mjs +139 -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 +106 -0
- package/tests/WatchDebounceTest.res.mjs +128 -0
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// Unit tests for LocalHost's pure helpers — plugin-name derivation + discovery.
|
|
2
|
+
// These touch only the filesystem (no reventless-local import), so they run under
|
|
3
|
+
// the normal Jest suite. The cold-load integration (`loadGraph`, which instantiates
|
|
4
|
+
// reventless-local's Platform) can't run under Jest's experimental-vm-modules — see
|
|
5
|
+
// tests/SplitApiFixtures.res — and lives in test/LocalHostHostTest.res
|
|
6
|
+
// (`pnpm run test:host`, run via `node --test`).
|
|
7
|
+
//
|
|
8
|
+
// Fixtures are a throwaway temp tree (same pattern as DiscoveryTest) so the test is
|
|
9
|
+
// hermetic and doesn't depend on the example plugins being compiled.
|
|
10
|
+
|
|
11
|
+
open JestGlobals
|
|
12
|
+
|
|
13
|
+
@module("node:os") external tmpdir: unit => string = "tmpdir"
|
|
14
|
+
@module("node:path") external join: (string, string) => string = "join"
|
|
15
|
+
|
|
16
|
+
type mkdirOpts = {recursive: bool}
|
|
17
|
+
@module("node:fs/promises")
|
|
18
|
+
external mkdir: (string, mkdirOpts) => promise<Nullable.t<string>> = "mkdir"
|
|
19
|
+
@module("node:fs/promises") external writeFile: (string, string) => promise<unit> = "writeFile"
|
|
20
|
+
type rmOpts = {recursive: bool, force: bool}
|
|
21
|
+
@module("node:fs/promises") external rm: (string, rmOpts) => promise<unit> = "rm"
|
|
22
|
+
|
|
23
|
+
describe("LocalHost.packageNameToPluginName", () => {
|
|
24
|
+
testPromise("PascalCases scoped / dashed / underscored package names", async () => {
|
|
25
|
+
expect(LocalHost.packageNameToPluginName("@scope/my-catalog"))->toBe("MyCatalog")
|
|
26
|
+
expect(LocalHost.packageNameToPluginName("online-shop"))->toBe("OnlineShop")
|
|
27
|
+
expect(
|
|
28
|
+
LocalHost.packageNameToPluginName("online-shop-aggregates-catalog"),
|
|
29
|
+
)->toBe("OnlineShopAggregatesCatalog")
|
|
30
|
+
})
|
|
31
|
+
})
|
|
32
|
+
|
|
33
|
+
describe("LocalHost name derivation + discovery", () => {
|
|
34
|
+
testPromise(
|
|
35
|
+
"derivePluginName prefers plugin.json then PascalCase(package.json); discover skips non-plugins",
|
|
36
|
+
async () => {
|
|
37
|
+
let root = join(tmpdir(), "reventless-localhost-test")
|
|
38
|
+
let _ = await rm(root, {recursive: true, force: true})
|
|
39
|
+
|
|
40
|
+
// a: explicit plugin.json name + a compiled composition root.
|
|
41
|
+
let a = join(root, "a")
|
|
42
|
+
let aSrc = join(a, "src")
|
|
43
|
+
let _ = await mkdir(aSrc, {recursive: true})
|
|
44
|
+
let _ = await writeFile(join(a, "package.json"), `{"name":"@x/a-pkg"}`)
|
|
45
|
+
let _ = await writeFile(join(aSrc, "plugin.json"), `{"name":"Catalog"}`)
|
|
46
|
+
let _ = await writeFile(join(aSrc, "Plugin.res.mjs"), "")
|
|
47
|
+
|
|
48
|
+
// b: no plugin.json → name falls back to PascalCase(package.json name).
|
|
49
|
+
let b = join(root, "b")
|
|
50
|
+
let bSrc = join(b, "src")
|
|
51
|
+
let _ = await mkdir(bSrc, {recursive: true})
|
|
52
|
+
let _ = await writeFile(join(b, "package.json"), `{"name":"@scope/my-ordering"}`)
|
|
53
|
+
let _ = await writeFile(join(bSrc, "Plugin.res.mjs"), "")
|
|
54
|
+
|
|
55
|
+
// c: has src/ but no Plugin.res.mjs → discover must skip it.
|
|
56
|
+
let c = join(root, "c")
|
|
57
|
+
let _ = await mkdir(join(c, "src"), {recursive: true})
|
|
58
|
+
|
|
59
|
+
expect(LocalHost.derivePluginName(~pluginSrcDir=aSrc))->toBe("Catalog")
|
|
60
|
+
expect(LocalHost.derivePluginName(~pluginSrcDir=bSrc))->toBe("MyOrdering")
|
|
61
|
+
|
|
62
|
+
let refs = LocalHost.discover(~packageDirs=[a, b, c])
|
|
63
|
+
expect(refs->Array.map((r: LocalHost.pluginRef) => r.name))->toEqual(["Catalog", "MyOrdering"])
|
|
64
|
+
let first: LocalHost.pluginRef = refs->Array.getUnsafe(0)
|
|
65
|
+
expect(first.modulePath->String.endsWith("Plugin.res.mjs"))->toBe(true)
|
|
66
|
+
expect(first.packageDir)->toBe(a)
|
|
67
|
+
|
|
68
|
+
let _ = await rm(root, {recursive: true, force: true})
|
|
69
|
+
},
|
|
70
|
+
)
|
|
71
|
+
})
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodeos from "node:os";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Promises from "node:fs/promises";
|
|
6
|
+
import * as LocalHost$ReventlessGwt from "../src/LocalHost.res.mjs";
|
|
7
|
+
|
|
8
|
+
globalThis.describe("LocalHost.packageNameToPluginName", () => {
|
|
9
|
+
globalThis.test("PascalCases scoped / dashed / underscored package names", async () => {
|
|
10
|
+
globalThis.expect(LocalHost$ReventlessGwt.packageNameToPluginName("@scope/my-catalog")).toBe("MyCatalog");
|
|
11
|
+
globalThis.expect(LocalHost$ReventlessGwt.packageNameToPluginName("online-shop")).toBe("OnlineShop");
|
|
12
|
+
globalThis.expect(LocalHost$ReventlessGwt.packageNameToPluginName("online-shop-aggregates-catalog")).toBe("OnlineShopAggregatesCatalog");
|
|
13
|
+
});
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
globalThis.describe("LocalHost name derivation + discovery", () => {
|
|
17
|
+
globalThis.test("derivePluginName prefers plugin.json then PascalCase(package.json); discover skips non-plugins", async () => {
|
|
18
|
+
let root = Nodepath.join(Nodeos.tmpdir(), "reventless-localhost-test");
|
|
19
|
+
await Promises.rm(root, {
|
|
20
|
+
recursive: true,
|
|
21
|
+
force: true
|
|
22
|
+
});
|
|
23
|
+
let a = Nodepath.join(root, "a");
|
|
24
|
+
let aSrc = Nodepath.join(a, "src");
|
|
25
|
+
await Promises.mkdir(aSrc, {
|
|
26
|
+
recursive: true
|
|
27
|
+
});
|
|
28
|
+
await Promises.writeFile(Nodepath.join(a, "package.json"), `{"name":"@x/a-pkg"}`);
|
|
29
|
+
await Promises.writeFile(Nodepath.join(aSrc, "plugin.json"), `{"name":"Catalog"}`);
|
|
30
|
+
await Promises.writeFile(Nodepath.join(aSrc, "Plugin.res.mjs"), "");
|
|
31
|
+
let b = Nodepath.join(root, "b");
|
|
32
|
+
let bSrc = Nodepath.join(b, "src");
|
|
33
|
+
await Promises.mkdir(bSrc, {
|
|
34
|
+
recursive: true
|
|
35
|
+
});
|
|
36
|
+
await Promises.writeFile(Nodepath.join(b, "package.json"), `{"name":"@scope/my-ordering"}`);
|
|
37
|
+
await Promises.writeFile(Nodepath.join(bSrc, "Plugin.res.mjs"), "");
|
|
38
|
+
let c = Nodepath.join(root, "c");
|
|
39
|
+
await Promises.mkdir(Nodepath.join(c, "src"), {
|
|
40
|
+
recursive: true
|
|
41
|
+
});
|
|
42
|
+
globalThis.expect(LocalHost$ReventlessGwt.derivePluginName(aSrc)).toBe("Catalog");
|
|
43
|
+
globalThis.expect(LocalHost$ReventlessGwt.derivePluginName(bSrc)).toBe("MyOrdering");
|
|
44
|
+
let refs = LocalHost$ReventlessGwt.discover([
|
|
45
|
+
a,
|
|
46
|
+
b,
|
|
47
|
+
c
|
|
48
|
+
]);
|
|
49
|
+
globalThis.expect(refs.map(r => r.name)).toEqual([
|
|
50
|
+
"Catalog",
|
|
51
|
+
"MyOrdering"
|
|
52
|
+
]);
|
|
53
|
+
let first = refs[0];
|
|
54
|
+
globalThis.expect(first.modulePath.endsWith("Plugin.res.mjs")).toBe(true);
|
|
55
|
+
globalThis.expect(first.packageDir).toBe(a);
|
|
56
|
+
await Promises.rm(root, {
|
|
57
|
+
recursive: true,
|
|
58
|
+
force: true
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
/* Not a pure module */
|
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
// Worked examples for Mapping_GWT — one for each producer/consumer combination
|
|
2
|
+
// (Aggr→Aggr, Aggr→DCB, DCB→Aggr, DCB→DCB). See
|
|
3
|
+
// `docs/analysis/event-source-connection-matrix.md` for the connection matrix
|
|
4
|
+
// and `docs/plans/reventless-gwt.md` Stage 5 for the plan context.
|
|
5
|
+
//
|
|
6
|
+
// Each test exercises the full source-command → mapping → target-command →
|
|
7
|
+
// target-decide pipeline end-to-end against the unified `GwtSource`/`GwtTarget`
|
|
8
|
+
// module types.
|
|
9
|
+
|
|
10
|
+
S.enableJson()
|
|
11
|
+
|
|
12
|
+
// ---------------------------------------------------------------------------
|
|
13
|
+
// Shared Aggregate specs + behaviours used by the Aggr→* cases below.
|
|
14
|
+
// ---------------------------------------------------------------------------
|
|
15
|
+
|
|
16
|
+
module CategorySpec = {
|
|
17
|
+
module Id = Reventless.Id.StringPure
|
|
18
|
+
let name = "Category"
|
|
19
|
+
|
|
20
|
+
@schema
|
|
21
|
+
type command = AddCategory({categoryId: string, name: string})
|
|
22
|
+
|
|
23
|
+
@schema
|
|
24
|
+
type event = CategoryAdded({categoryId: string, name: string})
|
|
25
|
+
|
|
26
|
+
@schema
|
|
27
|
+
type error = CategoryAlreadyExists
|
|
28
|
+
|
|
29
|
+
let moduleUrl = ""
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
module CategoryBehavior = {
|
|
33
|
+
module Spec = CategorySpec
|
|
34
|
+
type state = NotCreated | Created
|
|
35
|
+
let initialState = NotCreated
|
|
36
|
+
let snapshot = None
|
|
37
|
+
|
|
38
|
+
let evolve = (_state, event: CategorySpec.event) =>
|
|
39
|
+
switch event {
|
|
40
|
+
| CategoryAdded(_) => Created
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
let decide = (state, cmd: CategorySpec.command) =>
|
|
44
|
+
switch (state, cmd) {
|
|
45
|
+
| (NotCreated, AddCategory({categoryId, name})) =>
|
|
46
|
+
Ok([CategorySpec.CategoryAdded({categoryId, name})])
|
|
47
|
+
| (Created, AddCategory(_)) => Error(CategorySpec.CategoryAlreadyExists)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
let moduleUrl = ""
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Target aggregate for Aggr→Aggr and DCB→Aggr cases.
|
|
54
|
+
module ProductSpec = {
|
|
55
|
+
module Id = Reventless.Id.StringPure
|
|
56
|
+
let name = "Product"
|
|
57
|
+
|
|
58
|
+
@schema
|
|
59
|
+
type command = MirrorCategory({productId: string, categoryId: string, name: string})
|
|
60
|
+
|
|
61
|
+
@schema
|
|
62
|
+
type event = ProductCategoryMirrored({productId: string, categoryId: string, name: string})
|
|
63
|
+
|
|
64
|
+
@schema
|
|
65
|
+
type error = ProductAlreadyMirrored
|
|
66
|
+
|
|
67
|
+
let moduleUrl = ""
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module ProductBehavior = {
|
|
71
|
+
module Spec = ProductSpec
|
|
72
|
+
type state = Pristine | Mirrored
|
|
73
|
+
let initialState = Pristine
|
|
74
|
+
let snapshot = None
|
|
75
|
+
|
|
76
|
+
let evolve = (_state, event: ProductSpec.event) =>
|
|
77
|
+
switch event {
|
|
78
|
+
| ProductCategoryMirrored(_) => Mirrored
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
let decide = (state, cmd: ProductSpec.command) =>
|
|
82
|
+
switch (state, cmd) {
|
|
83
|
+
| (Pristine, MirrorCategory({productId, categoryId, name})) =>
|
|
84
|
+
Ok([ProductSpec.ProductCategoryMirrored({productId, categoryId, name})])
|
|
85
|
+
| (Mirrored, MirrorCategory(_)) => Error(ProductSpec.ProductAlreadyMirrored)
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
let moduleUrl = ""
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
// Shared StateChangeSlice specs used by the *→DCB and DCB→* cases.
|
|
93
|
+
// ---------------------------------------------------------------------------
|
|
94
|
+
|
|
95
|
+
module NotificationSlice = {
|
|
96
|
+
let name = "Notification"
|
|
97
|
+
|
|
98
|
+
type state = {sent: bool}
|
|
99
|
+
let initialState = {sent: false}
|
|
100
|
+
|
|
101
|
+
@schema
|
|
102
|
+
type consumedEvent =
|
|
103
|
+
| NotificationSent({
|
|
104
|
+
notificationId: @s.matches(Reventless.DcbTag.string) string,
|
|
105
|
+
})
|
|
106
|
+
|
|
107
|
+
let evolve = (_state, event) =>
|
|
108
|
+
switch event {
|
|
109
|
+
| NotificationSent(_) => {sent: true}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
@schema
|
|
113
|
+
type command =
|
|
114
|
+
| SendNotification({
|
|
115
|
+
notificationId: @s.matches(Reventless.DcbTag.string) string,
|
|
116
|
+
message: string,
|
|
117
|
+
})
|
|
118
|
+
|
|
119
|
+
@schema
|
|
120
|
+
type error = NotificationAlreadySent
|
|
121
|
+
|
|
122
|
+
@schema
|
|
123
|
+
type event =
|
|
124
|
+
| NotificationSent({
|
|
125
|
+
notificationId: @s.matches(Reventless.DcbTag.string) string,
|
|
126
|
+
message: string,
|
|
127
|
+
})
|
|
128
|
+
|
|
129
|
+
let decide = (state, command) =>
|
|
130
|
+
switch command {
|
|
131
|
+
| SendNotification({notificationId, message}) =>
|
|
132
|
+
state.sent
|
|
133
|
+
? Error(NotificationAlreadySent)
|
|
134
|
+
: Ok([NotificationSent({notificationId, message})])
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module InventorySlice = {
|
|
139
|
+
let name = "Inventory"
|
|
140
|
+
|
|
141
|
+
type state = {reserved: bool}
|
|
142
|
+
let initialState = {reserved: false}
|
|
143
|
+
|
|
144
|
+
@schema
|
|
145
|
+
type consumedEvent =
|
|
146
|
+
| StockReserved({itemId: @s.matches(Reventless.DcbTag.string) string})
|
|
147
|
+
|
|
148
|
+
let evolve = (_state, event) =>
|
|
149
|
+
switch event {
|
|
150
|
+
| StockReserved(_) => {reserved: true}
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
@schema
|
|
154
|
+
type command =
|
|
155
|
+
| ReserveStock({
|
|
156
|
+
itemId: @s.matches(Reventless.DcbTag.string) string,
|
|
157
|
+
quantity: int,
|
|
158
|
+
})
|
|
159
|
+
|
|
160
|
+
@schema
|
|
161
|
+
type error = OutOfStock
|
|
162
|
+
|
|
163
|
+
@schema
|
|
164
|
+
type event =
|
|
165
|
+
| StockReserved({
|
|
166
|
+
itemId: @s.matches(Reventless.DcbTag.string) string,
|
|
167
|
+
quantity: int,
|
|
168
|
+
})
|
|
169
|
+
|
|
170
|
+
let decide = (state, command) =>
|
|
171
|
+
switch command {
|
|
172
|
+
| ReserveStock({itemId, quantity}) =>
|
|
173
|
+
state.reserved ? Error(OutOfStock) : Ok([StockReserved({itemId, quantity})])
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// Adapter aliases — built once, shared across mappings.
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
|
|
181
|
+
module CategorySource = Mapping_GWT.FromBehavior(CategorySpec, CategoryBehavior)
|
|
182
|
+
module ProductTarget = Mapping_GWT.FromBehavior(ProductSpec, ProductBehavior)
|
|
183
|
+
module NotificationSource = Mapping_GWT.FromStateChangeSlice(NotificationSlice)
|
|
184
|
+
module NotificationTarget = Mapping_GWT.FromStateChangeSlice(NotificationSlice)
|
|
185
|
+
module InventoryTarget = Mapping_GWT.FromStateChangeSlice(InventorySlice)
|
|
186
|
+
|
|
187
|
+
// ---------------------------------------------------------------------------
|
|
188
|
+
// Case 1 — Aggr → Aggr: CategoryAdded triggers MirrorCategory on Product.
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
|
|
191
|
+
module AggrToAggrMapping = {
|
|
192
|
+
module Source = CategorySource
|
|
193
|
+
module Target = ProductTarget
|
|
194
|
+
|
|
195
|
+
let map = (_sourceId, event: CategorySpec.event, _q) =>
|
|
196
|
+
switch event {
|
|
197
|
+
| CategoryAdded({categoryId, name}) => [
|
|
198
|
+
Reventless.EventMapping.Publish(
|
|
199
|
+
("prod-for-" ++ categoryId)->ProductSpec.Id.makeFromString,
|
|
200
|
+
ProductSpec.MirrorCategory({productId: "prod-for-" ++ categoryId, categoryId, name}),
|
|
201
|
+
),
|
|
202
|
+
]
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
module AggrToAggrGwt = Mapping_GWT.Make(AggrToAggrMapping)
|
|
207
|
+
|
|
208
|
+
AggrToAggrGwt.describe("Category → Product (Aggr → Aggr)", () =>
|
|
209
|
+
AggrToAggrGwt.test("AddCategory produces ProductCategoryMirrored on product", () =>
|
|
210
|
+
AggrToAggrGwt.givenSourceEvents([])
|
|
211
|
+
->AggrToAggrGwt.andTargetEvents([])
|
|
212
|
+
->AggrToAggrGwt.whenSourceCmd("c1", AddCategory({categoryId: "c1", name: "Books"}))
|
|
213
|
+
->AggrToAggrGwt.thenTargetEvent(
|
|
214
|
+
"prod-for-c1",
|
|
215
|
+
ProductCategoryMirrored({productId: "prod-for-c1", categoryId: "c1", name: "Books"}),
|
|
216
|
+
)
|
|
217
|
+
)
|
|
218
|
+
)
|
|
219
|
+
|
|
220
|
+
// ---------------------------------------------------------------------------
|
|
221
|
+
// Case 2 — Aggr → DCB: CategoryAdded triggers SendNotification on Notification slice.
|
|
222
|
+
// ---------------------------------------------------------------------------
|
|
223
|
+
|
|
224
|
+
module AggrToDcbMapping = {
|
|
225
|
+
module Source = CategorySource
|
|
226
|
+
module Target = NotificationTarget
|
|
227
|
+
|
|
228
|
+
let map = (_sourceId, event: CategorySpec.event, _q) =>
|
|
229
|
+
switch event {
|
|
230
|
+
| CategoryAdded({categoryId, name}) => [
|
|
231
|
+
Reventless.EventMapping.Publish(
|
|
232
|
+
("notif-" ++ categoryId)->NotificationTarget.Id.makeFromString,
|
|
233
|
+
NotificationSlice.SendNotification({
|
|
234
|
+
notificationId: "notif-" ++ categoryId,
|
|
235
|
+
message: `Category ${name} was created`,
|
|
236
|
+
}),
|
|
237
|
+
),
|
|
238
|
+
]
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module AggrToDcbGwt = Mapping_GWT.Make(AggrToDcbMapping)
|
|
243
|
+
|
|
244
|
+
AggrToDcbGwt.describe("Category → Notification (Aggr → DCB)", () =>
|
|
245
|
+
AggrToDcbGwt.test("AddCategory triggers NotificationSent on the DCB slice", () =>
|
|
246
|
+
AggrToDcbGwt.givenSourceEvents([])
|
|
247
|
+
->AggrToDcbGwt.andTargetEvents([])
|
|
248
|
+
->AggrToDcbGwt.whenSourceCmd("c2", AddCategory({categoryId: "c2", name: "Media"}))
|
|
249
|
+
->AggrToDcbGwt.thenTargetEvent(
|
|
250
|
+
"notif-c2",
|
|
251
|
+
NotificationSlice.NotificationSent({
|
|
252
|
+
notificationId: "notif-c2",
|
|
253
|
+
message: "Category Media was created",
|
|
254
|
+
}),
|
|
255
|
+
)
|
|
256
|
+
)
|
|
257
|
+
)
|
|
258
|
+
|
|
259
|
+
// ---------------------------------------------------------------------------
|
|
260
|
+
// Case 3 — DCB → Aggr: Inventory StockReserved triggers MirrorCategory on Product.
|
|
261
|
+
// ---------------------------------------------------------------------------
|
|
262
|
+
|
|
263
|
+
module InventorySource = Mapping_GWT.FromStateChangeSlice(InventorySlice)
|
|
264
|
+
|
|
265
|
+
module DcbToAggrMapping = {
|
|
266
|
+
module Source = InventorySource
|
|
267
|
+
module Target = ProductTarget
|
|
268
|
+
|
|
269
|
+
let map = (_sourceId, event: InventorySlice.event, _q) =>
|
|
270
|
+
switch event {
|
|
271
|
+
| StockReserved({itemId, quantity: _}) => [
|
|
272
|
+
Reventless.EventMapping.Publish(
|
|
273
|
+
("prod-" ++ itemId)->ProductSpec.Id.makeFromString,
|
|
274
|
+
ProductSpec.MirrorCategory({
|
|
275
|
+
productId: "prod-" ++ itemId,
|
|
276
|
+
categoryId: itemId,
|
|
277
|
+
name: itemId,
|
|
278
|
+
}),
|
|
279
|
+
),
|
|
280
|
+
]
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
module DcbToAggrGwt = Mapping_GWT.Make(DcbToAggrMapping)
|
|
285
|
+
|
|
286
|
+
DcbToAggrGwt.describe("Inventory → Product (DCB → Aggr)", () =>
|
|
287
|
+
DcbToAggrGwt.test("ReserveStock triggers ProductCategoryMirrored", () =>
|
|
288
|
+
DcbToAggrGwt.givenSourceEvents([])
|
|
289
|
+
->DcbToAggrGwt.andTargetEvents([])
|
|
290
|
+
->DcbToAggrGwt.whenSourceCmd("i1", ReserveStock({itemId: "i1", quantity: 5}))
|
|
291
|
+
->DcbToAggrGwt.thenTargetEvent(
|
|
292
|
+
"prod-i1",
|
|
293
|
+
ProductCategoryMirrored({productId: "prod-i1", categoryId: "i1", name: "i1"}),
|
|
294
|
+
)
|
|
295
|
+
)
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
// ---------------------------------------------------------------------------
|
|
299
|
+
// Case 4 — DCB → DCB: Inventory StockReserved triggers SendNotification on Notification slice.
|
|
300
|
+
// ---------------------------------------------------------------------------
|
|
301
|
+
|
|
302
|
+
module DcbToDcbMapping = {
|
|
303
|
+
module Source = InventorySource
|
|
304
|
+
module Target = NotificationTarget
|
|
305
|
+
|
|
306
|
+
let map = (_sourceId, event: InventorySlice.event, _q) =>
|
|
307
|
+
switch event {
|
|
308
|
+
| StockReserved({itemId, quantity}) => [
|
|
309
|
+
Reventless.EventMapping.Publish(
|
|
310
|
+
("reserved-" ++ itemId)->NotificationTarget.Id.makeFromString,
|
|
311
|
+
NotificationSlice.SendNotification({
|
|
312
|
+
notificationId: "reserved-" ++ itemId,
|
|
313
|
+
message: `Reserved ${quantity->Int.toString} of ${itemId}`,
|
|
314
|
+
}),
|
|
315
|
+
),
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
module DcbToDcbGwt = Mapping_GWT.Make(DcbToDcbMapping)
|
|
321
|
+
|
|
322
|
+
DcbToDcbGwt.describe("Inventory → Notification (DCB → DCB)", () => {
|
|
323
|
+
DcbToDcbGwt.test("ReserveStock triggers NotificationSent", () =>
|
|
324
|
+
DcbToDcbGwt.givenSourceEvents([])
|
|
325
|
+
->DcbToDcbGwt.andTargetEvents([])
|
|
326
|
+
->DcbToDcbGwt.whenSourceCmd("i7", ReserveStock({itemId: "i7", quantity: 2}))
|
|
327
|
+
->DcbToDcbGwt.thenTargetEvent(
|
|
328
|
+
"reserved-i7",
|
|
329
|
+
NotificationSlice.NotificationSent({
|
|
330
|
+
notificationId: "reserved-i7",
|
|
331
|
+
message: "Reserved 2 of i7",
|
|
332
|
+
}),
|
|
333
|
+
)
|
|
334
|
+
)
|
|
335
|
+
|
|
336
|
+
// Target-error coverage: if the notification was already sent, SendNotification
|
|
337
|
+
// fails with `NotificationAlreadySent`. `thenTargetError` surfaces the target's
|
|
338
|
+
// decide error without requiring the source to produce no events.
|
|
339
|
+
DcbToDcbGwt.test("existing NotificationSent causes target decide to reject", () =>
|
|
340
|
+
DcbToDcbGwt.givenSourceEvents([])
|
|
341
|
+
->DcbToDcbGwt.andTargetEvents([
|
|
342
|
+
(
|
|
343
|
+
"reserved-i7",
|
|
344
|
+
[NotificationSlice.NotificationSent({notificationId: "reserved-i7"})],
|
|
345
|
+
),
|
|
346
|
+
])
|
|
347
|
+
->DcbToDcbGwt.whenSourceCmd("i7", ReserveStock({itemId: "i7", quantity: 2}))
|
|
348
|
+
->DcbToDcbGwt.thenTargetError(NotificationAlreadySent)
|
|
349
|
+
)
|
|
350
|
+
})
|