UNPKG

yed2kingly

Version:

Supports the conversion of graphs drawn with the yed editor into Kingly state machines

322 lines (281 loc) 8.94 kB
const parser = require('fast-xml-parser'); const {YED_DEEP_HISTORY_STATE, YED_SHALLOW_HISTORY_STATE, YED_ENTRY_STATE, SEP, DEFAULT_ACTION_FACTORY_STR,DEFAULT_ACTION_FACTORY} = require('./properties'); const {historyState, SHALLOW, DEEP, fsmContracts, createStateMachine} = require("kingly"); const { mapOverObj } = require('fp-rosetree'); function T() { return true} // Error handling function tryCatchFactory(errors) { // @side-effect: we modify errors in place return function tryCatch(fn, errCb) { return function tryCatch(...args) { try {return fn.apply(fn, args);} catch (e) { return errCb(e, errors, args); } }; } } class Yed2KinglyConversionError extends Error { constructor(m) { super(m); this.errors = m; this.message = m.map(({ when, location, info, message }) => { // formatted message const fm = `At ${location}: ${when} => ${message}`; console.info(fm, info); console.error(m); return fm }).concat([`See extra info in console`]).join('\n'); } } function handleAggregateEdgesPerFromEventKeyErrors(e, errors, [hashMap, yedEdge]) { errors.push({ when: `building (from, event) hashmap`, location: `computeTransitionsAndStatesFromXmlString > aggregateEdgesPerFromEventKey`, info: { hashMap: JSON.parse(JSON.stringify(hashMap)), yedEdge }, message: e.message, possibleCauses: [ `File is not a valid graphML file`, `File is not a valid yed-generated graphML file`, `You found a bug in yed2Kindly converter`, // `You found a bug in fast-xml=parser (unlikely)`, ], original: e }); return {} } function handleParseGraphMlStringErrors(e, errors, [yedString]) { errors.push({ when: `parsing the graphml string`, location: `computeTransitionsAndStatesFromXmlString > parser.parse`, info: { yedString }, possibleCauses: [ `File is not an XML file`, `File is not a graphML file`, `File is not a yed-generated graphML file`, // `File was read incorrectly from disk`, // `You found a bug in fast-xml=parser (unlikely)`, // `You found a bug in yed2Kindly converter (maybe)`, ], message: e.message, original: e } ); return {} } // Predicates function isCompoundState(graphObj) { return graphObj['@_yfiles.foldertype'] === 'group' } function isGraphRoot(graphObj) { return 'key' in graphObj } function isInitialTransition(yedFrom, userFrom) { return userFrom === YED_ENTRY_STATE } function isTopLevelInitTransition(yedFrom, userFrom) { // yEd internal naming is nX::Ny::... so a top-level node will be just nX return isInitialTransition(yedFrom, userFrom) && yedFrom.split('::').length === 1 } function isHistoryDestinationState(stateYed2KinglyMap, yedTo) { const x = stateYed2KinglyMap[yedTo].trim(); return x === YED_SHALLOW_HISTORY_STATE || x === YED_DEEP_HISTORY_STATE } function isDeepHistoryDestinationState(stateYed2KinglyMap, yedTo) { const x = stateYed2KinglyMap[yedTo].trim(); return isHistoryDestinationState(stateYed2KinglyMap, yedTo) && x === YED_DEEP_HISTORY_STATE } // Conversion helpers // iff no predicate, and only one transition in array function isSimplifiableSyntax(arrGuardsTargetActions) { return arrGuardsTargetActions.length === 1 && !arrGuardsTargetActions[0].predicate } function getYedParentNode(yedFrom) { return yedFrom.split('::').slice(0, -1).join('::') } function yedState2KinglyState(stateYed2KinglyMap, yedState) { return [yedState, stateYed2KinglyMap[yedState]].join(SEP); } function computeKinglyDestinationState(stateYed2KinglyMap, yedTo) { if (isHistoryDestinationState(stateYed2KinglyMap, yedTo)) { return isDeepHistoryDestinationState(stateYed2KinglyMap, yedTo) ? historyState(DEEP, yedState2KinglyState(stateYed2KinglyMap, getYedParentNode(yedTo))) : historyState(SHALLOW, yedState2KinglyState(stateYed2KinglyMap, getYedParentNode(yedTo))) } else { return yedState2KinglyState(stateYed2KinglyMap, yedTo); } } function mapActionFactoryStrToActionFactoryFn(actionFactories, actionFactoryArr) { return chain(actionFactoryArr.filter(Boolean), actionFactories) } function mapGuardStrToGuardFn(guards, predicateArr) { return every(predicateArr.filter(Boolean), guards) } function markArrayFunctionStr(_, arr){ return arr.map(str => ['', '', '', str, '', '', ''].join(SEP)); } function markFunctionNoop(_, str){ return () => ({outputs: [], updates: []}) } function markGuardNoop(_, str){ return () => true } function contains(as, bs){ // returns true if every a in as can be found in bs return as.every(a => bs.indexOf(a) > -1) } // Parsing function parseGraphMlString(yedString) { // true as third param validates the xml string prior to parsing, possibly throws // cf. https://github.com/NaturalIntelligence/fast-xml-parser#xml-to-json // Validator returns the following object in case of error; // { // err: { // code: code, // msg: message, // line: lineNumber, // }, // }; const jsonObj = parser.parse(yedString, { ignoreAttributes: false }, true); if (!jsonObj.graphml) throw `Not a graphml file? Could not find a <graphml> tag!` return jsonObj } // Test helpers function isFunction(obj) { return typeof obj === 'function' } function isPOJO(obj) { const proto = Object.prototype; const gpo = Object.getPrototypeOf; if (obj === null || typeof obj !== "object") { return false; } return gpo(obj) === proto; } function formatResult(result) { if (!isPOJO(result)) { return result } else { return mapOverObj({ key: x => x, leafValue: prop => isFunction(prop) ? (prop.name || prop.displayName || 'anonymous') : Array.isArray(prop) ? prop.map(formatResult) : prop }, result) } } const fakeConsole = { log: () => {}, error: () => {}, warn: () => {}, info: () => {}, debug: () => {}, group: () => {}, groupEnd: () => {}, }; function checkKinglyContracts(states, events, transitions) { try { return createStateMachine({ initialExtendedState: void 0, events, states, transitions, updateState: () => { }, }, {debug: {console: fakeConsole, checkContracts: fsmContracts}}); } catch (err) { console.error(err) return null } } function displayTransitionJSON(t) { return JSON.stringify(t, (key, value) => { if (typeof value === 'function') { return value.name === '' ? '<anonymous>' : `function ${ value.name }()`; } return value }); } // https://stackoverflow.com/questions/12303989/cartesian-product-of-multiple-arrays-in-javascript // let output = cartesian([1,2],[10,20],[100,200,300]); // This is the output of that command: // // ``` // [ [ 1, 10, 100 ], // [ 1, 10, 200 ], // [ 1, 10, 300 ], // [ 1, 20, 100 ], // [ 1, 20, 200 ], // [ 1, 20, 300 ], // [ 2, 10, 100 ], // [ 2, 10, 200 ], // [ 2, 10, 300 ], // [ 2, 20, 100 ], // [ 2, 20, 200 ], // [ 2, 20, 300 ] ] // ``` const f = (a, b) => [].concat(...a.map(d => b.map(e => [].concat(d, e)))); const cartesian = (a, b, ...c) => (b ? cartesian(f(a, b), ...c) : a); function trimInside(str) { return str.replace(/\n/gm, ' ').replace(/\r/gm, ' ').replace(/\s+/g, " "); } function chain(arrFns, actions) { return function chain_(s, ed, stg) { return ( arrFns.reduce(function(acc, fn) { // console.warn(`chain_`, fn) var r = actions[fn](s, ed, stg); return { updates: acc.updates.concat(r.updates), outputs: acc.outputs.concat(r.outputs), }; }, { updates: [], outputs: [] }) ); }; } function every(arrFns, guards) { return function every_(s, ed, stg) { return ( arrFns.reduce(function(acc, fn) { var r = guards[fn](s, ed, stg); return r && acc; }, true) ); }; } module.exports = { T, tryCatchFactory, Yed2KinglyConversionError, handleAggregateEdgesPerFromEventKeyErrors, handleParseGraphMlStringErrors, isCompoundState, isGraphRoot, isInitialTransition, isTopLevelInitTransition, isSimplifiableSyntax, getYedParentNode, yedState2KinglyState, computeKinglyDestinationState, mapActionFactoryStrToActionFactoryFn, mapGuardStrToGuardFn, parseGraphMlString, formatResult, cartesian, markFunctionStr: markArrayFunctionStr, markFunctionNoop, markGuardNoop, checkKinglyContracts, fakeConsole, contains, displayTransitionJSON, trimInside, chain, every }