@reventlessdev/reventless-gwt 1.0.0-alpha.230 → 1.0.0-alpha.232

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,21 @@
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.232 (2026-10-01)
7
+
8
+ ### Features
9
+
10
+ * **spec, core, gwt, ppx:** a command acts only on what its caller owns ([2636f0e](https://github.com/ReventlessDev/reventless-core/commit/2636f0ee44797ee2c0b0c40a7107cf306113173f))
11
+
12
+
13
+ # 1.0.0-alpha.231 (2026-09-29)
14
+
15
+ **Note:** Version bump only for package @reventlessdev/reventless-gwt
16
+
17
+
18
+
19
+
20
+
6
21
  # 1.0.0-alpha.230 (2026-09-28)
7
22
 
8
23
  **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.230",
3
+ "version": "1.0.0-alpha.232",
4
4
  "description": "Given-When-Then DSLs and test harness for Reventless slice testing",
5
5
  "license": "Apache-2.0",
6
6
  "jest": {
@@ -16,10 +16,10 @@
16
16
  "sury": "11.0.0",
17
17
  "@reventlessdev/rescript-effect": "0.1.0-alpha.34",
18
18
  "@reventlessdev/rescript-jest": "1.0.0-alpha.11",
19
- "@reventlessdev/reventless-core": "3.0.0-alpha.283",
20
- "@reventlessdev/reventless-infra": "3.0.0-alpha.181",
21
- "@reventlessdev/rescript-node": "2.0.0-alpha.19",
22
- "@reventlessdev/reventless-spec": "3.0.0-alpha.153"
19
+ "@reventlessdev/reventless-core": "3.0.0-alpha.285",
20
+ "@reventlessdev/reventless-infra": "3.0.0-alpha.183",
21
+ "@reventlessdev/rescript-node": "2.0.0-alpha.20",
22
+ "@reventlessdev/reventless-spec": "3.0.0-alpha.155"
23
23
  },
24
24
  "devDependencies": {
25
25
  "rescript": "12.3.1",
@@ -22,6 +22,121 @@ open ReventlessCore
22
22
  // and slice users *cannot* use [MakeFromAggregate] (their Spec/Behavior shape
23
23
  // differs). Pick the matching entry point.
24
24
 
25
+ // ---------------------------------------------------------------------------
26
+ // Who issues the command, for slices and aggregates that mark `@owner` on the
27
+ // events they decide on. `asCaller` names the caller before `whenCmd`; the
28
+ // command is then checked against the owners the given events record, exactly
29
+ // as the handler checks it before `decide`. Without `asCaller` the command
30
+ // carries no caller claim, which the handler reads as the platform acting for
31
+ // itself — so every scenario written before this existed is unchanged.
32
+ // ---------------------------------------------------------------------------
33
+ module Caller = {
34
+ external unsafeAsString: 'id => string = "%identity"
35
+
36
+ /**
37
+ A caller who owns only what records their id.
38
+
39
+ Takes the id as the scenario has it — `Caller.owner(c1)` with `c1: CustomerId.t`,
40
+ or a plain string — because every `Id.Make` identity is a string on the wire.
41
+ The type is not checked, and need not be: an id of the wrong entity matches no
42
+ recorded owner, so it can only be refused, which fails the scenario that
43
+ expected the owner to act. A value that is not a string at all is refused here.
44
+ */
45
+ let owner = (id: 'id): Message.CallerClaim.t =>
46
+ if Reventless.OwnerScope.isJsString(id) {
47
+ Owned({userId: id->unsafeAsString})
48
+ } else {
49
+ JsError.throwWithMessage(
50
+ "Caller.owner: expected an id (a string at runtime, such as an Id.Make identity)",
51
+ )
52
+ }
53
+ /** A caller in an elevated group, acting on anyone's behalf. */
54
+ let operator: Message.CallerClaim.t = Exempt
55
+ /** A caller with no identity. */
56
+ let anonymous: Message.CallerClaim.t = Unidentified
57
+ }
58
+
59
+ module type ActingSpec = {
60
+ let name: string
61
+ @schema
62
+ type history
63
+ @schema
64
+ type command
65
+ }
66
+
67
+ module Acting = (Spec: ActingSpec) => {
68
+ let caller: ref<option<Message.CallerClaim.t>> = ref(None)
69
+ let refusal: ref<option<CommandTopic_Helpers.rejectedResult>> = ref(None)
70
+
71
+ let ownerFieldsByEventType = Reventless.Owner.fieldNamesByVariant(
72
+ Spec.historySchema->S.castToUnknown,
73
+ )
74
+
75
+ let asCaller = (history, claim) => {
76
+ caller := Some(claim)
77
+ history
78
+ }
79
+
80
+ // Every given event is the command's own history here, so all of them count.
81
+ // The caller is consumed: the next scenario starts with no claim.
82
+ let refuses = (history: array<Spec.history>, command: Spec.command): bool => {
83
+ let claim = caller.contents
84
+ caller := None
85
+ refusal := None
86
+ if ownerFieldsByEventType->Dict.keysToArray->Array.length == 0 {
87
+ false
88
+ } else {
89
+ let owners = history->Array.reduce([], (owners, event) => {
90
+ let (eventType, data) = event->Message.encode(Spec.historySchema)->Message.splitMessage
91
+ switch ownerFieldsByEventType->Dict.get(eventType) {
92
+ | Some(fields) => owners->Reventless.OwnerScope.recordOwners(~fields, data)
93
+ | None => owners
94
+ }
95
+ })
96
+ refusal :=
97
+ Reventless.OwnerScope.decideActing(~claim, ~recordedOwners=Some(owners))
98
+ ->CommandTopic_Helpers.ownershipRefusal(
99
+ ~component=Spec.name,
100
+ ~command=command->Message.encode(Spec.commandSchema)->Message.variantNameOfJson,
101
+ )
102
+ ->Option.map(r => r.rejected)
103
+ refusal.contents->Option.isSome
104
+ }
105
+ }
106
+
107
+ let encRefusal = (r: CommandTopic_Helpers.rejectedResult) =>
108
+ JSON.Encode.object(
109
+ Dict.fromArray([
110
+ ("errorCode", JSON.Encode.string(r.errorCode)),
111
+ ("errorDetail", JSON.Encode.string(r.errorDetail)),
112
+ ]),
113
+ )
114
+
115
+ // Any other `then*` after a refused command fails: `decide` never ran, so
116
+ // there is nothing for it to compare.
117
+ let unexpectedRefusal = (): option<Outcome.outcome> =>
118
+ refusal.contents->Option.map(r =>
119
+ Outcome.fail(
120
+ ErrorMismatch({expected: JSON.Encode.null, actual: Some(encRefusal(r)), actualEvents: []}),
121
+ )
122
+ )
123
+
124
+ let thenRefused = (~encEvents, ~decided: option<JSON.t>, events) =>
125
+ switch refusal.contents {
126
+ | Some(_) => Outcome.pass
127
+ | None =>
128
+ Outcome.fail(
129
+ ErrorMismatch({
130
+ expected: JSON.Encode.object(
131
+ Dict.fromArray([("errorCode", JSON.Encode.string("Forbidden"))]),
132
+ ),
133
+ actual: decided,
134
+ actualEvents: encEvents(events),
135
+ }),
136
+ )
137
+ }
138
+ }
139
+
25
140
  // ---------------------------------------------------------------------------
26
141
  // Slice form — replaces the legacy `StateChangeSlice_GWT`.
27
142
  // ---------------------------------------------------------------------------
@@ -60,6 +175,10 @@ module type T = {
60
175
 
61
176
  let givenEvents: array<Spec.consumedEvent> => array<Spec.consumedEvent>
62
177
 
178
+ // Who issues the next `whenCmd`; see `Caller`.
179
+ module Caller: module type of Caller
180
+ let asCaller: (array<Spec.consumedEvent>, Message.CallerClaim.t) => array<Spec.consumedEvent>
181
+
63
182
  let whenCmd: (array<Spec.consumedEvent>, Spec.command) => array<Spec.event>
64
183
 
65
184
  let thenEvent: (array<Spec.event>, Spec.event) => Outcome.outcome
@@ -68,6 +187,8 @@ module type T = {
68
187
  let thenEventWithError: (array<Spec.event>, Spec.event, Spec.error) => Outcome.outcome
69
188
  let thenEventsWithError: (array<Spec.event>, array<Spec.event>, Spec.error) => Outcome.outcome
70
189
  let thenError: (array<Spec.event>, Spec.error) => Outcome.outcome
190
+ // Refused before `decide` because the caller does not own what it acts on.
191
+ let thenRefused: array<Spec.event> => Outcome.outcome
71
192
 
72
193
  // DCB optimistic-concurrency assertions.
73
194
  let thenAppendsConditionedOn: (array<Spec.event>, Reventless.DcbTag.query) => Outcome.outcome
@@ -242,6 +363,16 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
242
363
  module Core = AssertionCore(Spec)
243
364
  let errors = Core.errors
244
365
 
366
+ module Caller = Caller
367
+ module Guard = Acting({
368
+ let name = Spec.name
369
+ type history = Spec.consumedEvent
370
+ let historySchema = Spec.consumedEventSchema
371
+ type command = Spec.command
372
+ let commandSchema = Spec.commandSchema
373
+ })
374
+ let asCaller = Guard.asCaller
375
+
245
376
  // DCB append-condition derived inside [whenCmd]; [None] until first call.
246
377
  let derivedCondition: ref<option<Reventless.DcbTag.appendCondition>> = ref(None)
247
378
 
@@ -379,12 +510,15 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
379
510
  }
380
511
  }
381
512
 
382
- let state = currentState(history)
383
- switch Behavior.decide(state, command) {
384
- | Ok(events) => events
385
- | Error(error) =>
386
- errors := [error]
513
+ if Guard.refuses(history, command) {
387
514
  []
515
+ } else {
516
+ switch Behavior.decide(currentState(history), command) {
517
+ | Ok(events) => events
518
+ | Error(error) =>
519
+ errors := [error]
520
+ []
521
+ }
388
522
  }
389
523
  }
390
524
 
@@ -392,7 +526,17 @@ module Make = (Spec: BehaviorSpec, Behavior: Behavior with module Spec := Spec):
392
526
  let whenCmd = (history, cmd) => history->exec(cmd)
393
527
 
394
528
  let checkAppendCondition = (): option<Outcome.outcome> =>
395
- appendConditionFailure.contents->Option.map(m => Outcome.fail(m))
529
+ switch appendConditionFailure.contents {
530
+ | Some(m) => Some(Outcome.fail(m))
531
+ | None => Guard.unexpectedRefusal()
532
+ }
533
+
534
+ let thenRefused = events =>
535
+ Guard.thenRefused(
536
+ ~encEvents=Core.encEvents,
537
+ ~decided=errors.contents->Array.get(0)->Option.map(Core.encError),
538
+ events,
539
+ )
396
540
 
397
541
  // The append-condition footgun is surfaced before the shared comparison core
398
542
  // runs; `thenAppends*` below bypass it deliberately.
@@ -503,6 +647,10 @@ module type AggregateT = {
503
647
 
504
648
  let givenEvents: array<Spec.event> => array<Spec.event>
505
649
 
650
+ // Who issues the next `whenCmd`; see `Caller`.
651
+ module Caller: module type of Caller
652
+ let asCaller: (array<Spec.event>, Message.CallerClaim.t) => array<Spec.event>
653
+
506
654
  let whenCmd: (array<Spec.event>, Spec.command) => array<Spec.event>
507
655
 
508
656
  let thenEvent: (array<Spec.event>, Spec.event) => Outcome.outcome
@@ -521,6 +669,8 @@ module type AggregateT = {
521
669
  ) => Outcome.outcome
522
670
  let thenEventsWithError: (array<Spec.event>, array<Spec.event>, Spec.error) => Outcome.outcome
523
671
  let thenError: (array<Spec.event>, Spec.error) => Outcome.outcome
672
+ // Refused before `decide` because the caller does not own what it acts on.
673
+ let thenRefused: array<Spec.event> => Outcome.outcome
524
674
  }
525
675
 
526
676
  module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with module Spec = Spec): (
@@ -536,14 +686,27 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
536
686
  module Core = AssertionCore(Spec)
537
687
  let errors = Core.errors
538
688
 
689
+ module Caller = Caller
690
+ module Guard = Acting({
691
+ let name = Spec.name
692
+ type history = Spec.event
693
+ let historySchema = Spec.eventSchema
694
+ type command = Spec.command
695
+ let commandSchema = Spec.commandSchema
696
+ })
697
+ let asCaller = Guard.asCaller
698
+
539
699
  let exec = (history, command): array<Spec.event> => {
540
700
  errors := []
541
- let state = currentState(history)
542
- switch Behavior.decide(state, command) {
543
- | Ok(events) => events
544
- | Error(error) =>
545
- errors := [error]
701
+ if Guard.refuses(history, command) {
546
702
  []
703
+ } else {
704
+ switch Behavior.decide(currentState(history), command) {
705
+ | Ok(events) => events
706
+ | Error(error) =>
707
+ errors := [error]
708
+ []
709
+ }
547
710
  }
548
711
  }
549
712
 
@@ -551,20 +714,34 @@ module MakeFromAggregate = (Spec: AggregateSpec, Behavior: Behavior.T with modul
551
714
  let whenCmd = (history, cmd) => history->exec(cmd)
552
715
 
553
716
  // The aggregate flavour has no append-condition footgun, so the shared core's
554
- // comparisons are exposed directly; only `thenCompare*` (custom equality) is
555
- // specific to this surface.
556
- let thenEvents = Core.compareEvents
557
- let thenCompareEvents = Core.compareEventsWith
717
+ // comparisons are exposed directly once a refusal has been ruled out; only
718
+ // `thenCompare*` (custom equality) is specific to this surface.
719
+ let unlessRefused = (events, compare) =>
720
+ switch Guard.unexpectedRefusal() {
721
+ | Some(o) => o
722
+ | None => compare(events)
723
+ }
724
+ let thenEvents = (events, expected) => events->unlessRefused(Core.compareEvents(_, expected))
725
+ let thenCompareEvents = (events, expected, cmp) =>
726
+ events->unlessRefused(Core.compareEventsWith(_, expected, cmp))
558
727
  let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent])
559
728
  let thenCompareEvent = (events, expectedEvent, cmp) =>
560
729
  thenCompareEvents(events, [expectedEvent], cmp)
561
- let thenNoEvent = Core.compareNoEvent
730
+ let thenNoEvent = events => events->unlessRefused(Core.compareNoEvent)
562
731
 
563
- let thenError = (events, expectedError) => Core.matchesError(events, [], expectedError)
732
+ let thenError = (events, expectedError) =>
733
+ events->unlessRefused(Core.matchesError(_, [], expectedError))
564
734
 
565
735
  let thenEventWithError = (events, expectedEvent, expectedError) =>
566
- Core.matchesError(events, [expectedEvent], expectedError)
736
+ events->unlessRefused(Core.matchesError(_, [expectedEvent], expectedError))
567
737
 
568
738
  let thenEventsWithError = (events, expectedEvents, expectedError) =>
569
- Core.matchesError(events, expectedEvents, expectedError)
739
+ events->unlessRefused(Core.matchesError(_, expectedEvents, expectedError))
740
+
741
+ let thenRefused = events =>
742
+ Guard.thenRefused(
743
+ ~encEvents=Core.encEvents,
744
+ ~decided=errors.contents->Array.get(0)->Option.map(Core.encError),
745
+ events,
746
+ )
570
747
  }
@@ -2,15 +2,111 @@
2
2
 
3
3
  import * as Stdlib_Array from "@rescript/runtime/lib/es6/Stdlib_Array.js";
4
4
  import * as Stdlib_Option from "@rescript/runtime/lib/es6/Stdlib_Option.js";
5
+ import * as Stdlib_JsError from "@rescript/runtime/lib/es6/Stdlib_JsError.js";
6
+ import * as Owner$Reventless from "@reventlessdev/reventless-spec/src/components/Owner.res.mjs";
5
7
  import * as Primitive_object from "@rescript/runtime/lib/es6/Primitive_object.js";
6
8
  import * as Primitive_option from "@rescript/runtime/lib/es6/Primitive_option.js";
7
9
  import * as Primitive_string from "@rescript/runtime/lib/es6/Primitive_string.js";
8
10
  import * as DcbTag$Reventless from "@reventlessdev/reventless-spec/src/components/DcbTag.res.mjs";
9
11
  import * as DcbDecode$Reventless from "@reventlessdev/reventless-spec/src/components/DcbDecode.res.mjs";
10
12
  import * as Outcome$ReventlessGwt from "./Outcome.res.mjs";
13
+ import * as OwnerScope$Reventless from "@reventlessdev/reventless-spec/src/types/OwnerScope.res.mjs";
11
14
  import * as JestBind$ReventlessGwt from "./JestBind.res.mjs";
12
15
  import * as Message$ReventlessCore from "@reventlessdev/reventless-core/src/Message.res.mjs";
13
16
  import * as DcbScopeInference$Reventless from "@reventlessdev/reventless-spec/src/components/DcbScopeInference.res.mjs";
17
+ import * as CommandTopic_Helpers$ReventlessCore from "@reventlessdev/reventless-core/src/components/CommandTopic/CommandTopic_Helpers.res.mjs";
18
+
19
+ function owner(id) {
20
+ if (OwnerScope$Reventless.isJsString(id)) {
21
+ return {
22
+ TAG: "Owned",
23
+ userId: id
24
+ };
25
+ } else {
26
+ return Stdlib_JsError.throwWithMessage("Caller.owner: expected an id (a string at runtime, such as an Id.Make identity)");
27
+ }
28
+ }
29
+
30
+ let Caller = {
31
+ owner: owner,
32
+ operator: "Exempt",
33
+ anonymous: "Unidentified"
34
+ };
35
+
36
+ function Acting(Spec) {
37
+ let caller = {
38
+ contents: undefined
39
+ };
40
+ let refusal = {
41
+ contents: undefined
42
+ };
43
+ let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(Spec.historySchema);
44
+ let asCaller = (history, claim) => {
45
+ caller.contents = claim;
46
+ return history;
47
+ };
48
+ let refuses = (history, command) => {
49
+ let claim = caller.contents;
50
+ caller.contents = undefined;
51
+ refusal.contents = undefined;
52
+ if (Object.keys(ownerFieldsByEventType).length === 0) {
53
+ return false;
54
+ }
55
+ let owners = Stdlib_Array.reduce(history, [], (owners, event) => {
56
+ let match = Message$ReventlessCore.splitMessage(Message$ReventlessCore.encode(event, Spec.historySchema));
57
+ let fields = ownerFieldsByEventType[match[0]];
58
+ if (fields !== undefined) {
59
+ return OwnerScope$Reventless.recordOwners(owners, fields, match[1]);
60
+ } else {
61
+ return owners;
62
+ }
63
+ });
64
+ refusal.contents = Stdlib_Option.map(CommandTopic_Helpers$ReventlessCore.ownershipRefusal(OwnerScope$Reventless.decideActing(claim, owners), Spec.name, Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, Spec.commandSchema))), r => r.rejected);
65
+ return Stdlib_Option.isSome(refusal.contents);
66
+ };
67
+ let encRefusal = r => Object.fromEntries([
68
+ [
69
+ "errorCode",
70
+ r.errorCode
71
+ ],
72
+ [
73
+ "errorDetail",
74
+ r.errorDetail
75
+ ]
76
+ ]);
77
+ let unexpectedRefusal = () => Stdlib_Option.map(refusal.contents, r => Outcome$ReventlessGwt.fail({
78
+ TAG: "ErrorMismatch",
79
+ expected: null,
80
+ actual: encRefusal(r),
81
+ actualEvents: []
82
+ }));
83
+ let thenRefused = (encEvents, decided, events) => {
84
+ let match = refusal.contents;
85
+ if (match !== undefined) {
86
+ return Outcome$ReventlessGwt.pass;
87
+ } else {
88
+ return Outcome$ReventlessGwt.fail({
89
+ TAG: "ErrorMismatch",
90
+ expected: Object.fromEntries([[
91
+ "errorCode",
92
+ "Forbidden"
93
+ ]]),
94
+ actual: decided,
95
+ actualEvents: encEvents(events)
96
+ });
97
+ }
98
+ };
99
+ return {
100
+ caller: caller,
101
+ refusal: refusal,
102
+ ownerFieldsByEventType: ownerFieldsByEventType,
103
+ asCaller: asCaller,
104
+ refuses: refuses,
105
+ encRefusal: encRefusal,
106
+ unexpectedRefusal: unexpectedRefusal,
107
+ thenRefused: thenRefused
108
+ };
109
+ }
14
110
 
15
111
  function encodeTag(t) {
16
112
  let d = {};
@@ -149,6 +245,7 @@ function Make(Spec) {
149
245
  contents: []
150
246
  };
151
247
  let encEvent = e => Message$ReventlessCore.encode(e, Spec.eventSchema);
248
+ let encEvents = evs => evs.map(encEvent);
152
249
  let encError = err => Message$ReventlessCore.encode(err, Spec.errorSchema);
153
250
  let unexpectedError = events => {
154
251
  let actual = Stdlib_Option.map(errors.contents[0], encError);
@@ -188,6 +285,55 @@ function Make(Spec) {
188
285
  });
189
286
  }
190
287
  };
