@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,423 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
4
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
5
|
+
import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
|
|
6
|
+
import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
|
|
7
|
+
import * as DcbTag$Reventless from "./DcbTag.res.mjs";
|
|
8
|
+
|
|
9
|
+
function extractVariantInfo(variantSchema) {
|
|
10
|
+
if (variantSchema.type !== "object") {
|
|
11
|
+
return;
|
|
12
|
+
}
|
|
13
|
+
let properties = variantSchema.properties;
|
|
14
|
+
return Stdlib_Option.map(DcbTag$Reventless.variantTagName(variantSchema.items), tagName => {
|
|
15
|
+
let taggedFields = Stdlib_Array.filterMap(Object.entries(properties), param => {
|
|
16
|
+
if (DcbTag$Reventless.isTagged(param[1])) {
|
|
17
|
+
return param[0];
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
return {
|
|
21
|
+
tagName: tagName,
|
|
22
|
+
fields: properties,
|
|
23
|
+
taggedFields: taggedFields
|
|
24
|
+
};
|
|
25
|
+
});
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function extractAllVariants(schema) {
|
|
29
|
+
switch (schema.type) {
|
|
30
|
+
case "string" :
|
|
31
|
+
let name = schema.const;
|
|
32
|
+
if (name !== undefined) {
|
|
33
|
+
return [{
|
|
34
|
+
tagName: name,
|
|
35
|
+
fields: {},
|
|
36
|
+
taggedFields: []
|
|
37
|
+
}];
|
|
38
|
+
} else {
|
|
39
|
+
return [];
|
|
40
|
+
}
|
|
41
|
+
case "object" :
|
|
42
|
+
let info = extractVariantInfo(schema);
|
|
43
|
+
if (info !== undefined) {
|
|
44
|
+
return [info];
|
|
45
|
+
} else {
|
|
46
|
+
return [];
|
|
47
|
+
}
|
|
48
|
+
case "union" :
|
|
49
|
+
return Stdlib_Array.filterMap(schema.anyOf, extractVariantInfo);
|
|
50
|
+
default:
|
|
51
|
+
return [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isPayloadLess(info) {
|
|
56
|
+
return Object.entries(info.fields).length === 0;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function schemasAreCompatible(_a, _b) {
|
|
60
|
+
while (true) {
|
|
61
|
+
let b = _b;
|
|
62
|
+
let a = _a;
|
|
63
|
+
switch (a.type) {
|
|
64
|
+
case "string" :
|
|
65
|
+
return b.type === "string";
|
|
66
|
+
case "number" :
|
|
67
|
+
return b.type === "number";
|
|
68
|
+
case "bigint" :
|
|
69
|
+
return b.type === "bigint";
|
|
70
|
+
case "boolean" :
|
|
71
|
+
return b.type === "boolean";
|
|
72
|
+
case "array" :
|
|
73
|
+
let aItem = a.additionalItems;
|
|
74
|
+
if (aItem === "strip" || aItem === "strict") {
|
|
75
|
+
return false;
|
|
76
|
+
}
|
|
77
|
+
if (b.type !== "array") {
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
let bItem = b.additionalItems;
|
|
81
|
+
if (bItem === "strip" || bItem === "strict") {
|
|
82
|
+
return false;
|
|
83
|
+
}
|
|
84
|
+
_b = bItem;
|
|
85
|
+
_a = aItem;
|
|
86
|
+
continue;
|
|
87
|
+
case "object" :
|
|
88
|
+
if (b.type === "object") {
|
|
89
|
+
return propsCompatible(a.properties, b.properties);
|
|
90
|
+
} else {
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
case "union" :
|
|
94
|
+
if (b.type !== "union") {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
let av = extractAllVariants(a);
|
|
98
|
+
let bv = extractAllVariants(b);
|
|
99
|
+
let at = av.map(v => v.tagName).toSorted(Primitive_string.compare);
|
|
100
|
+
let bt = bv.map(v => v.tagName).toSorted(Primitive_string.compare);
|
|
101
|
+
if (Primitive_object.equal(at, bt)) {
|
|
102
|
+
return at.every(tag => {
|
|
103
|
+
let match = av.find(v => v.tagName === tag);
|
|
104
|
+
let match$1 = bv.find(v => v.tagName === tag);
|
|
105
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
106
|
+
return propsCompatible(match.fields, match$1.fields);
|
|
107
|
+
} else {
|
|
108
|
+
return false;
|
|
109
|
+
}
|
|
110
|
+
});
|
|
111
|
+
} else {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
default:
|
|
115
|
+
return false;
|
|
116
|
+
}
|
|
117
|
+
};
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function propsCompatible(aProps, bProps) {
|
|
121
|
+
let aKeys = Object.keys(aProps).toSorted(Primitive_string.compare);
|
|
122
|
+
let bKeys = Object.keys(bProps).toSorted(Primitive_string.compare);
|
|
123
|
+
if (Primitive_object.equal(aKeys, bKeys)) {
|
|
124
|
+
return aKeys.every(k => {
|
|
125
|
+
let match = aProps[k];
|
|
126
|
+
let match$1 = bProps[k];
|
|
127
|
+
if (match !== undefined && match$1 !== undefined) {
|
|
128
|
+
return schemasAreCompatible(match, match$1);
|
|
129
|
+
} else {
|
|
130
|
+
return false;
|
|
131
|
+
}
|
|
132
|
+
});
|
|
133
|
+
} else {
|
|
134
|
+
return false;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function schemaTypeName(schema) {
|
|
139
|
+
switch (schema.type) {
|
|
140
|
+
case "never" :
|
|
141
|
+
return "never";
|
|
142
|
+
case "string" :
|
|
143
|
+
return "string";
|
|
144
|
+
case "number" :
|
|
145
|
+
return "float";
|
|
146
|
+
case "bigint" :
|
|
147
|
+
return "bigint";
|
|
148
|
+
case "boolean" :
|
|
149
|
+
return "bool";
|
|
150
|
+
case "array" :
|
|
151
|
+
return "array";
|
|
152
|
+
case "object" :
|
|
153
|
+
return "object";
|
|
154
|
+
case "union" :
|
|
155
|
+
return "union";
|
|
156
|
+
case "ref" :
|
|
157
|
+
return "ref";
|
|
158
|
+
default:
|
|
159
|
+
return "unknown";
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function dedupeKeys(keys) {
|
|
164
|
+
let seen = new Set();
|
|
165
|
+
return keys.filter(k => {
|
|
166
|
+
if (seen.has(k)) {
|
|
167
|
+
return false;
|
|
168
|
+
} else {
|
|
169
|
+
seen.add(k);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
});
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
function validateCompositeReads(slices, producedTagKeys) {
|
|
176
|
+
let warnings = [];
|
|
177
|
+
slices.forEach(param => {
|
|
178
|
+
let commandSchema = param[1];
|
|
179
|
+
let sliceName = param[0];
|
|
180
|
+
if (DcbTag$Reventless.hasTaggedArrayFields(commandSchema)) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
let commandTagKeysByVariant = DcbTag$Reventless.extractTagKeysByEventType(commandSchema);
|
|
184
|
+
let consumedTypes = DcbTag$Reventless.extractVariantNames(param[2]);
|
|
185
|
+
Object.values(commandTagKeysByVariant).forEach(cmdKeys => {
|
|
186
|
+
let querySet = dedupeKeys(cmdKeys);
|
|
187
|
+
if (querySet.length >= 2) {
|
|
188
|
+
consumedTypes.forEach(consumedType => {
|
|
189
|
+
let producedKeys = producedTagKeys[consumedType];
|
|
190
|
+
if (producedKeys === undefined) {
|
|
191
|
+
return;
|
|
192
|
+
}
|
|
193
|
+
let producedSet = dedupeKeys(producedKeys);
|
|
194
|
+
let isSuperset = querySet.every(k => producedSet.includes(k));
|
|
195
|
+
if (!(isSuperset && producedSet.length > querySet.length)) {
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
let extra = producedSet.filter(k => !querySet.includes(k));
|
|
199
|
+
warnings.push({
|
|
200
|
+
sliceName: sliceName,
|
|
201
|
+
message: `composite read on tags [` + querySet.join(", ") + `] will silently miss '` + consumedType + `', which also carries [` + extra.join(", ") + `] — the tag_composite key includes ALL of an event's tags, so a composite query matches only events tagged exactly [` + querySet.join(", ") + `]. Align the event's tag set with the query, or read '` + consumedType + `' via a single-tag clause.`
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
});
|
|
207
|
+
});
|
|
208
|
+
return warnings;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function validateProducedAndConsumed(produced, consumed) {
|
|
212
|
+
let errors = [];
|
|
213
|
+
let producerMap = {};
|
|
214
|
+
produced.forEach(param => {
|
|
215
|
+
let sliceName = param[0];
|
|
216
|
+
let variants = extractAllVariants(param[1]);
|
|
217
|
+
variants.forEach(variant => {
|
|
218
|
+
let existing = Stdlib_Option.getOr(producerMap[variant.tagName], []);
|
|
219
|
+
producerMap[variant.tagName] = existing.concat([{
|
|
220
|
+
sliceName: sliceName,
|
|
221
|
+
variant: variant
|
|
222
|
+
}]);
|
|
223
|
+
});
|
|
224
|
+
});
|
|
225
|
+
Object.entries(producerMap).forEach(param => {
|
|
226
|
+
let entries = param[1];
|
|
227
|
+
if (entries.length <= 1) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
let tagName = param[0];
|
|
231
|
+
let first = entries[0];
|
|
232
|
+
let firstFields = Object.keys(first.variant.fields).toSorted(Primitive_string.compare);
|
|
233
|
+
let firstTagged = first.variant.taggedFields.toSorted(Primitive_string.compare);
|
|
234
|
+
entries.forEach((entry, idx) => {
|
|
235
|
+
if (idx <= 0) {
|
|
236
|
+
return;
|
|
237
|
+
}
|
|
238
|
+
let entryFields = Object.keys(entry.variant.fields).toSorted(Primitive_string.compare);
|
|
239
|
+
let entryTagged = entry.variant.taggedFields.toSorted(Primitive_string.compare);
|
|
240
|
+
if (firstFields.length !== entryFields.length || firstFields.some(f => !entryFields.includes(f))) {
|
|
241
|
+
let missingInEntry = firstFields.filter(f => !entryFields.includes(f));
|
|
242
|
+
let extraInEntry = entryFields.filter(f => !firstFields.includes(f));
|
|
243
|
+
let parts = [];
|
|
244
|
+
if (missingInEntry.length !== 0) {
|
|
245
|
+
parts.push(`'` + first.sliceName + `' has fields [` + missingInEntry.join(", ") + `] which '` + entry.sliceName + `' is missing`);
|
|
246
|
+
}
|
|
247
|
+
if (extraInEntry.length !== 0) {
|
|
248
|
+
parts.push(`'` + entry.sliceName + `' has fields [` + extraInEntry.join(", ") + `] which '` + first.sliceName + `' is missing`);
|
|
249
|
+
}
|
|
250
|
+
errors.push({
|
|
251
|
+
sliceName: entry.sliceName,
|
|
252
|
+
message: `Producers '` + first.sliceName + `' and '` + entry.sliceName + `' both produce '` + tagName + `' but with different fields: ` + parts.join("; ")
|
|
253
|
+
});
|
|
254
|
+
} else {
|
|
255
|
+
firstFields.forEach(fieldName => {
|
|
256
|
+
let firstSchema = first.variant.fields[fieldName];
|
|
257
|
+
let entrySchema = entry.variant.fields[fieldName];
|
|
258
|
+
if (!schemasAreCompatible(firstSchema, entrySchema)) {
|
|
259
|
+
errors.push({
|
|
260
|
+
sliceName: entry.sliceName,
|
|
261
|
+
message: `Producers '` + first.sliceName + `' and '` + entry.sliceName + `' both produce '` + tagName + `' but field '` + fieldName + `' has type '` + schemaTypeName(firstSchema) + `' in '` + first.sliceName + `' and '` + schemaTypeName(entrySchema) + `' in '` + entry.sliceName + `'`
|
|
262
|
+
});
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
});
|
|
266
|
+
}
|
|
267
|
+
if (!(firstTagged.length !== entryTagged.length || firstTagged.some(f => !entryTagged.includes(f)))) {
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
let untaggedInEntry = firstTagged.filter(f => !entryTagged.includes(f));
|
|
271
|
+
let untaggedInFirst = entryTagged.filter(f => !firstTagged.includes(f));
|
|
272
|
+
let parts$1 = [];
|
|
273
|
+
if (untaggedInEntry.length !== 0) {
|
|
274
|
+
parts$1.push(`[` + untaggedInEntry.join(", ") + `] tagged in '` + first.sliceName + `' but not in '` + entry.sliceName + `'`);
|
|
275
|
+
}
|
|
276
|
+
if (untaggedInFirst.length !== 0) {
|
|
277
|
+
parts$1.push(`[` + untaggedInFirst.join(", ") + `] tagged in '` + entry.sliceName + `' but not in '` + first.sliceName + `'`);
|
|
278
|
+
}
|
|
279
|
+
errors.push({
|
|
280
|
+
sliceName: entry.sliceName,
|
|
281
|
+
message: `Producers '` + first.sliceName + `' and '` + entry.sliceName + `' both produce '` + tagName + `' but tag annotations differ: ` + parts$1.join("; ")
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
});
|
|
285
|
+
consumed.forEach(param => {
|
|
286
|
+
let sliceName = param[0];
|
|
287
|
+
let variants = extractAllVariants(param[1]);
|
|
288
|
+
variants.forEach(variant => {
|
|
289
|
+
let producers = producerMap[variant.tagName];
|
|
290
|
+
if (producers !== undefined) {
|
|
291
|
+
let producer = producers[0];
|
|
292
|
+
if (!isPayloadLess(variant)) {
|
|
293
|
+
Object.entries(variant.fields).forEach(param => {
|
|
294
|
+
let consumedFieldSchema = param[1];
|
|
295
|
+
let fieldName = param[0];
|
|
296
|
+
let producedFieldSchema = producer.variant.fields[fieldName];
|
|
297
|
+
if (producedFieldSchema !== undefined) {
|
|
298
|
+
if (!schemasAreCompatible(consumedFieldSchema, producedFieldSchema)) {
|
|
299
|
+
errors.push({
|
|
300
|
+
sliceName: sliceName,
|
|
301
|
+
message: `Slice '` + sliceName + `' consumes '` + variant.tagName + `.` + fieldName + `' as ` + schemaTypeName(consumedFieldSchema) + ` but producer declares it as ` + schemaTypeName(producedFieldSchema)
|
|
302
|
+
});
|
|
303
|
+
return;
|
|
304
|
+
} else {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
} else {
|
|
308
|
+
errors.push({
|
|
309
|
+
sliceName: sliceName,
|
|
310
|
+
message: `Slice '` + sliceName + `' consumes '` + variant.tagName + `.` + fieldName + `' (` + schemaTypeName(consumedFieldSchema) + `) but produced '` + variant.tagName + `' has no field '` + fieldName + `'`
|
|
311
|
+
});
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
});
|
|
315
|
+
return;
|
|
316
|
+
} else {
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
errors.push({
|
|
321
|
+
sliceName: sliceName,
|
|
322
|
+
message: `Slice '` + sliceName + `' consumes '` + variant.tagName + `' but no slice produces it`
|
|
323
|
+
});
|
|
324
|
+
});
|
|
325
|
+
});
|
|
326
|
+
if (errors.length !== 0) {
|
|
327
|
+
return {
|
|
328
|
+
TAG: "Error",
|
|
329
|
+
_0: errors
|
|
330
|
+
};
|
|
331
|
+
} else {
|
|
332
|
+
return {
|
|
333
|
+
TAG: "Ok",
|
|
334
|
+
_0: undefined
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function validateCrossPartitionScope(producers) {
|
|
340
|
+
let perProducer = producers.map(param => {
|
|
341
|
+
let schema = param[1];
|
|
342
|
+
let cpSet = new Set();
|
|
343
|
+
DcbTag$Reventless.extractCrossPartitionTagKeys(schema).forEach(k => {
|
|
344
|
+
cpSet.add(k);
|
|
345
|
+
});
|
|
346
|
+
let carried = new Set();
|
|
347
|
+
Object.values(DcbTag$Reventless.extractTagKeysByEventType(schema)).forEach(ks => {
|
|
348
|
+
ks.forEach(k => {
|
|
349
|
+
carried.add(k);
|
|
350
|
+
});
|
|
351
|
+
});
|
|
352
|
+
return [
|
|
353
|
+
param[0],
|
|
354
|
+
cpSet,
|
|
355
|
+
carried
|
|
356
|
+
];
|
|
357
|
+
});
|
|
358
|
+
let globalCp = new Set();
|
|
359
|
+
perProducer.forEach(param => {
|
|
360
|
+
Array.from(param[1].values()).forEach(k => {
|
|
361
|
+
globalCp.add(k);
|
|
362
|
+
});
|
|
363
|
+
});
|
|
364
|
+
let warnings = [];
|
|
365
|
+
perProducer.forEach(param => {
|
|
366
|
+
let cpSet = param[1];
|
|
367
|
+
let name = param[0];
|
|
368
|
+
Array.from(param[2].values()).toSorted(Primitive_string.compare).forEach(k => {
|
|
369
|
+
if (globalCp.has(k) && !cpSet.has(k)) {
|
|
370
|
+
warnings.push({
|
|
371
|
+
sliceName: name,
|
|
372
|
+
message: `tag '` + k + `' is declared @crossPartition by another producer but is partition-scoped here — DCB tag scope must agree across every event type that carries the tag (read-scope and fence-scope follow one global per-key flag). Add @crossPartition to '` + k + `' here, or remove it from the other producer.`
|
|
373
|
+
});
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
});
|
|
377
|
+
});
|
|
378
|
+
return warnings;
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
function validateScopeVsInference(annotations, inferred) {
|
|
382
|
+
let contradictions = [];
|
|
383
|
+
let redundancies = [];
|
|
384
|
+
annotations.forEach(param => {
|
|
385
|
+
let name = param[0];
|
|
386
|
+
param[1].forEach(k => {
|
|
387
|
+
if (Primitive_object.equal(inferred.partitionBySlice[name], k)) {
|
|
388
|
+
contradictions.push({
|
|
389
|
+
sliceName: name,
|
|
390
|
+
message: `tag '` + k + `' is marked @crossPartition but inference resolves it as this slice's own partition key — a slice's own identity is never a cross-partition read. Remove the annotation.`
|
|
391
|
+
});
|
|
392
|
+
return;
|
|
393
|
+
} else if (inferred.crossPartitionTagKeys.includes(k)) {
|
|
394
|
+
redundancies.push({
|
|
395
|
+
sliceName: name,
|
|
396
|
+
message: `tag '` + k + `' is marked @crossPartition but the framework already infers it as a cross-partition read from the slice graph — the annotation is redundant and can be removed.`
|
|
397
|
+
});
|
|
398
|
+
return;
|
|
399
|
+
} else {
|
|
400
|
+
return;
|
|
401
|
+
}
|
|
402
|
+
});
|
|
403
|
+
});
|
|
404
|
+
return {
|
|
405
|
+
contradictions: contradictions,
|
|
406
|
+
redundancies: redundancies
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
export {
|
|
411
|
+
extractVariantInfo,
|
|
412
|
+
extractAllVariants,
|
|
413
|
+
isPayloadLess,
|
|
414
|
+
schemasAreCompatible,
|
|
415
|
+
propsCompatible,
|
|
416
|
+
schemaTypeName,
|
|
417
|
+
dedupeKeys,
|
|
418
|
+
validateCompositeReads,
|
|
419
|
+
validateProducedAndConsumed,
|
|
420
|
+
validateCrossPartitionScope,
|
|
421
|
+
validateScopeVsInference,
|
|
422
|
+
}
|
|
423
|
+
/* DcbTag-Reventless Not a pure module */
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Spec for a composite display-name derivation.
|
|
3
|
+
|
|
4
|
+
`fields` names the source fields in declaration order; `separator` is inserted
|
|
5
|
+
between successive non-empty parts when `computeLabel` is applied.
|
|
6
|
+
|
|
7
|
+
The ppx attaches this spec to a state schema whenever one or more fields carry
|
|
8
|
+
the `@displayName` attribute. The projection runtime reads the spec at write
|
|
9
|
+
time and composes the label into a synthetic `displayName` field on state.
|
|
10
|
+
*/
|
|
11
|
+
type displayNameSpec = {
|
|
12
|
+
fields: array<string>,
|
|
13
|
+
separator: string,
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
/** Sury metadata ID used to attach a `displayNameSpec` to a state schema. */
|
|
17
|
+
let displayNameId: S.Metadata.Id.t<displayNameSpec> =
|
|
18
|
+
S.Metadata.Id.make(~namespace="reventless", ~name="displayName")
|
|
19
|
+
|
|
20
|
+
/** Returns the spec attached to a state schema, if any. */
|
|
21
|
+
let getSpec = (schema: S.t<unknown>): option<displayNameSpec> =>
|
|
22
|
+
S.Metadata.get(schema, ~id=displayNameId)
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
Composes a display-name label from a state dict and a spec.
|
|
26
|
+
|
|
27
|
+
Reads each source field by name from the dict. Entries that are absent,
|
|
28
|
+
`null`, or empty strings are skipped. Remaining string values are joined
|
|
29
|
+
with `spec.separator`. The result never has a leading, trailing, or
|
|
30
|
+
doubled separator.
|
|
31
|
+
*/
|
|
32
|
+
let computeLabel = (spec: displayNameSpec, state: dict<JSON.t>): string =>
|
|
33
|
+
spec.fields
|
|
34
|
+
->Array.filterMap(fieldName =>
|
|
35
|
+
switch state->Dict.get(fieldName) {
|
|
36
|
+
| Some(JSON.String(s)) if s != "" => Some(s)
|
|
37
|
+
| _ => None
|
|
38
|
+
}
|
|
39
|
+
)
|
|
40
|
+
->Array.join(spec.separator)
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as S from "sury/src/S.res.mjs";
|
|
4
|
+
import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
|
|
5
|
+
|
|
6
|
+
let displayNameId = S.Metadata.Id.make("reventless", "displayName");
|
|
7
|
+
|
|
8
|
+
function getSpec(schema) {
|
|
9
|
+
return S.Metadata.get(schema, displayNameId);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function computeLabel(spec, state) {
|
|
13
|
+
return Stdlib_Array.filterMap(spec.fields, fieldName => {
|
|
14
|
+
let match = state[fieldName];
|
|
15
|
+
if (typeof match === "string" && match !== "") {
|
|
16
|
+
return match;
|
|
17
|
+
}
|
|
18
|
+
}).join(spec.separator);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export {
|
|
22
|
+
displayNameId,
|
|
23
|
+
getSpec,
|
|
24
|
+
computeLabel,
|
|
25
|
+
}
|
|
26
|
+
/* displayNameId Not a pure module */
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Module type for an extension point's identity and schema specification.
|
|
3
|
+
|
|
4
|
+
An extension point is a bidirectional protocol boundary: it exposes a
|
|
5
|
+
command topic for extensions to publish to, and an event topic that
|
|
6
|
+
extensions subscribe to. The `directive` type is used for internal
|
|
7
|
+
routing signals that are not exposed as public events.
|
|
8
|
+
|
|
9
|
+
Note: `module Id = Id.String` is fixed — extension points always use string IDs.
|
|
10
|
+
*/
|
|
11
|
+
module type Spec = {
|
|
12
|
+
module Id = Id.String
|
|
13
|
+
|
|
14
|
+
/** Logical extension point name (used as a topic name prefix). */
|
|
15
|
+
let name: string
|
|
16
|
+
|
|
17
|
+
/** Commands that extensions can send to this extension point. Must carry `@schema`. */
|
|
18
|
+
@schema
|
|
19
|
+
type command
|
|
20
|
+
/** Events that this extension point emits to subscribed extensions. Must carry `@schema`. */
|
|
21
|
+
@schema
|
|
22
|
+
type event
|
|
23
|
+
/** Internal routing directives (not exposed externally). Must carry `@schema`. */
|
|
24
|
+
@schema
|
|
25
|
+
type directive
|
|
26
|
+
}
|
|
27
|
+
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Module types for a DCB inbound translation slice.
|
|
3
|
+
|
|
4
|
+
An `InboundTranslationSlice` receives external input (webhooks, API calls) and
|
|
5
|
+
translates it into domain commands via an anti-corruption layer.
|
|
6
|
+
|
|
7
|
+
Unlike outbound slices, inbound slices are triggered externally via
|
|
8
|
+
`operations.receive` rather than by subscribing to domain events.
|
|
9
|
+
|
|
10
|
+
```
|
|
11
|
+
External Input -> Anti-Corruption Layer (translate) -> Command
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Plan 02 splits the merged spec into two module types:
|
|
15
|
+
|
|
16
|
+
- `Spec` — types, identity, schemas, target name. (No state — translation is
|
|
17
|
+
a pure function of external input.)
|
|
18
|
+
- `Translation` — the single `translate` function.
|
|
19
|
+
|
|
20
|
+
@example
|
|
21
|
+
```rescript
|
|
22
|
+
// PaymentWebhook.res
|
|
23
|
+
let name = "PaymentWebhook"
|
|
24
|
+
|
|
25
|
+
@schema type externalInput = {paymentId: string, orderId: string, status: string}
|
|
26
|
+
@schema type command = ConfirmPayment({orderId: @s.matches(DcbTag.string) string, paymentId: string})
|
|
27
|
+
|
|
28
|
+
let translate = input => switch input.status {
|
|
29
|
+
| "completed" => Ok([(input.orderId, ConfirmPayment({orderId: input.orderId, paymentId: input.paymentId}))])
|
|
30
|
+
| _ => Error("Unknown payment status: " ++ input.status)
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
The lean Spec for an InboundTranslationSlice — types, identity, schemas.
|
|
37
|
+
*/
|
|
38
|
+
module type Spec = {
|
|
39
|
+
/** Logical name of this inbound translation slice (used as a component prefix). */
|
|
40
|
+
let name: string
|
|
41
|
+
let moduleUrl: string
|
|
42
|
+
|
|
43
|
+
/** The external input type received from the outside world. Must carry `@schema`. */
|
|
44
|
+
@schema
|
|
45
|
+
type externalInput
|
|
46
|
+
|
|
47
|
+
/** The command type produced by the anti-corruption layer. Must carry `@schema`. */
|
|
48
|
+
@schema
|
|
49
|
+
type command
|
|
50
|
+
|
|
51
|
+
/** Name of the aggregate or StateChangeSlice that receives the produced command. */
|
|
52
|
+
let targetName: string
|
|
53
|
+
|
|
54
|
+
/** Optional display name of the foreign system this anti-corruption slice receives
|
|
55
|
+
from (e.g. `"SupplierFeed"`). Drives the **external box** drawn outside the plugin
|
|
56
|
+
in the Event Graph / Context Map (see docs/plans/translation-external-boxes.md).
|
|
57
|
+
Auto-injected by `@@reventless.spec` defaulting to `None` — set it to name the box. */
|
|
58
|
+
let externalSystem: option<string>
|
|
59
|
+
|
|
60
|
+
/** Authorization rule evaluated at the GraphQL resolver entry before any
|
|
61
|
+
external input is translated. Auto-injected by `@@reventless.spec` and
|
|
62
|
+
on structurally-detected inline spec modules — defaults to
|
|
63
|
+
`AllowAuthenticated`. */
|
|
64
|
+
let commandAuthorization: command => Authorization.permission
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
The Translation — the synchronous translate function.
|
|
69
|
+
*/
|
|
70
|
+
module type Translation = {
|
|
71
|
+
module Spec: Spec
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
Translate: convert external input into domain commands.
|
|
75
|
+
Returns:
|
|
76
|
+
- `Ok([(targetId, cmd), ...])` to publish one or more commands
|
|
77
|
+
- `Ok([])` for idempotent no-ops (nothing to publish)
|
|
78
|
+
- `Error(msg)` to reject the input
|
|
79
|
+
*/
|
|
80
|
+
let translate: Spec.externalInput => result<array<(string, Spec.command)>, string>
|
|
81
|
+
|
|
82
|
+
/** File URL of this Translation module (`import.meta.url`). */
|
|
83
|
+
let moduleUrl: string
|
|
84
|
+
}
|
|
85
|
+
|