sverweij/state-machine-cat

View on GitHub
src/render/scjson/index.mts

Summary

Maintainability
Test Coverage
/* eslint-disable security/detect-object-injection */
import type {
  ActionTypeType,
  IActionType,
  IRenderOptions,
  IState,
  IStateMachine,
  ITransition,
} from "types/state-machine-cat.mjs";
import StateMachineModel from "../../state-machine-model.mjs";
import type {
  ISCJSONMachine,
  ISCJSONState,
  ISCJSONTransition,
} from "./scjson.js";
import makeValidXMLName from "./make-valid-xml-name.mjs";
import makeValidEventNames from "./make-valid-event-names.mjs";

const STATE_TYPE2SCXML_STATE_KIND: { [stateType: string]: string } = {
  regular: "state",
  initial: "initial",
  final: "final",
  terminate: "final",
  parallel: "parallel",
  history: "history",
  deephistory: "history",
};

function stateType2SCXMLStateKind(pStateType: string): string {
  return STATE_TYPE2SCXML_STATE_KIND[pStateType] || "state";
}

function transformTransition(pTransition: ITransition): ISCJSONTransition {
  const lReturnValue: ISCJSONTransition = {
    target: makeValidXMLName(pTransition.to),
  };

  if (pTransition.event) {
    lReturnValue.event = makeValidEventNames(pTransition.event);
  }
  if (pTransition.cond) {
    lReturnValue.cond = pTransition.cond;
  }
  if (pTransition.action) {
    lReturnValue.action = pTransition.action;
  }
  if (pTransition.type) {
    lReturnValue.type = pTransition.type;
  }
  return lReturnValue;
}

function extractTriggers(
  pTriggers: IActionType[],
  pTriggerType: ActionTypeType,
): string[] {
  return pTriggers
    .filter((pTrigger) => pTrigger.type === pTriggerType)
    .map((pTrigger) => pTrigger.body);
}

function pullOutActionType(
  pReturnValue: ISCJSONState,
  pTriggersType: "onentries" | "onexits",
  pActions: IActionType[],
  pActionType: ActionTypeType,
): void {
  const lTriggerArray: string[] = extractTriggers(pActions, pActionType);

  if (lTriggerArray.length > 0) {
    pReturnValue[pTriggersType] = (pReturnValue[pTriggersType] || []).concat(
      lTriggerArray,
    );
  }
}

function transformTriggers(pReturnValue: ISCJSONState, pState: IState): void {
  if (pState.actions) {
    pullOutActionType(pReturnValue, "onentries", pState.actions, "entry");
    pullOutActionType(pReturnValue, "onentries", pState.actions, "activity");
    pullOutActionType(pReturnValue, "onexits", pState.actions, "exit");
  }
}

function transformTransitions(
  pReturnValue: ISCJSONState,
  pState: IState,
  pTransitions: ITransition[],
): void {
  const lTransitions = pTransitions
    .filter((pTransition) => pTransition.from === pState.name)
    .map(transformTransition);

  if (lTransitions.length > 0) {
    pReturnValue.transitions = lTransitions;
  }
}

function transformCompositeState(
  pReturnValue: ISCJSONState,
  pState: IState,
  pTransitions: ITransition[],
): void {
  if (pState.statemachine) {
    // recursion, so ...
    // eslint-disable-next-line no-use-before-define, no-undefined
    const lRenderedState = render(pState.statemachine, undefined, pTransitions);

    pReturnValue.states = (pReturnValue.states || []).concat(
      lRenderedState.states,
    );
    if (lRenderedState.initial) {
      pReturnValue.initial = lRenderedState.initial;
    }
  }
}

function transformState(pTransitions: ITransition[]) {
  pTransitions = pTransitions || [];

  return (pState: IState): ISCJSONState => {
    const lReturnValue: ISCJSONState = {
      kind: stateType2SCXMLStateKind(pState.type),
      id: makeValidXMLName(pState.name),
    };

    if (pState.type === "deephistory") {
      // as 'shallow' is the default anyway, we leave it out
      lReturnValue.type = "deep";
    }

    transformTriggers(lReturnValue, pState);

    transformTransitions(lReturnValue, pState, pTransitions);

    transformCompositeState(lReturnValue, pState, pTransitions);
    return lReturnValue;
  };
}

function findInitialPseudoStateName(
  pStateMachine: IStateMachine,
): string | undefined {
  const lInitial = pStateMachine.states.filter(
    (pState) => pState.type === "initial",
  );

  if (lInitial.length > 0) {
    return lInitial[0].name;
  }
  // eslint-disable-next-line no-undefined
  return undefined;
}

function findInitialStateName(
  pStateMachine: IStateMachine,
  pInitialPseudoStateName?: string,
): string | undefined {
  let lReturnValue = pInitialPseudoStateName;

  if (pInitialPseudoStateName && pStateMachine.transitions) {
    const lInitialTransitions = pStateMachine.transitions.filter(
      (pTransition) => pTransition.from === pInitialPseudoStateName,
    );

    if (lInitialTransitions.length > 0 && !lInitialTransitions[0].action) {
      lReturnValue = lInitialTransitions[0].to;
    }
  }
  return lReturnValue;
}

export default function render(
  pStateMachine: IStateMachine,
  _pOptions?: IRenderOptions,
  pTransitions?: ITransition[],
): ISCJSONMachine {
  const lInitialPseudoStateName = findInitialPseudoStateName(pStateMachine);
  const lInitialStateName = findInitialStateName(
    pStateMachine,
    lInitialPseudoStateName,
  );
  const lReturnValue: ISCJSONMachine = {
    states: pStateMachine.states
      .filter((pState) => {
        if (
          lInitialStateName &&
          lInitialStateName !== lInitialPseudoStateName
        ) {
          return pState.type !== "initial";
        }
        return true;
      })
      .map(
        transformState(
          pTransitions ||
            new StateMachineModel(pStateMachine).flattenedTransitions,
        ),
      ),
  };

  if (lInitialStateName) {
    lReturnValue.initial = makeValidXMLName(lInitialStateName);
  }
  return lReturnValue;
}