288
+ let name = Spec.name;
289
+ let historySchema = Spec.consumedEventSchema;
290
+ let commandSchema = Spec.commandSchema;
291
+ let caller = {
292
+ contents: undefined
293
+ };
294
+ let refusal = {
295
+ contents: undefined
296
+ };
297
+ let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(historySchema);
298
+ let asCaller = (history, claim) => {
299
+ caller.contents = claim;
300
+ return history;
301
+ };
302
+ let refuses = (history, command) => {
303
+ let claim = caller.contents;
304
+ caller.contents = undefined;
305
+ refusal.contents = undefined;
306
+ if (Object.keys(ownerFieldsByEventType).length === 0) {
307
+ return false;
308
+ }
309
+ let owners = Stdlib_Array.reduce(history, [], (owners, event) => {
310
+ let match = Message$ReventlessCore.splitMessage(Message$ReventlessCore.encode(event, historySchema));
311
+ let fields = ownerFieldsByEventType[match[0]];
312
+ if (fields !== undefined) {
313
+ return OwnerScope$Reventless.recordOwners(owners, fields, match[1]);
314
+ } else {
315
+ return owners;
316
+ }
317
+ });
318
+ refusal.contents = Stdlib_Option.map(CommandTopic_Helpers$ReventlessCore.ownershipRefusal(OwnerScope$Reventless.decideActing(claim, owners), name, Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema))), r => r.rejected);
319
+ return Stdlib_Option.isSome(refusal.contents);
320
+ };
321
+ let encRefusal = r => Object.fromEntries([
322
+ [
323
+ "errorCode",
324
+ r.errorCode
325
+ ],
326
+ [
327
+ "errorDetail",
328
+ r.errorDetail
329
+ ]
330
+ ]);
331
+ let unexpectedRefusal = () => Stdlib_Option.map(refusal.contents, r => Outcome$ReventlessGwt.fail({
332
+ TAG: "ErrorMismatch",
333
+ expected: null,
334
+ actual: encRefusal(r),
335
+ actualEvents: []
336
+ }));
191
337
  let derivedCondition = {
192
338
  contents: undefined
193
339
  };
