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

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.
Files changed (37) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/package.json +6 -6
  3. package/src/Automation_GWT.res +130 -0
  4. package/src/Automation_GWT.res.mjs +193 -0
  5. package/src/Behavior_GWT.res +6 -6
  6. package/src/Behavior_GWT.res.mjs +5 -5
  7. package/src/Capabilities_Fake.res +157 -0
  8. package/src/Capabilities_Fake.res.mjs +254 -0
  9. package/src/Flow_GWT.res +112 -1
  10. package/src/Flow_GWT.res.mjs +165 -11
  11. package/src/InboundTranslation_GWT.res +112 -0
  12. package/src/InboundTranslation_GWT.res.mjs +190 -1
  13. package/src/OutboundTranslation_GWT.res +207 -4
  14. package/src/OutboundTranslation_GWT.res.mjs +257 -1
  15. package/src/StateChange/ExternalAddCategorySlice.res +1 -1
  16. package/src/StateChange/ExternalAddCategorySlice.res.mjs +2 -2
  17. package/src/StateChange/WithFixtures.res +1 -1
  18. package/src/StateChange/WithFixtures.res.mjs +2 -2
  19. package/src/StateChange/WithManualOpen.res +1 -1
  20. package/src/StateChange/WithManualOpen.res.mjs +2 -2
  21. package/tests/AutomationFromSliceGwtTest.res +145 -0
  22. package/tests/AutomationFromSliceGwtTest.res.mjs +353 -0
  23. package/tests/AutomationGwtTest.res.mjs +2 -2
  24. package/tests/DelegateGwtTest.res +5 -5
  25. package/tests/DelegateGwtTest.res.mjs +27 -27
  26. package/tests/FlowAggregateGwtTest.res.mjs +4 -4
  27. package/tests/FlowCrossPluginGwtTest.res +4 -4
  28. package/tests/FlowCrossPluginGwtTest.res.mjs +22 -22
  29. package/tests/FlowGwtTest.res.mjs +6 -6
  30. package/tests/InboundFromSliceGwtTest.res +63 -0
  31. package/tests/InboundFromSliceGwtTest.res.mjs +164 -0
  32. package/tests/InboundTranslationGwtTest.res.mjs +2 -7
  33. package/tests/MappingGwtTest.res.mjs +12 -12
  34. package/tests/OutboundFromSliceGwtTest.res +124 -0
  35. package/tests/OutboundFromSliceGwtTest.res.mjs +268 -0
  36. package/tests/StateChangeSliceAuthorizeGwtTest.res.mjs +20 -16
  37. package/tests/StateChangeSliceGwtTest.res.mjs +6 -6
