@reventlessdev/reventless-spec 3.0.0-alpha.61
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 +625 -0
- package/LICENSE +202 -0
- package/README.md +114 -0
- package/package.json +34 -0
- package/rescript.json +25 -0
- package/run-generator.mjs +2 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +168 -0
- package/src/LogPrefix.res.mjs +148 -0
- package/src/PackageVersion.res +73 -0
- package/src/PackageVersion.res.mjs +81 -0
- package/src/components/Aggregate.res +64 -0
- package/src/components/Aggregate.res.mjs +2 -0
- package/src/components/AutomationSlice.res +279 -0
- package/src/components/AutomationSlice.res.mjs +30 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +128 -0
- package/src/components/DcbDecode.res.mjs +117 -0
- package/src/components/DcbTag.res +1256 -0
- package/src/components/DcbTag.res.mjs +869 -0
- package/src/components/DcbValidation.res +362 -0
- package/src/components/DcbValidation.res.mjs +360 -0
- package/src/components/DisplayName.res +40 -0
- package/src/components/DisplayName.res.mjs +26 -0
- package/src/components/ExtensionPoint.res +27 -0
- package/src/components/ExtensionPoint.res.mjs +2 -0
- package/src/components/InboundTranslationSlice.res +79 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +108 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +373 -0
- package/src/components/Plugin.res.mjs +249 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +51 -0
- package/src/components/Reference.res.mjs +37 -0
- package/src/components/StateAnnotations.res +65 -0
- package/src/components/StateAnnotations.res.mjs +15 -0
- package/src/components/StateChangeSlice.res +131 -0
- package/src/components/StateChangeSlice.res.mjs +2 -0
- package/src/components/StateViewSlice.res +99 -0
- package/src/components/StateViewSlice.res.mjs +2 -0
- package/src/components/Task.res +62 -0
- package/src/components/Task.res.mjs +2 -0
- package/src/generator/Codegen.res +703 -0
- package/src/generator/Codegen.res.mjs +485 -0
- package/src/generator/Config.res +85 -0
- package/src/generator/Config.res.mjs +78 -0
- package/src/generator/Discovery.res +204 -0
- package/src/generator/Discovery.res.mjs +213 -0
- package/src/generator/Generator_Node.res +49 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +386 -0
- package/src/generator/Pairing.res.mjs +353 -0
- package/src/generator/PluginGenerator.res +53 -0
- package/src/generator/PluginGenerator.res.mjs +70 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +78 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/EventMapping.res +100 -0
- package/src/types/EventMapping.res.mjs +15 -0
- package/src/types/Handler.res +30 -0
- package/src/types/Handler.res.mjs +2 -0
- package/src/types/Id.res +75 -0
- package/src/types/Id.res.mjs +37 -0
- package/src/types/Identity.res +46 -0
- package/src/types/Identity.res.mjs +51 -0
- package/src/types/Message.res +197 -0
- package/src/types/Message.res.mjs +108 -0
- package/src/types/Projection.res +220 -0
- package/src/types/Projection.res.mjs +44 -0
- package/src/types/QueryEngine.res +123 -0
- package/src/types/QueryEngine.res.mjs +12 -0
- package/src/types/ReadConsistency.res +38 -0
- package/src/types/ReadConsistency.res.mjs +29 -0
- package/src/types/Schedule.res +65 -0
- package/src/types/Schedule.res.mjs +68 -0
- package/src/types/SideEffect.res +45 -0
- package/src/types/SideEffect.res.mjs +2 -0
- package/src/types/StoredEvent.res +46 -0
- package/src/types/StoredEvent.res.mjs +32 -0
- package/src/types/Visibility.res +24 -0
- package/src/types/Visibility.res.mjs +25 -0
|
@@ -0,0 +1,703 @@
|
|
|
1
|
+
// Renders a Pairing.resolved into Plugin.res source text.
|
|
2
|
+
|
|
3
|
+
// Strip suffix from a string if it ends with that suffix.
|
|
4
|
+
let stripSuffix = (s: string, suffix: string): string =>
|
|
5
|
+
if s->String.endsWith(suffix) {
|
|
6
|
+
s->String.slice(~start=0, ~end=s->String.length - suffix->String.length)
|
|
7
|
+
} else {
|
|
8
|
+
s
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Derive EP module name from mapping stem: "ProductsExtensionPointMapping" → "ProductsExtensionPoint"
|
|
12
|
+
let epModuleName = (mappingStem: string): string => stripSuffix(mappingStem, "Mapping")
|
|
13
|
+
|
|
14
|
+
// ── Section renderers ────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
let renderSlices = (
|
|
17
|
+
~platformFactory: string,
|
|
18
|
+
~suffix: string,
|
|
19
|
+
~implSuffix: string,
|
|
20
|
+
stems: array<string>,
|
|
21
|
+
): array<string> =>
|
|
22
|
+
stems->Array.map(stem =>
|
|
23
|
+
" module "
|
|
24
|
+
++ stem
|
|
25
|
+
++ suffix
|
|
26
|
+
++ " = Platform."
|
|
27
|
+
++ platformFactory
|
|
28
|
+
++ ".Make("
|
|
29
|
+
++ stem
|
|
30
|
+
++ ", "
|
|
31
|
+
++ stem
|
|
32
|
+
++ implSuffix
|
|
33
|
+
++ ")"
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
// StateChangeSlices opt into async dispatch via `@@reventless.async` on the
|
|
37
|
+
// spec file; the rendered factory becomes MakeAsync (CommandPending response).
|
|
38
|
+
let renderStateChangeSlices = (
|
|
39
|
+
stems: array<string>,
|
|
40
|
+
asyncStems: Dict.t<bool>,
|
|
41
|
+
): array<string> =>
|
|
42
|
+
stems->Array.map(stem => {
|
|
43
|
+
let factory = switch Dict.get(asyncStems, stem) {
|
|
44
|
+
| Some(true) => "MakeAsync"
|
|
45
|
+
| _ => "Make"
|
|
46
|
+
}
|
|
47
|
+
" module "
|
|
48
|
+
++ stem
|
|
49
|
+
++ "Slice = Platform.StateChangeSlice."
|
|
50
|
+
++ factory
|
|
51
|
+
++ "("
|
|
52
|
+
++ stem
|
|
53
|
+
++ ", "
|
|
54
|
+
++ stem
|
|
55
|
+
++ Pairing.implSuffixForStateChange
|
|
56
|
+
++ ")"
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
// AutomationSlice — 2-arg form. The merged `_Automation.res` shape exposes
|
|
60
|
+
// both `process` and the `mappings` array, so plugin assembly drops to
|
|
61
|
+
// `Platform.AutomationSlice.Make(<Stem>, <Stem>_Automation)`. Legacy 3-file
|
|
62
|
+
// shapes use a one-line bridge in `_Automation.res` to re-export `mappings`
|
|
63
|
+
// from the sibling `_Mappings.res`.
|
|
64
|
+
let renderAutomationSlices = (stems: array<string>): array<string> =>
|
|
65
|
+
stems->Array.map(stem =>
|
|
66
|
+
" module "
|
|
67
|
+
++ stem
|
|
68
|
+
++ "Slice = Platform.AutomationSlice.Make("
|
|
69
|
+
++ stem
|
|
70
|
+
++ ", "
|
|
71
|
+
++ stem
|
|
72
|
+
++ "_Automation)"
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
let renderAggregates = (aggregates: array<Pairing.aggregateDef>): array<string> =>
|
|
76
|
+
aggregates->Array.flatMap(({spec, behavior, eventMappings, isAsync}) => {
|
|
77
|
+
let em = eventMappings->Option.getOr("ReventlessInfra.NoEventMappings.Make(" ++ spec ++ ")")
|
|
78
|
+
let factory = isAsync ? "MakeAsync" : "Make"
|
|
79
|
+
[
|
|
80
|
+
" module " ++ spec ++ "Aggregate = Platform.Aggregate." ++ factory ++ "(",
|
|
81
|
+
" " ++ spec ++ ",",
|
|
82
|
+
" " ++ behavior ++ ",",
|
|
83
|
+
" " ++ em ++ ",",
|
|
84
|
+
" )",
|
|
85
|
+
]
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
let renderReadModels = (readModels: array<Pairing.readModelDef>): array<string> =>
|
|
89
|
+
readModels->Array.flatMap(({readModel, projections, stream}) => {
|
|
90
|
+
// The projections file declares `let mappings` via `@@reventless.mappings`,
|
|
91
|
+
// so we reference it directly. The wrapping module name appends `ReadModel`
|
|
92
|
+
// so the LHS doesn't shadow the bare-named spec module (e.g., `Categories`).
|
|
93
|
+
let wrapperName = if readModel->String.endsWith("ReadModel") {
|
|
94
|
+
readModel
|
|
95
|
+
} else {
|
|
96
|
+
readModel ++ "ReadModel"
|
|
97
|
+
}
|
|
98
|
+
// `ReadModelStream/` read models get a DynamoDB-Stream-backed QueryDb so the
|
|
99
|
+
// platform wires a StateTopic Lambda for AppSync Events (Source B) live updates.
|
|
100
|
+
let factory = stream ? "Platform.ReadModelStream.Make" : "Platform.ReadModel.Make"
|
|
101
|
+
[
|
|
102
|
+
" module " ++ wrapperName ++ " = " ++ factory ++ "(" ++ readModel ++ ", " ++ projections ++ ")",
|
|
103
|
+
]
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
let renderTasks = (tasks: array<string>): array<string> =>
|
|
107
|
+
tasks->Array.map(stem =>
|
|
108
|
+
" module " ++ stem ++ "Task = Platform.Task.Make(" ++ stem ++ ")"
|
|
109
|
+
)
|
|
110
|
+
|
|
111
|
+
let renderExtensionPoints = (extensionPoints: array<Pairing.extensionPointDef>): array<string> =>
|
|
112
|
+
extensionPoints->Array.flatMap(({group, mappings}) => {
|
|
113
|
+
let count = mappings->Array.length
|
|
114
|
+
let firstMapping = mappings->Array.getUnsafe(0)
|
|
115
|
+
let moduleName = switch group {
|
|
116
|
+
| None => epModuleName(firstMapping)
|
|
117
|
+
| Some(g) => g
|
|
118
|
+
}
|
|
119
|
+
switch count {
|
|
120
|
+
| 0 => []
|
|
121
|
+
| 1 =>
|
|
122
|
+
[" module " ++ moduleName ++ " = Platform.ExtensionPoint.Make(" ++ firstMapping ++ ")"]
|
|
123
|
+
| 2 =>
|
|
124
|
+
let m2 = mappings->Array.getUnsafe(1)
|
|
125
|
+
[
|
|
126
|
+
" module " ++ moduleName ++ " = Platform.ExtensionPoint.Make2(",
|
|
127
|
+
" " ++ firstMapping ++ ",",
|
|
128
|
+
" " ++ m2 ++ ",",
|
|
129
|
+
" )",
|
|
130
|
+
]
|
|
131
|
+
| 3 =>
|
|
132
|
+
let m2 = mappings->Array.getUnsafe(1)
|
|
133
|
+
let m3 = mappings->Array.getUnsafe(2)
|
|
134
|
+
[
|
|
135
|
+
" module " ++ moduleName ++ " = Platform.ExtensionPoint.Make3(",
|
|
136
|
+
" " ++ firstMapping ++ ",",
|
|
137
|
+
" " ++ m2 ++ ",",
|
|
138
|
+
" " ++ m3 ++ ",",
|
|
139
|
+
" )",
|
|
140
|
+
]
|
|
141
|
+
| _ =>
|
|
142
|
+
// MakeMulti — inline module expression
|
|
143
|
+
let mappingLines =
|
|
144
|
+
mappings->Array.map(m => " module(" ++ m ++ "),")
|
|
145
|
+
Array.flat([
|
|
146
|
+
[
|
|
147
|
+
" module " ++ moduleName ++ " = Platform.ExtensionPoint.MakeMulti({",
|
|
148
|
+
" module Spec = " ++ firstMapping ++ ".ExtensionPoint",
|
|
149
|
+
" module type Mapping = ReventlessInfra.ExtensionPointMapping.T with module ExtensionPoint := Spec",
|
|
150
|
+
" let name = Spec.name",
|
|
151
|
+
" let moduleUrl: string = %raw(`import.meta.url`)",
|
|
152
|
+
" let mappings: array<module(Mapping)> = [",
|
|
153
|
+
],
|
|
154
|
+
mappingLines,
|
|
155
|
+
[
|
|
156
|
+
" ]",
|
|
157
|
+
" })",
|
|
158
|
+
],
|
|
159
|
+
])
|
|
160
|
+
}
|
|
161
|
+
})
|
|
162
|
+
|
|
163
|
+
let renderExtensions = (extensions: array<string>): array<string> =>
|
|
164
|
+
extensions->Array.map(stem =>
|
|
165
|
+
" module " ++ stem ++ " = Platform.Extension.Make(" ++ stem ++ ".Mapping)"
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
// AWS-specific renderers were removed in favour of the canonical ones — the
|
|
169
|
+
// AWS Plugin.res now emits `open <Namespace>` so bare stems resolve, and the
|
|
170
|
+
// AWS Platform.{Aggregate,ReadModel}.Make functors delegate to the
|
|
171
|
+
// `*_Builder_Single` strategy under the hood (see reventless-aws/src/Platform.res).
|
|
172
|
+
|
|
173
|
+
// ── make() call ──────────────────────────────────────────────────────────────
|
|
174
|
+
|
|
175
|
+
let renderMakeParam = (
|
|
176
|
+
~param: string,
|
|
177
|
+
~items: array<string>,
|
|
178
|
+
~moduleSuffix: string,
|
|
179
|
+
): option<string> =>
|
|
180
|
+
if items->Array.length === 0 {
|
|
181
|
+
None
|
|
182
|
+
} else {
|
|
183
|
+
let entries = items->Array.map(s => "module(" ++ s ++ moduleSuffix ++ ")")
|
|
184
|
+
Some(" ~" ++ param ++ "=[" ++ entries->Array.join(", ") ++ "],")
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
let renderStateViewSlicesMakeParam = (
|
|
188
|
+
slices: array<string>,
|
|
189
|
+
streamSlices: array<string>,
|
|
190
|
+
): option<string> => {
|
|
191
|
+
let entries = Array.flat([
|
|
192
|
+
slices->Array.map(s => "module(" ++ s ++ "Slice)"),
|
|
193
|
+
streamSlices->Array.map(s => "module(" ++ s ++ "StreamSlice)"),
|
|
194
|
+
])
|
|
195
|
+
if entries->Array.length === 0 {
|
|
196
|
+
None
|
|
197
|
+
} else {
|
|
198
|
+
Some(" ~stateViewSlices=[" ++ entries->Array.join(", ") ++ "],")
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
let renderReadModelMakeParam = (readModels: array<Pairing.readModelDef>): option<string> =>
|
|
203
|
+
if readModels->Array.length === 0 {
|
|
204
|
+
None
|
|
205
|
+
} else {
|
|
206
|
+
// The wrapping module name in Plugin.res is the legacy `<rm>` (when the
|
|
207
|
+
// spec already ends in `ReadModel`) or `<rm>ReadModel` (post-Phase-3.3,
|
|
208
|
+
// when the spec is the bare plural like `Categories`). Mirrors the LHS
|
|
209
|
+
// chosen by [renderReadModels] above.
|
|
210
|
+
let entries = readModels->Array.map(({readModel}) => {
|
|
211
|
+
let wrapperName = if readModel->String.endsWith("ReadModel") {
|
|
212
|
+
readModel
|
|
213
|
+
} else {
|
|
214
|
+
readModel ++ "ReadModel"
|
|
215
|
+
}
|
|
216
|
+
"module(" ++ wrapperName ++ ")"
|
|
217
|
+
})
|
|
218
|
+
Some(" ~readModels=[" ++ entries->Array.join(", ") ++ "],")
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
let renderAggregateMakeParam = (aggregates: array<Pairing.aggregateDef>): option<string> =>
|
|
222
|
+
if aggregates->Array.length === 0 {
|
|
223
|
+
None
|
|
224
|
+
} else {
|
|
225
|
+
let entries = aggregates->Array.map(({spec}) => "module(" ++ spec ++ "Aggregate)")
|
|
226
|
+
Some(" ~aggregates=[" ++ entries->Array.join(", ") ++ "],")
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
let renderEpMakeParam = (extensionPoints: array<Pairing.extensionPointDef>): option<string> =>
|
|
230
|
+
if extensionPoints->Array.length === 0 {
|
|
231
|
+
None
|
|
232
|
+
} else {
|
|
233
|
+
let entries = extensionPoints->Array.map(({group, mappings}) => {
|
|
234
|
+
let firstMapping = mappings->Array.getUnsafe(0)
|
|
235
|
+
let moduleName = switch group {
|
|
236
|
+
| None => epModuleName(firstMapping)
|
|
237
|
+
| Some(g) => g
|
|
238
|
+
}
|
|
239
|
+
"module(" ++ moduleName ++ ")"
|
|
240
|
+
})
|
|
241
|
+
Some(" ~extensionPoints=[" ++ entries->Array.join(", ") ++ "],")
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
let renderExtensionMakeParam = (extensions: array<string>): option<string> =>
|
|
245
|
+
if extensions->Array.length === 0 {
|
|
246
|
+
None
|
|
247
|
+
} else {
|
|
248
|
+
let entries = extensions->Array.map(s => "module(" ++ s ++ ")")
|
|
249
|
+
Some(" ~extensions=[" ++ entries->Array.join(", ") ++ "],")
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
let renderTaskMakeParam = (tasks: array<string>): option<string> =>
|
|
253
|
+
if tasks->Array.length === 0 {
|
|
254
|
+
None
|
|
255
|
+
} else {
|
|
256
|
+
let entries = tasks->Array.map(s => "module(" ++ s ++ "Task)")
|
|
257
|
+
Some(" ~tasks=[" ++ entries->Array.join(", ") ++ "],")
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
let renderPluginStructureCall = (
|
|
261
|
+
~name: string,
|
|
262
|
+
~aggregates: array<Pairing.aggregateDef>,
|
|
263
|
+
~readModels: array<Pairing.readModelDef>,
|
|
264
|
+
~stateViewSlices: array<string>,
|
|
265
|
+
~stateViewSlicesStream: array<string>,
|
|
266
|
+
~stateChangeSlices: array<string>,
|
|
267
|
+
~automationSlices: array<string>,
|
|
268
|
+
~outboundTranslationSlices: array<string>,
|
|
269
|
+
~inboundTranslationSlices: array<string>,
|
|
270
|
+
~extensions: array<string>,
|
|
271
|
+
~extensionPoints: array<Pairing.extensionPointDef>,
|
|
272
|
+
): option<array<string>> => {
|
|
273
|
+
// The structure call carries the EP *mapping* files (one per Delegate
|
|
274
|
+
// connection), not the wrapped ExtensionPoint module — Plugin_Structure reads
|
|
275
|
+
// each mapping's Delegate to derive the extension point's source event types.
|
|
276
|
+
let epMappingStems = extensionPoints->Array.flatMap(({mappings}) => mappings)
|
|
277
|
+
let hasComponents =
|
|
278
|
+
aggregates->Array.length > 0 ||
|
|
279
|
+
readModels->Array.length > 0 ||
|
|
280
|
+
stateViewSlices->Array.length > 0 ||
|
|
281
|
+
stateViewSlicesStream->Array.length > 0 ||
|
|
282
|
+
stateChangeSlices->Array.length > 0 ||
|
|
283
|
+
automationSlices->Array.length > 0 ||
|
|
284
|
+
outboundTranslationSlices->Array.length > 0 ||
|
|
285
|
+
inboundTranslationSlices->Array.length > 0 ||
|
|
286
|
+
extensions->Array.length > 0 ||
|
|
287
|
+
epMappingStems->Array.length > 0
|
|
288
|
+
if !hasComponents {
|
|
289
|
+
None
|
|
290
|
+
} else {
|
|
291
|
+
let ls: array<string> = []
|
|
292
|
+
ls->Array.push(" let pluginStructure = Platform.Plugin.makePluginDefinition(")
|
|
293
|
+
ls->Array.push(" ~name=\"" ++ name ++ "\",")
|
|
294
|
+
if aggregates->Array.length > 0 {
|
|
295
|
+
let entries = aggregates->Array.map(({spec}) => "module(" ++ spec ++ "Aggregate)")
|
|
296
|
+
ls->Array.push(" ~aggregates=[" ++ entries->Array.join(", ") ++ "],")
|
|
297
|
+
}
|
|
298
|
+
if readModels->Array.length > 0 {
|
|
299
|
+
let entries = readModels->Array.map(({readModel}) => {
|
|
300
|
+
let wrapperName = if readModel->String.endsWith("ReadModel") {
|
|
301
|
+
readModel
|
|
302
|
+
} else {
|
|
303
|
+
readModel ++ "ReadModel"
|
|
304
|
+
}
|
|
305
|
+
"module(" ++ wrapperName ++ ")"
|
|
306
|
+
})
|
|
307
|
+
ls->Array.push(" ~readModels=[" ++ entries->Array.join(", ") ++ "],")
|
|
308
|
+
}
|
|
309
|
+
let allStateViewEntries = Array.flat([
|
|
310
|
+
stateViewSlices->Array.map(s => "module(" ++ s ++ "Slice)"),
|
|
311
|
+
stateViewSlicesStream->Array.map(s => "module(" ++ s ++ "StreamSlice)"),
|
|
312
|
+
])
|
|
313
|
+
if allStateViewEntries->Array.length > 0 {
|
|
314
|
+
ls->Array.push(" ~stateViewSlices=[" ++ allStateViewEntries->Array.join(", ") ++ "],")
|
|
315
|
+
}
|
|
316
|
+
if stateChangeSlices->Array.length > 0 {
|
|
317
|
+
let entries = stateChangeSlices->Array.map(s => "module(" ++ s ++ "Slice)")
|
|
318
|
+
ls->Array.push(" ~stateChangeSlices=[" ++ entries->Array.join(", ") ++ "],")
|
|
319
|
+
}
|
|
320
|
+
if automationSlices->Array.length > 0 {
|
|
321
|
+
let entries = automationSlices->Array.map(s => "module(" ++ s ++ "Slice)")
|
|
322
|
+
ls->Array.push(" ~automationSlices=[" ++ entries->Array.join(", ") ++ "],")
|
|
323
|
+
}
|
|
324
|
+
if outboundTranslationSlices->Array.length > 0 {
|
|
325
|
+
let entries = outboundTranslationSlices->Array.map(s => "module(" ++ s ++ "Slice)")
|
|
326
|
+
ls->Array.push(" ~outboundTranslationSlices=[" ++ entries->Array.join(", ") ++ "],")
|
|
327
|
+
}
|
|
328
|
+
if inboundTranslationSlices->Array.length > 0 {
|
|
329
|
+
let entries = inboundTranslationSlices->Array.map(s => "module(" ++ s ++ "Slice)")
|
|
330
|
+
ls->Array.push(" ~inboundTranslationSlices=[" ++ entries->Array.join(", ") ++ "],")
|
|
331
|
+
}
|
|
332
|
+
if extensions->Array.length > 0 {
|
|
333
|
+
let entries = extensions->Array.map(s => "module(" ++ s ++ ")")
|
|
334
|
+
ls->Array.push(" ~extensions=[" ++ entries->Array.join(", ") ++ "],")
|
|
335
|
+
}
|
|
336
|
+
if epMappingStems->Array.length > 0 {
|
|
337
|
+
let entries = epMappingStems->Array.map(s => "module(" ++ s ++ ")")
|
|
338
|
+
ls->Array.push(" ~extensionPoints=[" ++ entries->Array.join(", ") ++ "],")
|
|
339
|
+
}
|
|
340
|
+
ls->Array.push(" )")
|
|
341
|
+
Some(ls)
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
// ── Duplicate spec-stem lint ─────────────────────────────────────────────────
|
|
346
|
+
// Within one plugin, every spec file's stem produces a top-level ReScript
|
|
347
|
+
// module name. Two spec files with the same stem (across folders) collide at
|
|
348
|
+
// link time with a confusing error. We catch the collision here with a clear
|
|
349
|
+
// message naming both file paths.
|
|
350
|
+
|
|
351
|
+
let validateUniqueSpecStems = (~discovered: array<Discovery.discoveredFile>) => {
|
|
352
|
+
// Each discovered .res file becomes a top-level ReScript module named after
|
|
353
|
+
// its filename stem. Two files with the same stem in different folders
|
|
354
|
+
// collide at link time, so we surface the conflict here with both paths.
|
|
355
|
+
let byStem: Dict.t<array<string>> = Dict.make()
|
|
356
|
+
discovered->Array.forEach((d: Discovery.discoveredFile) => {
|
|
357
|
+
let existing = byStem->Dict.get(d.stem)->Option.getOr([])
|
|
358
|
+
existing->Array.push(d.relPath)
|
|
359
|
+
byStem->Dict.set(d.stem, existing)
|
|
360
|
+
})
|
|
361
|
+
byStem
|
|
362
|
+
->Dict.toArray
|
|
363
|
+
->Array.forEach(((stem, paths)) => {
|
|
364
|
+
if paths->Array.length > 1 {
|
|
365
|
+
let pathList = paths->Array.toSorted((a, b) => if a < b {-1.0} else if a > b {1.0} else {0.0})->Array.join("\n - ")
|
|
366
|
+
JsError.throwWithMessage(
|
|
367
|
+
"Generator: stem `" ++
|
|
368
|
+
stem ++
|
|
369
|
+
"` is used by multiple files. Filename stems must be unique across a plugin so the generated module names don't collide:\n - " ++
|
|
370
|
+
pathList,
|
|
371
|
+
)
|
|
372
|
+
}
|
|
373
|
+
})
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
// ── targetName validation ────────────────────────────────────────────────────
|
|
377
|
+
|
|
378
|
+
let validateSliceTargets = (~resolved: Pairing.resolved) => {
|
|
379
|
+
let knownReceivers = Array.concat(
|
|
380
|
+
resolved.aggregates->Array.map(({spec}) => spec),
|
|
381
|
+
resolved.stateChangeSlices,
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
let check = (~kind: string, stem: string, target: option<string>) =>
|
|
385
|
+
switch target {
|
|
386
|
+
| None =>
|
|
387
|
+
JsError.throwWithMessage(
|
|
388
|
+
"Generator: " ++
|
|
389
|
+
kind ++
|
|
390
|
+
" `" ++
|
|
391
|
+
stem ++
|
|
392
|
+
"` is missing `let targetName = \"...\"`. Valid targets: " ++
|
|
393
|
+
knownReceivers->Array.join(", "),
|
|
394
|
+
)
|
|
395
|
+
| Some(t) when !(knownReceivers->Array.includes(t)) =>
|
|
396
|
+
JsError.throwWithMessage(
|
|
397
|
+
"Generator: " ++
|
|
398
|
+
kind ++
|
|
399
|
+
" `" ++
|
|
400
|
+
stem ++
|
|
401
|
+
"` declares targetName = \"" ++
|
|
402
|
+
t ++
|
|
403
|
+
"\" but no such command receiver exists. Valid targets: " ++
|
|
404
|
+
knownReceivers->Array.join(", "),
|
|
405
|
+
)
|
|
406
|
+
| Some(_) => ()
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
resolved.automationSlices->Array.forEach(stem =>
|
|
410
|
+
check(
|
|
411
|
+
~kind="AutomationSlice",
|
|
412
|
+
stem,
|
|
413
|
+
resolved.automationSliceTargets->Dict.get(stem)->Option.getOr(None),
|
|
414
|
+
)
|
|
415
|
+
)
|
|
416
|
+
|
|
417
|
+
resolved.inboundTranslationSlices->Array.forEach(stem =>
|
|
418
|
+
check(
|
|
419
|
+
~kind="InboundTranslationSlice",
|
|
420
|
+
stem,
|
|
421
|
+
resolved.inboundTranslationSliceTargets->Dict.get(stem)->Option.getOr(None),
|
|
422
|
+
)
|
|
423
|
+
)
|
|
424
|
+
|
|
425
|
+
// OutboundTranslationSlice: only validate when targetName = Some(name) (None = fire-and-forget)
|
|
426
|
+
resolved.outboundTranslationSlices->Array.forEach(stem => {
|
|
427
|
+
switch resolved.outboundTranslationSliceTargets->Dict.get(stem)->Option.getOr(None) {
|
|
428
|
+
| Some(t) when !(knownReceivers->Array.includes(t)) =>
|
|
429
|
+
JsError.throwWithMessage(
|
|
430
|
+
"Generator: OutboundTranslationSlice `" ++
|
|
431
|
+
stem ++
|
|
432
|
+
"` declares targetName = Some(\"" ++
|
|
433
|
+
t ++
|
|
434
|
+
"\") but no such command receiver exists. Valid targets: " ++
|
|
435
|
+
knownReceivers->Array.join(", "),
|
|
436
|
+
)
|
|
437
|
+
| _ => ()
|
|
438
|
+
}
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
// ── Main.res render (AWS mode only) ─────────────────────────────────────────
|
|
443
|
+
|
|
444
|
+
let renderMain = (~config: Config.config): string => {
|
|
445
|
+
let name = config.name
|
|
446
|
+
[
|
|
447
|
+
"// AUTO-GENERATED — do not edit. Run `npm run generate` to update.",
|
|
448
|
+
"// " ++ name ++ " plugin — AWS deployment.",
|
|
449
|
+
"",
|
|
450
|
+
"module Platform = ReventlessAws.Platform.Make()",
|
|
451
|
+
"module " ++ name ++ " = Plugin.Make(Platform)",
|
|
452
|
+
"",
|
|
453
|
+
"let default = Platform.deployPlugin(",
|
|
454
|
+
" ~plugin=module(" ++ name ++ "),",
|
|
455
|
+
")",
|
|
456
|
+
"",
|
|
457
|
+
]->Array.join("\n")
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// ── Aws-variant render ───────────────────────────────────────────────────────
|
|
461
|
+
// The AWS Plugin.res delegates to the source plugin's `Plugin.Make` functor;
|
|
462
|
+
// all slice/aggregate/readmodel declarations and the federation override
|
|
463
|
+
// env-var read live in the Composition variant. The wrapper just retypes the
|
|
464
|
+
// Platform constraint and re-exports `make` for the AWS deploy path.
|
|
465
|
+
|
|
466
|
+
// Convert a PascalCase plugin name to SCREAMING_SNAKE_CASE for env var naming.
|
|
467
|
+
// "Ordering" → "ORDERING"; "OnlineShop" → "ONLINE_SHOP".
|
|
468
|
+
let pluginNameToEnvBase = (name: string): string =>
|
|
469
|
+
name
|
|
470
|
+
->String.split("")
|
|
471
|
+
->Array.mapWithIndex((ch, i) => {
|
|
472
|
+
let isUpper = ch !== "" && ch === ch->String.toUpperCase && ch !== ch->String.toLowerCase
|
|
473
|
+
i > 0 && isUpper ? "_" ++ ch : ch
|
|
474
|
+
})
|
|
475
|
+
->Array.join("")
|
|
476
|
+
->String.toUpperCase
|
|
477
|
+
|
|
478
|
+
let renderAwsWrapper = (~compositionNamespace: string): string => {
|
|
479
|
+
[
|
|
480
|
+
"// AUTO-GENERATED — do not edit. Run `npm run generate` to update.",
|
|
481
|
+
"",
|
|
482
|
+
"module Make = (",
|
|
483
|
+
" Platform: ReventlessInfra.Platform.T",
|
|
484
|
+
" with type api = ReventlessAws.Types.AppSync.api",
|
|
485
|
+
" and type role = ReventlessAws.Types.AppSync.role,",
|
|
486
|
+
") => {",
|
|
487
|
+
" module Composition = " ++ compositionNamespace ++ ".Plugin.Make(Platform)",
|
|
488
|
+
" let make = () => Composition.make()",
|
|
489
|
+
"}",
|
|
490
|
+
"",
|
|
491
|
+
]->Array.join("\n")
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
// ── Composition-variant render ───────────────────────────────────────────────
|
|
495
|
+
|
|
496
|
+
let renderComposition = (~config: Config.config, ~resolved: Pairing.resolved): string => {
|
|
497
|
+
let lines: array<string> = []
|
|
498
|
+
|
|
499
|
+
// Header
|
|
500
|
+
lines->Array.push("// AUTO-GENERATED — do not edit. Run `npm run generate` to update.")
|
|
501
|
+
// Federation override URL — empty/unset means Auto UI renders every fragment.
|
|
502
|
+
// Read once at module init; same env var is honoured by the AWS wrapper.
|
|
503
|
+
lines->Array.push("")
|
|
504
|
+
lines->Array.push(
|
|
505
|
+
"@val external uiBundleUrl: option<string> = \"process.env." ++
|
|
506
|
+
pluginNameToEnvBase(config.name) ++
|
|
507
|
+
"_UI_BUNDLE_URL\"",
|
|
508
|
+
)
|
|
509
|
+
lines->Array.push("")
|
|
510
|
+
lines->Array.push("module Make = (Platform: ReventlessInfra.Platform.T) => {")
|
|
511
|
+
|
|
512
|
+
let push = (sectionLines: array<string>) => {
|
|
513
|
+
sectionLines->Array.forEach(l => lines->Array.push(l))
|
|
514
|
+
lines->Array.push("")
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
// StateChangeSlices — per-stem MakeAsync opt-in via @@reventless.async
|
|
518
|
+
if resolved.stateChangeSlices->Array.length > 0 {
|
|
519
|
+
lines->Array.push(" // StateChangeSlices")
|
|
520
|
+
renderStateChangeSlices(
|
|
521
|
+
resolved.stateChangeSlices,
|
|
522
|
+
resolved.asyncStateChangeSlices,
|
|
523
|
+
)->push
|
|
524
|
+
}
|
|
525
|
+
|
|
526
|
+
// StateViewSlices
|
|
527
|
+
if resolved.stateViewSlices->Array.length > 0 {
|
|
528
|
+
lines->Array.push(" // StateViewSlices")
|
|
529
|
+
renderSlices(
|
|
530
|
+
~platformFactory="StateViewSlice",
|
|
531
|
+
~suffix="Slice",
|
|
532
|
+
~implSuffix=Pairing.implSuffixForStateView,
|
|
533
|
+
resolved.stateViewSlices,
|
|
534
|
+
)->push
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
// StateViewSliceStreams
|
|
538
|
+
if resolved.stateViewSlicesStream->Array.length > 0 {
|
|
539
|
+
lines->Array.push(" // StateViewSliceStreams")
|
|
540
|
+
renderSlices(
|
|
541
|
+
~platformFactory="StateViewSliceStream",
|
|
542
|
+
~suffix="StreamSlice",
|
|
543
|
+
~implSuffix=Pairing.implSuffixForStateView,
|
|
544
|
+
resolved.stateViewSlicesStream,
|
|
545
|
+
)->push
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
// AutomationSlices — 2-arg form: (Spec, _Automation)
|
|
549
|
+
if resolved.automationSlices->Array.length > 0 {
|
|
550
|
+
lines->Array.push(" // AutomationSlices")
|
|
551
|
+
renderAutomationSlices(resolved.automationSlices)->push
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// OutboundTranslationSlices
|
|
555
|
+
if resolved.outboundTranslationSlices->Array.length > 0 {
|
|
556
|
+
lines->Array.push(" // OutboundTranslationSlices")
|
|
557
|
+
renderSlices(
|
|
558
|
+
~platformFactory="OutboundTranslationSlice",
|
|
559
|
+
~suffix="Slice",
|
|
560
|
+
~implSuffix=Pairing.implSuffixForTranslation,
|
|
561
|
+
resolved.outboundTranslationSlices,
|
|
562
|
+
)->push
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
// InboundTranslationSlices
|
|
566
|
+
if resolved.inboundTranslationSlices->Array.length > 0 {
|
|
567
|
+
lines->Array.push(" // InboundTranslationSlices")
|
|
568
|
+
renderSlices(
|
|
569
|
+
~platformFactory="InboundTranslationSlice",
|
|
570
|
+
~suffix="Slice",
|
|
571
|
+
~implSuffix=Pairing.implSuffixForTranslation,
|
|
572
|
+
resolved.inboundTranslationSlices,
|
|
573
|
+
)->push
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Aggregates
|
|
577
|
+
if resolved.aggregates->Array.length > 0 {
|
|
578
|
+
lines->Array.push(" // Aggregates")
|
|
579
|
+
renderAggregates(resolved.aggregates)->push
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// ReadModels
|
|
583
|
+
if resolved.readModels->Array.length > 0 {
|
|
584
|
+
lines->Array.push(" // ReadModels")
|
|
585
|
+
renderReadModels(resolved.readModels)->push
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
// Tasks
|
|
589
|
+
if resolved.tasks->Array.length > 0 {
|
|
590
|
+
lines->Array.push(" // Tasks")
|
|
591
|
+
renderTasks(resolved.tasks)->push
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
// ExtensionPoints
|
|
595
|
+
if resolved.extensionPoints->Array.length > 0 {
|
|
596
|
+
lines->Array.push(" // ExtensionPoints")
|
|
597
|
+
renderExtensionPoints(resolved.extensionPoints)->push
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
// Extensions
|
|
601
|
+
if resolved.extensions->Array.length > 0 {
|
|
602
|
+
lines->Array.push(" // Extensions")
|
|
603
|
+
renderExtensions(resolved.extensions)->push
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
// pluginStructure
|
|
607
|
+
let pluginStructureLines = renderPluginStructureCall(
|
|
608
|
+
~name=config.name,
|
|
609
|
+
~aggregates=resolved.aggregates,
|
|
610
|
+
~readModels=resolved.readModels,
|
|
611
|
+
~stateViewSlices=resolved.stateViewSlices,
|
|
612
|
+
~stateViewSlicesStream=resolved.stateViewSlicesStream,
|
|
613
|
+
~stateChangeSlices=resolved.stateChangeSlices,
|
|
614
|
+
~automationSlices=resolved.automationSlices,
|
|
615
|
+
~outboundTranslationSlices=resolved.outboundTranslationSlices,
|
|
616
|
+
~inboundTranslationSlices=resolved.inboundTranslationSlices,
|
|
617
|
+
~extensions=resolved.extensions,
|
|
618
|
+
~extensionPoints=resolved.extensionPoints,
|
|
619
|
+
)
|
|
620
|
+
let hasPluginStructure = pluginStructureLines->Option.isSome
|
|
621
|
+
switch pluginStructureLines {
|
|
622
|
+
| Some(defLines) =>
|
|
623
|
+
defLines->Array.forEach(l => lines->Array.push(l))
|
|
624
|
+
lines->Array.push("")
|
|
625
|
+
| None => ()
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
// make() call — Composition variant only (Aws variant uses renderAwsWrapper).
|
|
629
|
+
// Auto UI is always derived from pluginStructure by the host-shell. The
|
|
630
|
+
// uiFragments manifest is purely a federation-override map: empty
|
|
631
|
+
// remoteEntryUrl ⇒ Auto UI renders the fragment. The wiring is emitted
|
|
632
|
+
// whenever pluginStructure exists, so any plugin (aggregate/DCB/hybrid)
|
|
633
|
+
// can opt into a custom React bundle via the env var.
|
|
634
|
+
lines->Array.push(" let make = () =>")
|
|
635
|
+
lines->Array.push(" Platform.Plugin.make(")
|
|
636
|
+
lines->Array.push(" ~name=\"" ++ config.name ++ "\",")
|
|
637
|
+
lines->Array.push(" ~heartbeatInterval=" ++ config.heartbeatInterval->Int.toString ++ ",")
|
|
638
|
+
|
|
639
|
+
let uiFragmentsParam = if hasPluginStructure {
|
|
640
|
+
let ls: array<string> = []
|
|
641
|
+
ls->Array.push(" ~uiFragments=?uiBundleUrl->Option.map(url =>")
|
|
642
|
+
ls->Array.push(" Platform.Plugin.makeAutoUIManifest(")
|
|
643
|
+
ls->Array.push(" ~remoteEntryUrl=url,")
|
|
644
|
+
ls->Array.push(" ~name=\"" ++ config.name ++ "\",")
|
|
645
|
+
ls->Array.push(" ~pluginStructure,")
|
|
646
|
+
ls->Array.push(" ~readModelPositions=[\"platform-summary\"],")
|
|
647
|
+
ls->Array.push(" ~aggregatePositions=[\"resource-detail\"],")
|
|
648
|
+
ls->Array.push(" )")
|
|
649
|
+
ls->Array.push(" ),")
|
|
650
|
+
Some(ls)
|
|
651
|
+
} else {
|
|
652
|
+
None
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
let makeParams = [
|
|
656
|
+
renderEpMakeParam(resolved.extensionPoints),
|
|
657
|
+
renderExtensionMakeParam(resolved.extensions),
|
|
658
|
+
renderAggregateMakeParam(resolved.aggregates),
|
|
659
|
+
renderReadModelMakeParam(resolved.readModels),
|
|
660
|
+
renderTaskMakeParam(resolved.tasks),
|
|
661
|
+
renderMakeParam(~param="stateChangeSlices", ~items=resolved.stateChangeSlices, ~moduleSuffix="Slice"),
|
|
662
|
+
renderStateViewSlicesMakeParam(resolved.stateViewSlices, resolved.stateViewSlicesStream),
|
|
663
|
+
renderMakeParam(~param="automationSlices", ~items=resolved.automationSlices, ~moduleSuffix="Slice"),
|
|
664
|
+
renderMakeParam(
|
|
665
|
+
~param="outboundTranslationSlices",
|
|
666
|
+
~items=resolved.outboundTranslationSlices,
|
|
667
|
+
~moduleSuffix="Slice",
|
|
668
|
+
),
|
|
669
|
+
renderMakeParam(
|
|
670
|
+
~param="inboundTranslationSlices",
|
|
671
|
+
~items=resolved.inboundTranslationSlices,
|
|
672
|
+
~moduleSuffix="Slice",
|
|
673
|
+
),
|
|
674
|
+
hasPluginStructure ? Some(" ~pluginStructure=pluginStructure,") : None,
|
|
675
|
+
]
|
|
676
|
+
|
|
677
|
+
makeParams->Array.filterMap(x => x)->Array.forEach(line => lines->Array.push(line))
|
|
678
|
+
switch uiFragmentsParam {
|
|
679
|
+
| Some(uiLines) => uiLines->Array.forEach(line => lines->Array.push(line))
|
|
680
|
+
| None => ()
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
lines->Array.push(" )")
|
|
684
|
+
lines->Array.push("}")
|
|
685
|
+
lines->Array.push("")
|
|
686
|
+
|
|
687
|
+
lines->Array.join("\n")
|
|
688
|
+
}
|
|
689
|
+
|
|
690
|
+
// ── Top-level render ─────────────────────────────────────────────────────────
|
|
691
|
+
|
|
692
|
+
let render = (
|
|
693
|
+
~config: Config.config,
|
|
694
|
+
~resolved: Pairing.resolved,
|
|
695
|
+
~discovered: array<Discovery.discoveredFile>,
|
|
696
|
+
): string => {
|
|
697
|
+
validateUniqueSpecStems(~discovered)
|
|
698
|
+
validateSliceTargets(~resolved)
|
|
699
|
+
switch config.variant {
|
|
700
|
+
| Aws({compositionNamespace}) => renderAwsWrapper(~compositionNamespace)
|
|
701
|
+
| Composition => renderComposition(~config, ~resolved)
|
|
702
|
+
}
|
|
703
|
+
}
|