Skip to content

Instantly share code, notes, and snippets.

@rob-gordon
Created January 25, 2023 14:34
Show Gist options
  • Save rob-gordon/7bc2aefcd6cc353e45cbe0c1f48351c5 to your computer and use it in GitHub Desktop.
Save rob-gordon/7bc2aefcd6cc353e45cbe0c1f48351c5 to your computer and use it in GitHub Desktop.
Create a context-less xstate machine from a template literal
import {
AnyEventObject,
BaseActionObject,
StateNodeConfig,
TransitionConfig,
TransitionsConfig,
createMachine,
interpret,
} from "xstate";
import { parse } from "graph-selector";
const machine = fsm`
one [initial]
BANG: two
BOOP: three
BEEP: four
MEEP: (one)
BANG: (five)
BING: five [final]
`;
const service = interpret(machine).start();
service.send("BANG");
service.send("BEEP");
service.send("MEEP");
service.send("BING");
console.log(`\n-----\ncurrent state: ${service.state.value}\n-----\n`);
/*
*/
/** A template literal for creating context-less state machines */
function fsm(text: TemplateStringsArray) {
return machineFromString(text[0]);
}
function machineFromString(text: string) {
return createMachine<{}>(graphWithoutContextToFSM(text));
}
function graphWithoutContextToFSM(text: string) {
return graphToFSM(text, {}, {});
}
function graphToFSM<T>(
text: string,
context: T,
actions: Record<string, TransitionConfig<T, AnyEventObject>["actions"]>
) {
const KEYS_TO_REMOVE = ["id", "label", "classes"];
const { nodes, edges } = parse(text);
// Build States
let states: StateNodeConfig<
T,
any,
AnyEventObject,
BaseActionObject
>["states"] = {};
// Store initial state
let initial: string = "";
for (const node of nodes) {
states[node.data.label] = {};
const currentState = states[node.data.label];
// if has initial flag, add to intial
if (node.data["initial"]) initial = node.data.label;
const edgesFrom = edges.filter((edge: any) => edge.source === node.data.id);
for (const edge of edgesFrom) {
if (!("on" in currentState)) {
currentState.on = {};
}
let on = currentState.on as TransitionsConfig<T, AnyEventObject>;
const target =
nodes.find((node: any) => node.data.id === edge.target)?.data.label ||
"";
if (!target) continue;
// Check for an action (attribute on the edge, that isn't id, label, or classes)
// take the first one
const action =
Object.keys(edge.data).filter(
(key) => KEYS_TO_REMOVE.indexOf(key) === -1
)?.[0] || "";
const transition = edge.data.label;
if (action && !(action in actions))
throw new Error(`Action ${action} not found`);
if (action) {
on[transition] = {
target,
action: actions[action],
};
} else {
on[transition] = target;
}
}
if (node.data["final"]) {
currentState["final"] = true;
}
}
if (!initial) throw new Error("Missing initial state");
const result = {
id: "my-machine",
initial,
context,
states,
};
return result;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment