state-transducer
Version:
Extended Hierarchical State Transducer library
215 lines (190 loc) • 8.24 kB
JavaScript
import {
HISTORY_STATE_NAME, INIT_STATE, SEP, TRANSITION_LABEL_START_SYMBOL, TRANSITION_SYMBOL
} from "./properties"
import {
getDisplayName, format_history_transition_state_name, format_transition_label, get_all_transitions, is_entry_transition,
is_from_control_state, is_history_transition, is_to_history_control_state_of, times
} from './helpers'
import { arrayTreeLenses, objectTreeLenses, postOrderTraverseTree } from "fp-rosetree"
function generateStatePlantUmlHeader(state, optDisplayName) {
return optDisplayName
? `state "${optDisplayName}" as ${state} <<NoContent>>`
: `state "${getDisplayName(state)}" as ${state} <<NoContent>>`
}
/**
* Converts a transducer definition to a textual format for interpretation by PlantUml tools
* @param {FSM_Def} fsmDef
* @param {*} settings
*/
export function toPlantUml(fsmDef, settings) {
const { states, transitions } = fsmDef;
const { getChildren, constructTree, getLabel } = objectTreeLenses;
const stringify = path => path.join(SEP);
const getChildrenNumber = (tree, traversalState) => getChildren(tree, traversalState).length;
const traverse = {
seed: () => Map,
visit: (pathMap, traversalState, tree) => {
const { path } = traversalState.get(tree);
const treeLabel = getLabel(tree);
const controlState = Object.keys(treeLabel)[0];
const childrenTranslation = times(
index => pathMap.get(stringify(path.concat(index))),
getChildrenNumber(tree, traversalState)
);
const translation = stateToPlantUML(controlState, childrenTranslation, transitions);
pathMap.set(stringify(path), translation);
return pathMap;
}
};
const translationMap = postOrderTraverseTree(objectTreeLenses, traverse, { [INIT_STATE]: states });
const mappedTree = translationMap.get('0');
translationMap.clear();
return mappedTree;
}
/**
* Convert a state machine specs into a plantUML format, limiting its conversion scope to a given control state and
* its nested hierarchy
* @param {ControlState} controlState
* @param {Array<String>} childrenTranslation conversion of the states nested in the given control state
* @param {Array<Transition>} transitions Full set of transitions as defined in the state machine specs
* CONTRACT : All control states must have different names...
*/
function stateToPlantUML(controlState, childrenTranslation, transitions) {
return [
`${generateStatePlantUmlHeader(controlState, '')} {`,
childrenTranslation.join('\n'),
format_history_states(controlState, transitions),
format_entry_transitions(controlState, transitions),
`}`,
translate_transitions(controlState, transitions)
]
.filter(x => x !== '\n' && x !== '')
.join('\n');
}
function format_history_states(controlState, transitions) {
// creates the history states as orig.dest.H
// e.g. state "H" as CD_stepping_forwards.CD_Loaded_Group.H <<NoContent>>
const historyStatesObj = transitions.reduce((accTranslation, transition) => {
const allTransitions = get_all_transitions(transition);
return allTransitions
.filter(is_history_transition)
.filter(is_to_history_control_state_of(controlState))
.reduce((acc, transition) => {
acc[format_history_transition_state_name(transition)] = void 0;
return acc
}, accTranslation)
}, {});
const historyStates = Object.keys(historyStatesObj);
return historyStates.map(historyState => {
return `${generateStatePlantUmlHeader(historyState, HISTORY_STATE_NAME)}`
}).join('\n')
}
function translate_transitions(controlState, transitions) {
const historyTransitionTranslation = format_history_transitions(controlState, transitions);
const standardTransitionTranslation = format_standard_transitions(controlState, transitions);
return [
historyTransitionTranslation,
standardTransitionTranslation
]
.filter(Boolean)
.join('\n')
}
function format_standard_transitions(controlState, transitions) {
// The only transition from initial state are INIT transitions and that's already taken care of elsewhere
if (controlState === INIT_STATE) return ''
else return transitions.map(transition => {
const allTransitions = get_all_transitions(transition)
return allTransitions
.filter(is_from_control_state(controlState))
.filter(transition => !is_entry_transition(transition))
.filter(transition => !is_history_transition(transition))
.map(({ from, event, predicate, to, action }) => {
return [
from,
TRANSITION_SYMBOL,
to,
TRANSITION_LABEL_START_SYMBOL,
format_transition_label(event, predicate, action),
].join(' ')
}).join('\n');
})
// necessary because [].join('\n') is "" so I need to take those out to avoid unnecessary '\n' down the road
.filter(Boolean)
.join('\n');
}
function format_entry_transitions(controlState, transitions) {
const translation = transitions.reduce((accTranslation, transition) => {
const allTransitions = get_all_transitions(transition);
return allTransitions
.filter(is_entry_transition)
.filter(is_from_control_state(controlState))
.reduce((acc, transition) => {
const { from, to, predicate, action } = transition;
acc.push(
`[*] ${TRANSITION_SYMBOL} ${to} ${TRANSITION_LABEL_START_SYMBOL} ${format_transition_label("", predicate, action)}`
);
return acc
}, accTranslation)
}, []);
return translation.join('\n')
}
function format_history_transitions(controlState, transitions) {
return transitions.map(transition => {
const allTransitions = get_all_transitions(transition)
return allTransitions
.filter(is_from_control_state(controlState))
.filter(is_history_transition)
.map(({ from, event, predicate, to, action }) => {
return [
from,
TRANSITION_SYMBOL,
format_history_transition_state_name({ from, to }),
TRANSITION_LABEL_START_SYMBOL,
format_transition_label(event, predicate, action),
].join(' ')
}).join('\n');
})
.filter(Boolean)
.join('\n');
}
export function toDagreVisualizerFormat(fsmDef) {
// only thing to do here is to replace functions (guards and actions) by their name, and keep only
// the states and transitions properties
// ah no I also need to turn the states obj tree into an array-based tree... grrr
const { states, transitions } = fsmDef;
const { getLabel, getChildren } = objectTreeLenses;
const { constructTree } = arrayTreeLenses;
const getChildrenNumber = (tree, traversalState) => getChildren(tree, traversalState).length;
const stringify = path => path.join(SEP);
const traverse = {
seed: () => Map,
visit: (pathMap, traversalState, tree) => {
const { path } = traversalState.get(tree);
const treeLabel = getLabel(tree);
const controlState = Object.keys(treeLabel)[0];
const children = times(
index => pathMap.get(stringify(path.concat(index))),
getChildrenNumber(tree, traversalState)
);
pathMap.set(stringify(path), constructTree(controlState, children));
return pathMap;
}
};
const _translatedStates = postOrderTraverseTree(objectTreeLenses, traverse, { [INIT_STATE]: states });
const translatedStates = _translatedStates.get('0');
const translatedTransitions = transitions.map(transition => {
const { from, to, event, guards, action } = transition;
if (guards) {
const translatedGuards = guards.map(guard => {
const { predicate, to, action } = guard;
return { predicate: predicate.name, to, action: action.name }
})
return { from, event, guards: translatedGuards }
}
else {
// case {from, to event, action}
return { from, to, event, action: action.name || 'no action name?' }
}
});
return JSON.stringify({ states: translatedStates, transitions: translatedTransitions })
}