@@ -270,15 +416,41 @@ function Make(Spec) {
270
416
  };
271
417
  }
272
418
  }
273
- let state = Stdlib_Array.reduce(history, Behavior.initialState, Behavior.evolve);
274
- let events = Behavior.decide(state, cmd);
419
+ if (refuses(history, cmd)) {
420
+ return [];
421
+ }
422
+ let events = Behavior.decide(Stdlib_Array.reduce(history, Behavior.initialState, Behavior.evolve), cmd);
275
423
  if (events.TAG === "Ok") {
276
424
  return events._0;
277
425
  }
278
426
  errors.contents = [events._0];
279
427
  return [];
280
428
  };
281
- let checkAppendCondition = () => Stdlib_Option.map(appendConditionFailure.contents, Outcome$ReventlessGwt.fail);
429
+ let checkAppendCondition = () => {
430
+ let m = appendConditionFailure.contents;
431
+ if (m !== undefined) {
432
+ return Outcome$ReventlessGwt.fail(m);
433
+ } else {
434
+ return unexpectedRefusal();
435
+ }
436
+ };
437
+ let thenRefused = events => {
438
+ let decided = Stdlib_Option.map(errors.contents[0], encError);
439
+ let match = refusal.contents;
440
+ if (match !== undefined) {
441
+ return Outcome$ReventlessGwt.pass;
442
+ } else {
443
+ return Outcome$ReventlessGwt.fail({
444
+ TAG: "ErrorMismatch",
445
+ expected: Object.fromEntries([[
446
+ "errorCode",
447
+ "Forbidden"
448
+ ]]),
449
+ actual: decided,
450
+ actualEvents: encEvents(events)
451
+ });
452
+ }
453
+ };
282
454
  let thenEvents = (events, expectedEvents) => {
283
455
  let o = checkAppendCondition();
284
456
  if (o !== undefined) {
@@ -383,6 +555,8 @@ function Make(Spec) {
383
555
  todo: JestBind$ReventlessGwt.todo,
384
556
  test: test,
385
557
  givenEvents: givenEvents,
558
+ Caller: Caller,
559
+ asCaller: asCaller,
386
560
  whenCmd: whenCmd,
387
561
  thenEvent: thenEvent,
388
562
  thenEvents: thenEvents,
@@ -390,6 +564,7 @@ function Make(Spec) {
390
564
  thenEventWithError: thenEventWithError,
391
565
  thenEventsWithError: thenEventsWithError,
392
566
  thenError: thenError,
567
+ thenRefused: thenRefused,
393
568
  thenAppendsConditionedOn: thenAppendsConditionedOn,
394
569
  thenAppendsConditionedOnExactly: thenAppendsConditionedOnExactly
395
570
  };
@@ -403,6 +578,7 @@ function MakeFromAggregate(Spec) {
403
578
  contents: []
404
579
  };
405
580
  let encEvent = e => Message$ReventlessCore.encode(e, Spec.eventSchema);
581
+ let encEvents = evs => evs.map(encEvent);
406
582
  let encError = err => Message$ReventlessCore.encode(err, Spec.errorSchema);
407
583
  let unexpectedError = events => {
408
584
  let actual = Stdlib_Option.map(errors.contents[0], encError);
@@ -413,32 +589,6 @@ function MakeFromAggregate(Spec) {
413
589
  actualEvents: events.map(encEvent)
414
590
  });
415
591
  };
416
- let compareEvents = (events, expectedEvents) => {
417
- if (errors.contents.length !== 0) {
418
- return unexpectedError(events);
419
- } else if (Primitive_object.equal(events.map(encEvent), expectedEvents.map(encEvent))) {
420
- return Outcome$ReventlessGwt.pass;
421
- } else {
422
- return Outcome$ReventlessGwt.fail({
423
- TAG: "EventsMismatch",
424
- expected: expectedEvents.map(encEvent),
425
- actual: events.map(encEvent)
426
- });
427
- }
428
- };
429
- let compareEventsWith = (events, expectedEvents, cmp) => {
430
- if (errors.contents.length !== 0) {
431
- return unexpectedError(events);
432
- } else if (events.length === expectedEvents.length && Stdlib_Array.zip(events, expectedEvents).every(param => cmp(param[0], param[1]))) {
433
- return Outcome$ReventlessGwt.pass;
434
- } else {
435
- return Outcome$ReventlessGwt.fail({
436
- TAG: "EventsMismatch",
437
- expected: expectedEvents.map(encEvent),
438
- actual: events.map(encEvent)
439
- });
440
- }
441
- };
442
592
  let compareNoEvent = events => {
443
593
  if (errors.contents.length !== 0) {
444
594
  return unexpectedError(events);
@@ -480,41 +630,149 @@ function MakeFromAggregate(Spec) {
480
630
  });
481
631
  }
482
632
  };
633
+ let name = Spec.name;
634
+ let historySchema = Spec.eventSchema;
635
+ let commandSchema = Spec.commandSchema;
636
+ let caller = {
637
+ contents: undefined
638
+ };
639
+ let refusal = {
640
+ contents: undefined
641
+ };
642
+ let ownerFieldsByEventType = Owner$Reventless.fieldNamesByVariant(historySchema);
643
+ let asCaller = (history, claim) => {
644
+ caller.contents = claim;
645
+ return history;
646
+ };
647
+ let refuses = (history, command) => {
648
+ let claim = caller.contents;
649
+ caller.contents = undefined;
650
+ refusal.contents = undefined;
651
+ if (Object.keys(ownerFieldsByEventType).length === 0) {
652
+ return false;
653
+ }
654
+ let owners = Stdlib_Array.reduce(history, [], (owners, event) => {
655
+ let match = Message$ReventlessCore.splitMessage(Message$ReventlessCore.encode(event, historySchema));
656
+ let fields = ownerFieldsByEventType[match[0]];
657
+ if (fields !== undefined) {
658
+ return OwnerScope$Reventless.recordOwners(owners, fields, match[1]);
659
+ } else {
660
+ return owners;
661
+ }
662
+ });
663
+ refusal.contents = Stdlib_Option.map(CommandTopic_Helpers$ReventlessCore.ownershipRefusal(OwnerScope$Reventless.decideActing(claim, owners), name, Message$ReventlessCore.variantNameOfJson(Message$ReventlessCore.encode(command, commandSchema))), r => r.rejected);
664
+ return Stdlib_Option.isSome(refusal.contents);
665
+ };
666
+ let encRefusal = r => Object.fromEntries([
667
+ [
668
+ "errorCode",
669
+ r.errorCode
670
+ ],
671
+ [
672
+ "errorDetail",
673
+ r.errorDetail
674
+ ]
675
+ ]);
676
+ let unexpectedRefusal = () => Stdlib_Option.map(refusal.contents, r => Outcome$ReventlessGwt.fail({
677
+ TAG: "ErrorMismatch",
678
+ expected: null,
679
+ actual: encRefusal(r),
680
+ actualEvents: []
681
+ }));
483
682
  let givenEvents = events => events;
484
683
  let whenCmd = (history, cmd) => {
485
684
  errors.contents = [];
486
- let state = Stdlib_Array.reduce(history, Behavior.initialState, Behavior.evolve);
487
- let events = Behavior.decide(state, cmd);
685
+ if (refuses(history, cmd)) {
686
+ return [];
687
+ }
688
+ let events = Behavior.decide(Stdlib_Array.reduce(history, Behavior.initialState, Behavior.evolve), cmd);
488
689
  if (events.TAG === "Ok") {
489
690
  return events._0;
490
691
  }
491
692
  errors.contents = [events._0];
492
693
  return [];
493
694
  };
494
- let thenEvent = (events, expectedEvent) => compareEvents(events, [expectedEvent]);
495
- let thenCompareEvent = (events, expectedEvent, cmp) => compareEventsWith(events, [expectedEvent], cmp);
496
- let thenError = (events, expectedError) => matchesError(events, [], expectedError);
497
- let thenEventWithError = (events, expectedEvent, expectedError) => matchesError(events, [expectedEvent], expectedError);
498
- let thenEventsWithError = matchesError;
695
+ let unlessRefused = (events, compare) => {
696
+ let o = unexpectedRefusal();
697
+ if (o !== undefined) {
698
+ return o;
699
+ } else {
700
+ return compare(events);
701
+ }
702
+ };
703
+ let thenEvents = (events, expected) => unlessRefused(events, __x => {
704
+ if (errors.contents.length !== 0) {
705
+ return unexpectedError(__x);
706
+ } else if (Primitive_object.equal(__x.map(encEvent), expected.map(encEvent))) {
707
+ return Outcome$ReventlessGwt.pass;
708
+ } else {
709
+ return Outcome$ReventlessGwt.fail({
710
+ TAG: "EventsMismatch",
711
+ expected: expected.map(encEvent),
712
+ actual: __x.map(encEvent)
713
+ });
714
+ }
715
+ });
716
+ let thenCompareEvents = (events, expected, cmp) => unlessRefused(events, __x => {
717
+ if (errors.contents.length !== 0) {
718
+ return unexpectedError(__x);
719
+ } else if (__x.length === expected.length && Stdlib_Array.zip(__x, expected).every(param => cmp(param[0], param[1]))) {
720
+ return Outcome$ReventlessGwt.pass;
721
+ } else {
722
+ return Outcome$ReventlessGwt.fail({
723
+ TAG: "EventsMismatch",
724
+ expected: expected.map(encEvent),
725
+ actual: __x.map(encEvent)
726
+ });
727
+ }
728
+ });
729
+ let thenEvent = (events, expectedEvent) => thenEvents(events, [expectedEvent]);
730
+ let thenCompareEvent = (events, expectedEvent, cmp) => thenCompareEvents(events, [expectedEvent], cmp);
731
+ let thenNoEvent = events => unlessRefused(events, compareNoEvent);
732
+ let thenError = (events, expectedError) => unlessRefused(events, __x => matchesError(__x, [], expectedError));
733
+ let thenEventWithError = (events, expectedEvent, expectedError) => unlessRefused(events, __x => matchesError(__x, [expectedEvent], expectedError));
734
+ let thenEventsWithError = (events, expectedEvents, expectedError) => unlessRefused(events, __x => matchesError(__x, expectedEvents, expectedError));
735
+ let thenRefused = events => {
736
+ let decided = Stdlib_Option.map(errors.contents[0], encError);
737
+ let match = refusal.contents;
738
+ if (match !== undefined) {
739
+ return Outcome$ReventlessGwt.pass;
740
+ } else {
741
+ return Outcome$ReventlessGwt.fail({
742
+ TAG: "ErrorMismatch",
743
+ expected: Object.fromEntries([[
744
+ "errorCode",
745
+ "Forbidden"
746
+ ]]),
747
+ actual: decided,
748
+ actualEvents: encEvents(events)
749
+ });
750
+ }
751
+ };
499
752
  return {
500
753
  Spec: Spec,
501
754
  describe: JestBind$ReventlessGwt.describe,
502
755
  test: test,
503
756
  givenEvents: givenEvents,
757
+ Caller: Caller,
758
+ asCaller: asCaller,
504
759
  whenCmd: whenCmd,
505
760
  thenEvent: thenEvent,
506
761
  thenCompareEvent: thenCompareEvent,
507
- thenNoEvent: compareNoEvent,
762
+ thenNoEvent: thenNoEvent,
508
763
  thenEventWithError: thenEventWithError,
509
- thenEvents: compareEvents,
510
- thenCompareEvents: compareEventsWith,
764
+ thenEvents: thenEvents,
765
+ thenCompareEvents: thenCompareEvents,
511
766
  thenEventsWithError: thenEventsWithError,
512
- thenError: thenError
767
+ thenError: thenError,
768
+ thenRefused: thenRefused
513
769
  };
514
770
  };
