@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,99 @@
|
|
|
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 Nodepath from "node:path";
|
|
6
|
+
import * as Stdlib_JSON from "@rescript/runtime/lib/es6/Stdlib_JSON.js";
|
|
7
|
+
import * as Nodemodule from "node:module";
|
|
8
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
9
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
10
|
+
import * as PluginName$Reventless from "@reventlessdev/reventless-spec/src/components/PluginName.res.mjs";
|
|
11
|
+
|
|
12
|
+
let dynamicImport = ((u) => import(u));
|
|
13
|
+
|
|
14
|
+
let counter = {
|
|
15
|
+
contents: 0
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function bustedUrl(absolutePath) {
|
|
19
|
+
counter.contents = counter.contents + 1 | 0;
|
|
20
|
+
return Nodeurl.pathToFileURL(absolutePath).href + "?t=" + counter.contents.toString();
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function strField(json, key) {
|
|
24
|
+
return Stdlib_Option.flatMap(Stdlib_Option.flatMap(Stdlib_JSON.Decode.object(json), d => d[key]), Stdlib_JSON.Decode.string);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function readJson(path) {
|
|
28
|
+
try {
|
|
29
|
+
return JSON.parse(Nodefs.readFileSync(path, "utf8"));
|
|
30
|
+
} catch (exn) {
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function derivePluginName(pluginSrcDir) {
|
|
36
|
+
let pluginJson = Nodepath.join(pluginSrcDir, "plugin.json");
|
|
37
|
+
let pluginJsonName = Nodefs.existsSync(pluginJson) ? Stdlib_Option.flatMap(readJson(pluginJson), j => strField(j, "name")) : undefined;
|
|
38
|
+
let packageJsonName = Stdlib_Option.flatMap(readJson(Nodepath.join(Nodepath.dirname(pluginSrcDir), "package.json")), j => strField(j, "name"));
|
|
39
|
+
return PluginName$Reventless.resolve(pluginJsonName, packageJsonName);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function discover(packageDirs) {
|
|
43
|
+
return Stdlib_Array.filterMap(packageDirs, dir => {
|
|
44
|
+
let srcDir = Nodepath.join(dir, "src");
|
|
45
|
+
let modulePath = Nodepath.join(srcDir, "Plugin.res.mjs");
|
|
46
|
+
if (Nodefs.existsSync(modulePath)) {
|
|
47
|
+
return {
|
|
48
|
+
name: derivePluginName(srcDir),
|
|
49
|
+
modulePath: modulePath,
|
|
50
|
+
packageDir: dir
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
let localPlatformSpecifier = "@reventlessdev/reventless-local/src/Platform.res.mjs";
|
|
57
|
+
|
|
58
|
+
function resolveLocalPlatform(fromPackageDir) {
|
|
59
|
+
try {
|
|
60
|
+
return Nodemodule.createRequire(Nodepath.join(fromPackageDir, "package.json")).resolve(localPlatformSpecifier);
|
|
61
|
+
} catch (exn) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
async function loadGraph(platformModulePath, plugins) {
|
|
67
|
+
let platMod = await dynamicImport(bustedUrl(platformModulePath));
|
|
68
|
+
let plat = platMod.Make();
|
|
69
|
+
let structures = [];
|
|
70
|
+
for (let i = 0, i_finish = plugins.length; i < i_finish; ++i) {
|
|
71
|
+
let plugin = plugins[i];
|
|
72
|
+
let pluginMod = await dynamicImport(bustedUrl(plugin.modulePath));
|
|
73
|
+
let built = pluginMod.Make(plat);
|
|
74
|
+
structures.push([
|
|
75
|
+
plugin.name,
|
|
76
|
+
built.pluginStructure
|
|
77
|
+
]);
|
|
78
|
+
}
|
|
79
|
+
return {
|
|
80
|
+
structures: structures
|
|
81
|
+
};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let packageNameToPluginName = PluginName$Reventless.fromPackageName;
|
|
85
|
+
|
|
86
|
+
export {
|
|
87
|
+
dynamicImport,
|
|
88
|
+
counter,
|
|
89
|
+
bustedUrl,
|
|
90
|
+
packageNameToPluginName,
|
|
91
|
+
strField,
|
|
92
|
+
readJson,
|
|
93
|
+
derivePluginName,
|
|
94
|
+
discover,
|
|
95
|
+
localPlatformSpecifier,
|
|
96
|
+
resolveLocalPlatform,
|
|
97
|
+
loadGraph,
|
|
98
|
+
}
|
|
99
|
+
/* node:fs Not a pure module */
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
open ReventlessCore
|
|
2
|
+
|
|
3
|
+
// Stage 5 — cross-pattern mapping GWT.
|
|
4
|
+
//
|
|
5
|
+
// Generalises `EventMapping_GWT` so both the source and target of a mapping
|
|
6
|
+
// can be an Aggregate (Behavior) or a StateChangeSlice. Covers all four
|
|
7
|
+
// producer/consumer combinations: Aggr→Aggr, Aggr→DCB, DCB→Aggr, DCB→DCB.
|
|
8
|
+
// See `docs/analysis/event-source-connection-matrix.md` for the full matrix
|
|
9
|
+
// and `docs/plans/reventless-gwt.md` Stage 5 for the migration plan.
|
|
10
|
+
//
|
|
11
|
+
// The unified `GwtSource`/`GwtTarget` module type is intentionally wider than
|
|
12
|
+
// either `Aggregate.Spec` or the `StateChangeSlice.SliceSpec`: it names every
|
|
13
|
+
// field both kinds need (`name`, `Id`, `state`, `command`, `event`, `error`,
|
|
14
|
+
// `decide`, `evolve`) and adds `consumedEvent` — the type `evolve` folds over.
|
|
15
|
+
// Aggregates satisfy this with `consumedEvent = event`; DCB slices supply a
|
|
16
|
+
// distinct cross-entity event type.
|
|
17
|
+
|
|
18
|
+
// -- Unified Source / Target / Mapping module types ---------------------------
|
|
19
|
+
|
|
20
|
+
module type GwtSource = {
|
|
21
|
+
let name: string
|
|
22
|
+
module Id: Reventless.Id.T
|
|
23
|
+
|
|
24
|
+
type state
|
|
25
|
+
let initialState: state
|
|
26
|
+
|
|
27
|
+
@schema
|
|
28
|
+
type consumedEvent
|
|
29
|
+
let evolve: (state, consumedEvent) => state
|
|
30
|
+
|
|
31
|
+
@schema
|
|
32
|
+
type command
|
|
33
|
+
|
|
34
|
+
@schema
|
|
35
|
+
type event
|
|
36
|
+
|
|
37
|
+
@schema
|
|
38
|
+
type error
|
|
39
|
+
|
|
40
|
+
let decide: (state, command) => result<array<event>, error>
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module type GwtTarget = GwtSource
|
|
44
|
+
|
|
45
|
+
module type Mapping = {
|
|
46
|
+
module Source: GwtSource
|
|
47
|
+
module Target: GwtTarget
|
|
48
|
+
|
|
49
|
+
let map: (
|
|
50
|
+
Source.Id.t,
|
|
51
|
+
Source.event,
|
|
52
|
+
Reventless.QueryEngine.operations,
|
|
53
|
+
) => array<Reventless.EventMapping.action<Target.Id.t, Target.command>>
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// -- Adapter functors --------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
// Adapter: any `Aggregate.Spec` + matching `Behavior.T` becomes a `GwtSource`.
|
|
59
|
+
// Aggregates evolve their own events, so `consumedEvent = event`.
|
|
60
|
+
module FromBehavior = (
|
|
61
|
+
Spec: Reventless.Aggregate.Spec,
|
|
62
|
+
Behavior: Behavior.T with module Spec = Spec,
|
|
63
|
+
) => {
|
|
64
|
+
let name = Spec.name
|
|
65
|
+
module Id = Spec.Id
|
|
66
|
+
type state = Behavior.state
|
|
67
|
+
let initialState = Behavior.initialState
|
|
68
|
+
|
|
69
|
+
type consumedEvent = Spec.event
|
|
70
|
+
let consumedEventSchema = Spec.eventSchema
|
|
71
|
+
let evolve = Behavior.evolve
|
|
72
|
+
|
|
73
|
+
type command = Spec.command
|
|
74
|
+
let commandSchema = Spec.commandSchema
|
|
75
|
+
|
|
76
|
+
type event = Spec.event
|
|
77
|
+
let eventSchema = Spec.eventSchema
|
|
78
|
+
|
|
79
|
+
type error = Spec.error
|
|
80
|
+
let errorSchema = Spec.errorSchema
|
|
81
|
+
|
|
82
|
+
let decide = Behavior.decide
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Adapter: a StateChangeSlice spec becomes a `GwtSource`/`GwtTarget`. DCB
|
|
86
|
+
// entity identifiers are tag values — plain strings — so the adapter uses
|
|
87
|
+
// `Id.StringPure`.
|
|
88
|
+
module type FromSliceSpec = {
|
|
89
|
+
let name: string
|
|
90
|
+
|
|
91
|
+
type state
|
|
92
|
+
let initialState: state
|
|
93
|
+
|
|
94
|
+
@schema
|
|
95
|
+
type consumedEvent
|
|
96
|
+
let evolve: (state, consumedEvent) => state
|
|
97
|
+
|
|
98
|
+
@schema
|
|
99
|
+
type command
|
|
100
|
+
|
|
101
|
+
@schema
|
|
102
|
+
type error
|
|
103
|
+
|
|
104
|
+
@schema
|
|
105
|
+
type event
|
|
106
|
+
|
|
107
|
+
let decide: (state, command) => result<array<event>, error>
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
module FromStateChangeSlice = (Spec: FromSliceSpec) => {
|
|
111
|
+
let name = Spec.name
|
|
112
|
+
module Id = Reventless.Id.StringPure
|
|
113
|
+
type state = Spec.state
|
|
114
|
+
let initialState = Spec.initialState
|
|
115
|
+
|
|
116
|
+
type consumedEvent = Spec.consumedEvent
|
|
117
|
+
let consumedEventSchema = Spec.consumedEventSchema
|
|
118
|
+
let evolve = Spec.evolve
|
|
119
|
+
|
|
120
|
+
type command = Spec.command
|
|
121
|
+
let commandSchema = Spec.commandSchema
|
|
122
|
+
|
|
123
|
+
type event = Spec.event
|
|
124
|
+
let eventSchema = Spec.eventSchema
|
|
125
|
+
|
|
126
|
+
type error = Spec.error
|
|
127
|
+
let errorSchema = Spec.errorSchema
|
|
128
|
+
|
|
129
|
+
let decide = Spec.decide
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// -- Mapping_GWT.T + Make ----------------------------------------------------
|
|
133
|
+
|
|
134
|
+
module type T = {
|
|
135
|
+
module Source: GwtSource
|
|
136
|
+
module Target: GwtTarget
|
|
137
|
+
|
|
138
|
+
// A scenario carries the source history plus the target per-id history
|
|
139
|
+
// through the pipe chain. Pipe-first (`->`) places it as the first arg of
|
|
140
|
+
// every subsequent combinator, so the chain reads top-to-bottom.
|
|
141
|
+
type scenario = (
|
|
142
|
+
array<Source.consumedEvent>,
|
|
143
|
+
array<(string, array<Target.consumedEvent>)>,
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
let describe: (string, unit => unit) => unit
|
|
147
|
+
let test: (string, ~timeout: int=?, unit => promise<Outcome.outcome>) => unit
|
|
148
|
+
|
|
149
|
+
let givenSourceEvents: array<Source.consumedEvent> => scenario
|
|
150
|
+
let andTargetEvents: (scenario, array<(string, array<Target.consumedEvent>)>) => scenario
|
|
151
|
+
|
|
152
|
+
let whenSourceCmd: (scenario, string, Source.command) => promise<dict<array<Target.event>>>
|
|
153
|
+
|
|
154
|
+
let thenTargetEvents: (
|
|
155
|
+
promise<dict<array<Target.event>>>,
|
|
156
|
+
array<(string, array<Target.event>)>,
|
|
157
|
+
) => promise<Outcome.outcome>
|
|
158
|
+
|
|
159
|
+
let thenTargetEvent: (
|
|
160
|
+
promise<dict<array<Target.event>>>,
|
|
161
|
+
string,
|
|
162
|
+
Target.event,
|
|
163
|
+
) => promise<Outcome.outcome>
|
|
164
|
+
|
|
165
|
+
let thenNoTargetEvent: promise<dict<array<Target.event>>> => promise<Outcome.outcome>
|
|
166
|
+
|
|
167
|
+
let thenSourceError: (
|
|
168
|
+
promise<dict<array<Target.event>>>,
|
|
169
|
+
Source.error,
|
|
170
|
+
) => promise<Outcome.outcome>
|
|
171
|
+
|
|
172
|
+
let thenTargetError: (
|
|
173
|
+
promise<dict<array<Target.event>>>,
|
|
174
|
+
Target.error,
|
|
175
|
+
) => promise<Outcome.outcome>
|
|
176
|
+
|
|
177
|
+
let thenTargetEventsWithError: (
|
|
178
|
+
promise<dict<array<Target.event>>>,
|
|
179
|
+
array<(string, array<Target.event>)>,
|
|
180
|
+
Target.error,
|
|
181
|
+
) => promise<Outcome.outcome>
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
module Make = (M: Mapping): (T with module Source = M.Source and module Target = M.Target) => {
|
|
185
|
+
module Source = M.Source
|
|
186
|
+
module Target = M.Target
|
|
187
|
+
|
|
188
|
+
type scenario = (
|
|
189
|
+
array<Source.consumedEvent>,
|
|
190
|
+
array<(string, array<Target.consumedEvent>)>,
|
|
191
|
+
)
|
|
192
|
+
|
|
193
|
+
S.enableJson()
|
|
194
|
+
|
|
195
|
+
let describe = JestBind.describe
|
|
196
|
+
let sliceName = `${Source.name}→${Target.name}`
|
|
197
|
+
let test = (name, ~timeout=?, body) =>
|
|
198
|
+
JestBind.testPromise(~slice=sliceName, name, ~timeout?, body)
|
|
199
|
+
|
|
200
|
+
// QueryEngine stub — `EventMapping.map` can accept a QueryEngine for async
|
|
201
|
+
// lookups. The GWT runs pure; we hand it an empty stub so anything that
|
|
202
|
+
// actually needs a database resolves to empty results.
|
|
203
|
+
let queryEngine: Reventless.QueryEngine.operations = {
|
|
204
|
+
scan: (~readModelName as _, ~filterConfigs as _, ~limit as _) => []->Promise.resolve,
|
|
205
|
+
query: (
|
|
206
|
+
~readModelName as _: string,
|
|
207
|
+
~key as _: option<string>=?,
|
|
208
|
+
~id as _: Reventless.QueryEngine.value,
|
|
209
|
+
~subIdConfig as _: option<Reventless.QueryEngine.SubId.config>=?,
|
|
210
|
+
~filterConfigs as _: option<array<Reventless.QueryEngine.Filter.config>>=?,
|
|
211
|
+
~ascending as _: option<bool>=?,
|
|
212
|
+
~limit as _: option<int>=?,
|
|
213
|
+
) => []->Promise.resolve,
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
let sourceErrors = ref([])
|
|
217
|
+
let targetErrors = ref([])
|
|
218
|
+
|
|
219
|
+
let execSource = (history: array<Source.consumedEvent>, cmd: Source.command): array<
|
|
220
|
+
Source.event,
|
|
221
|
+
> => {
|
|
222
|
+
sourceErrors := []
|
|
223
|
+
let state = history->Array.reduce(Source.initialState, Source.evolve)
|
|
224
|
+
switch Source.decide(state, cmd) {
|
|
225
|
+
| Ok(events) => events
|
|
226
|
+
| Error(e) =>
|
|
227
|
+
sourceErrors := [e]
|
|
228
|
+
[]
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
let execTarget = (history: array<Target.consumedEvent>, cmd: Target.command): array<
|
|
233
|
+
Target.event,
|
|
234
|
+
> => {
|
|
235
|
+
let state = history->Array.reduce(Target.initialState, Target.evolve)
|
|
236
|
+
switch Target.decide(state, cmd) {
|
|
237
|
+
| Ok(events) => events
|
|
238
|
+
| Error(e) =>
|
|
239
|
+
targetErrors := [e]
|
|
240
|
+
[]
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let givenSourceEvents = (sourceHistory): scenario => (sourceHistory, [])
|
|
245
|
+
let andTargetEvents = ((sourceHistory, _), targetHistory): scenario => (
|
|
246
|
+
sourceHistory,
|
|
247
|
+
targetHistory,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
// Execute the mapping: source command → source events → mapping actions →
|
|
251
|
+
// target commands → target decide → target events grouped by target id.
|
|
252
|
+
let whenSourceCmd = async ((sourceHistory, targetHistory): scenario, sourceId, cmd) => {
|
|
253
|
+
targetErrors := []
|
|
254
|
+
let sourceEvents = execSource(sourceHistory, cmd)
|
|
255
|
+
let targetActions =
|
|
256
|
+
sourceEvents
|
|
257
|
+
->Array.map(sourceEvent =>
|
|
258
|
+
M.map(sourceId->Source.Id.makeFromString, sourceEvent, queryEngine)
|
|
259
|
+
)
|
|
260
|
+
->Array.flat
|
|
261
|
+
let targetHistories = targetHistory->Dict.fromArray
|
|
262
|
+
let commands = (
|
|
263
|
+
await targetActions
|
|
264
|
+
->Array.map(async action =>
|
|
265
|
+
switch action {
|
|
266
|
+
| Publish(id, command) => [(id, command)]
|
|
267
|
+
| PublishDelayed(id, command, _) => [(id, command)]
|
|
268
|
+
| PublishAsync(p) => await p
|
|
269
|
+
| _ => []
|
|
270
|
+
}
|
|
271
|
+
)
|
|
272
|
+
->Promise.all
|
|
273
|
+
)->Array.flat
|
|
274
|
+
commands->Array.reduce(Dict.make(), (targetEvents, (id, command)) => {
|
|
275
|
+
let idStr = id->Target.Id.toString
|
|
276
|
+
let hist = targetHistories->Dict.get(idStr)->Option.getOr([])
|
|
277
|
+
let newEvents = execTarget(hist, command)
|
|
278
|
+
targetEvents->Dict.set(
|
|
279
|
+
idStr,
|
|
280
|
+
targetEvents->Dict.get(idStr)->Option.getOr([])->Array.concat(newEvents),
|
|
281
|
+
)
|
|
282
|
+
targetEvents
|
|
283
|
+
})
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// -- Encoders ------------------------------------------------------------
|
|
287
|
+
|
|
288
|
+
let encTargetEvent = (e: Target.event) => e->Message.encode(Target.eventSchema)
|
|
289
|
+
let encTargetEvents = evs => evs->Array.map(encTargetEvent)
|
|
290
|
+
let encSourceError = (err: Source.error) => err->Message.encode(Source.errorSchema)
|
|
291
|
+
let encTargetError = (err: Target.error) => err->Message.encode(Target.errorSchema)
|
|
292
|
+
|
|
293
|
+
let encDict = (d: dict<array<Target.event>>): array<JSON.t> =>
|
|
294
|
+
d
|
|
295
|
+
->Dict.toArray
|
|
296
|
+
->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
|
|
297
|
+
->Array.map(((id, events)) => {
|
|
298
|
+
let obj = Dict.make()
|
|
299
|
+
obj->Dict.set("id", JSON.Encode.string(id))
|
|
300
|
+
obj->Dict.set("events", JSON.Encode.array(events->encTargetEvents))
|
|
301
|
+
JSON.Encode.object(obj)
|
|
302
|
+
})
|
|
303
|
+
|
|
304
|
+
let encExpected = (pairs: array<(string, array<Target.event>)>): array<JSON.t> =>
|
|
305
|
+
pairs
|
|
306
|
+
->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
|
|
307
|
+
->Array.map(((id, events)) => {
|
|
308
|
+
let obj = Dict.make()
|
|
309
|
+
obj->Dict.set("id", JSON.Encode.string(id))
|
|
310
|
+
obj->Dict.set("events", JSON.Encode.array(events->encTargetEvents))
|
|
311
|
+
JSON.Encode.object(obj)
|
|
312
|
+
})
|
|
313
|
+
|
|
314
|
+
// Surface a source/target error not expected by the current assertion.
|
|
315
|
+
let unexpectedSideError = (actualDict: dict<array<Target.event>>): Outcome.outcome => {
|
|
316
|
+
let actualEvents = encDict(actualDict)
|
|
317
|
+
switch sourceErrors.contents->Array.get(0) {
|
|
318
|
+
| Some(srcErr) =>
|
|
319
|
+
Outcome.fail(
|
|
320
|
+
ErrorMismatch({
|
|
321
|
+
expected: JSON.Encode.null,
|
|
322
|
+
actual: Some(encSourceError(srcErr)),
|
|
323
|
+
actualEvents,
|
|
324
|
+
}),
|
|
325
|
+
)
|
|
326
|
+
| None =>
|
|
327
|
+
switch targetErrors.contents->Array.get(0) {
|
|
328
|
+
| Some(tgtErr) =>
|
|
329
|
+
Outcome.fail(
|
|
330
|
+
ErrorMismatch({
|
|
331
|
+
expected: JSON.Encode.null,
|
|
332
|
+
actual: Some(encTargetError(tgtErr)),
|
|
333
|
+
actualEvents,
|
|
334
|
+
}),
|
|
335
|
+
)
|
|
336
|
+
| None => Outcome.pass
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
let dictPairsEq = (a: dict<array<Target.event>>, b: dict<array<Target.event>>) => {
|
|
342
|
+
let ka = a->Dict.keysToArray->Array.toSorted(String.compare)
|
|
343
|
+
let kb = b->Dict.keysToArray->Array.toSorted(String.compare)
|
|
344
|
+
ka == kb &&
|
|
345
|
+
ka->Array.every(k => {
|
|
346
|
+
let av = a->Dict.get(k)->Option.getOr([])
|
|
347
|
+
let bv = b->Dict.get(k)->Option.getOr([])
|
|
348
|
+
av == bv
|
|
349
|
+
})
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// -- Then combinators ----------------------------------------------------
|
|
353
|
+
|
|
354
|
+
let thenTargetEvents = async (targetEvents, expectedTargetEvents) => {
|
|
355
|
+
let actualDict = await targetEvents
|
|
356
|
+
switch unexpectedSideError(actualDict) {
|
|
357
|
+
| Error(_) as out => out
|
|
358
|
+
| Ok() =>
|
|
359
|
+
let expectedDict = expectedTargetEvents->Dict.fromArray
|
|
360
|
+
if dictPairsEq(actualDict, expectedDict) {
|
|
361
|
+
Outcome.pass
|
|
362
|
+
} else {
|
|
363
|
+
Outcome.fail(
|
|
364
|
+
EventsMismatch({
|
|
365
|
+
expected: encExpected(expectedTargetEvents),
|
|
366
|
+
actual: encDict(actualDict),
|
|
367
|
+
}),
|
|
368
|
+
)
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
let thenTargetEvent = async (targetEvents, id, expectedTargetEvent) => {
|
|
374
|
+
let actualDict = await targetEvents
|
|
375
|
+
switch unexpectedSideError(actualDict) {
|
|
376
|
+
| Error(_) as out => out
|
|
377
|
+
| Ok() =>
|
|
378
|
+
let pairs = actualDict->Dict.toArray
|
|
379
|
+
let ok =
|
|
380
|
+
pairs->Array.length == 1 &&
|
|
381
|
+
switch pairs->Array.get(0) {
|
|
382
|
+
| Some((actualId, [actualEvent])) =>
|
|
383
|
+
actualId == id && actualEvent == expectedTargetEvent
|
|
384
|
+
| _ => false
|
|
385
|
+
}
|
|
386
|
+
if ok {
|
|
387
|
+
Outcome.pass
|
|
388
|
+
} else {
|
|
389
|
+
Outcome.fail(
|
|
390
|
+
EventsMismatch({
|
|
391
|
+
expected: encExpected([(id, [expectedTargetEvent])]),
|
|
392
|
+
actual: encDict(actualDict),
|
|
393
|
+
}),
|
|
394
|
+
)
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
let thenNoTargetEvent = targetEvents => thenTargetEvents(targetEvents, [])
|
|
400
|
+
|
|
401
|
+
let thenSourceError = async (targetEvents, expectedError) => {
|
|
402
|
+
let actualDict = await targetEvents
|
|
403
|
+
let actualEvents = encDict(actualDict)
|
|
404
|
+
let expectedJson = encSourceError(expectedError)
|
|
405
|
+
switch sourceErrors.contents->Array.get(0) {
|
|
406
|
+
| None =>
|
|
407
|
+
Outcome.fail(
|
|
408
|
+
ErrorMismatch({expected: expectedJson, actual: None, actualEvents}),
|
|
409
|
+
)
|
|
410
|
+
| Some(actual) if actual != expectedError =>
|
|
411
|
+
Outcome.fail(
|
|
412
|
+
ErrorMismatch({
|
|
413
|
+
expected: expectedJson,
|
|
414
|
+
actual: Some(encSourceError(actual)),
|
|
415
|
+
actualEvents,
|
|
416
|
+
}),
|
|
417
|
+
)
|
|
418
|
+
| Some(_) => Outcome.pass
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
let thenTargetError = async (targetEvents, expectedError) => {
|
|
423
|
+
let actualDict = await targetEvents
|
|
424
|
+
let actualEvents = encDict(actualDict)
|
|
425
|
+
let expectedJson = encTargetError(expectedError)
|
|
426
|
+
switch targetErrors.contents->Array.get(0) {
|
|
427
|
+
| None =>
|
|
428
|
+
Outcome.fail(
|
|
429
|
+
ErrorMismatch({expected: expectedJson, actual: None, actualEvents}),
|
|
430
|
+
)
|
|
431
|
+
| Some(actual) if actual != expectedError =>
|
|
432
|
+
Outcome.fail(
|
|
433
|
+
ErrorMismatch({
|
|
434
|
+
expected: expectedJson,
|
|
435
|
+
actual: Some(encTargetError(actual)),
|
|
436
|
+
actualEvents,
|
|
437
|
+
}),
|
|
438
|
+
)
|
|
439
|
+
| Some(_) => Outcome.pass
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
let thenTargetEventsWithError = async (targetEvents, expectedTargetEvents, expectedError) => {
|
|
444
|
+
let actualDict = await targetEvents
|
|
445
|
+
let actualEvents = encDict(actualDict)
|
|
446
|
+
let expectedErrorJson = encTargetError(expectedError)
|
|
447
|
+
switch targetErrors.contents->Array.get(0) {
|
|
448
|
+
| None =>
|
|
449
|
+
Outcome.fail(
|
|
450
|
+
ErrorMismatch({expected: expectedErrorJson, actual: None, actualEvents}),
|
|
451
|
+
)
|
|
452
|
+
| Some(actual) if actual != expectedError =>
|
|
453
|
+
Outcome.fail(
|
|
454
|
+
ErrorMismatch({
|
|
455
|
+
expected: expectedErrorJson,
|
|
456
|
+
actual: Some(encTargetError(actual)),
|
|
457
|
+
actualEvents,
|
|
458
|
+
}),
|
|
459
|
+
)
|
|
460
|
+
| Some(_) =>
|
|
461
|
+
let expectedDict = expectedTargetEvents->Dict.fromArray
|
|
462
|
+
if dictPairsEq(actualDict, expectedDict) {
|
|
463
|
+
Outcome.pass
|
|
464
|
+
} else {
|
|
465
|
+
Outcome.fail(
|
|
466
|
+
EventsMismatch({
|
|
467
|
+
expected: encExpected(expectedTargetEvents),
|
|
468
|
+
actual: actualEvents,
|
|
469
|
+
}),
|
|
470
|
+
)
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
}
|