slim
Version:
Compile a Kingly state machine drawn in the yed professional graph editor down to JavaScript
223 lines (201 loc) • 9.48 kB
JavaScript
module.exports = function slim(argv) {
const { Try, TryModule, Success, Failure, None, Some, Left, Right } = require('funfix');
const { initHistoryDataStructure, computeHistoryMaps, ACTION_IDENTITY, INIT_EVENT, INIT_STATE } = require('kingly');
const { templateIntro, transitionWithoutGuard, transitionWithGuards, mainLoop, cjsExports, esmExports } = require('./templates');
const prettier = require('prettier'); // From jest: terrific to format object nicely
const fs = require('fs');
const { Command } = require('commander');
const { computeTransitionsAndStatesFromXmlString } = require('./conversion');
const { checkKinglyContracts, isTransitionWithoutGuard, getIndexedHistoryStates, computeParentMapFromHistoryMaps, getCommentsHeader, frontHeader } = require('./helpers');
const program = new Command();
const prettyFormat = require('pretty-format');
// Configure syntax, parse and run
program
.version('0.1.0')
.arguments('<file>')
.action(compileYedFile);
program.parse(process.argv);
// Conversion function
// NTH: May could be the export through an option (to generate module.exports = ...)
// NTH: handle several files at the same time
// NTH: add an output option
function compileYedFile(_file) {
const file = _file.endsWith('.graphml') ? _file : `${file}.graphml`;
// console.warn(`\nfile`, file)
const result = readFile(file)
.flatMap(compileYedString)
.flatMap(writeCompiledFiles(file));
return result.fold(
error => {
console.error(error);
process.exit(1);
},
result => {
// console.log(`template:`, result);
},
);
}
function compileYedString(yedString) {
return Try.of(() => {
const {
states,
stateYed2KinglyMap,
events,
transitionsWithoutGuardsActions,
// only exists to check if machine is valid Kingly machine
transitionsWithFakeGuardsActions,
getKinglyTransitions,
errors,
} = computeTransitionsAndStatesFromXmlString(yedString);
if (errors.length > 0) {
console.error(`${errors.length} error(s) found! See log.`);
errors.map(console.error);
throw new Error(errors);
}
const isValid = checkKinglyContracts(states, events, transitionsWithFakeGuardsActions);
// This is an option actually. If not null then it is an array of errors
if (!isValid) {
console.error(isValid);
throw new Error(`The input graph does not represent a valid Kingly machine! Cf. log.`);
}
// So we read the file, we have our transitions, states and events for a valid graph,
// let's compile
const historyMaps = computeHistoryMaps(states);
const stateListWithNok = [INIT_STATE].concat(historyMaps.stateList);
const stateIndexList = stateListWithNok.reduce((acc, cs, i) => (acc[cs] = i, acc), {});
const initialHistoryStateKingly = initHistoryDataStructure(historyMaps.stateList);
const initialHistoryState = getIndexedHistoryStates(initialHistoryStateKingly, stateListWithNok);
const stateAncestors = historyMaps.stateAncestors;
const parentMap = computeParentMapFromHistoryMaps(historyMaps, stateIndexList, stateListWithNok);
const transitions = transitionsWithoutGuardsActions;
// console.warn(`transitions `, prettyFormat(transitions ))
const transitionsPerOrigin = transitions.reduce((acc, transition) => {
const { from, event } = transition;
acc[from] = acc[from] || {};
acc[from][event] = transition;
return acc;
}, {});
const isStateWithEventlessTransition = transitions.reduce((isStateWithEventlessTransition, transition) => {
const { from, event } = transition;
if (event === '') {
isStateWithEventlessTransition[from] = true;
}
;
return isStateWithEventlessTransition;
}, {});
const isCompoundControlState = historyMaps.stateList.reduce((acc, state) => {
if (stateAncestors && stateAncestors[state]) {
stateAncestors[state].forEach(stateAncestor => acc[stateAncestor] = true);
}
return acc;
}, {});
const nextEventMap = stateListWithNok.map(cs => {
if (isStateWithEventlessTransition[cs]) {
return '';
}
else if (isCompoundControlState[cs]) {
return INIT_EVENT;
}
return -1;
});
const usesHistoryStates = Object.keys(isCompoundControlState).length > 0 && transitions.some(transition => {
if (transition.guards) return transition.guards.some(({ to }) => typeof to === 'object');
else return typeof transition.to === 'object';
});
// The usesHistoryStates guard is important as transitioning to an
// history pseudo state may lead to a transition to a compound state,
// hence we have an hidden automatic transition here
// NOTE: while being false does not mean that there is an automatic event
// being false means that there is none such event
const hasAutomaticEvents = usesHistoryStates || Object.keys(nextEventMap).some(state => nextEventMap[state] === '' || nextEventMap[state] === INIT_EVENT);
const hasChainedActions = transitions.some(transition => {
const guards = transition.guards;
return guards.some(g => g.action.filter(Boolean).length > 1);
});
// Start the compiled file with the shape of actions and guards
// to pass to the `createStateMachine` factory function
const commentsHeader = getCommentsHeader(transitionsWithoutGuardsActions, stateListWithNok);
const compiledContents = [
frontHeader,
commentsHeader,
templateIntro({ usesHistoryStates, hasAutomaticEvents, nextEventMap, hasChainedActions }),
`function createStateMachine(fsmDefForCompile, stg) {`,
`var actions = fsmDefForCompile.actionFactories;`,
`var guards = fsmDefForCompile.guards;`,
`var updateState = fsmDefForCompile.updateState;`,
`var initialExtendedState = fsmDefForCompile.initialExtendedState;`,
``,
Object.keys(stateAncestors).length === 0 ? `
// Initialize machine state,
`.trim() : `
// Initialize machine state,
var parentMap = ${JSON.stringify(parentMap)};
`.trim(),
// We use the conrol state index, i.e. a number, not the full string to save bytes
`// Start with pre-initial state ${JSON.stringify(INIT_STATE)}`,
`var cs = ${JSON.stringify(stateIndexList[INIT_STATE])};`,
`var es = initialExtendedState;`,
usesHistoryStates ? `var hs = ${JSON.stringify(initialHistoryState)};\n` : ``,
Object.keys(stateAncestors).length !== 0 ? `
function getAncestors(cs) {return parentMap[cs] ? [parentMap[cs]].concat(getAncestors(parentMap[cs])) : []} ;
` : '',
eventHandlers({
transitions,
transitionsPerOrigin,
stateAncestors,
usesHistoryStates,
stateListWithNok,
stateIndexList,
}),
// Now the main function
mainLoop({ nextEventMap, hasAutomaticEvents, stateAncestors }),
`}`,
].join('\n ').trim();
return compiledContents;
});
}
function readFile(file) {
return Try.of(() => fs.readFileSync(file, 'utf8'));
}
function writeCompiledFiles(file) {
return compiledContents => Try.of(() => {
// Write the esm output file
const esmContents = [compiledContents, esmExports].join('\n\n');
const prettyEsmFileContents = prettier.format(esmContents, { semi: true, parser: 'babel', printWidth: 120 });
// const prettyEsmFileContents = esmContents;
fs.writeFileSync(`${file}.fsm.compiled.js`, prettyEsmFileContents);
// Write the cjs output file
const cjsContents = [compiledContents, cjsExports, ''].join('\n\n');
const prettyCjsFileContents = prettier.format(cjsContents, { semi: true, parser: 'babel', printWidth: 120 });
fs.writeFileSync(`${file}.fsm.compiled.cjs`, prettyCjsFileContents);
});
}
function eventHandlers({ transitions, transitionsPerOrigin, stateAncestors, usesHistoryStates, stateListWithNok, stateIndexList }) {
const eventHandlers = stateListWithNok.map(cs => {
const transitionRecord = transitionsPerOrigin[cs];
if (transitionRecord) {
const events = Object.keys(transitionRecord);
return `{` +
events.reduce((str, event) => {
const transition = transitionsPerOrigin[cs][event];
const { guards } = transition;
// console.warn(`transition`, transition)
return str + `
${JSON.stringify(event)}: ${
isTransitionWithoutGuard(guards)
? transitionWithoutGuard(guards[0].action, guards[0].to, usesHistoryStates, stateIndexList)
: transitionWithGuards(guards, usesHistoryStates, stateIndexList)
}
`;
}, '') +
`}`;
}
else {
return null;
}
});
return `var eventHandlers = [${eventHandlers.join()}]
`;
}
// TODO: cf. TODO.md
};