515
771
  }
516
772
 
517
773
  export {
774
+ Caller,
775
+ Acting,
518
776
  encodeTag,
519
777
  encodeQueryItem,
520
778
  encodeQuery,
@@ -523,4 +781,4 @@ export {
523
781
  Make,
524
782
  MakeFromAggregate,
525
783
  }
526
- /* DcbTag-Reventless Not a pure module */
784
+ /* Owner-Reventless Not a pure module */
@@ -0,0 +1,33 @@
1
+ open JestGlobals
2
+
3
+ // `Caller.owner` takes an id as a scenario holds it, typed or not, so a test
4
+ // never converts one by hand.
5
+
6
+ module CustomerId = Reventless.Id.Make({
7
+ let key = "customerId"
8
+ })
9
+
10
+ describe("Caller.owner", () => {
11
+ testSync("takes a typed id as the user id it stands for", () =>
12
+ expect(Behavior_GWT.Caller.owner(CustomerId.makeFromString("c1")))->toEqual(
13
+ Reventless.Message.CallerClaim.Owned({userId: "c1"}),
14
+ )
15
+ )
16
+
17
+ testSync("takes a plain string too", () =>
18
+ expect(Behavior_GWT.Caller.owner("c1"))->toEqual(
19
+ Reventless.Message.CallerClaim.Owned({userId: "c1"}),
20
+ )
21
+ )
22
+
23
+ // Anything that is not a string at runtime is not an id, and comparing it
24
+ // with a recorded owner would only ever refuse — a silent wrong answer.
25
+ testSync("refuses a value that is not an id", () =>
26
+ expect(
27
+ switch Behavior_GWT.Caller.owner(42) {
28
+ | _ => false
29
+ | exception _ => true
30
+ },
31
+ )->toBe(true)
32
+ )
33
+ })
@@ -0,0 +1,38 @@
1
+ // Generated by ReScript, PLEASE EDIT WITH CARE
2
+
3
+ import * as Id$Reventless from "@reventlessdev/reventless-spec/src/types/Id.res.mjs";
4
+ import * as Behavior_GWT$ReventlessGwt from "../src/Behavior_GWT.res.mjs";
5
+
6
+ let CustomerId = Id$Reventless.Make({
7
+ key: "customerId"
8
+ });
9
+
10
+ globalThis.describe("Caller.owner", () => {
11
+ globalThis.test("takes a typed id as the user id it stands for", () => {
12
+ globalThis.expect(Behavior_GWT$ReventlessGwt.Caller.owner(CustomerId.makeFromString("c1"))).toEqual({
13
+ TAG: "Owned",
14
+ userId: "c1"
15
+ });
16
+ });
17
+ globalThis.test("takes a plain string too", () => {
18
+ globalThis.expect(Behavior_GWT$ReventlessGwt.Caller.owner("c1")).toEqual({
19
+ TAG: "Owned",
20
+ userId: "c1"
21
+ });
22
+ });
23
+ globalThis.test("refuses a value that is not an id", () => {
24
+ let tmp;
25
+ try {
26
+ Behavior_GWT$ReventlessGwt.Caller.owner(42);
27
+ tmp = false;
28
+ } catch (exn) {
29
+ tmp = true;
30
+ }
31
+ globalThis.expect(tmp).toBe(true);
32
+ });
33
+ });
34
+
35
+ export {
36
+ CustomerId,
37
+ }
38
+ /* CustomerId Not a pure module */
@@ -39,6 +39,10 @@ let Spec = include.Spec;
39
39
 
40
40
  let todo = include.todo;
41
41
 
42
+ let Caller = include.Caller;
43
+
44
+ let asCaller = include.asCaller;
45
+
42
46
  let thenEvents = include.thenEvents;
43
47
 
44
48
  let thenNoEvent = include.thenNoEvent;
@@ -47,6 +51,8 @@ let thenEventWithError = include.thenEventWithError;
47
51
 
48
52
  let thenEventsWithError = include.thenEventsWithError;
49
53
 
54
+ let thenRefused = include.thenRefused;
55
+
50
56
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
51
57
 
52
58
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -57,6 +63,8 @@ export {
57
63
  todo,
58
64
  test,
59
65
  givenEvents,
66
+ Caller,
67
+ asCaller,
60
68
  whenCmd,
61
69
  thenEvent,
62
70
  thenEvents,
@@ -64,6 +72,7 @@ export {
64
72
  thenEventWithError,
65
73
  thenEventsWithError,
66
74
  thenError,
75
+ thenRefused,
67
76
  thenAppendsConditionedOn,
68
77
  thenAppendsConditionedOnExactly,
69
78
  }
@@ -23,6 +23,10 @@ let Spec = include.Spec;
23
23
 
24
24
  let todo = include.todo;
25
25
 
26
+ let Caller = include.Caller;
27
+
28
+ let asCaller = include.asCaller;
29
+
26
30
  let thenEvents = include.thenEvents;
27
31
 
28
32
  let thenNoEvent = include.thenNoEvent;
@@ -33,6 +37,8 @@ let thenEventsWithError = include.thenEventsWithError;
33
37
 
34
38
  let thenError = include.thenError;
35
39
 
40
+ let thenRefused = include.thenRefused;
41
+
36
42
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
37
43
 
38
44
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -43,6 +49,8 @@ export {
43
49
  todo,
44
50
  test,
45
51
  givenEvents,
52
+ Caller,
53
+ asCaller,
46
54
  whenCmd,
47
55
  thenEvent,
48
56
  thenEvents,
@@ -50,6 +58,7 @@ export {
50
58
  thenEventWithError,
51
59
  thenEventsWithError,
52
60
  thenError,
61
+ thenRefused,
53
62
  thenAppendsConditionedOn,
54
63
  thenAppendsConditionedOnExactly,
55
64
  }
@@ -23,6 +23,10 @@ let Spec = include.Spec;
23
23
 
24
24
  let todo = include.todo;
25
25
 
26
+ let Caller = include.Caller;
27
+
28
+ let asCaller = include.asCaller;
29
+
26
30
  let thenEvents = include.thenEvents;
27
31
 
28
32
  let thenNoEvent = include.thenNoEvent;
@@ -33,6 +37,8 @@ let thenEventsWithError = include.thenEventsWithError;
33
37
 
34
38
  let thenError = include.thenError;
35
39
 
40
+ let thenRefused = include.thenRefused;
41
+
36
42
  let thenAppendsConditionedOn = include.thenAppendsConditionedOn;
37
43
 
38
44
  let thenAppendsConditionedOnExactly = include.thenAppendsConditionedOnExactly;
@@ -43,6 +49,8 @@ export {
43
49
  todo,
44
50
  test,
45
51
  givenEvents,
52
+ Caller,
53
+ asCaller,
46
54
  whenCmd,
47
55
  thenEvent,
48
56
  thenEvents,
@@ -50,6 +58,7 @@ export {
50
58
  thenEventWithError,
51
59
  thenEventsWithError,
52
60
  thenError,
61
+ thenRefused,
53
62
  thenAppendsConditionedOn,
54
63
  thenAppendsConditionedOnExactly,
55
64
  }
@@ -405,6 +405,10 @@ let Spec = include.Spec;
405
405
 
406
406
  let todo = include.todo;
407
407
 
408
+ let Caller = include.Caller;
409
+
410
+ let asCaller = include.asCaller;
411
+
408
412
  let thenEvents = include.thenEvents;
409
413
 
410
414
  let thenNoEvent = include.thenNoEvent;
@@ -413,6 +417,8 @@ let thenEventWithError = include.thenEventWithError;
413
417
 
414
418
  let thenEventsWithError = include.thenEventsWithError;
415
419
 
420
+ let thenRefused = include.thenRefused;
421
+
416
422
  export {
417
423
  AddCategorySlice,
418
424
  AddCategorySliceBehavior,
@@ -421,6 +427,8 @@ export {
421
427
  todo,
422
428
  test,
423
429
  givenEvents,
430
+ Caller,
431
+ asCaller,
424
432
  whenCmd,
425
433
  thenEvent,
426
434
  thenEvents,
@@ -428,6 +436,7 @@ export {
428
436
  thenEventWithError,
429
437
  thenEventsWithError,
430
438
  thenError,
439
+ thenRefused,
431
440
  thenAppendsConditionedOn,
432
441
  thenAppendsConditionedOnExactly,
433
442
  MissingTagSlice,