@reventlessdev/reventless-gwt 1.0.0-alpha.238 → 1.0.0-alpha.239

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 CHANGED
@@ -3,6 +3,16 @@
3
3
  All notable changes to this project will be documented in this file.
4
4
  See [Conventional Commits](https://conventionalcommits.org) for commit guidelines.
5
5
 
6
+ # 1.0.0-alpha.239 (2026-10-10)
7
+
8
+ ### Bug Fixes
9
+
10
+ * an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
11
+ ### Features
12
+
13
+ * **gwt:** automation and translation tests take the slice as written, and the sidecars read them ([93a0919](https://github.com/ReventlessDev/reventless-core/commit/93a0919a0d66730cf9bf89144ad8fba3cb025332))
14
+
15
+
6
16
  # 1.0.0-alpha.238 (2026-10-08)
7
17
 
8
18
  **Note:** Version bump only for package @reventlessdev/reventless-gwt
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@reventlessdev/reventless-gwt",
3
- "version": "1.0.0-alpha.238",
3
+ "version": "1.0.0-alpha.239",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -15,11 +15,11 @@
15
15
  "dependencies": {
16
16
  "sury": "11.0.0",
17
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
18
+ "@reventlessdev/reventless-core": "3.0.0-alpha.292",
18
19
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
19
- "@reventlessdev/reventless-infra": "3.0.0-alpha.188",
20
- "@reventlessdev/reventless-core": "3.0.0-alpha.291",
21
20
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.160"
21
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.189",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.161"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
@@ -56,6 +56,9 @@ module type T = {
56
56
  let thenScenarioTodos: (scenario, array<(string, Spec.todoItem)>) => Outcome.outcome
57
57
  }
58
58
 
59
+ /** Deprecated: takes an adapter module that flattens one mapping. Use
60
+ `FromSlice`, which takes the slice as written; this goes one release after
61
+ the examples have moved. */
59
62
  module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
60
63
  module Spec = Spec
61
64
 
@@ -194,3 +197,130 @@ module Make = (Spec: SliceSpec): (T with module Spec = Spec) => {
194
197
 
195
198
  let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected)
196
199
  }
200
+
201
+ // The slice as written: its `Spec` and its `_Automation` body, with every
202
+ // mapping, `process` and `onExhausted`. Per-source verbs come from
203
+ // `Mapping(M)`, because a mapping's event type is hidden inside `mappings`.
204
+ module FromSlice = (
205
+ Spec: Reventless.AutomationSlice.Spec,
206
+ Automation: Reventless.AutomationSlice.Automation with module Spec := Spec,
207
+ ) => {
208
+ module Route = AutomationSlice_Route.Make(Spec, Automation)
209
+
210
+ let describe = JestBind.describe
211
+ let test = (name, body) => JestBind.test(~slice=Spec.name, name, body)
212
+
213
+ let testContext: Reventless.AutomationSlice.context = {
214
+ environment: "test",
215
+ platformName: "test",
216
+ pluginName: "test",
217
+ sliceName: Spec.name,
218
+ }
219
+
220
+ /** A source event as the runtime receives it: encoded, and addressed by its
221
+ source's name. Built with `Mapping(M).event`. */
222
+ type sourced = {sourceName: string, sourceId: string, payload: JSON.t}
223
+
224
+ type scenario = {
225
+ todos: array<(string, Spec.todoItem)>,
226
+ commands: array<(string, Spec.command)>,
227
+ }
228
+
229
+ let encTodos = (arr: array<(string, Spec.todoItem)>) =>
230
+ arr->Array.map(((id, t)) => (id, t->Message.encode(Spec.todoItemSchema)))
231
+ let encPair = ((id, cmd): (string, Spec.command)) => {
232
+ let d = Dict.make()
233
+ d->Dict.set("id", JSON.Encode.string(id))
234
+ d->Dict.set("command", cmd->Message.encode(Spec.commandSchema))
235
+ JSON.Encode.object(d)
236
+ }
237
+
238
+ let thenTodos = (actual, expected) =>
239
+ actual == expected
240
+ ? Outcome.pass
241
+ : Outcome.fail(TodoMismatch({expected: encTodos(expected), actual: encTodos(actual)}))
242
+
243
+ let thenResolved = (actual: option<string>, expected: option<string>) => {
244
+ let asPair = opt => opt->Option.mapOr([], id => [(id, JSON.Encode.null)])
245
+ actual == expected
246
+ ? Outcome.pass
247
+ : Outcome.fail(TodoMismatch({expected: asPair(expected), actual: asPair(actual)}))
248
+ }
249
+
250
+ module Mapping = (M: Automation.Mapping) => {
251
+ let event = (e: M.sourceEvent, ~sourceId="") => {
252
+ sourceName: M.sourceName,
253
+ sourceId,
254
+ payload: e->Message.encode(M.sourceEventSchema),
255
+ }
256
+ let givenEvent = (e: M.sourceEvent) => e
257
+ let whenCollect = (e, ~sourceId="", ~context=testContext) => M.collect(e, ~sourceId, context)
258
+ let whenResolve = M.resolve
259
+ }
260
+
261
+ // Unit: process and abandonment
262
+ let givenTodo = (id: string, item: Spec.todoItem) => (id, item)
263
+ let whenProcess = ((id, item)) => Automation.process(id, item)
264
+ let whenExhausted = ((id, item)) => Automation.onExhausted(id, item)
265
+ let thenCommand = (actual, expectedId, expectedCmd) =>
266
+ switch actual {
267
+ | Some((id, cmd)) if id == expectedId && cmd == expectedCmd => Outcome.pass
268
+ | actual =>
269
+ Outcome.fail(
270
+ EventsMismatch({
271
+ expected: [encPair((expectedId, expectedCmd))],
272
+ actual: actual->Option.mapOr([], p => [encPair(p)]),
273
+ }),
274
+ )
275
+ }
276
+ let thenNoCommand = actual =>
277
+ switch actual {
278
+ | None => Outcome.pass
279
+ | Some(p) => Outcome.fail(NoEventExpected({actual: [encPair(p)]}))
280
+ }
281
+
282
+ // Scenario: a sweep routes each event in turn, as phase 1 does, so to-dos keep
283
+ // the order the events produced them.
284
+ let givenEvents = (es: array<sourced>) => es
285
+ let routeAll = (events: array<sourced>, context) =>
286
+ events->Array.flatMap(e =>
287
+ Route.route(e.payload, ~sourceName=e.sourceName, ~sourceId=e.sourceId, context)
288
+ )
289
+ /** Applies routed events as the runtime's to-do list does: the first writer of
290
+ an id wins, and a resolve completes only a row that already exists. */
291
+ let sweep = (routed: array<AutomationSlice_Route.routed<Spec.todoItem>>) => {
292
+ let rows: array<(string, Spec.todoItem, ref<bool>)> = []
293
+ routed->Array.forEach(r => {
294
+ r.collected->Array.forEach(((id, item)) =>
295
+ if !(rows->Array.some(((seen, _, _)) => seen == id)) {
296
+ rows->Array.push((id, item, ref(false)))
297
+ }
298
+ )
299
+ r.resolved->Option.forEach(id =>
300
+ rows->Array.forEach(
301
+ ((seen, _, done)) =>
302
+ if seen == id {
303
+ done := true
304
+ },
305
+ )
306
+ )
307
+ })
308
+ let todos = rows->Array.filterMap(((id, item, done)) => done.contents ? None : Some((id, item)))
309
+ {todos, commands: todos->Array.filterMap(((id, item)) => Automation.process(id, item))}
310
+ }
311
+ let whenSweep = (events, ~context=testContext) => sweep(routeAll(events, context))
312
+ let thenCommands = (scenario, expected) =>
313
+ scenario.commands == expected
314
+ ? Outcome.pass
315
+ : Outcome.fail(
316
+ EventsMismatch({
317
+ expected: expected->Array.map(encPair),
318
+ actual: scenario.commands->Array.map(encPair),
319
+ }),
320
+ )
321
+ let andThenEvents = (scenario, events, ~context=testContext) => {
322
+ let resolved = routeAll(events, context)->Array.filterMap(r => r.resolved)
323
+ {...scenario, todos: scenario.todos->Array.filter(((id, _)) => !(resolved->Array.includes(id)))}
324
+ }
325
+ let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected)
326
+ }
@@ -1,11 +1,13 @@
1
1
  // Generated by ReScript, PLEASE EDIT WITH CARE
2
2
 
3
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";
4
5
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
5
6
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
6
7
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
7
8
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
8
9
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
10
+ import * as AutomationSlice_Route$ReventlessCore from "@reventlessdev/reventless-core/src/components/AutomationSlice/AutomationSlice_Route.res.mjs";
9
11
 
10
12
  function Make(Spec) {
11
13
  let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
@@ -152,7 +154,198 @@ function Make(Spec) {
152
154
  };
153
155
  }
154
156
 
157
+ function FromSlice(Spec) {
158
+ return Automation => {
159
+ let Route = AutomationSlice_Route$ReventlessCore.Make(Spec)(Automation);
160
+ let test = (name, body) => JestBind$ReventlessGwt.test(Spec.name, name, body);
161
+ let testContext_sliceName = Spec.name;
162
+ let testContext = {
163
+ environment: "test",
164
+ platformName: "test",
165
+ pluginName: "test",
166
+ sliceName: testContext_sliceName
167
+ };
168
+ let encTodos = arr => arr.map(param => [
169
+ param[0],
170
+ Message$ReventlessCore.encode(param[1], Spec.todoItemSchema)
171
+ ]);
172
+ let encPair = param => {
173
+ let d = {};
174
+ d["id"] = param[0];
175
+ d["command"] = Message$ReventlessCore.encode(param[1], Spec.commandSchema);
176
+ return d;
177
+ };
178
+ let thenTodos = (actual, expected) => {
179
+ if (Primitive_object.equal(actual, expected)) {
180
+ return Outcome$ReventlessGwt.pass;
181
+ } else {
182
+ return Outcome$ReventlessGwt.fail({
183
+ TAG: "TodoMismatch",
184
+ expected: encTodos(expected),
185
+ actual: encTodos(actual)
186
+ });
187
+ }
188
+ };
189
+ let thenResolved = (actual, expected) => {
190
+ let asPair = opt => Stdlib_Option.mapOr(opt, [], id => [[
191
+ id,
192
+ null
193
+ ]]);
194
+ if (Primitive_object.equal(actual, expected)) {
195
+ return Outcome$ReventlessGwt.pass;
196
+ } else {
197
+ return Outcome$ReventlessGwt.fail({
198
+ TAG: "TodoMismatch",
199
+ expected: asPair(expected),
200
+ actual: asPair(actual)
201
+ });
202
+ }
203
+ };
204
+ let Mapping = M => {
205
+ let event = (e, sourceIdOpt) => {
206
+ let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
207
+ return {
208
+ sourceName: M.sourceName,
209
+ sourceId: sourceId,
210
+ payload: Message$ReventlessCore.encode(e, M.sourceEventSchema)
211
+ };
212
+ };
213
+ let givenEvent = e => e;
214
+ let whenCollect = (e, sourceIdOpt, contextOpt) => {
215
+ let sourceId = sourceIdOpt !== undefined ? sourceIdOpt : "";
216
+ let context = contextOpt !== undefined ? contextOpt : testContext;
217
+ return M.collect(e, sourceId, context);
218
+ };
219
+ return {
220
+ event: event,
221
+ givenEvent: givenEvent,
222
+ whenCollect: whenCollect,
223
+ whenResolve: M.resolve
224
+ };
225
+ };
226
+ let givenTodo = (id, item) => [
227
+ id,
228
+ item
229
+ ];
230
+ let whenProcess = param => Automation.process(param[0], param[1]);
231
+ let whenExhausted = param => Automation.onExhausted(param[0], param[1]);
232
+ let thenCommand = (actual, expectedId, expectedCmd) => {
233
+ if (actual !== undefined && actual[0] === expectedId && Primitive_object.equal(actual[1], expectedCmd)) {
234
+ return Outcome$ReventlessGwt.pass;
235
+ }
236
+ return Outcome$ReventlessGwt.fail({
237
+ TAG: "EventsMismatch",
238
+ expected: [encPair([
239
+ expectedId,
240
+ expectedCmd
241
+ ])],
242
+ actual: Stdlib_Option.mapOr(actual, [], p => [encPair(p)])
243
+ });
244
+ };
245
+ let thenNoCommand = actual => {
246
+ if (actual !== undefined) {
247
+ return Outcome$ReventlessGwt.fail({
248
+ TAG: "NoEventExpected",
249
+ actual: [encPair(actual)]
250
+ });
251
+ } else {
252
+ return Outcome$ReventlessGwt.pass;
253
+ }
254
+ };
255
+ let givenEvents = es => es;
256
+ let routeAll = (events, context) => events.flatMap(e => Route.route(e.payload, e.sourceName, e.sourceId, context));
257
+ let sweep = routed => {
258
+ let rows = [];
259
+ routed.forEach(r => {
260
+ r.collected.forEach(param => {
261
+ let id = param[0];
262
+ if (!rows.some(param => param[0] === id)) {
263
+ rows.push([
264
+ id,
265
+ param[1],
266
+ {
267
+ contents: false
268
+ }
269
+ ]);
270
+ return;
271
+ }
272
+ });
273
+ Stdlib_Option.forEach(r.resolved, id => {
274
+ rows.forEach(param => {
275
+ if (param[0] === id) {
276
+ param[2].contents = true;
277
+ return;
278
+ }
279
+ });
280
+ });
281
+ });
282
+ let todos = Stdlib_Array.filterMap(rows, param => {
283
+ if (param[2].contents) {
284
+ return;
285
+ } else {
286
+ return [
287
+ param[0],
288
+ param[1]
289
+ ];
290
+ }
291
+ });
292
+ return {
293
+ todos: todos,
294
+ commands: Stdlib_Array.filterMap(todos, param => Automation.process(param[0], param[1]))
295
+ };
296
+ };
297
+ let whenSweep = (events, contextOpt) => {
298
+ let context = contextOpt !== undefined ? contextOpt : testContext;
299
+ return sweep(routeAll(events, context));
300
+ };
301
+ let thenCommands = (scenario, expected) => {
302
+ if (Primitive_object.equal(scenario.commands, expected)) {
303
+ return Outcome$ReventlessGwt.pass;
304
+ } else {
305
+ return Outcome$ReventlessGwt.fail({
306
+ TAG: "EventsMismatch",
307
+ expected: expected.map(encPair),
308
+ actual: scenario.commands.map(encPair)
309
+ });
310
+ }
311
+ };
312
+ let andThenEvents = (scenario, events, contextOpt) => {
313
+ let context = contextOpt !== undefined ? contextOpt : testContext;
314
+ let resolved = Stdlib_Array.filterMap(routeAll(events, context), r => r.resolved);
315
+ return {
316
+ todos: scenario.todos.filter(param => !resolved.includes(param[0])),
317
+ commands: scenario.commands
318
+ };
319
+ };
320
+ let thenScenarioTodos = (scenario, expected) => thenTodos(scenario.todos, expected);
321
+ return {
322
+ Route: Route,
323
+ describe: JestBind$ReventlessGwt.describe,
324
+ test: test,
325
+ testContext: testContext,
326
+ encTodos: encTodos,
327
+ encPair: encPair,
328
+ thenTodos: thenTodos,
329
+ thenResolved: thenResolved,
330
+ Mapping: Mapping,
331
+ givenTodo: givenTodo,
332
+ whenProcess: whenProcess,
333
+ whenExhausted: whenExhausted,
334
+ thenCommand: thenCommand,
335
+ thenNoCommand: thenNoCommand,
336
+ givenEvents: givenEvents,
337
+ routeAll: routeAll,
338
+ sweep: sweep,
339
+ whenSweep: whenSweep,
340
+ thenCommands: thenCommands,
341
+ andThenEvents: andThenEvents,
342
+ thenScenarioTodos: thenScenarioTodos
343
+ };
344
+ };
345
+ }
346
+
155
347
  export {
156
348
  Make,
349
+ FromSlice,
157
350
  }
158
351
  /* Message-ReventlessCore Not a pure module */
@@ -0,0 +1,157 @@
1
+ // Recording fakes for `Reventless.Capabilities.t`: every call a `translate` makes
2
+ // is kept, in order, and each answer can be scripted. What an outbound test
3
+ // asserts with `thenSent`.
4
+
5
+ module Capabilities = Reventless.Capabilities
6
+ module Geocoding = Reventless.Geocoding
7
+ module Messaging = Reventless.Messaging
8
+ module IdentityProvider = Reventless.IdentityProvider
9
+ module Secrets = Reventless.Secrets
10
+
11
+ /** One call a slice made through its capabilities. */
12
+ type call =
13
+ | Geocoded({text: string})
14
+ | Sent({recipient: Messaging.recipient, message: Messaging.message})
15
+ | PrincipalCreated({
16
+ contact: Messaging.recipient,
17
+ credential: IdentityProvider.credential,
18
+ groups: array<string>,
19
+ })
20
+ | AddedToGroup({principal: IdentityProvider.principal, group: string})
21
+ | RemovedFromGroup({principal: IdentityProvider.principal, group: string})
22
+ | PrincipalDeleted({principal: IdentityProvider.principal})
23
+ | TokenDrawn({length: int, alphabet: Secrets.alphabet})
24
+ | Hashed({input: string})
25
+
26
+ type t = {
27
+ capabilities: Capabilities.t,
28
+ /** The calls made so far, oldest first. */
29
+ calls: unit => array<call>,
30
+ }
31
+
32
+ /**
33
+ Fakes whose answers are given per capability. An answer left out is a plain
34
+ success: a geocoder with no candidates, a send with a numbered receipt, a
35
+ principal with a numbered id, `token` (or `t` repeated to length), and a hash
36
+ that prefixes `"hash:"`. `~secrets` replaces the secret source whole, e.g. with
37
+ `Secrets.unavailable`; its calls are still recorded.
38
+ */
39
+ let make = (
40
+ ~geocode: option<Geocoding.search>=?,
41
+ ~send: option<Messaging.send>=?,
42
+ ~channels: array<Messaging.channel>=[Email, Sms],
43
+ ~pushServices: array<Messaging.pushService>=[],
44
+ ~createPrincipal: option<
45
+ (
46
+ ~contact: Messaging.recipient,
47
+ ~credential: IdentityProvider.credential,
48
+ ~groups: array<string>,
49
+ ) => promise<result<IdentityProvider.principal, IdentityProvider.failure>>,
50
+ >=?,
51
+ ~token: option<string>=?,
52
+ ~hash: option<string => string>=?,
53
+ ~secrets: option<Secrets.t>=?,
54
+ (),
55
+ ): t => {
56
+ let log: array<call> = []
57
+ let record = c => log->Array.push(c)
58
+ let geocode = async (~text) => {
59
+ record(Geocoded({text: text}))
60
+ switch geocode {
61
+ | Some(f) => await f(~text)
62
+ | None => Ok([])
63
+ }
64
+ }
65
+ let send = async (~recipient, ~message) => {
66
+ record(Sent({recipient, message}))
67
+ switch send {
68
+ | Some(f) => await f(~recipient, ~message)
69
+ | None => Ok({Messaging.ref: `receipt-${Int.toString(log->Array.length)}`})
70
+ }
71
+ }
72
+ let createPrincipal = async (~contact, ~credential, ~groups) => {
73
+ record(PrincipalCreated({contact, credential, groups}))
74
+ switch createPrincipal {
75
+ | Some(f) => await f(~contact, ~credential, ~groups)
76
+ | None => Ok({IdentityProvider.providerId: `principal-${Int.toString(log->Array.length)}`})
77
+ }
78
+ }
79
+ let capabilities: Capabilities.t = {
80
+ geocode,
81
+ messaging: Messaging.makeProvider(~emailAndSms=channels, ~pushServices, ~send),
82
+ identityProvider: IdentityProvider.make(
83
+ ~createPrincipal,
84
+ ~addToGroup=async (~principal, ~group) => {
85
+ record(AddedToGroup({principal, group}))
86
+ Ok()
87
+ },
88
+ ~removeFromGroup=async (~principal, ~group) => {
89
+ record(RemovedFromGroup({principal, group}))
90
+ Ok()
91
+ },
92
+ ~deletePrincipal=async (~principal) => {
93
+ record(PrincipalDeleted({principal: principal}))
94
+ Ok()
95
+ },
96
+ ~operations=[CreatePrincipal, AddToGroup, RemoveFromGroup, DeletePrincipal],
97
+ ),
98
+ secrets: {
99
+ randomToken: (~length, ~alphabet) => {
100
+ record(TokenDrawn({length, alphabet}))
101
+ switch secrets {
102
+ | Some(s) => s.randomToken(~length, ~alphabet)
103
+ | None => Ok(token->Option.getOr("t"->String.repeat(length)))
104
+ }
105
+ },
106
+ hash: input => {
107
+ record(Hashed({input: input}))
108
+ switch secrets {
109
+ | Some(s) => s.hash(input)
110
+ | None => Ok(hash->Option.mapOr(`hash:${input}`, f => f(input)))
111
+ }
112
+ },
113
+ },
114
+ }
115
+ {capabilities, calls: () => log->Array.copy}
116
+ }
117
+
118
+ /** A readable rendering of a call, for a mismatch. */
119
+ let toJson = (c: call): JSON.t => {
120
+ let recipientText = (r: Messaging.recipient) =>
121
+ switch r {
122
+ | ToEmail(email) => `email:${email}`
123
+ | ToSms(phone) => `sms:${phone}`
124
+ | ToPush(address) => `push:${address->Messaging.serviceOf->Messaging.pushServiceToString}`
125
+ }
126
+ let obj = (tag, fields) =>
127
+ JSON.Encode.object(Dict.fromArray([("TAG", JSON.Encode.string(tag)), ...fields]))
128
+ let str = JSON.Encode.string
129
+ switch c {
130
+ | Geocoded({text}) => obj("Geocoded", [("text", str(text))])
131
+ | Sent({recipient, message}) =>
132
+ obj(
133
+ "Sent",
134
+ [
135
+ ("recipient", str(recipientText(recipient))),
136
+ ("subject", message.subject->Option.mapOr(JSON.Encode.null, str)),
137
+ ("body", str(message.body)),
138
+ ],
139
+ )
140
+ | PrincipalCreated({contact, groups, _}) =>
141
+ obj(
142
+ "PrincipalCreated",
143
+ [
144
+ ("contact", str(recipientText(contact))),
145
+ ("groups", groups->Array.map(str)->JSON.Encode.array),
146
+ ],
147
+ )
148
+ | AddedToGroup({principal, group}) =>
149
+ obj("AddedToGroup", [("principal", str(principal.providerId)), ("group", str(group))])
150
+ | RemovedFromGroup({principal, group}) =>
151
+ obj("RemovedFromGroup", [("principal", str(principal.providerId)), ("group", str(group))])
152
+ | PrincipalDeleted({principal}) =>
153
+ obj("PrincipalDeleted", [("principal", str(principal.providerId))])
154
+ | TokenDrawn({length, _}) => obj("TokenDrawn", [("length", JSON.Encode.int(length))])
155
+ | Hashed({input}) => obj("Hashed", [("input", str(input))])
156
+ }
157
+ }