package/CHANGELOG.md CHANGED
@@ -3,6 +3,27 @@
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.240 (2026-10-10)
7
+
8
+ * feat!: a command's rule is looked up by its name, without a cast ([5f02c40](https://github.com/ReventlessDev/reventless-core/commit/5f02c4081d077bf4a6951d8c9b2b16553a8ca87b))
9
+
10
+ ### BREAKING CHANGES
11
+
12
+ * a spec that writes `commandAuthorization` by hand writes
13
+ `authorizationOf = (name: string) => …` instead; the PPX refuses the old name.
14
+
15
+
16
+
17
+ # 1.0.0-alpha.239 (2026-10-10)
18
+
19
+ ### Bug Fixes
20
+
21
+ * an inbound translation is gated alike on every door, and encodes its commands by schema ([6ec3ce6](https://github.com/ReventlessDev/reventless-core/commit/6ec3ce672e53e2c162c64a90d103be691391be5e))
22
+ ### Features
23
+
24
+ * **gwt:** automation and translation tests take the slice as written, and the sidecars read them ([93a0919](https://github.com/ReventlessDev/reventless-core/commit/93a0919a0d66730cf9bf89144ad8fba3cb025332))
25
+
26
+
6
27
  # 1.0.0-alpha.238 (2026-10-08)
7
28
 
8
29
  **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.240",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -14,17 +14,17 @@
14
14
  },
15
15
  "dependencies": {
16
16
  "sury": "11.0.0",
17
- "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
18
17
  "@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
18
  "@reventlessdev/rescript-node": "2.0.0-alpha.22",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.160"
19
+ "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
20
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.190",
21
+ "@reventlessdev/reventless-core": "3.0.0-alpha.293",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.162"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
26
26
  "sury-ppx": "11.0.0",
27
- "@reventlessdev/reventless-ppx": "1.0.0-alpha.98"
27
+ "@reventlessdev/reventless-ppx": "1.0.0-alpha.99"
28
28
  },
29
29
  "peerDependencies": {
30
30
  "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 */
@@ -119,7 +119,7 @@ module type ActingSpec = {
119
119
  @schema
120
120
  type command
121
121
  type role
122
- let commandAuthorization: command => Reventless.Authorization.rule<role>
122
+ let authorizationOf: string => Reventless.Authorization.rule<role>
123
123
  }
124
124
 
125
125
  module Acting = (Spec: ActingSpec) => {
@@ -142,7 +142,7 @@ module Acting = (Spec: ActingSpec) => {
142
142
  // a role maps to is a deployment's fact, not the scenario's.
143
143
  let refusedByRule = (who: Caller.t, command: Spec.command): bool =>
144
144
  if (
145
- Spec.commandAuthorization(command)
145
+ Spec.authorizationOf(commandName(command))
146
146
  ->Reventless.Authorization.named
147
147
  ->Reventless.Authorization.admits(~signedIn=who.signedIn, ~holds=role =>
148
148
  who.roles->Array.includes(role)
@@ -240,7 +240,7 @@ module type BehaviorSpec = {
240
240
  type event
241
241
 
242
242
  type role
243
- let commandAuthorization: command => Reventless.Authorization.rule<role>
243
+ let authorizationOf: string => Reventless.Authorization.rule<role>
244
244
  }
245
245
 
246
246
  module type Behavior = {
@@ -460,7 +460,7 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
460
460
  type command = Spec.command
461
461
  let commandSchema = Spec.commandSchema
462
462
  type role = Spec.role
463
- let commandAuthorization = Spec.commandAuthorization
463
+ let authorizationOf = Spec.authorizationOf
464
464
  })
465
465
  let asCaller = Guard.asCaller
466
466
 
@@ -729,7 +729,7 @@ module type AggregateSpec = {
729
729
  @schema
730
730
  type error
731
731
  type role
732
- let commandAuthorization: command => Reventless.Authorization.rule<role>
732
+ let authorizationOf: string => Reventless.Authorization.rule<role>
733
733
  }
734
734
 
735
735
  module type AggregateT = {
@@ -790,7 +790,7 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
790
790
  type command = Spec.command
791
791
  let commandSchema = Spec.commandSchema
792
792
  type role = Spec.role
793
- let commandAuthorization = Spec.commandAuthorization
793
+ let authorizationOf = Spec.authorizationOf
794
794
  })
795
795
  let asCaller = Guard.asCaller
796
796
 
@@ -106,7 +106,7 @@ function Acting(Spec) {
106
106
  };
107
107
  let commandName = command => Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema));
108
108
  let refusedByRule = (who, command) => {
109
- if (Authorization$Reventless.admits(Authorization$Reventless.named(Spec.commandAuthorization(command)), who.signedIn, role => who.roles.includes(role))) {
109
+ if (Authorization$Reventless.admits(Authorization$Reventless.named(Spec.authorizationOf(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema)))), who.signedIn, role => who.roles.includes(role))) {
110
110
  return false;
111
111
  } else {
112
112
  refusal.contents = {
@@ -376,7 +376,7 @@ function Make(Spec) {
376
376
  let name = Spec.name;
377
377
  let historySchema = Spec.consumedEventSchema;
378
378
  let commandSchema = Spec.commandSchema;
379
- let commandAuthorization = Spec.commandAuthorization;
379
+ let authorizationOf = Spec.authorizationOf;
380
380
  let caller = {
381
381
  contents: undefined
382
382
  };
@@ -394,7 +394,7 @@ function Make(Spec) {
394
394
  refusal.contents = undefined;
395
395
  let claim = Stdlib_Option.map(who, w => w.claim);
396
396
  if (Stdlib_Option.mapOr(who, false, __x => {
397
- if (Authorization$Reventless.admits(Authorization$Reventless.named(commandAuthorization(command)), __x.signedIn, role => __x.roles.includes(role))) {
397
+ if (Authorization$Reventless.admits(Authorization$Reventless.named(authorizationOf(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema)))), __x.signedIn, role => __x.roles.includes(role))) {
398
398
  return false;
399
399
  } else {
400
400
  refusal.contents = {
@@ -747,7 +747,7 @@ function MakeFromAggregate(Spec) {
747
747
  let name = Spec.name;
748
748
  let historySchema = Spec.eventSchema;
749
749
  let commandSchema = Spec.commandSchema;
750
- let commandAuthorization = Spec.commandAuthorization;
750
+ let authorizationOf = Spec.authorizationOf;
751
751
  let caller = {
752
752
  contents: undefined
753
753
  };
@@ -765,7 +765,7 @@ function MakeFromAggregate(Spec) {
765
765
  refusal.contents = undefined;
766
766
  let claim = Stdlib_Option.map(who, w => w.claim);
767
767
  if (Stdlib_Option.mapOr(who, false, __x => {
768
- if (Authorization$Reventless.admits(Authorization$Reventless.named(commandAuthorization(command)), __x.signedIn, role => __x.roles.includes(role))) {
768
+ if (Authorization$Reventless.admits(Authorization$Reventless.named(authorizationOf(Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema)))), __x.signedIn, role => __x.roles.includes(role))) {
769
769
  return false;
770
770
  } else {
771
771
  refusal.contents = {
@@ -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
+ }