@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.
Files changed (184) hide show
  1. package/CHANGELOG.md +916 -0
  2. package/LICENSE +202 -0
  3. package/bin/reventless-dev.mjs +21 -0
  4. package/package.json +54 -0
  5. package/rescript.json +36 -0
  6. package/src/.gwtignore +10 -0
  7. package/src/Automation_GWT.res +207 -0
  8. package/src/Automation_GWT.res.mjs +160 -0
  9. package/src/Behavior_GWT.res +569 -0
  10. package/src/Behavior_GWT.res.mjs +523 -0
  11. package/src/Bind.res +29 -0
  12. package/src/Bind.res.mjs +22 -0
  13. package/src/BuildClassifier.res +147 -0
  14. package/src/BuildClassifier.res.mjs +136 -0
  15. package/src/Cancellation.res +37 -0
  16. package/src/Cancellation.res.mjs +46 -0
  17. package/src/ChildProcess.res +102 -0
  18. package/src/ChildProcess.res.mjs +107 -0
  19. package/src/Cli.res +905 -0
  20. package/src/Cli.res.mjs +952 -0
  21. package/src/Collector.res +212 -0
  22. package/src/Collector.res.mjs +218 -0
  23. package/src/ComponentMeta.res +77 -0
  24. package/src/ComponentMeta.res.mjs +91 -0
  25. package/src/ComponentScan.res +103 -0
  26. package/src/ComponentScan.res.mjs +105 -0
  27. package/src/Delegate_GWT.res +303 -0
  28. package/src/Delegate_GWT.res.mjs +372 -0
  29. package/src/Diff.res +91 -0
  30. package/src/Diff.res.mjs +98 -0
  31. package/src/Discovery.res +105 -0
  32. package/src/Discovery.res.mjs +99 -0
  33. package/src/DomainDeadCode.res +98 -0
  34. package/src/DomainDeadCode.res.mjs +72 -0
  35. package/src/DomainGraph.res +0 -0
  36. package/src/DomainGraph.res.mjs +0 -0
  37. package/src/EventMapping_GWT.res +29 -0
  38. package/src/EventMapping_GWT.res.mjs +44 -0
  39. package/src/Filter.res +45 -0
  40. package/src/Filter.res.mjs +47 -0
  41. package/src/Flow_GWT.res +664 -0
  42. package/src/Flow_GWT.res.mjs +604 -0
  43. package/src/FormatterHuman.res +109 -0
  44. package/src/FormatterHuman.res.mjs +115 -0
  45. package/src/FormatterJson.res +264 -0
  46. package/src/FormatterJson.res.mjs +322 -0
  47. package/src/FormatterJunit.res +93 -0
  48. package/src/FormatterJunit.res.mjs +90 -0
  49. package/src/FormatterTap.res +100 -0
  50. package/src/FormatterTap.res.mjs +131 -0
  51. package/src/FormatterVsCode.res +352 -0
  52. package/src/FormatterVsCode.res.mjs +546 -0
  53. package/src/Hint.res +119 -0
  54. package/src/Hint.res.mjs +124 -0
  55. package/src/InboundTranslation_GWT.res +83 -0
  56. package/src/InboundTranslation_GWT.res.mjs +94 -0
  57. package/src/JestBind.res +70 -0
  58. package/src/JestBind.res.mjs +63 -0
  59. package/src/Loader.res +24 -0
  60. package/src/Loader.res.mjs +23 -0
  61. package/src/LocalHost.res +119 -0
  62. package/src/LocalHost.res.mjs +99 -0
  63. package/src/Mapping_GWT.res +474 -0
  64. package/src/Mapping_GWT.res.mjs +322 -0
  65. package/src/MismatchRender.res +72 -0
  66. package/src/MismatchRender.res.mjs +127 -0
  67. package/src/MultiSourceProjection_GWT.res +316 -0
  68. package/src/MultiSourceProjection_GWT.res.mjs +354 -0
  69. package/src/OutboundTranslation_GWT.res +238 -0
  70. package/src/OutboundTranslation_GWT.res.mjs +186 -0
  71. package/src/Outcome.res +97 -0
  72. package/src/Outcome.res.mjs +93 -0
  73. package/src/PackageScan.res +80 -0
  74. package/src/PackageScan.res.mjs +81 -0
  75. package/src/PlatformRunner.res +181 -0
  76. package/src/PlatformRunner.res.mjs +176 -0
  77. package/src/PlatformScan.res +142 -0
  78. package/src/PlatformScan.res.mjs +132 -0
  79. package/src/ProcessManager.res +135 -0
  80. package/src/ProcessManager.res.mjs +119 -0
  81. package/src/Projection_GWT.res +312 -0
  82. package/src/Projection_GWT.res.mjs +332 -0
  83. package/src/Query_GWT.res +379 -0
  84. package/src/Query_GWT.res.mjs +367 -0
  85. package/src/RenderRescript.res +125 -0
  86. package/src/RenderRescript.res.mjs +141 -0
  87. package/src/RunWorker.res +24 -0
  88. package/src/RunWorker.res.mjs +16 -0
  89. package/src/RunnerTypes.res +68 -0
  90. package/src/RunnerTypes.res.mjs +60 -0
  91. package/src/SideEffect_GWT.res +157 -0
  92. package/src/SideEffect_GWT.res.mjs +113 -0
  93. package/src/StateChange/ExternalAddCategorySlice.res +25 -0
  94. package/src/StateChange/ExternalAddCategorySlice.res.mjs +34 -0
  95. package/src/StateChange/ExternalAddCategorySlice_Behavior.res +19 -0
  96. package/src/StateChange/ExternalAddCategorySlice_Behavior.res.mjs +46 -0
  97. package/src/StateChange/WithFixtures.res +22 -0
  98. package/src/StateChange/WithFixtures.res.mjs +31 -0
  99. package/src/StateChange/WithFixtures_Behavior.res +17 -0
  100. package/src/StateChange/WithFixtures_Behavior.res.mjs +37 -0
  101. package/src/StateChange/WithManualOpen.res +23 -0
  102. package/src/StateChange/WithManualOpen.res.mjs +31 -0
  103. package/src/StateChange/WithManualOpen_Behavior.res +17 -0
  104. package/src/StateChange/WithManualOpen_Behavior.res.mjs +37 -0
  105. package/src/StubRuntime.res +54 -0
  106. package/src/StubRuntime.res.mjs +55 -0
  107. package/src/TestFixtures.res +16 -0
  108. package/src/TestFixtures.res.mjs +34 -0
  109. package/src/Watch.res +108 -0
  110. package/src/Watch.res.mjs +85 -0
  111. package/src/WatcherProbe.res +56 -0
  112. package/src/WatcherProbe.res.mjs +59 -0
  113. package/src/Worker.res +24 -0
  114. package/src/Worker.res.mjs +9 -0
  115. package/tests/AutomationGwtTest.res +71 -0
  116. package/tests/AutomationGwtTest.res.mjs +189 -0
  117. package/tests/BuildClassifierTest.res +133 -0
  118. package/tests/BuildClassifierTest.res.mjs +176 -0
  119. package/tests/CliRunEntryTest.res +57 -0
  120. package/tests/CliRunEntryTest.res.mjs +65 -0
  121. package/tests/CliWatchScopeTest.res +62 -0
  122. package/tests/CliWatchScopeTest.res.mjs +95 -0
  123. package/tests/ComponentMetaTest.res +67 -0
  124. package/tests/ComponentMetaTest.res.mjs +71 -0
  125. package/tests/DelegateGwtTest.res +487 -0
  126. package/tests/DelegateGwtTest.res.mjs +891 -0
  127. package/tests/DiscoveryTest.res +42 -0
  128. package/tests/DiscoveryTest.res.mjs +43 -0
  129. package/tests/DomainDeadCodeTest.res +119 -0
  130. package/tests/DomainDeadCodeTest.res.mjs +158 -0
  131. package/tests/DomainGraphTest.res +448 -0
  132. package/tests/DomainGraphTest.res.mjs +505 -0
  133. package/tests/FlowAggregateGwtTest.res +171 -0
  134. package/tests/FlowAggregateGwtTest.res.mjs +273 -0
  135. package/tests/FlowCrossPluginGwtTest.res +340 -0
  136. package/tests/FlowCrossPluginGwtTest.res.mjs +798 -0
  137. package/tests/FlowGwtTest.res +222 -0
  138. package/tests/FlowGwtTest.res.mjs +433 -0
  139. package/tests/FormatterGoldenTest.res +125 -0
  140. package/tests/FormatterGoldenTest.res.mjs +162 -0
  141. package/tests/FormatterVsCodeMessageTest.res +41 -0
  142. package/tests/FormatterVsCodeMessageTest.res.mjs +42 -0
  143. package/tests/InboundTranslationGwtTest.res +34 -0
  144. package/tests/InboundTranslationGwtTest.res.mjs +99 -0
  145. package/tests/LocalHostIntegration.res +70 -0
  146. package/tests/LocalHostIntegration.res.mjs +64 -0
  147. package/tests/LocalHostTest.res +71 -0
  148. package/tests/LocalHostTest.res.mjs +63 -0
  149. package/tests/MappingGwtTest.res +350 -0
  150. package/tests/MappingGwtTest.res.mjs +596 -0
  151. package/tests/OutboundTranslationGwtTest.res +59 -0
  152. package/tests/OutboundTranslationGwtTest.res.mjs +139 -0
  153. package/tests/PlatformRunnerTest.res +66 -0
  154. package/tests/PlatformRunnerTest.res.mjs +85 -0
  155. package/tests/PlatformScanTest.res +61 -0
  156. package/tests/PlatformScanTest.res.mjs +56 -0
  157. package/tests/QueryGwtTest.res +161 -0
  158. package/tests/QueryGwtTest.res.mjs +314 -0
  159. package/tests/QueryResolverGwtTest.res +71 -0
  160. package/tests/QueryResolverGwtTest.res.mjs +136 -0
  161. package/tests/RunnerUnitTest.res +219 -0
  162. package/tests/RunnerUnitTest.res.mjs +244 -0
  163. package/tests/SideEffect/FakeOrderNotification.res +23 -0
  164. package/tests/SideEffect/FakeOrderNotification.res.mjs +36 -0
  165. package/tests/SideEffect/FakeOrderNotificationGwtTest.res +35 -0
  166. package/tests/SideEffect/FakeOrderNotificationGwtTest.res.mjs +73 -0
  167. package/tests/SideEffect/MockEmail.res +18 -0
  168. package/tests/SideEffect/MockEmail.res.mjs +47 -0
  169. package/tests/StateChange/ExternalAddCategorySlice_GWT.res +27 -0
  170. package/tests/StateChange/ExternalAddCategorySlice_GWT.res.mjs +70 -0
  171. package/tests/StateChange/WithFixtures_Fixtures.res +9 -0
  172. package/tests/StateChange/WithFixtures_Fixtures.res.mjs +20 -0
  173. package/tests/StateChange/WithFixtures_GWT.res +23 -0
  174. package/tests/StateChange/WithFixtures_GWT.res.mjs +56 -0
  175. package/tests/StateChange/WithManualOpen_Fixtures.res +7 -0
  176. package/tests/StateChange/WithManualOpen_Fixtures.res.mjs +20 -0
  177. package/tests/StateChange/WithManualOpen_GWT.res +17 -0
  178. package/tests/StateChange/WithManualOpen_GWT.res.mjs +56 -0
  179. package/tests/StateChangeSliceGwtTest.res +160 -0
  180. package/tests/StateChangeSliceGwtTest.res.mjs +280 -0
  181. package/tests/StateViewSliceGwtTest.res +64 -0
  182. package/tests/StateViewSliceGwtTest.res.mjs +161 -0
  183. package/tests/WatchDebounceTest.res +106 -0
  184. 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
+ }