@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,279 @@
|
|
|
1
|
+
/**
|
|
2
|
+
Module types for a DCB automation slice (TODO List Pattern).
|
|
3
|
+
|
|
4
|
+
An `AutomationSlice` listens to the shared `DcbEventLog` event topic, accumulates
|
|
5
|
+
pending work items (TODO list), and processes them exactly once by issuing commands.
|
|
6
|
+
Completion events mark TODO items as done, closing the automation loop.
|
|
7
|
+
|
|
8
|
+
```
|
|
9
|
+
Event(s) → TODO List (read model) → Processor → Command → Event(s)
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Plan 02 splits the merged spec into two module types:
|
|
13
|
+
|
|
14
|
+
- `Spec` — types, identity, schemas, configuration constants. Per D2,
|
|
15
|
+
`todoItem` lives here (the persisted TODO state has a schema queried for
|
|
16
|
+
sweeping pending items — externally-observable, like ReadModel state).
|
|
17
|
+
- `Automation` — the three pure functions `collect`, `resolve`, `process`.
|
|
18
|
+
|
|
19
|
+
@example
|
|
20
|
+
```rescript
|
|
21
|
+
// ShipOrder.res
|
|
22
|
+
let name = "ShipOrder"
|
|
23
|
+
|
|
24
|
+
@schema type todoItem = {orderId: string, shippingAddress: string}
|
|
25
|
+
@schema type command = CreateShipment({orderId: @s.matches(DcbTag.string) string, address: string})
|
|
26
|
+
|
|
27
|
+
let collect = event => switch event {
|
|
28
|
+
| OrderPlaced({orderId, shippingAddress}) =>
|
|
29
|
+
[(orderId, {orderId, shippingAddress})]
|
|
30
|
+
| _ => []
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
let resolve = event => switch event {
|
|
34
|
+
| ShipmentCreated({orderId}) => Some(orderId)
|
|
35
|
+
| _ => None
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
let process = (id, item) =>
|
|
39
|
+
Some((id, CreateShipment({orderId: item.orderId, address: item.shippingAddress})))
|
|
40
|
+
|
|
41
|
+
let maxRetries = 3
|
|
42
|
+
let heartbeatInterval = 60
|
|
43
|
+
```
|
|
44
|
+
*/
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
The lean Spec for an AutomationSlice — types, identity, schemas, sweep config.
|
|
48
|
+
|
|
49
|
+
Plan 04 dropped `consumedEvent` from the Spec — the framework now derives the
|
|
50
|
+
consumed-event set from each per-source Mapping's `sourceEventSchema`, so a
|
|
51
|
+
manually-declared union is no longer necessary. Single-source slices have one
|
|
52
|
+
`Mapping` whose `sourceEventSchema` IS the consumed-event schema; multi-source
|
|
53
|
+
slices have several Mappings and the framework walks all of them.
|
|
54
|
+
*/
|
|
55
|
+
module type Spec = {
|
|
56
|
+
/** Logical name of this automation slice (used as a component prefix). */
|
|
57
|
+
let name: string
|
|
58
|
+
let moduleUrl: string
|
|
59
|
+
|
|
60
|
+
/** The TODO item state — what data is accumulated for each pending work item. Must carry `@schema`. */
|
|
61
|
+
@schema
|
|
62
|
+
type todoItem
|
|
63
|
+
|
|
64
|
+
/** The command type produced by the processor. Must carry `@schema`. */
|
|
65
|
+
@schema
|
|
66
|
+
type command
|
|
67
|
+
|
|
68
|
+
/** Maximum number of retries for a failed processing attempt. */
|
|
69
|
+
let maxRetries: int
|
|
70
|
+
|
|
71
|
+
/** Heartbeat interval in seconds for sweeping pending/failed items. */
|
|
72
|
+
let heartbeatInterval: int
|
|
73
|
+
|
|
74
|
+
/** Name of the aggregate or StateChangeSlice that receives the produced command. */
|
|
75
|
+
let targetName: string
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ── Plan 04: Mixed-source mappings ───────────────────────────────────────────
|
|
79
|
+
//
|
|
80
|
+
// An AutomationSlice can declare per-source mappings that consume Aggregate
|
|
81
|
+
// events alongside the slice's own DcbEventLog events. Each mapping carries its
|
|
82
|
+
// own `collect`/`resolve` (over the source's `event` type) plus a `toTags`
|
|
83
|
+
// validation step run before each command publish. `process` remains shared in
|
|
84
|
+
// the slice's `Automation` module since it operates on `todoItem` regardless of
|
|
85
|
+
// the originating source.
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
Ambient deployment context plumbed to every mapping function.
|
|
89
|
+
|
|
90
|
+
`Plugin_Builder` constructs this from its existing `~environment`, `~name` and
|
|
91
|
+
related parameters. `collect` and `resolve` use it to complete partial event
|
|
92
|
+
payloads (e.g. when a DCB tag field is supplied by deployment metadata rather
|
|
93
|
+
than the source event); `toTags` uses it to validate that all tag fields on the
|
|
94
|
+
target command will be populated.
|
|
95
|
+
|
|
96
|
+
The record is intentionally narrow — extending it is a deliberate framework
|
|
97
|
+
change, not an open-dict escape hatch. Runtime registry lookups are out of
|
|
98
|
+
scope (see Plan 04 / decision 3).
|
|
99
|
+
*/
|
|
100
|
+
type context = {
|
|
101
|
+
environment: string,
|
|
102
|
+
platformName: string,
|
|
103
|
+
pluginName: string,
|
|
104
|
+
sliceName: string,
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
A compiled per-source-to-AutomationSlice mapping.
|
|
109
|
+
|
|
110
|
+
Created by `AutomationSlice.Mapping.Make(Source, Target, Impl)`. The mapping
|
|
111
|
+
binds `sourceEvent` (decode target) plus `collect`/`resolve` to the slice's
|
|
112
|
+
`todoItem` type. `process` remains in the slice's `Automation` module — it is
|
|
113
|
+
source-agnostic.
|
|
114
|
+
|
|
115
|
+
Validation belongs upstream of the mapping: `collect` should return `[]` for
|
|
116
|
+
events that shouldn't enter the TODO list, and the command schema's
|
|
117
|
+
`@compositePartitionTag` / `@s.matches(DcbTag.string)` annotations enforce
|
|
118
|
+
DCB-tag invariants at encode time (failures mark the item Failed → retry).
|
|
119
|
+
*/
|
|
120
|
+
module type Mapping = {
|
|
121
|
+
module SourceId: Id.T
|
|
122
|
+
@schema
|
|
123
|
+
type sourceEvent
|
|
124
|
+
type todoItem
|
|
125
|
+
type command
|
|
126
|
+
let sourceEventSchema: S.t<sourceEvent>
|
|
127
|
+
let sourceName: string
|
|
128
|
+
let collect: (sourceEvent, context) => array<(string, todoItem)>
|
|
129
|
+
let resolve: sourceEvent => option<string>
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
The Automation — the source-agnostic processing function.
|
|
134
|
+
|
|
135
|
+
Plan 04 moved `collect` and `resolve` to per-source `Mapping` modules (where
|
|
136
|
+
they switch over the source's own `event` type). `process` stays here because
|
|
137
|
+
it operates on `todoItem`, which is uniform across sources.
|
|
138
|
+
|
|
139
|
+
The "close PPX gaps" plan merged the previous `Mappings` shape into this type:
|
|
140
|
+
the same module that exposes `process` also exposes the per-source `mappings`
|
|
141
|
+
array and the inner `module type Mapping`. This collapses the Plan 04 3-arg
|
|
142
|
+
`AutomationSlice.Make(Spec, Automation, Mappings)` into a 2-arg
|
|
143
|
+
`Make(Spec, Automation)` and lets the merged `_Automation.res` file (process +
|
|
144
|
+
per-source `Mapping.Make` + `let mappings`) live as a single source file.
|
|
145
|
+
*/
|
|
146
|
+
module type Automation = {
|
|
147
|
+
module Spec: Spec
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
Process: given a pending TODO item, produce a command.
|
|
151
|
+
The processor calls this for each pending item. Returns the target ID and command.
|
|
152
|
+
May return `None` to skip processing (e.g., wait for more data).
|
|
153
|
+
*/
|
|
154
|
+
let process: (string, Spec.todoItem) => option<(string, Spec.command)>
|
|
155
|
+
|
|
156
|
+
/** File URL of this Automation module (`import.meta.url`). */
|
|
157
|
+
let moduleUrl: string
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
The per-source `Mapping` type — substituted onto the slice's `Spec`. The
|
|
161
|
+
PPX-injected `module M = Reventless.AutomationSlice.Mappings.Make(Spec)`
|
|
162
|
+
+ `module type Mapping = M.Mapping` produces exactly this shape; legacy
|
|
163
|
+
3-file files re-export it via `module type Mapping = <Stem>_Mappings.Mapping`.
|
|
164
|
+
*/
|
|
165
|
+
module type Mapping = Mapping
|
|
166
|
+
with type todoItem = Spec.todoItem
|
|
167
|
+
and type command = Spec.command
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
The per-source mappings registered with this slice. Each entry is a first-class
|
|
171
|
+
module satisfying `Mapping`. The framework walks these to discover source
|
|
172
|
+
topics and dispatch decoded events to the matching `collect`/`resolve`.
|
|
173
|
+
*/
|
|
174
|
+
let mappings: array<module(Mapping)>
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
The implementation passed to `AutomationSlice.Mapping.Make`. Caller-supplied
|
|
179
|
+
`collect`/`resolve` are bound to source/target types via destructive
|
|
180
|
+
substitution in the functor.
|
|
181
|
+
*/
|
|
182
|
+
module type MappingImpl = {
|
|
183
|
+
type sourceEvent
|
|
184
|
+
type todoItem
|
|
185
|
+
type command
|
|
186
|
+
let collect: (sourceEvent, context) => array<(string, todoItem)>
|
|
187
|
+
let resolve: sourceEvent => option<string>
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
A collection of `Mapping` modules for a single AutomationSlice target.
|
|
192
|
+
|
|
193
|
+
Pass a `Mappings` module to the AutomationSlice plugin builder to register all
|
|
194
|
+
source-to-slice mappings. The shape mirrors `Projection.Mappings` for
|
|
195
|
+
ReadModels.
|
|
196
|
+
|
|
197
|
+
@example
|
|
198
|
+
```rescript
|
|
199
|
+
// AutoFulfillment_Mappings.res
|
|
200
|
+
module M = Reventless.AutomationSlice.Mappings.Make(AutoFulfillmentSpec)
|
|
201
|
+
module type Mapping = M.Mapping
|
|
202
|
+
|
|
203
|
+
let mappings: array<module(Mapping)> = [
|
|
204
|
+
module(FromOrderShipped),
|
|
205
|
+
module(FromStockReserved),
|
|
206
|
+
]
|
|
207
|
+
```
|
|
208
|
+
*/
|
|
209
|
+
module type Mappings = {
|
|
210
|
+
module Target: Spec
|
|
211
|
+
module type Mapping = Mapping
|
|
212
|
+
with type todoItem = Target.todoItem
|
|
213
|
+
and type command = Target.command
|
|
214
|
+
let moduleUrl: string
|
|
215
|
+
let mappings: array<module(Mapping)>
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
/**
|
|
219
|
+
Builds an `AutomationSlice.Mapping` from a `Source`, the slice's `Spec`, and a
|
|
220
|
+
`MappingImpl`.
|
|
221
|
+
|
|
222
|
+
@example
|
|
223
|
+
```rescript
|
|
224
|
+
// AutoFulfillment/FromOrderShipped.res
|
|
225
|
+
module FromOrderShipped = Reventless.AutomationSlice.Mapping.Make(
|
|
226
|
+
OrderSpec, // Source: Aggregate spec module
|
|
227
|
+
AutoFulfillmentSpec, // Target: this slice's spec
|
|
228
|
+
{
|
|
229
|
+
let collect = (event, _ctx) =>
|
|
230
|
+
switch event {
|
|
231
|
+
| OrderSpec.OrderShipped({orderId, productId}) =>
|
|
232
|
+
[(orderId ++ ":" ++ productId, {AutoFulfillmentSpec.orderId, productId})]
|
|
233
|
+
| _ => []
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
let resolve = event =>
|
|
237
|
+
switch event {
|
|
238
|
+
| OrderSpec.OrderRefunded({orderId, productId}) =>
|
|
239
|
+
Some(orderId ++ ":" ++ productId)
|
|
240
|
+
| _ => None
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
)
|
|
244
|
+
```
|
|
245
|
+
*/
|
|
246
|
+
module Mapping = {
|
|
247
|
+
module Make = (
|
|
248
|
+
Source: Projection.Source,
|
|
249
|
+
Target: Spec,
|
|
250
|
+
Impl: MappingImpl
|
|
251
|
+
with type sourceEvent := Source.event
|
|
252
|
+
and type todoItem := Target.todoItem
|
|
253
|
+
and type command := Target.command,
|
|
254
|
+
): (
|
|
255
|
+
Mapping
|
|
256
|
+
with type sourceEvent = Source.event
|
|
257
|
+
and type todoItem = Target.todoItem
|
|
258
|
+
and type command = Target.command
|
|
259
|
+
and module SourceId = Source.Id
|
|
260
|
+
) => {
|
|
261
|
+
module SourceId = Source.Id
|
|
262
|
+
@schema
|
|
263
|
+
type sourceEvent = Source.event
|
|
264
|
+
type todoItem = Target.todoItem
|
|
265
|
+
type command = Target.command
|
|
266
|
+
let sourceName = Source.name
|
|
267
|
+
let collect = Impl.collect
|
|
268
|
+
let resolve = Impl.resolve
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
module Mappings = {
|
|
273
|
+
module Make = (Target: Spec) => {
|
|
274
|
+
module type Mapping = Mapping
|
|
275
|
+
with type todoItem = Target.todoItem
|
|
276
|
+
and type command = Target.command
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
function Make(Source) {
|
|
5
|
+
return Target => (Impl => ({
|
|
6
|
+
SourceId: Source.Id,
|
|
7
|
+
sourceEventSchema: Source.eventSchema,
|
|
8
|
+
sourceName: Source.name,
|
|
9
|
+
collect: Impl.collect,
|
|
10
|
+
resolve: Impl.resolve
|
|
11
|
+
}));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
let Mapping = {
|
|
15
|
+
Make: Make
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
function Make$1(Target) {
|
|
19
|
+
return {};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
let Mappings = {
|
|
23
|
+
Make: Make$1
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export {
|
|
27
|
+
Mapping,
|
|
28
|
+
Mappings,
|
|
29
|
+
}
|
|
30
|
+
/* No side effect */
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
The per-plugin capability manifest — `capabilities.json`, written beside the
|
|
3
|
+
generated `Plugin.res` by the plugin package's build.
|
|
4
|
+
|
|
5
|
+
Each entry states one capability the plugin's fields declare they need, keyed
|
|
6
|
+
by the store's qualified `{plugin}.{store}` identity, with the declaring
|
|
7
|
+
`(component, field)` sites as provenance. The keys are taken verbatim from
|
|
8
|
+
`pluginStructure.requiredStores` — the manifest is a rendering of the
|
|
9
|
+
structure, never a second scan of the sources, so the two cannot spell one
|
|
10
|
+
fact differently.
|
|
11
|
+
|
|
12
|
+
The platform generator unions these files across a deployment's plugins and
|
|
13
|
+
emits the platform's capability list from them.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
@schema
|
|
17
|
+
type kind = ObjectStore
|
|
18
|
+
|
|
19
|
+
/** The declaration site: the component's spec name and the field carrying the
|
|
20
|
+
`@storageRef` annotation, plus the store exactly as that field spells it.
|
|
21
|
+
|
|
22
|
+
`annotation` is optional only to keep reading a manifest emitted before it
|
|
23
|
+
existed: a plugin built earlier still parses, and a reader that cannot say
|
|
24
|
+
what the source says omits the claim rather than inventing one. Every
|
|
25
|
+
manifest emitted now carries it. */
|
|
26
|
+
@schema
|
|
27
|
+
type provenance = {component: string, field: string, annotation?: string}
|
|
28
|
+
|
|
29
|
+
@schema
|
|
30
|
+
type entry = {
|
|
31
|
+
kind: kind,
|
|
32
|
+
key: string,
|
|
33
|
+
declaredBy: array<provenance>,
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
@schema
|
|
37
|
+
type t = {capabilities: array<entry>}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
Build the manifest from a plugin's structure.
|
|
41
|
+
|
|
42
|
+
Keys iterate `requiredStores` itself, so a manifest's key set is byte-identical
|
|
43
|
+
to what the deployed plugin reports at runtime — the contract the deploy-time
|
|
44
|
+
coverage assertion checks against. A structure with no declarations yields an
|
|
45
|
+
empty `capabilities` list, not an absent file: "declares nothing" is a
|
|
46
|
+
statement, and the generator reading the manifests must be able to tell it
|
|
47
|
+
apart from "was never built".
|
|
48
|
+
*/
|
|
49
|
+
let fromStructure = (structure: Plugin.pluginStructure): t => {
|
|
50
|
+
let declarations = structure.requiredStoreDeclarations->Option.getOr([])
|
|
51
|
+
{
|
|
52
|
+
capabilities: structure.requiredStores
|
|
53
|
+
->Option.getOr([])
|
|
54
|
+
->Array.map(key => {
|
|
55
|
+
kind: ObjectStore,
|
|
56
|
+
key,
|
|
57
|
+
declaredBy: declarations->Array.filterMap(d =>
|
|
58
|
+
d.store == key
|
|
59
|
+
? Some({component: d.component, field: d.field, annotation: ?d.annotation})
|
|
60
|
+
: None
|
|
61
|
+
),
|
|
62
|
+
}),
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/** Deterministic rendering: 2-space indent, trailing newline. Rebuilding with
|
|
67
|
+
no source change must produce a byte-identical file, so the committed
|
|
68
|
+
manifest never churns. */
|
|
69
|
+
let render = (manifest: t): string =>
|
|
70
|
+
JSON.stringify(manifest->S.reverseConvertToJsonOrThrow(schema), ~space=2) ++ "\n"
|
|
71
|
+
|
|
72
|
+
/** One call for the emit CLI: structure in, `capabilities.json` content out. */
|
|
73
|
+
let renderForStructure = (structure: Plugin.pluginStructure): string =>
|
|
74
|
+
structure->fromStructure->render
|
|
@@ -0,0 +1,61 @@
|
|
|
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
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
6
|
+
|
|
7
|
+
let kindSchema = S.literal("ObjectStore");
|
|
8
|
+
|
|
9
|
+
let provenanceSchema = S.schema(s => ({
|
|
10
|
+
component: s.m(S.string),
|
|
11
|
+
field: s.m(S.string),
|
|
12
|
+
annotation: s.m(S.option(S.string))
|
|
13
|
+
}));
|
|
14
|
+
|
|
15
|
+
let entrySchema = S.schema(s => ({
|
|
16
|
+
kind: s.m(kindSchema),
|
|
17
|
+
key: s.m(S.string),
|
|
18
|
+
declaredBy: s.m(S.array(provenanceSchema))
|
|
19
|
+
}));
|
|
20
|
+
|
|
21
|
+
let schema = S.schema(s => ({
|
|
22
|
+
capabilities: s.m(S.array(entrySchema))
|
|
23
|
+
}));
|
|
24
|
+
|
|
25
|
+
function fromStructure(structure) {
|
|
26
|
+
let declarations = Stdlib_Option.getOr(structure.requiredStoreDeclarations, []);
|
|
27
|
+
return {
|
|
28
|
+
capabilities: Stdlib_Option.getOr(structure.requiredStores, []).map(key => ({
|
|
29
|
+
kind: "ObjectStore",
|
|
30
|
+
key: key,
|
|
31
|
+
declaredBy: Stdlib_Array.filterMap(declarations, d => {
|
|
32
|
+
if (d.store === key) {
|
|
33
|
+
return {
|
|
34
|
+
component: d.component,
|
|
35
|
+
field: d.field,
|
|
36
|
+
annotation: d.annotation
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
})
|
|
40
|
+
}))
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function render(manifest) {
|
|
45
|
+
return JSON.stringify(S.reverseConvertToJsonOrThrow(manifest, schema), undefined, 2) + "\n";
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function renderForStructure(structure) {
|
|
49
|
+
return render(fromStructure(structure));
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export {
|
|
53
|
+
kindSchema,
|
|
54
|
+
provenanceSchema,
|
|
55
|
+
entrySchema,
|
|
56
|
+
schema,
|
|
57
|
+
fromStructure,
|
|
58
|
+
render,
|
|
59
|
+
renderForStructure,
|
|
60
|
+
}
|
|
61
|
+
/* kindSchema Not a pure module */
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Single source of truth for the Reventless component-kind vocabulary: the kinds
|
|
2
|
+
// themselves, every accepted folder spelling (singular / plural / short form),
|
|
3
|
+
// the canonical folder name, and the body-file suffixes. The plugin generator
|
|
4
|
+
// (`generator/Discovery`) and the external tooling that classifies files by
|
|
5
|
+
// folder (`reventless-gwt`'s `ComponentMeta`) both derive from this, so the two
|
|
6
|
+
// can't drift — previously the generator accepted plural/short folder names that
|
|
7
|
+
// the gwt discovery silently didn't recognise.
|
|
8
|
+
|
|
9
|
+
type t =
|
|
10
|
+
| StateChangeSlice
|
|
11
|
+
| StateViewSlice
|
|
12
|
+
| StateViewSliceStream
|
|
13
|
+
| AutomationSlice
|
|
14
|
+
| InboundTranslationSlice
|
|
15
|
+
| OutboundTranslationSlice
|
|
16
|
+
| Aggregate
|
|
17
|
+
| ReadModel
|
|
18
|
+
| ReadModelStream
|
|
19
|
+
| Task
|
|
20
|
+
| ExtensionPoint
|
|
21
|
+
| Extension
|
|
22
|
+
|
|
23
|
+
let all = [
|
|
24
|
+
StateChangeSlice,
|
|
25
|
+
StateViewSlice,
|
|
26
|
+
StateViewSliceStream,
|
|
27
|
+
AutomationSlice,
|
|
28
|
+
InboundTranslationSlice,
|
|
29
|
+
OutboundTranslationSlice,
|
|
30
|
+
Aggregate,
|
|
31
|
+
ReadModel,
|
|
32
|
+
ReadModelStream,
|
|
33
|
+
Task,
|
|
34
|
+
ExtensionPoint,
|
|
35
|
+
Extension,
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
// The canonical (singular) folder name for a kind — the spelling the naming
|
|
39
|
+
// conventions and the graph `kind` field use.
|
|
40
|
+
let folderName = (t: t): string =>
|
|
41
|
+
switch t {
|
|
42
|
+
| StateChangeSlice => "StateChangeSlice"
|
|
43
|
+
| StateViewSlice => "StateViewSlice"
|
|
44
|
+
| StateViewSliceStream => "StateViewSliceStream"
|
|
45
|
+
| AutomationSlice => "AutomationSlice"
|
|
46
|
+
| InboundTranslationSlice => "InboundTranslationSlice"
|
|
47
|
+
| OutboundTranslationSlice => "OutboundTranslationSlice"
|
|
48
|
+
| Aggregate => "Aggregate"
|
|
49
|
+
| ReadModel => "ReadModel"
|
|
50
|
+
| ReadModelStream => "ReadModelStream"
|
|
51
|
+
| Task => "Task"
|
|
52
|
+
| ExtensionPoint => "ExtensionPoint"
|
|
53
|
+
| Extension => "Extension"
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// Every folder spelling that denotes a kind → the kind. Tolerates the singular,
|
|
57
|
+
// plural, and (for slices) short forms the generator has always accepted.
|
|
58
|
+
let folderToKind = (folder: string): option<t> =>
|
|
59
|
+
switch folder {
|
|
60
|
+
| "StateChange" | "StateChanges" | "StateChangeSlice" | "StateChangeSlices" =>
|
|
61
|
+
Some(StateChangeSlice)
|
|
62
|
+
| "StateView" | "StateViews" | "StateViewSlice" | "StateViewSlices" => Some(StateViewSlice)
|
|
63
|
+
| "StateViewSliceStream" | "StateViewSliceStreams" => Some(StateViewSliceStream)
|
|
64
|
+
| "Automation" | "Automations" | "AutomationSlice" | "AutomationSlices" => Some(AutomationSlice)
|
|
65
|
+
| "InboundTranslation"
|
|
66
|
+
| "InboundTranslations"
|
|
67
|
+
| "InboundTranslationSlice"
|
|
68
|
+
| "InboundTranslationSlices" =>
|
|
69
|
+
Some(InboundTranslationSlice)
|
|
70
|
+
| "OutboundTranslation"
|
|
71
|
+
| "OutboundTranslations"
|
|
72
|
+
| "OutboundTranslationSlice"
|
|
73
|
+
| "OutboundTranslationSlices" =>
|
|
74
|
+
Some(OutboundTranslationSlice)
|
|
75
|
+
| "Aggregate" | "Aggregates" => Some(Aggregate)
|
|
76
|
+
| "ReadModel" | "ReadModels" => Some(ReadModel)
|
|
77
|
+
| "ReadModelStream" | "ReadModelStreams" => Some(ReadModelStream)
|
|
78
|
+
| "Task" | "Tasks" => Some(Task)
|
|
79
|
+
| "ExtensionPoint" | "ExtensionPoints" => Some(ExtensionPoint)
|
|
80
|
+
| "Extension" | "Extensions" => Some(Extension)
|
|
81
|
+
| _ => None
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
let isKindFolder = (folder: string): bool => folderToKind(folder)->Option.isSome
|
|
85
|
+
|
|
86
|
+
// Body-file suffixes stripped to recover a component's spec stem (longest first,
|
|
87
|
+
// so `_ExtensionPointMapping` is removed before `_ExtensionPoint`). Spec and body
|
|
88
|
+
// files in one folder collapse to the same component name.
|
|
89
|
+
let bodySuffixes = [
|
|
90
|
+
"_ExtensionPointMapping",
|
|
91
|
+
"_ExtensionPoint",
|
|
92
|
+
"_Extension",
|
|
93
|
+
"_Projections",
|
|
94
|
+
"_Projection",
|
|
95
|
+
"_Mappings",
|
|
96
|
+
"_Behavior",
|
|
97
|
+
"_Automation",
|
|
98
|
+
"_Translation",
|
|
99
|
+
]
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// Generated by ReScript, PLEASE EDIT WITH CARE
|
|
2
|
+
|
|
3
|
+
import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
|
|
4
|
+
|
|
5
|
+
let all = [
|
|
6
|
+
"StateChangeSlice",
|
|
7
|
+
"StateViewSlice",
|
|
8
|
+
"StateViewSliceStream",
|
|
9
|
+
"AutomationSlice",
|
|
10
|
+
"InboundTranslationSlice",
|
|
11
|
+
"OutboundTranslationSlice",
|
|
12
|
+
"Aggregate",
|
|
13
|
+
"ReadModel",
|
|
14
|
+
"ReadModelStream",
|
|
15
|
+
"Task",
|
|
16
|
+
"ExtensionPoint",
|
|
17
|
+
"Extension"
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
function folderName(t) {
|
|
21
|
+
switch (t) {
|
|
22
|
+
case "StateChangeSlice" :
|
|
23
|
+
return "StateChangeSlice";
|
|
24
|
+
case "StateViewSlice" :
|
|
25
|
+
return "StateViewSlice";
|
|
26
|
+
case "StateViewSliceStream" :
|
|
27
|
+
return "StateViewSliceStream";
|
|
28
|
+
case "AutomationSlice" :
|
|
29
|
+
return "AutomationSlice";
|
|
30
|
+
case "InboundTranslationSlice" :
|
|
31
|
+
return "InboundTranslationSlice";
|
|
32
|
+
case "OutboundTranslationSlice" :
|
|
33
|
+
return "OutboundTranslationSlice";
|
|
34
|
+
case "Aggregate" :
|
|
35
|
+
return "Aggregate";
|
|
36
|
+
case "ReadModel" :
|
|
37
|
+
return "ReadModel";
|
|
38
|
+
case "ReadModelStream" :
|
|
39
|
+
return "ReadModelStream";
|
|
40
|
+
case "Task" :
|
|
41
|
+
return "Task";
|
|
42
|
+
case "ExtensionPoint" :
|
|
43
|
+
return "ExtensionPoint";
|
|
44
|
+
case "Extension" :
|
|
45
|
+
return "Extension";
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function folderToKind(folder) {
|
|
50
|
+
switch (folder) {
|
|
51
|
+
case "Aggregate" :
|
|
52
|
+
case "Aggregates" :
|
|
53
|
+
return "Aggregate";
|
|
54
|
+
case "Automation" :
|
|
55
|
+
case "AutomationSlice" :
|
|
56
|
+
case "AutomationSlices" :
|
|
57
|
+
case "Automations" :
|
|
58
|
+
return "AutomationSlice";
|
|
59
|
+
case "ExtensionPoint" :
|
|
60
|
+
case "ExtensionPoints" :
|
|
61
|
+
return "ExtensionPoint";
|
|
62
|
+
case "Extension" :
|
|
63
|
+
case "Extensions" :
|
|
64
|
+
return "Extension";
|
|
65
|
+
case "InboundTranslation" :
|
|
66
|
+
case "InboundTranslationSlice" :
|
|
67
|
+
case "InboundTranslationSlices" :
|
|
68
|
+
case "InboundTranslations" :
|
|
69
|
+
return "InboundTranslationSlice";
|
|
70
|
+
case "OutboundTranslation" :
|
|
71
|
+
case "OutboundTranslationSlice" :
|
|
72
|
+
case "OutboundTranslationSlices" :
|
|
73
|
+
case "OutboundTranslations" :
|
|
74
|
+
return "OutboundTranslationSlice";
|
|
75
|
+
case "ReadModelStream" :
|
|
76
|
+
case "ReadModelStreams" :
|
|
77
|
+
return "ReadModelStream";
|
|
78
|
+
case "ReadModel" :
|
|
79
|
+
case "ReadModels" :
|
|
80
|
+
return "ReadModel";
|
|
81
|
+
case "StateChange" :
|
|
82
|
+
case "StateChangeSlice" :
|
|
83
|
+
case "StateChangeSlices" :
|
|
84
|
+
case "StateChanges" :
|
|
85
|
+
return "StateChangeSlice";
|
|
86
|
+
case "StateViewSliceStream" :
|
|
87
|
+
case "StateViewSliceStreams" :
|
|
88
|
+
return "StateViewSliceStream";
|
|
89
|
+
case "StateView" :
|
|
90
|
+
case "StateViewSlice" :
|
|
91
|
+
case "StateViewSlices" :
|
|
92
|
+
case "StateViews" :
|
|
93
|
+
return "StateViewSlice";
|
|
94
|
+
case "Task" :
|
|
95
|
+
case "Tasks" :
|
|
96
|
+
return "Task";
|
|
97
|
+
default:
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function isKindFolder(folder) {
|
|
103
|
+
return Stdlib_Option.isSome(folderToKind(folder));
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
let bodySuffixes = [
|
|
107
|
+
"_ExtensionPointMapping",
|
|
108
|
+
"_ExtensionPoint",
|
|
109
|
+
"_Extension",
|
|
110
|
+
"_Projections",
|
|
111
|
+
"_Projection",
|
|
112
|
+
"_Mappings",
|
|
113
|
+
"_Behavior",
|
|
114
|
+
"_Automation",
|
|
115
|
+
"_Translation"
|
|
116
|
+
];
|
|
117
|
+
|
|
118
|
+
export {
|
|
119
|
+
all,
|
|
120
|
+
folderName,
|
|
121
|
+
folderToKind,
|
|
122
|
+
isKindFolder,
|
|
123
|
+
bodySuffixes,
|
|
124
|
+
}
|
|
125
|
+
/* No side effect */
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
A named counter (e.g. `"ProductCount"`). Used as a key in the counts database.
|
|
3
|
+
Multiple counters can exist within the same `Counter` component.
|
|
4
|
+
*/
|
|
5
|
+
type counterId = string
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
An opaque reference string that de-duplicates counter increments.
|
|
9
|
+
|
|
10
|
+
Each unique `reference` is counted at most once per `counterId`.
|
|
11
|
+
Typically set to the event's message ID to make increments idempotent.
|
|
12
|
+
*/
|
|
13
|
+
type reference = string
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
A counter threshold target.
|
|
17
|
+
|
|
18
|
+
When the count for `counterId` reaches `target`, the event mapping
|
|
19
|
+
framework fires the associated command.
|
|
20
|
+
*/
|
|
21
|
+
type counterTarget = {
|
|
22
|
+
counterId: counterId,
|
|
23
|
+
target: int,
|
|
24
|
+
}
|