Skip to content

Commit

Permalink
[v5] Allow guard objects to reference other guard objects (#4064)
Browse files Browse the repository at this point in the history
* Allow a guard object to reference another guard object

* Add TODO

* Update packages/core/src/guards.ts

* Update packages/core/test/stateIn.test.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Add changeset

* Add more tests

* Update .changeset/red-berries-occur.md

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

* Update packages/core/src/guards.ts

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>

---------

Co-authored-by: Mateusz Burzyński <mateuszburzynski@gmail.com>
  • Loading branch information
davidkpiano and Andarist committed Jun 17, 2023
1 parent 69dc9c1 commit 0478972
Show file tree
Hide file tree
Showing 5 changed files with 188 additions and 19 deletions.
30 changes: 30 additions & 0 deletions .changeset/red-berries-occur.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
---
'xstate': patch
---

Guard objects can now reference other guard objects:

```ts
const machine = createMachine(
{
initial: 'home',
states: {
home: {
on: {
NEXT: {
target: 'success',
guard: 'hasSelection'
}
}
},
success: {}
}
},
{
guards: {
// `hasSelection` is a guard object that references the `stateIn` guard
hasSelection: stateIn('selected')
}
}
);
```
60 changes: 45 additions & 15 deletions packages/core/src/guards.ts
Original file line number Diff line number Diff line change
Expand Up @@ -108,14 +108,29 @@ export function toGuardDefinition<
TEvent extends EventObject
>(
guardConfig: GuardConfig<TContext, TEvent>,
getPredicate?: (guardType: string) => GuardPredicate<TContext, TEvent>
getPredicate?: (
guardType: string
) => GuardPredicate<TContext, TEvent> | GuardDefinition<TContext, TEvent>
): GuardDefinition<TContext, TEvent> {
// TODO: check for cycles and consider a refactor to more lazily evaluated guards
// TODO: resolve this more recursively: https://github.com/statelyai/xstate/pull/4064#discussion_r1229915724
if (isString(guardConfig)) {
return {
type: guardConfig,
predicate: getPredicate?.(guardConfig) || undefined,
params: { type: guardConfig }
};
const predicateOrDef = getPredicate?.(guardConfig);

if (isFunction(predicateOrDef)) {
return {
type: guardConfig,
predicate: predicateOrDef,
params: { type: guardConfig }
};
} else if (predicateOrDef) {
return predicateOrDef;
} else {
return {
type: guardConfig,
params: { type: guardConfig }
};
}
}

if (isFunction(guardConfig)) {
Expand All @@ -129,13 +144,28 @@ export function toGuardDefinition<
};
}

return {
type: guardConfig.type,
params: guardConfig.params || guardConfig,
children: (
guardConfig.children as Array<GuardConfig<TContext, TEvent>>
)?.map((childGuard) => toGuardDefinition(childGuard, getPredicate)),
predicate:
getPredicate?.(guardConfig.type) || (guardConfig as any).predicate
};
const predicateOrDef = getPredicate?.(guardConfig.type);

if (isFunction(predicateOrDef)) {
return {
type: guardConfig.type,
params: guardConfig.params || guardConfig,
children: (
guardConfig.children as Array<GuardConfig<TContext, TEvent>>
)?.map((childGuard) => toGuardDefinition(childGuard, getPredicate)),
predicate:
getPredicate?.(guardConfig.type) || (guardConfig as any).predicate
};
} else if (predicateOrDef) {
return predicateOrDef;
} else {
return {
type: guardConfig.type,
params: guardConfig.params || guardConfig,
children: (
guardConfig.children as Array<GuardConfig<TContext, TEvent>>
)?.map((childGuard) => toGuardDefinition(childGuard, getPredicate)),
predicate: (guardConfig as any).predicate
};
}
}
13 changes: 9 additions & 4 deletions packages/core/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -834,10 +834,15 @@ type MachineImplementationsGuards<
>,
TIndexedEvents = Prop<Prop<TResolvedTypesMeta, 'resolved'>, 'indexedEvents'>
> = {
[K in keyof TEventsCausingGuards]?: GuardPredicate<
TContext,
Cast<Prop<TIndexedEvents, TEventsCausingGuards[K]>, EventObject>
>;
[K in keyof TEventsCausingGuards]?:
| GuardPredicate<
TContext,
Cast<Prop<TIndexedEvents, TEventsCausingGuards[K]>, EventObject>
>
| GuardConfig<
TContext,
Cast<Prop<TIndexedEvents, TEventsCausingGuards[K]>, EventObject>
>;
};

type MachineImplementationsActors<
Expand Down
64 changes: 64 additions & 0 deletions packages/core/test/guards.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -555,6 +555,70 @@ describe('referencing guards', () => {
Guard 'missing-predicate' is not implemented.'."
`);
});

it('should be possible to reference a composite guard that only uses inline predicates', () => {
const machine = createMachine(
{
initial: 'a',
states: {
a: {
on: {
EVENT: {
target: 'b',
guard: 'referenced'
}
}
},
b: {}
}
},
{
guards: {
referenced: not(() => false)
}
}
);

const actorRef = interpret(machine).start();
actorRef.send({ type: 'EVENT' });

expect(actorRef.getSnapshot().matches('b')).toBeTruthy();
});

it('should be possible to reference a composite guard that references other guards recursively', () => {
const machine = createMachine(
{
initial: 'a',
states: {
a: {
on: {
EVENT: {
target: 'b',
guard: 'referenced'
}
}
},
b: {}
}
},
{
guards: {
truthy: () => true,
falsy: () => false,
referenced: or([
() => false,
not('truthy'),
and([not('falsy'), 'truthy'])
])
}
}
);

const actorRef = interpret(machine).start();
actorRef.send({ type: 'EVENT' });

expect(actorRef.getSnapshot().matches('b')).toBeTruthy();
});
});

describe('guards - other', () => {
Expand Down
40 changes: 40 additions & 0 deletions packages/core/test/stateIn.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,4 +424,44 @@ describe('transition "in" check', () => {
actorRef.send({ type: 'TIMER' });
expect(actorRef.getSnapshot().value).toEqual('green');
});

it('should be possible to use a referenced `stateIn` guard', () => {
const machine = createMachine(
{
type: 'parallel',
// machine definition,
states: {
selected: {},
location: {
initial: 'home',
states: {
home: {
on: {
NEXT: {
target: 'success',
guard: 'hasSelection'
}
}
},
success: {}
}
}
}
},
{
guards: {
hasSelection: stateIn('selected')
}
}
);

const actor = interpret(machine).start();
actor.send({
type: 'NEXT'
});
expect(actor.getSnapshot().value).toEqual({
selected: {},
location: 'success'
});
});
});

0 comments on commit 0478972

Please sign in to comment.