yed2kingly
Advanced tools
Comparing version 0.4.0 to 0.5.0
const {INIT_STATE, INIT_EVENT} = require('kingly'); | ||
const {mapOverTree} = require('fp-rosetree'); | ||
const {lensPath, view, mergeAll, concat, forEachObjIndexed, find} = require('ramda'); | ||
const {lensPath, view, mergeAll, concat, forEachObjIndexed, find, difference} = require('ramda'); | ||
const nearley = require("nearley"); | ||
const prettyFormat = require("pretty-format"); | ||
const yedEdgeLabelGrammar = require("./yedEdgeLabelGrammar.js"); | ||
@@ -91,4 +92,7 @@ const { | ||
// NTH: implement rules | ||
// Only ever one [x] | ||
/** | ||
* | ||
* @param {String} _yedEdgeLabel cf. grammar. | ||
* @returns {{actionFactory: Array, event: String, guard: Array}[]} `actionFactory` and `guard` are arrays of action strings. For instance, "... / do this, do that" => actionFactory = ["do this", "do that"] | ||
*/ | ||
function parseYedLabel(_yedEdgeLabel) { | ||
@@ -110,2 +114,3 @@ // Parser for parsing edge labels | ||
const results = parser.results[0]; | ||
// console.warn(`results`, results) | ||
if (Array.isArray(results)){ | ||
@@ -119,3 +124,3 @@ arrTransitions = arrTransitions.concat(results); | ||
else { | ||
arrTransitions.push({event: "", guard: "", actions: ""}); | ||
arrTransitions.push({event: "", guard: [], actions: []}); | ||
} | ||
@@ -125,7 +130,9 @@ } | ||
return arrTransitions.map(transitionRecord => { | ||
// console.warn(`transitionRecord `, transitionRecord) | ||
const {event, guard, actions} = transitionRecord; | ||
return { | ||
actionFactory: actions.trim() || DEFAULT_ACTION_FACTORY_STR, | ||
actionFactory: actions.map(action => action.trim()), | ||
event: event.trim(), | ||
guard: guard.trim() | ||
guard: guard.map(guard => guard.trim()) | ||
} | ||
@@ -135,8 +142,9 @@ }) | ||
function aggregateEdgesPerFromEventKey({edges: hashMap, events}, yedEdge) { | ||
function aggregateEdgesPerFromEventKey(acc, yedEdge) { | ||
const {edges: hashMap, events} = acc; | ||
const from = view(lensPath(['@_source']), yedEdge).trim(); | ||
const to = view(lensPath(['@_target']), yedEdge).trim(); | ||
const yedEdgeLabel = getYedEdgeLabel(yedEdge); | ||
const transitionsRecords = parseYedLabel(yedEdgeLabel); | ||
transitionsRecords.forEach(transitionsRecord => { | ||
@@ -148,3 +156,3 @@ const {actionFactory, event, guard} = transitionsRecord; | ||
hashMap[fromEventKey] = hashMap[fromEventKey].concat([ | ||
{predicate: guard.trim(), to: to.trim(), actionFactory: actionFactory.trim()}, | ||
{predicate: guard.map(g => g.trim()), to: to.trim(), actionFactory: actionFactory.map(af => af.trim())}, | ||
]); | ||
@@ -160,8 +168,9 @@ if (event) events.add(event) | ||
* @param {Array} errors | ||
* @param actionFactories | ||
* @param guards | ||
* @param edges | ||
* @param actionFactories actions passed by the API user | ||
* @param guards guards passed by the API user | ||
* @param Array<{{arrGuardsTargetActions, fromEventKey}}> edges | ||
* @returns {Array} Array contains found errors, empty is no error found | ||
*/ | ||
function checkForMissingFunctions(errors, {actionFactories, guards}, edges) { | ||
// TODO: that could be refactored with applicative validation? | ||
forEachObjIndexed((arrGuardsTargetActions, fromEventKey) => { | ||
@@ -188,8 +197,12 @@ const [yedFrom, _event] = fromEventKey.split(SEP); | ||
arrGuardsTargetActions.every(guardsTargetActionRecord => { | ||
const {predicate: _predicateStr, to: yedTo, actionFactory: _actionFactoryStr} = guardsTargetActionRecord; | ||
const predicateStr = _predicateStr.trim(); | ||
const actionFactoryStr = _actionFactoryStr.trim(); | ||
// We do not check that it is a function here to allow a string for testing purposes | ||
const isGuardPassed = !predicateStr || guards && guards[predicateStr]; | ||
const isActionPassed = !actionFactoryStr || actionFactories[actionFactoryStr]; | ||
// We do not check that guards and actions are functions, we want to allow for strings | ||
const {predicate: _predicateList, to: yedTo, actionFactory: _actionFactoryList} = guardsTargetActionRecord; | ||
const providedGuards = Object.keys(guards); | ||
const expectedGuards = _predicateList.map(p => p.trim()); | ||
const providedActions = Object.keys(actionFactories); | ||
const expectedActions = _actionFactoryList.map(af => af.trim()); | ||
const notNeededGuards = difference(providedGuards, expectedGuards); | ||
const missingGuards = difference(expectedGuards, providedGuards); | ||
const notNeededActions = difference(providedActions, expectedActions); | ||
const missingActions = difference(expectedActions, providedActions); | ||
@@ -202,14 +215,26 @@ if (!isValidStateName(yedTo)) errors.push({ | ||
}); | ||
if (!isGuardPassed) errors.push({ | ||
if (notNeededGuards.length>0) errors.push({ | ||
when: `Checking that the transitions figuring in the graph can be mapped to functions implementing them`, | ||
location: `checkForMissingFunctions > getKinglyTransitions`, | ||
message: `Yed graph file mentions a guard named |${predicateStr}|. I could not find the implementation of that guard in the parameter |guards| passed!`, | ||
info: {guards} | ||
message: `I found guards passed as parameters that do not match to a guard in the yed graph! Please remove them!`, | ||
info: {notNeededGuards, guards, expectedGuards} | ||
}) | ||
if (!isActionPassed) errors.push({ | ||
if (missingGuards.length>0) errors.push({ | ||
when: `Checking that the transitions figuring in the graph can be mapped to functions implementing them`, | ||
location: `checkForMissingFunctions > getKinglyTransitions`, | ||
message: `Yed graph file mentions an action factory named |${actionFactoryStr}|. I could not find the implementation of that action factory in the parameter |actionFactories| passed!`, | ||
info: {actionFactories} | ||
message: `I found guards in the yed graph that cannot be matched to a JavaScript function! Please review the JavaScript guards that you passed.`, | ||
info: {missingGuards, guards, expectedGuards} | ||
}) | ||
if (notNeededActions.length>0) errors.push({ | ||
when: `Checking that the transitions figuring in the graph can be mapped to functions implementing them`, | ||
location: `checkForMissingFunctions > getKinglyTransitions`, | ||
message: `I found actions passed as parameters that do not match to an action in the yed graph! Please remove them!`, | ||
info: {notNeededActions, actionFactories, expectedActions} | ||
}) | ||
if (missingActions.length>0) errors.push({ | ||
when: `Checking that the transitions figuring in the graph can be mapped to functions implementing them`, | ||
location: `checkForMissingFunctions > getKinglyTransitions`, | ||
message: `I found actions in the yed graph that cannot be matched to a JavaScript function! Please review the JavaScript actions that you passed.`, | ||
info: {missingActions, actionFactories, expectedActions} | ||
}) | ||
}); | ||
@@ -238,2 +263,3 @@ }, edges); | ||
forEachObjIndexed((arrGuardsTargetActions, fromEventKey) => { | ||
// console.warn(`arrGuardsTargetActions`, arrGuardsTargetActions) | ||
// Example: | ||
@@ -240,0 +266,0 @@ // yedFrom: "n0::n0" ; userFrom: "entered by user" ; _from: "n0::n0[symbol]entered by user" |
@@ -128,14 +128,13 @@ const parser = require('fast-xml-parser'); | ||
function mapActionFactoryStrToActionFactoryFn(actionFactories, actionFactoryStr) { | ||
return actionFactoryStr === DEFAULT_ACTION_FACTORY_STR | ||
? DEFAULT_ACTION_FACTORY | ||
: actionFactories[actionFactoryStr] | ||
function mapActionFactoryStrToActionFactoryFn(actionFactories, actionFactoryArr) { | ||
console.warn(`mapActionFactoryStrToActionFactoryFn`, actionFactoryArr) | ||
return chain(actionFactoryArr.filter(Boolean), actionFactories) | ||
} | ||
function mapGuardStrToGuardFn(guards, predicateStr) { | ||
return guards[predicateStr] || T | ||
function mapGuardStrToGuardFn(guards, predicateArr) { | ||
return every(predicateArr.filter(Boolean), guards) | ||
} | ||
function markFunctionStr(_, str){ | ||
return ["", "", "", str, "", "", ""].join(SEP) | ||
function markArrayFunctionStr(_, arr){ | ||
return arr.map(str => ['', '', '', str, '', '', ''].join(SEP)); | ||
} | ||
@@ -265,2 +264,30 @@ | ||
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 = { | ||
@@ -285,3 +312,3 @@ T, | ||
cartesian, | ||
markFunctionStr, | ||
markFunctionStr: markArrayFunctionStr, | ||
markFunctionNoop, | ||
@@ -294,2 +321,4 @@ markGuardNoop, | ||
trimInside, | ||
chain, | ||
every | ||
} |
94
index.js
@@ -8,2 +8,3 @@ module.exports = function convert(argv) { | ||
const prettier = require("prettier"); | ||
const prettyFormat = require("pretty-format"); | ||
const fs = require('fs'); | ||
@@ -14,2 +15,3 @@ const {Command} = require('commander'); | ||
const {DEFAULT_ACTION_FACTORY_STR} = require('./properties'); | ||
const {implDoStr, implEveryStr} = require('./template'); | ||
const program = new Command(); | ||
@@ -24,2 +26,6 @@ | ||
function hasGuards(guards){ | ||
return guards.length > 1 || guards.length === 1 && guards[0].predicate.map(p => p.slice(3, -3)).filter(Boolean).length > 0 | ||
} | ||
// Conversion function | ||
@@ -61,7 +67,11 @@ // DOC: We export two files: one cjs for node.js and js for browser esm consumption | ||
// to hold the guards and actions | ||
// TODO: refactor that somewhere else | ||
let predicateList = new Set(); | ||
let actionList = new Set(); | ||
const transitionsStr = transitionsWithoutGuardsActions.map(transitionRecord => { | ||
if (transitionRecord.guards) { | ||
const {from, event, guards} = transitionRecord; | ||
const {from, event, guards} = transitionRecord; | ||
// console.warn(`transitionsWithoutGuardsActions > transitionRecord `, prettyFormat(transitionRecord)); | ||
if (guards.length === 0) throw `Got guards record that is empty array! We have a bug here!` | ||
if (hasGuards(guards) ) { | ||
return ` | ||
@@ -71,11 +81,10 @@ { from: "${from}", event: "${event}", guards: [ | ||
const {predicate, to, action} = guardRecord; | ||
const predicateStr = predicate.slice(3, -3); | ||
const actionStr = action.slice(3, -3); | ||
predicateList.add(predicateStr); | ||
actionList.add(actionStr); | ||
// actionList.push(actionStr); | ||
const predicates = predicate.map(x => x.slice(3, -3)).filter(Boolean); | ||
const actions = action.map(x => x.slice(3, -3)).filter(Boolean); | ||
predicates.forEach(x => predicateList.add(x)); | ||
actions.forEach(x => actionList.add(x)); | ||
return ` | ||
{predicate: guards["${predicateStr}"], to: ${JSON.stringify(to)}, action: aF["${actionStr}"]}, | ||
`.trim() | ||
{predicate: every(${JSON.stringify(predicates)}, guards), to: ${JSON.stringify(to)}, action: chain(${JSON.stringify(actions)}, aF)} | ||
`.trim().concat(", ") | ||
}).join("\n") | ||
@@ -87,8 +96,10 @@ } | ||
else { | ||
const {from, event, to, action} = transitionRecord; | ||
const actionStr = action.slice(3, -3); | ||
actionList.add(actionStr); | ||
const {predicate, to, action} = guards[0]; | ||
const predicates = predicate.map(x => x.slice(3, -3)).filter(Boolean); | ||
const actions = action.map(x => x.slice(3, -3)).filter(Boolean); | ||
predicates.forEach(x => predicateList.add(x)); | ||
actions.forEach(x => actionList.add(x)); | ||
return ` | ||
{ from: "${from}", event: "${event}", to: ${JSON.stringify(to)}, action: aF["${actionStr}"] } | ||
{ from: "${from}", event: "${event}", to: ${JSON.stringify(to)}, action: chain(${JSON.stringify(actions)}, aF)} | ||
`.trim().concat(", ") | ||
@@ -107,2 +118,8 @@ } | ||
// -----Guards------ | ||
/** | ||
* @param {E} extendedState | ||
* @param {D} eventData | ||
* @param {X} settings | ||
* @returns Boolean | ||
*/ | ||
// const guards = {${Array.from(predicateList) | ||
@@ -113,2 +130,9 @@ .map(pred => `\n// "${pred}": function (){},`) | ||
// -----Actions------ | ||
/** | ||
* @param {E} extendedState | ||
* @param {D} eventData | ||
* @param {X} settings | ||
* @returns {{updates: U[], outputs: O[]}} | ||
* (such that updateState:: E -> U[] -> E) | ||
*/ | ||
// const actions = {${Array.from(actionList) | ||
@@ -124,2 +148,7 @@ .map(action => action !== DEFAULT_ACTION_FACTORY_STR | ||
} | ||
${implDoStr} | ||
${implEveryStr} | ||
var NO_OUTPUT = ${JSON.stringify(NO_OUTPUT)}; | ||
@@ -134,8 +163,13 @@ var NO_STATE_UPDATE = ${JSON.stringify(NO_STATE_UPDATE)}; | ||
var predicateList = ${JSON.stringify(Array.from(predicateList))}; | ||
aF['ACTION_IDENTITY'] = ${ACTION_IDENTITY}; | ||
if (!contains(actionList, Object.keys(aF))) { | ||
console.error("Some actions are missing either in the graph, or in the action implementation object! Cf actionFactories (you passed that) vs. actionList (from the graph) below. They must have the same items!"); | ||
console.error({actionFactories: Object.keys(aF), actionList}); | ||
throw new Error("Some action are missing either in the graph, or in the action implementation object!") | ||
var passedAndMissingInGraph = Object.keys(aF).filter(function(k) { return actionList.indexOf(k) === -1}); | ||
passedAndMissingInGraph.length > 0 && console.error("So the following actions were passed in parameters but do not match any action in the graph! This may happen if you modified the name of an action in the graph, but kept using the older name in the implementation! Please check.", passedAndMissingInGraph); | ||
var inGraphButNotImplemented= actionList.filter(function(k) { return Object.keys(aF).indexOf(k) === -1}); | ||
inGraphButNotImplemented.length > 0 && console.error("So the following actions declared in the graph are not implemented! Please add the implementation. You can have a look at the comments of the generated fsm file for typing information.", inGraphButNotImplemented); | ||
throw new Error("Some actions implementations are missing either in the graph, or in the action implementation object!") | ||
} | ||
if (!contains(predicateList, Object.keys(guards))) { | ||
console.error("Some guards are missing either in the graph, or in the action implementation object! Cf guards (you passed that) vs. predicateList (from the graph) below. They must have the same items!"); | ||
console.error({guards: Object.keys(guards), predicateList}); | ||
@@ -150,4 +184,24 @@ throw new Error("Some guards are missing either in the graph, or in the guard implementation object!") | ||
} | ||
function createStateMachineFromGraph(fsmDefForCompile, settings){ | ||
var updateState = fsmDefForCompile.updateState; | ||
var initialExtendedState = fsmDefForCompile.initialExtendedState; | ||
var transitions = getKinglyTransitions({actionFactories: fsmDefForCompile.actionFactories, guards: fsmDefForCompile.guards}); | ||
var fsm = createStateMachine({ | ||
updateState, | ||
initialExtendedState, | ||
states, | ||
events, | ||
transitions | ||
}, settings); | ||
return fsm | ||
} | ||
`.trim(); | ||
const cjsImports = `var createStateMachine = require("kingly").createStateMachine;` | ||
const esmImports = `import {createStateMachine} from "kingly";`; | ||
const esmExports = ` | ||
@@ -157,3 +211,4 @@ export { | ||
states, | ||
getKinglyTransitions | ||
getKinglyTransitions, | ||
createStateMachineFromGraph | ||
} | ||
@@ -166,3 +221,4 @@ `.trim(); | ||
states, | ||
getKinglyTransitions | ||
getKinglyTransitions, | ||
createStateMachineFromGraph | ||
} | ||
@@ -172,3 +228,3 @@ `.trim(); | ||
// Write the esm output file | ||
const esmContents = [fileContents, esmExports].join("\n\n"); | ||
const esmContents = [esmImports, fileContents, esmExports].join("\n\n"); | ||
try { | ||
@@ -185,3 +241,3 @@ const prettyFileContents = prettier.format(esmContents, {semi: true, parser: "babel", printWidth: 120}); | ||
// Write the cjs output filee | ||
const cjsContents = [fileContents, cjsExports].join("\n\n"); | ||
const cjsContents = [cjsImports, fileContents, cjsExports].join("\n\n"); | ||
try { | ||
@@ -188,0 +244,0 @@ const prettyFileContents = prettier.format(cjsContents, {semi: true, parser: "babel", printWidth: 120}); |
{ | ||
"name": "yed2kingly", | ||
"version": "0.4.0", | ||
"version": "0.5.0", | ||
"author": "brucou", | ||
@@ -9,3 +9,4 @@ "description": "Supports the conversion of graphs drawn with the yed editor into Kingly state machines", | ||
"chai": "^4.2.0", | ||
"mocha": "^7.1.1" | ||
"mocha": "^7.1.1", | ||
"pretty-format": "^26.0.0" | ||
}, | ||
@@ -12,0 +13,0 @@ "bin": { |
49107
859
4