@reventlessdev/reventless-spec 3.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 +931 -0
- package/LICENSE +202 -0
- package/README.md +109 -0
- package/package.json +49 -0
- package/rescript.json +32 -0
- package/run-generator.mjs +2 -0
- package/run-platform-generator.mjs +2 -0
- package/scripts/generate-currency.mjs +215 -0
- package/scripts/iso-4217-list-one.xml +1956 -0
- package/src/AnsiStyle.res +40 -0
- package/src/AnsiStyle.res.mjs +54 -0
- package/src/LogPrefix.res +192 -0
- package/src/LogPrefix.res.mjs +159 -0
- package/src/PackageVersion.res +67 -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/CapabilityManifest.res +74 -0
- package/src/components/CapabilityManifest.res.mjs +61 -0
- package/src/components/ComponentKind.res +99 -0
- package/src/components/ComponentKind.res.mjs +125 -0
- package/src/components/Counter.res +24 -0
- package/src/components/Counter.res.mjs +2 -0
- package/src/components/DcbDecode.res +118 -0
- package/src/components/DcbDecode.res.mjs +100 -0
- package/src/components/DcbScopeInference.res +244 -0
- package/src/components/DcbScopeInference.res.mjs +177 -0
- package/src/components/DcbTag.res +1335 -0
- package/src/components/DcbTag.res.mjs +898 -0
- package/src/components/DcbValidation.res +427 -0
- package/src/components/DcbValidation.res.mjs +423 -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 +85 -0
- package/src/components/InboundTranslationSlice.res.mjs +2 -0
- package/src/components/OutboundTranslationSlice.res +153 -0
- package/src/components/OutboundTranslationSlice.res.mjs +2 -0
- package/src/components/Plugin.res +538 -0
- package/src/components/Plugin.res.mjs +264 -0
- package/src/components/PluginName.res +39 -0
- package/src/components/PluginName.res.mjs +45 -0
- package/src/components/ReadModel.res +199 -0
- package/src/components/ReadModel.res.mjs +18 -0
- package/src/components/Reference.res +55 -0
- package/src/components/Reference.res.mjs +50 -0
- package/src/components/Snapshot.res +26 -0
- package/src/components/Snapshot.res.mjs +2 -0
- package/src/components/StateAnnotations.res +97 -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 +123 -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 +842 -0
- package/src/generator/Codegen.res.mjs +565 -0
- package/src/generator/Config.res +106 -0
- package/src/generator/Config.res.mjs +69 -0
- package/src/generator/Discovery.res +230 -0
- package/src/generator/Discovery.res.mjs +198 -0
- package/src/generator/Generator_Node.res +14 -0
- package/src/generator/Generator_Node.res.mjs +18 -0
- package/src/generator/Pairing.res +460 -0
- package/src/generator/Pairing.res.mjs +415 -0
- package/src/generator/PlatformCodegen.res +207 -0
- package/src/generator/PlatformCodegen.res.mjs +154 -0
- package/src/generator/PlatformGenerator.res +126 -0
- package/src/generator/PlatformGenerator.res.mjs +114 -0
- package/src/generator/PlatformManifests.res +203 -0
- package/src/generator/PlatformManifests.res.mjs +212 -0
- package/src/generator/PluginGenerator.res +57 -0
- package/src/generator/PluginGenerator.res.mjs +73 -0
- package/src/semantic/Bytes.res +54 -0
- package/src/semantic/Bytes.res.mjs +38 -0
- package/src/semantic/Capabilities.res +43 -0
- package/src/semantic/Capabilities.res.mjs +17 -0
- package/src/semantic/Color.res +51 -0
- package/src/semantic/Color.res.mjs +29 -0
- package/src/semantic/Currency.res +598 -0
- package/src/semantic/Currency.res.mjs +743 -0
- package/src/semantic/DateRange.res +148 -0
- package/src/semantic/DateRange.res.mjs +74 -0
- package/src/semantic/Duration.res +53 -0
- package/src/semantic/Duration.res.mjs +26 -0
- package/src/semantic/Email.res +51 -0
- package/src/semantic/Email.res.mjs +31 -0
- package/src/semantic/GeoPoint.res +226 -0
- package/src/semantic/GeoPoint.res.mjs +190 -0
- package/src/semantic/Geocoding.res +127 -0
- package/src/semantic/Geocoding.res.mjs +36 -0
- package/src/semantic/Money.res +196 -0
- package/src/semantic/Money.res.mjs +138 -0
- package/src/semantic/Offload.res +294 -0
- package/src/semantic/Offload.res.mjs +191 -0
- package/src/semantic/Percent.res +53 -0
- package/src/semantic/Percent.res.mjs +33 -0
- package/src/semantic/Phone.res +55 -0
- package/src/semantic/Phone.res.mjs +29 -0
- package/src/semantic/Semantic.res +162 -0
- package/src/semantic/Semantic.res.mjs +95 -0
- package/src/semantic/StorageRef.res +164 -0
- package/src/semantic/StorageRef.res.mjs +111 -0
- package/src/semantic/Url.res +66 -0
- package/src/semantic/Url.res.mjs +48 -0
- package/src/types/Authorization.res +23 -0
- package/src/types/Authorization.res.mjs +33 -0
- package/src/types/Behavior.res +86 -0
- package/src/types/Behavior.res.mjs +2 -0
- package/src/types/DateTime.res +29 -0
- package/src/types/DateTime.res.mjs +16 -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 +326 -0
- package/src/types/Message.res.mjs +186 -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,415 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Nodefs from "node:fs";
|
|
4
|
+
import * as Nodepath from "node:path";
|
|
5
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
6
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
7
|
+
import * as Generator_Node$Reventless from "./Generator_Node.res.mjs";
|
|
8
|
+
|
|
9
|
+
let implSuffixForStateChange = "_Behavior";
|
|
10
|
+
|
|
11
|
+
let implSuffixForStateView = "_Projection";
|
|
12
|
+
|
|
13
|
+
let implSuffixForAutomation = "_Automation";
|
|
14
|
+
|
|
15
|
+
let implSuffixForTranslation = "_Translation";
|
|
16
|
+
|
|
17
|
+
let mappingsSuffixForAutomation = "_Mappings";
|
|
18
|
+
|
|
19
|
+
function isImplStem(stem) {
|
|
20
|
+
if (stem.endsWith(implSuffixForStateChange) || stem.endsWith(implSuffixForStateView) || stem.endsWith(implSuffixForAutomation) || stem.endsWith(implSuffixForTranslation)) {
|
|
21
|
+
return true;
|
|
22
|
+
} else {
|
|
23
|
+
return stem.endsWith(mappingsSuffixForAutomation);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function findEventMappings(srcDir) {
|
|
28
|
+
let dict = {};
|
|
29
|
+
let parentDirIsAggregate = relSegments => {
|
|
30
|
+
let n = relSegments.length;
|
|
31
|
+
if (n === 0) {
|
|
32
|
+
return false;
|
|
33
|
+
}
|
|
34
|
+
let parent = relSegments[n - 1 | 0];
|
|
35
|
+
if (parent === "Aggregate") {
|
|
36
|
+
return true;
|
|
37
|
+
} else {
|
|
38
|
+
return parent === "EventMappings";
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
let walk = (dir, segments) => {
|
|
42
|
+
Generator_Node$Reventless.readDir(dir).forEach(entry => {
|
|
43
|
+
let name = entry.name;
|
|
44
|
+
if (entry.isDirectory()) {
|
|
45
|
+
if (name !== "Plugin" && name !== "tests" && name !== "lib") {
|
|
46
|
+
return walk(Nodepath.join(dir, name), segments.concat([name]));
|
|
47
|
+
} else {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
if (!entry.isFile()) {
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
if (name.endsWith("_Mappings.res") && parentDirIsAggregate(segments)) {
|
|
55
|
+
let stem = name.slice(0, name.length - 4 | 0);
|
|
56
|
+
let aggName = stem.slice(0, stem.length - 9 | 0);
|
|
57
|
+
dict[aggName] = stem;
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
if (!name.endsWith("_EventMappings.res")) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
let stem$1 = name.slice(0, name.length - 4 | 0);
|
|
64
|
+
let aggName$1 = stem$1.slice(0, stem$1.length - 14 | 0);
|
|
65
|
+
let match = dict[aggName$1];
|
|
66
|
+
if (match !== undefined) {
|
|
67
|
+
return;
|
|
68
|
+
} else {
|
|
69
|
+
dict[aggName$1] = stem$1;
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
};
|
|
74
|
+
if (Nodefs.existsSync(srcDir)) {
|
|
75
|
+
walk(srcDir, []);
|
|
76
|
+
}
|
|
77
|
+
return dict;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function sortedStems(stems) {
|
|
81
|
+
return stems.toSorted((a, b) => {
|
|
82
|
+
if (a < b) {
|
|
83
|
+
return -1.0;
|
|
84
|
+
} else if (a > b) {
|
|
85
|
+
return 1.0;
|
|
86
|
+
} else {
|
|
87
|
+
return 0.0;
|
|
88
|
+
}
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function extractTargetName(filePath) {
|
|
93
|
+
try {
|
|
94
|
+
let content = Nodefs.readFileSync(filePath, "utf8");
|
|
95
|
+
let result = {
|
|
96
|
+
contents: undefined
|
|
97
|
+
};
|
|
98
|
+
content.split("\n").forEach(line => {
|
|
99
|
+
let trimmed = line.trimStart();
|
|
100
|
+
if (!trimmed.startsWith("let targetName = ")) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
let firstQuote = trimmed.indexOf("\"");
|
|
104
|
+
let lastQuote = trimmed.lastIndexOf("\"");
|
|
105
|
+
if (firstQuote >= 0 && lastQuote > firstQuote) {
|
|
106
|
+
result.contents = trimmed.slice(firstQuote + 1 | 0, lastQuote);
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
return result.contents;
|
|
111
|
+
} catch (exn) {
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function hasFileAttribute(filePath, attr) {
|
|
117
|
+
try {
|
|
118
|
+
let content = Nodefs.readFileSync(filePath, "utf8");
|
|
119
|
+
let found = {
|
|
120
|
+
contents: false
|
|
121
|
+
};
|
|
122
|
+
content.split("\n").forEach(line => {
|
|
123
|
+
let trimmed = line.trimStart();
|
|
124
|
+
if (trimmed.startsWith(attr + " ") || trimmed.startsWith(attr + "(") || trimmed === attr) {
|
|
125
|
+
found.contents = true;
|
|
126
|
+
return;
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
return found.contents;
|
|
130
|
+
} catch (exn) {
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function hasAsyncAttribute(filePath) {
|
|
136
|
+
return hasFileAttribute(filePath, "@@reventless.async");
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function hasSystemCallableAttribute(filePath) {
|
|
140
|
+
return hasFileAttribute(filePath, "@@reventless.systemCallable");
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function effectiveSpecName(filePath, stem) {
|
|
144
|
+
try {
|
|
145
|
+
let content = Nodefs.readFileSync(filePath, "utf8");
|
|
146
|
+
let name = {
|
|
147
|
+
contents: undefined
|
|
148
|
+
};
|
|
149
|
+
content.split("\n").forEach(line => {
|
|
150
|
+
let trimmed = line.trimStart();
|
|
151
|
+
if (!trimmed.startsWith("@@reventless.spec(")) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
let firstQuote = trimmed.indexOf("\"");
|
|
155
|
+
let lastQuote = trimmed.lastIndexOf("\"");
|
|
156
|
+
if (firstQuote >= 0 && lastQuote > firstQuote) {
|
|
157
|
+
name.contents = trimmed.slice(firstQuote + 1 | 0, lastQuote);
|
|
158
|
+
return;
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return Stdlib_Option.getOr(name.contents, stem);
|
|
162
|
+
} catch (exn) {
|
|
163
|
+
return stem;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function groupExtensionPoints(epFiles) {
|
|
168
|
+
let epByGroup = {};
|
|
169
|
+
let flatEpMappings = [];
|
|
170
|
+
epFiles.forEach(param => {
|
|
171
|
+
let epGroup = param.epGroup;
|
|
172
|
+
let stem = param.stem;
|
|
173
|
+
if (epGroup !== undefined) {
|
|
174
|
+
let arr = epByGroup[epGroup];
|
|
175
|
+
if (arr !== undefined) {
|
|
176
|
+
arr.push(stem);
|
|
177
|
+
} else {
|
|
178
|
+
epByGroup[epGroup] = [stem];
|
|
179
|
+
}
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
flatEpMappings.push(stem);
|
|
183
|
+
});
|
|
184
|
+
let extensionPoints = [];
|
|
185
|
+
if (flatEpMappings.length !== 0) {
|
|
186
|
+
extensionPoints.push({
|
|
187
|
+
group: undefined,
|
|
188
|
+
mappings: sortedStems(flatEpMappings)
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
Object.entries(epByGroup).forEach(param => {
|
|
192
|
+
extensionPoints.push({
|
|
193
|
+
group: param[0],
|
|
194
|
+
mappings: sortedStems(param[1])
|
|
195
|
+
});
|
|
196
|
+
});
|
|
197
|
+
return extensionPoints;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function resolve(discovered, srcDir) {
|
|
201
|
+
let eventMappings = findEventMappings(srcDir);
|
|
202
|
+
let stateChangeSlices = [];
|
|
203
|
+
let stateViewSlices = [];
|
|
204
|
+
let stateViewSlicesStream = [];
|
|
205
|
+
let automationSlices = [];
|
|
206
|
+
let inboundTranslationSlices = [];
|
|
207
|
+
let outboundTranslationSlices = [];
|
|
208
|
+
let automationSliceRelPaths = {};
|
|
209
|
+
let inboundTranslationSliceRelPaths = {};
|
|
210
|
+
let outboundTranslationSliceRelPaths = {};
|
|
211
|
+
let aggregateSpecs = [];
|
|
212
|
+
let aggregateSpecRelPaths = {};
|
|
213
|
+
let stateChangeSliceRelPaths = {};
|
|
214
|
+
let stateViewSliceRelPaths = {};
|
|
215
|
+
let aggregateBehaviors = [];
|
|
216
|
+
let readModelStems = [];
|
|
217
|
+
let readModelStreamStems = [];
|
|
218
|
+
let projectionsByRelPath = {};
|
|
219
|
+
let tasks = [];
|
|
220
|
+
let epFiles = [];
|
|
221
|
+
let extensions = [];
|
|
222
|
+
discovered.forEach(param => {
|
|
223
|
+
let relPath = param.relPath;
|
|
224
|
+
let componentType = param.componentType;
|
|
225
|
+
let stem = param.stem;
|
|
226
|
+
switch (componentType) {
|
|
227
|
+
case "StateChangeSlice" :
|
|
228
|
+
if (!isImplStem(stem)) {
|
|
229
|
+
stateChangeSlices.push(stem);
|
|
230
|
+
stateChangeSliceRelPaths[stem] = relPath;
|
|
231
|
+
return;
|
|
232
|
+
} else {
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
case "StateViewSlice" :
|
|
236
|
+
if (!isImplStem(stem)) {
|
|
237
|
+
stateViewSlices.push(stem);
|
|
238
|
+
stateViewSliceRelPaths[stem] = relPath;
|
|
239
|
+
return;
|
|
240
|
+
} else {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
case "StateViewSliceStream" :
|
|
244
|
+
if (!isImplStem(stem)) {
|
|
245
|
+
stateViewSlicesStream.push(stem);
|
|
246
|
+
stateViewSliceRelPaths[stem] = relPath;
|
|
247
|
+
return;
|
|
248
|
+
} else {
|
|
249
|
+
return;
|
|
250
|
+
}
|
|
251
|
+
case "AutomationSlice" :
|
|
252
|
+
if (!isImplStem(stem)) {
|
|
253
|
+
automationSlices.push(stem);
|
|
254
|
+
automationSliceRelPaths[stem] = relPath;
|
|
255
|
+
return;
|
|
256
|
+
} else {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
case "InboundTranslationSlice" :
|
|
260
|
+
if (!isImplStem(stem)) {
|
|
261
|
+
inboundTranslationSlices.push(stem);
|
|
262
|
+
inboundTranslationSliceRelPaths[stem] = relPath;
|
|
263
|
+
return;
|
|
264
|
+
} else {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
case "OutboundTranslationSlice" :
|
|
268
|
+
if (!isImplStem(stem)) {
|
|
269
|
+
outboundTranslationSlices.push(stem);
|
|
270
|
+
outboundTranslationSliceRelPaths[stem] = relPath;
|
|
271
|
+
return;
|
|
272
|
+
} else {
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
case "Aggregate" :
|
|
276
|
+
if (stem.endsWith("Behavior")) {
|
|
277
|
+
aggregateBehaviors.push(stem);
|
|
278
|
+
} else {
|
|
279
|
+
aggregateSpecs.push(stem);
|
|
280
|
+
aggregateSpecRelPaths[stem] = relPath;
|
|
281
|
+
}
|
|
282
|
+
return;
|
|
283
|
+
case "ReadModel" :
|
|
284
|
+
if (stem.endsWith("Projections")) {
|
|
285
|
+
projectionsByRelPath[stem] = relPath;
|
|
286
|
+
} else {
|
|
287
|
+
readModelStems.push(stem);
|
|
288
|
+
}
|
|
289
|
+
return;
|
|
290
|
+
case "ReadModelStream" :
|
|
291
|
+
if (stem.endsWith("Projections")) {
|
|
292
|
+
projectionsByRelPath[stem] = relPath;
|
|
293
|
+
} else {
|
|
294
|
+
readModelStreamStems.push(stem);
|
|
295
|
+
}
|
|
296
|
+
return;
|
|
297
|
+
case "Task" :
|
|
298
|
+
tasks.push(stem);
|
|
299
|
+
return;
|
|
300
|
+
case "ExtensionPoint" :
|
|
301
|
+
epFiles.push({
|
|
302
|
+
stem: stem,
|
|
303
|
+
componentType: componentType,
|
|
304
|
+
epGroup: param.epGroup,
|
|
305
|
+
relPath: relPath
|
|
306
|
+
});
|
|
307
|
+
return;
|
|
308
|
+
case "Extension" :
|
|
309
|
+
extensions.push(stem);
|
|
310
|
+
return;
|
|
311
|
+
}
|
|
312
|
+
});
|
|
313
|
+
let aggregates = Stdlib_Array.filterMap(sortedStems(aggregateSpecs), spec => {
|
|
314
|
+
let underscored = spec + "_Behavior";
|
|
315
|
+
let bare = spec + "Behavior";
|
|
316
|
+
let behaviorStem = aggregateBehaviors.includes(underscored) ? underscored : (
|
|
317
|
+
aggregateBehaviors.includes(bare) ? bare : undefined
|
|
318
|
+
);
|
|
319
|
+
if (behaviorStem !== undefined) {
|
|
320
|
+
let relPath = aggregateSpecRelPaths[spec];
|
|
321
|
+
let isAsync = relPath !== undefined ? hasFileAttribute(Nodepath.join(srcDir, relPath), "@@reventless.async") : false;
|
|
322
|
+
return {
|
|
323
|
+
spec: spec,
|
|
324
|
+
behavior: behaviorStem,
|
|
325
|
+
eventMappings: eventMappings[spec],
|
|
326
|
+
isAsync: isAsync
|
|
327
|
+
};
|
|
328
|
+
}
|
|
329
|
+
console.warn("Generator: Aggregate spec `" + spec + "` has no matching `" + underscored + "` or `" + bare + "` — skipping");
|
|
330
|
+
});
|
|
331
|
+
let pairReadModel = (stream, rm) => {
|
|
332
|
+
let baseName = rm.endsWith("ReadModel") ? rm.slice(0, rm.length - 9 | 0) : rm;
|
|
333
|
+
let underscoredProj = baseName + "_Projections";
|
|
334
|
+
let match = projectionsByRelPath[underscoredProj];
|
|
335
|
+
if (match !== undefined) {
|
|
336
|
+
return {
|
|
337
|
+
readModel: rm,
|
|
338
|
+
projections: underscoredProj,
|
|
339
|
+
stream: stream
|
|
340
|
+
};
|
|
341
|
+
} else {
|
|
342
|
+
console.warn("Generator: ReadModel `" + rm + "` has no matching `" + underscoredProj + ".res` — skipping");
|
|
343
|
+
return;
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
let readModels = Stdlib_Array.filterMap(sortedStems(readModelStems), rm => pairReadModel(false, rm)).concat(Stdlib_Array.filterMap(sortedStems(readModelStreamStems), rm => pairReadModel(true, rm)));
|
|
347
|
+
let extensionPoints = groupExtensionPoints(epFiles);
|
|
348
|
+
let buildTargets = (stems, relPaths) => {
|
|
349
|
+
let dict = {};
|
|
350
|
+
stems.forEach(stem => {
|
|
351
|
+
let relPath = relPaths[stem];
|
|
352
|
+
let target = relPath !== undefined ? extractTargetName(Nodepath.join(srcDir, relPath)) : undefined;
|
|
353
|
+
dict[stem] = target;
|
|
354
|
+
});
|
|
355
|
+
return dict;
|
|
356
|
+
};
|
|
357
|
+
let d = {};
|
|
358
|
+
let names = [];
|
|
359
|
+
let collect = (stems, relPaths) => {
|
|
360
|
+
stems.forEach(stem => {
|
|
361
|
+
let relPath = relPaths[stem];
|
|
362
|
+
if (relPath === undefined) {
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
let filePath = Nodepath.join(srcDir, relPath);
|
|
366
|
+
if (hasFileAttribute(filePath, "@@reventless.systemCallable")) {
|
|
367
|
+
names.push(effectiveSpecName(filePath, stem));
|
|
368
|
+
return;
|
|
369
|
+
}
|
|
370
|
+
});
|
|
371
|
+
};
|
|
372
|
+
return {
|
|
373
|
+
stateChangeSlices: sortedStems(stateChangeSlices),
|
|
374
|
+
stateViewSlices: sortedStems(stateViewSlices),
|
|
375
|
+
stateViewSlicesStream: sortedStems(stateViewSlicesStream),
|
|
376
|
+
automationSlices: sortedStems(automationSlices),
|
|
377
|
+
inboundTranslationSlices: sortedStems(inboundTranslationSlices),
|
|
378
|
+
outboundTranslationSlices: sortedStems(outboundTranslationSlices),
|
|
379
|
+
automationSliceTargets: buildTargets(automationSlices, automationSliceRelPaths),
|
|
380
|
+
inboundTranslationSliceTargets: buildTargets(inboundTranslationSlices, inboundTranslationSliceRelPaths),
|
|
381
|
+
outboundTranslationSliceTargets: buildTargets(outboundTranslationSlices, outboundTranslationSliceRelPaths),
|
|
382
|
+
asyncStateChangeSlices: (stateChangeSlices.forEach(stem => {
|
|
383
|
+
let relPath = stateChangeSliceRelPaths[stem];
|
|
384
|
+
if (relPath !== undefined && hasFileAttribute(Nodepath.join(srcDir, relPath), "@@reventless.async")) {
|
|
385
|
+
d[stem] = true;
|
|
386
|
+
return;
|
|
387
|
+
}
|
|
388
|
+
}), d),
|
|
389
|
+
systemCallableComponents: (collect(stateChangeSlices, stateChangeSliceRelPaths), collect(stateViewSlices, stateViewSliceRelPaths), collect(stateViewSlicesStream, stateViewSliceRelPaths), sortedStems(names)),
|
|
390
|
+
aggregates: aggregates,
|
|
391
|
+
readModels: readModels,
|
|
392
|
+
tasks: sortedStems(tasks),
|
|
393
|
+
extensionPoints: extensionPoints,
|
|
394
|
+
extensions: sortedStems(extensions)
|
|
395
|
+
};
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
export {
|
|
399
|
+
implSuffixForStateChange,
|
|
400
|
+
implSuffixForStateView,
|
|
401
|
+
implSuffixForAutomation,
|
|
402
|
+
implSuffixForTranslation,
|
|
403
|
+
mappingsSuffixForAutomation,
|
|
404
|
+
isImplStem,
|
|
405
|
+
findEventMappings,
|
|
406
|
+
sortedStems,
|
|
407
|
+
extractTargetName,
|
|
408
|
+
hasFileAttribute,
|
|
409
|
+
hasAsyncAttribute,
|
|
410
|
+
hasSystemCallableAttribute,
|
|
411
|
+
effectiveSpecName,
|
|
412
|
+
groupExtensionPoints,
|
|
413
|
+
resolve,
|
|
414
|
+
}
|
|
415
|
+
/* node:fs Not a pure module */
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
// Pure rendering for the platform generator: union the plugins' capability
|
|
2
|
+
// manifests and emit the committed `PlatformCapabilities.res`.
|
|
3
|
+
//
|
|
4
|
+
// Only the derived list is generated — the platform root stays hand-written
|
|
5
|
+
// and reads `PlatformCapabilities.capabilities`. That keeps a requirement
|
|
6
|
+
// change small and entirely about capabilities in review, and it keeps the
|
|
7
|
+
// generator out of the business of expressing everything a real root does
|
|
8
|
+
// (bootstrap calls, capability handles, stack references).
|
|
9
|
+
//
|
|
10
|
+
// I/O-free so the exact bytes are testable; `PlatformGenerator` owns the file
|
|
11
|
+
// system.
|
|
12
|
+
|
|
13
|
+
/** One plugin's manifest, tagged with the name `deploy-manifest.yaml` lists it
|
|
14
|
+
under — the name provenance comments cite. */
|
|
15
|
+
type pluginManifest = {
|
|
16
|
+
pluginName: string,
|
|
17
|
+
manifest: CapabilityManifest.t,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** One declaring site, keeping which plugin's manifest carried it. */
|
|
21
|
+
type provenance = {
|
|
22
|
+
pluginName: string,
|
|
23
|
+
component: string,
|
|
24
|
+
field: string,
|
|
25
|
+
annotation: option<string>,
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** One capability the platform must provision: the qualified `{plugin}.{store}`
|
|
29
|
+
key with every declaring site across every plugin. Identity is the key —
|
|
30
|
+
many fields, in several plugins, legitimately name one store. */
|
|
31
|
+
type unionEntry = {
|
|
32
|
+
kind: CapabilityManifest.kind,
|
|
33
|
+
key: string,
|
|
34
|
+
declaredBy: array<provenance>,
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Union the manifests: entries with one key collapse to one capability, their
|
|
38
|
+
provenance concatenated in manifest order. Sorted by key so the rendered
|
|
39
|
+
file is deterministic. */
|
|
40
|
+
let union = (manifests: array<pluginManifest>): array<unionEntry> => {
|
|
41
|
+
let byKey: dict<unionEntry> = Dict.make()
|
|
42
|
+
manifests->Array.forEach(({pluginName, manifest}) =>
|
|
43
|
+
manifest.capabilities->Array.forEach(entry => {
|
|
44
|
+
let sites =
|
|
45
|
+
entry.declaredBy->Array.map(site => {
|
|
46
|
+
pluginName,
|
|
47
|
+
component: site.component,
|
|
48
|
+
field: site.field,
|
|
49
|
+
annotation: site.annotation,
|
|
50
|
+
})
|
|
51
|
+
switch byKey->Dict.get(entry.key) {
|
|
52
|
+
| Some(existing) =>
|
|
53
|
+
byKey->Dict.set(entry.key, {...existing, declaredBy: existing.declaredBy->Array.concat(sites)})
|
|
54
|
+
| None => byKey->Dict.set(entry.key, {kind: entry.kind, key: entry.key, declaredBy: sites})
|
|
55
|
+
}
|
|
56
|
+
})
|
|
57
|
+
)
|
|
58
|
+
byKey
|
|
59
|
+
->Dict.valuesToArray
|
|
60
|
+
->Array.toSorted((a, b) => String.compare(a.key, b.key))
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
let quote = (s: string): string => s->JSON.Encode.string->JSON.stringify
|
|
64
|
+
|
|
65
|
+
// A key is `{plugin}.{store}` by construction (`Plugin_Structure` qualifies
|
|
66
|
+
// every declaration). The split is at the first dot: the plugin's registered
|
|
67
|
+
// name cannot contain one, the store name could in principle.
|
|
68
|
+
let splitKey = (key: string): option<(string, string)> =>
|
|
69
|
+
key
|
|
70
|
+
->String.indexOfOpt(".")
|
|
71
|
+
->Option.map(i => (
|
|
72
|
+
key->String.slice(~start=0, ~end=i),
|
|
73
|
+
key->String.slice(~start=i + 1),
|
|
74
|
+
))
|
|
75
|
+
|
|
76
|
+
// The provenance comment quotes the annotation as its author wrote it — taken
|
|
77
|
+
// from the manifest, which recorded it at the one place the owning plugin was
|
|
78
|
+
// unambiguous. Inferring it here is what this replaced: the manifest carries a
|
|
79
|
+
// deploy-manifest entry name and the key carries the registered name, and no
|
|
80
|
+
// rule relates the two (`platform-inspector` against `PlatformInspector` is an
|
|
81
|
+
// ordinary pairing), so any comparison here eventually quotes a string that is
|
|
82
|
+
// in no source file.
|
|
83
|
+
//
|
|
84
|
+
// A site with no recorded annotation predates the recording. It still names the
|
|
85
|
+
// field, which is the comment's job; it just makes no claim about the source
|
|
86
|
+
// text it cannot see.
|
|
87
|
+
let renderEntry = (entry: unionEntry): result<array<string>, string> =>
|
|
88
|
+
switch splitKey(entry.key) {
|
|
89
|
+
| None =>
|
|
90
|
+
Error(
|
|
91
|
+
`malformed capability key ${quote(entry.key)} — expected "{plugin}.{store}" (is the plugin's capabilities.json hand-edited?)`,
|
|
92
|
+
)
|
|
93
|
+
| Some((plugin, store)) => {
|
|
94
|
+
let comments =
|
|
95
|
+
entry.declaredBy->Array.map(site => {
|
|
96
|
+
let site_ = ` // ${site.pluginName}: ${site.component}.${site.field}`
|
|
97
|
+
switch site.annotation {
|
|
98
|
+
| Some(annotation) => `${site_} @storageRef(${quote(annotation)})`
|
|
99
|
+
| None => site_
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
let line = switch entry.kind {
|
|
103
|
+
| ObjectStore => ` ObjectStore({plugin: ${quote(plugin)}, store: ${quote(store)}}),`
|
|
104
|
+
}
|
|
105
|
+
Ok(comments->Array.concat([line]))
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
Two deployables each claiming to own one plugin name.
|
|
111
|
+
|
|
112
|
+
Nothing else in the toolchain catches this. The deploy manifest's keys are
|
|
113
|
+
*deployable* names with no rule relating them to registered ones, and the Plugin
|
|
114
|
+
aggregate is keyed by the registered name — so a second plugin registering an
|
|
115
|
+
existing name is not rejected but read as a **new version of the first**, and
|
|
116
|
+
silently supersedes it in the registry.
|
|
117
|
+
|
|
118
|
+
Ownership is read from the annotation as authored, which is the one signal that
|
|
119
|
+
separates a duplicate from legitimate sharing: an **unqualified**
|
|
120
|
+
`@storageRef("productImages")` means "a store of my own plugin", so the
|
|
121
|
+
deployable that wrote it is the plugin the key names. A **qualified**
|
|
122
|
+
`@storageRef("Catalog.productImages")` points at someone else's store and says
|
|
123
|
+
nothing about who owns it — that is the sanctioned cross-plugin form and must not
|
|
124
|
+
trip this.
|
|
125
|
+
|
|
126
|
+
Partial by construction: it can only see plugins that declare a store, because
|
|
127
|
+
capability manifests are the only per-plugin input the generator reads. A
|
|
128
|
+
duplicate between two store-less plugins is invisible here and stays uncaught.
|
|
129
|
+
*/
|
|
130
|
+
let duplicatePluginOwners = (entries: array<unionEntry>): array<(string, array<string>)> => {
|
|
131
|
+
let owners: dict<array<string>> = Dict.make()
|
|
132
|
+
entries->Array.forEach(entry =>
|
|
133
|
+
switch splitKey(entry.key) {
|
|
134
|
+
| None => ()
|
|
135
|
+
| Some((plugin, _)) =>
|
|
136
|
+
entry.declaredBy->Array.forEach(site =>
|
|
137
|
+
switch site.annotation {
|
|
138
|
+
// A site with no recorded annotation predates the recording, so it
|
|
139
|
+
// cannot be read either way and is skipped rather than guessed at.
|
|
140
|
+
| Some(annotation) if !(annotation->String.includes(".")) =>
|
|
141
|
+
let claimants = owners->Dict.get(plugin)->Option.getOr([])
|
|
142
|
+
if !(claimants->Array.includes(site.pluginName)) {
|
|
143
|
+
owners->Dict.set(plugin, Array.concat(claimants, [site.pluginName]))
|
|
144
|
+
}
|
|
145
|
+
| _ => ()
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
)
|
|
150
|
+
owners
|
|
151
|
+
->Dict.toArray
|
|
152
|
+
->Array.filter(((_, claimants)) => claimants->Array.length > 1)
|
|
153
|
+
->Array.toSorted(((a, _), (b, _)) => String.compare(a, b))
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
let duplicatePluginMessage = ((plugin, claimants): (string, array<string>)): string =>
|
|
157
|
+
`Plugin name "${plugin}" is registered by more than one deployable: ${claimants->Array.join(
|
|
158
|
+
", ",
|
|
159
|
+
)}.\n` ++
|
|
160
|
+
` A platform keys its plugin registry by name, so the second registration is read as a new ` ++
|
|
161
|
+
`VERSION of the first and supersedes it.\n` ++
|
|
162
|
+
` Give each deployable's plugin a distinct name (plugin.json), or — if these were meant to be ` ++
|
|
163
|
+
`one plugin — deploy only one of them.`
|
|
164
|
+
|
|
165
|
+
let header = [
|
|
166
|
+
"// AUTO-GENERATED — do not edit. Run `pnpm run generate:platform` to update.",
|
|
167
|
+
"//",
|
|
168
|
+
"// The platform's capability list, unioned from the committed",
|
|
169
|
+
"// `capabilities.json` manifests of the plugins deploy-manifest.yaml names.",
|
|
170
|
+
"// Each entry keeps its declaring fields as provenance, so when a capability",
|
|
171
|
+
"// disappears from this list, the diff says which field's change removed it.",
|
|
172
|
+
"",
|
|
173
|
+
]
|
|
174
|
+
|
|
175
|
+
/** Render the complete `PlatformCapabilities.res`. Deterministic and
|
|
176
|
+
newline-terminated: regenerating with no manifest change is byte-identical,
|
|
177
|
+
so the committed file never churns. */
|
|
178
|
+
let render = (manifests: array<pluginManifest>): result<string, string> => {
|
|
179
|
+
let entries = union(manifests)
|
|
180
|
+
let rendered = entries->Array.map(renderEntry)
|
|
181
|
+
switch (
|
|
182
|
+
duplicatePluginOwners(entries),
|
|
183
|
+
rendered->Array.findMap(r =>
|
|
184
|
+
switch r {
|
|
185
|
+
| Error(e) => Some(e)
|
|
186
|
+
| Ok(_) => None
|
|
187
|
+
}
|
|
188
|
+
),
|
|
189
|
+
) {
|
|
190
|
+
// Generation is the earliest point a deployment's plugins are seen together,
|
|
191
|
+
// so a name two of them both claim is refused here rather than left to
|
|
192
|
+
// supersede one of them at runtime.
|
|
193
|
+
| (duplicates, _) if duplicates->Array.length > 0 =>
|
|
194
|
+
Error(duplicates->Array.map(duplicatePluginMessage)->Array.join("\n\n"))
|
|
195
|
+
| (_, Some(e)) => Error(e)
|
|
196
|
+
| (_, None) => {
|
|
197
|
+
let body = if entries->Array.length == 0 {
|
|
198
|
+
["let capabilities: array<ReventlessInfra.Platform.capability> = []"]
|
|
199
|
+
} else {
|
|
200
|
+
["let capabilities: array<ReventlessInfra.Platform.capability> = ["]
|
|
201
|
+
->Array.concat(rendered->Array.flatMap(r => r->Result.getOr([])))
|
|
202
|
+
->Array.concat(["]"])
|
|
203
|
+
}
|
|
204
|
+
Ok(header->Array.concat(body)->Array.join("\n") ++ "\n")
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|