diff --git a/README.md b/README.md index bd46ea3..9c8c804 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,8 @@ Using npm: ```shell npm install --save-dev javascript-state-machine +or + yarn add javascript-state-machine ``` In Node.js: diff --git a/bin/examples b/bin/examples index e726661..cd6b339 100755 --- a/bin/examples +++ b/bin/examples @@ -4,6 +4,14 @@ // // This script is used to regenerate the example visualizations // +// It will scan all the .js files in the 'examples' folder and generate a +// * .dot - file with dot directives for graphviz +// * .svg - svg graphics representation +// * .png - png graphics representation +// +// If provided with an argument, it will only generate output for the specified +// argument. +// //================================================================================================= var fs = require('fs'), @@ -11,10 +19,25 @@ var fs = require('fs'), child = require('child_process'); //------------------------------------------------------------------------------------------------- +process.chdir(path.dirname(process.argv[1])); + +if (process.argv.length > 2) { + var fn = process.argv[2], + fnfull = '../examples/' + fn; + if (!fs.existsSync(fnfull)) { + console.log('Error - file: ' + fnfull + ' does not exist'); + } -fs.readdirSync('examples') - .filter(function(file) { return path.extname(file) === ".js" }) - .map(visualize); + else { + visualize(fn); + } +} + +else { + fs.readdirSync('../examples') + .filter(function(file) { return path.extname(file) === ".js" }) + .map(visualize); +} //------------------------------------------------------------------------------------------------- @@ -24,10 +47,10 @@ function visualize(example) { dot = fsm.visualize(), svg = dot2svg(dot), png = dot2png(dot); - console.log('visualizing examples/' + example); - fs.writeFileSync('examples/' + name + '.dot', dot); - fs.writeFileSync('examples/' + name + '.svg', svg); - fs.writeFileSync('examples/' + name + '.png', png, 'binary'); + console.log('visualizing ../examples/' + example); + fs.writeFileSync('../examples/' + name + '.dot', dot); + fs.writeFileSync('../examples/' + name + '.svg', svg); + fs.writeFileSync('../examples/' + name + '.png', png, 'binary'); } //------------------------------------------------------------------------------------------------- diff --git a/dist/state-machine-visualize.js b/dist/state-machine-visualize.js index 9c18e13..1458878 100644 --- a/dist/state-machine-visualize.js +++ b/dist/state-machine-visualize.js @@ -123,7 +123,9 @@ function dotcfg(fsm, options) { name = options.name, rankdir = dotcfg.rankdir(options.orientation), states = dotcfg.states(config, options), + statedefs = dotcfg.statedefs(config, options), transitions = dotcfg.transitions(config, options), + dotPrefix = config.dotPrefix, result = { } if (name) @@ -135,9 +137,15 @@ function dotcfg(fsm, options) { if (states && states.length > 0) result.states = states + if (statedefs && statedefs.length > 0) + result.statedefs = statedefs; + if (transitions && transitions.length > 0) result.transitions = transitions + if (dotPrefix) + result.dotPrefix = dotPrefix; + return result } @@ -164,6 +172,11 @@ dotcfg.states = function(config, options) { return states; } +dotcfg.statedefs = function(config, options) { + return config.options['statedefs']; + // can be null +} + dotcfg.transitions = function(config, options) { var n, max, transition, init = config.init, @@ -198,7 +211,6 @@ dotcfg.transition = function(name, from, to, dot, config, options, output) { else { output.push(mixin({}, { from: from, to: to, label: pad(name) }, dot || {})) } - } //------------------------------------------------------------------------------------------------- @@ -218,6 +230,8 @@ function dotify(dotcfg) { var name = dotcfg.name || 'fsm', states = dotcfg.states || [], transitions = dotcfg.transitions || [], + statedefs = dotcfg.statedefs, + dotPrefix = dotcfg.dotPrefix, rankdir = dotcfg.rankdir, output = [], n, max; @@ -225,8 +239,20 @@ function dotify(dotcfg) { output.push("digraph " + quote(name) + " {") if (rankdir) output.push(" rankdir=" + rankdir + ";") - for(n = 0, max = states.length ; n < max ; n++) - output.push(dotify.state(states[n])) + + if (dotPrefix) { + output.push(dotify.dotPrefix(dotPrefix)); + } + + if (statedefs) { + for(n = 0, max = statedefs.length ; n < max ; n++) + output.push(dotify.statedef(statedefs[n])) + } + else { + for(n = 0, max = states.length ; n < max ; n++) + output.push(dotify.gen(states[n])) + } + for(n = 0, max = transitions.length ; n < max ; n++) output.push(dotify.edge(transitions[n])) output.push("}") @@ -234,14 +260,34 @@ function dotify(dotcfg) { } -dotify.state = function(state) { +dotify.gen = function(state) { return " " + quote(state) + ";" } +dotify.statedef = function(statedef) { + var retstr = " " + quote(statedef.name); + if (statedef.dot) { + retstr += dotify.gen.attr(statedef.dot); + } + return retstr + ";"; +} + dotify.edge = function(edge) { return " " + quote(edge.from) + " -> " + quote(edge.to) + dotify.edge.attr(edge) + ";" } +dotify.dotPrefix = function(dotPrefix) { + var prefixStrAry = []; + var ix, key; + var keys = Object.keys(dotPrefix); + for (ix = 0; ix < keys.length; ix++) { + key = keys[ix]; + prefixStrAry.push(' ' + key + ' ' + dotify.gen.attr(dotPrefix[key]) + ";" ); + } + + return prefixStrAry.join("\n"); +} + dotify.edge.attr = function(edge) { var n, max, key, keys = Object.keys(edge).sort(), output = []; for(n = 0, max = keys.length ; n < max ; n++) { @@ -252,6 +298,15 @@ dotify.edge.attr = function(edge) { return output.length > 0 ? " [ " + output.join(" ; ") + " ]" : "" } +dotify.gen.attr = function(attrs) { + var n, max, key, keys = Object.keys(attrs).sort(), output = []; + for(n = 0, max = keys.length ; n < max ; n++) { + key = keys[n]; + output.push(key + "=" + quote(attrs[key])) + } + return output.length > 0 ? " [ " + output.join(", ") + " ]" : "" +} + //------------------------------------------------------------------------------------------------- visualize.dotcfg = dotcfg; diff --git a/dist/state-machine-visualize.min.js b/dist/state-machine-visualize.min.js index e517d3b..0489b0b 100644 --- a/dist/state-machine-visualize.min.js +++ b/dist/state-machine-visualize.min.js @@ -1 +1 @@ -!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachineVisualize",[],n):"object"==typeof exports?exports.StateMachineVisualize=n():t.StateMachineVisualize=n()}(this,function(){return function(t){function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var e={};return n.m=t,n.c=e,n.i=function(t){return t},n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=1)}([function(t,n,e){"use strict";t.exports=function(t,n){var e,r,o;for(e=1;e0&&(u.states=s),a&&a.length>0&&(u.transitions=a),u}function i(t){return" "+t+" "}function s(t){return'"'+t+'"'}function a(t){t=t||{};var n,e,r=t.name||"fsm",o=t.states||[],i=t.transitions||[],u=t.rankdir,f=[];for(f.push("digraph "+s(r)+" {"),u&&f.push(" rankdir="+u+";"),n=0,e=o.length;n "+s(t.to)+a.edge.attr(t)+";"},a.edge.attr=function(t){var n,e,r,o=Object.keys(t).sort(),i=[];for(n=0,e=o.length;n0?" [ "+i.join(" ; ")+" ]":""},r.dotcfg=o,r.dotify=a,t.exports=r}])}); \ No newline at end of file +!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachineVisualize",[],n):"object"==typeof exports?exports.StateMachineVisualize=n():t.StateMachineVisualize=n()}(this,function(){return function(t){function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}var e={};return n.m=t,n.c=e,n.i=function(t){return t},n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{configurable:!1,enumerable:!0,get:r})},n.n=function(t){var e=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(e,"a",e),e},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=1)}([function(t,n,e){"use strict";t.exports=function(t,n){var e,r,o;for(e=1;e0&&(c.states=s),f&&f.length>0&&(c.statedefs=f),a&&a.length>0&&(c.transitions=a),u&&(c.dotPrefix=u),c}function i(t){return" "+t+" "}function s(t){return'"'+t+'"'}function f(t){t=t||{};var n,e,r=t.name||"fsm",o=t.states||[],i=t.transitions||[],a=t.statedefs,u=t.dotPrefix,c=t.rankdir,d=[];if(d.push("digraph "+s(r)+" {"),c&&d.push(" rankdir="+c+";"),u&&d.push(f.dotPrefix(u)),a)for(n=0,e=a.length;n "+s(t.to)+f.edge.attr(t)+";"},f.dotPrefix=function(t){var n,e,r=[],o=Object.keys(t);for(n=0;n0?" [ "+i.join(" ; ")+" ]":""},f.gen.attr=function(t){var n,e,r,o=Object.keys(t).sort(),i=[];for(n=0,e=o.length;n0?" [ "+i.join(", ")+" ]":""},r.dotcfg=o,r.dotify=f,t.exports=r}])}); \ No newline at end of file diff --git a/dist/state-machine.js b/dist/state-machine.js index b8b9e37..ca03f59 100644 --- a/dist/state-machine.js +++ b/dist/state-machine.js @@ -210,9 +210,16 @@ function Config(options, StateMachine) { this.init = this.configureInitTransition(options.init); this.data = this.configureData(options.data); this.methods = this.configureMethods(options.methods); + this.dotPrefix = options['dotPrefix'] || this.defaults.dotPrefix; + this.hasStateDefs = false; this.map[this.defaults.wildcard] = {}; + if (options['statedefs']) { + this.hasStateDefs = true; + this.configureStateDefs(options['statedefs']); + } + this.configureTransitions(options.transitions || []); this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin); @@ -311,16 +318,35 @@ mixin(Config.prototype, { return plugins }, + configureStateDefs: function(statedefs) { + var n; + for (n = 0; n < statedefs.length; n++) { + this.addState(statedefs[n].name); + } + }, + configureTransitions: function(transitions) { - var i, n, transition, from, to, wildcard = this.defaults.wildcard; + var i, n, transition, fromStates, from, to, wildcard = this.defaults.wildcard; + var undefinedStates = []; for(n = 0 ; n < transitions.length ; n++) { transition = transitions[n]; - from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] + fromStates = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] to = transition.to || wildcard; - for(i = 0 ; i < from.length ; i++) { - this.mapTransition({ name: transition.name, from: from[i], to: to }); + if (this.hasStateDefs && to !== wildcard && this.states.indexOf(to) === -1) { + undefinedStates.push(to); + } + for(i = 0 ; i < fromStates.length ; i++) { + from = fromStates[i]; + if (this.hasStateDefs && from !== wildcard && from !== 'none' && + this.states.indexOf(from) === -1) { + undefinedStates.push(from); + } + this.mapTransition({ name: transition.name, from: from, to: to }); } } + if (undefinedStates.length > 0) { + throw new Error('Undefined states in transitions: "' + undefinedStates.join(', ') + '"'); + } }, transitionFor: function(state, transition) { @@ -640,6 +666,11 @@ StateMachine.defaults = { init: { name: 'init', from: 'none' + }, + dotPrefix: { + graph: { fontcolor: "dimgray", fontname:"Helvetica", splines: "spline"}, + node: {color: "dimgray", fontsize: 13, fontcolor: "dimgray", fontname: "Helvetica"}, + edge: { fontcolor: "dimgray", fontsize: 10, fontname: "Arial"} } } diff --git a/dist/state-machine.min.js b/dist/state-machine.min.js index b9439bc..616c530 100644 --- a/dist/state-machine.min.js +++ b/dist/state-machine.min.js @@ -1 +1 @@ -!function(t,n){"object"==typeof exports&&"object"==typeof module?module.exports=n():"function"==typeof define&&define.amd?define("StateMachine",[],n):"object"==typeof exports?exports.StateMachine=n():t.StateMachine=n()}(this,function(){return function(t){function n(e){if(i[e])return i[e].exports;var s=i[e]={i:e,l:!1,exports:{}};return t[e].call(s.exports,s,s.exports,n),s.l=!0,s.exports}var i={};return n.m=t,n.c=i,n.i=function(t){return t},n.d=function(t,i,e){n.o(t,i)||Object.defineProperty(t,i,{configurable:!1,enumerable:!0,get:e})},n.n=function(t){var i=t&&t.__esModule?function(){return t.default}:function(){return t};return n.d(i,"a",i),i},n.o=function(t,n){return Object.prototype.hasOwnProperty.call(t,n)},n.p="",n(n.s=5)}([function(t,n,i){"use strict";t.exports=function(t,n){var i,e,s;for(i=1;i=0:this.state===t},isPending:function(){return this.pending},can:function(t){return!this.isPending()&&!!this.seek(t)},cannot:function(t){return!this.can(t)},allStates:function(){return this.config.allStates()},allTransitions:function(){return this.config.allTransitions()},transitions:function(){return this.config.transitionsFor(this.state)},seek:function(t,n){var i=this.config.defaults.wildcard,e=this.config.transitionFor(this.state,t),s=e&&e.to;return"function"==typeof s?s.apply(this.context,n):s===i?this.state:s},fire:function(t,n){return this.transit(t,this.state,this.seek(t,n),n)},transit:function(t,n,i,e){var s=this.config.lifecycle,r=this.config.options.observeUnchangedState||n!==i;return i?this.isPending()?this.context.onPendingTransition(t,n,i):(this.config.addState(i),this.beginTransit(),e.unshift({transition:t,from:n,to:i,fsm:this.context}),this.observeEvents([this.observersForEvent(s.onBefore.transition),this.observersForEvent(s.onBefore[t]),r?this.observersForEvent(s.onLeave.state):a,r?this.observersForEvent(s.onLeave[n]):a,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:a,r?this.observersForEvent(s.onEnter.state):a,r?this.observersForEvent(s.onEnter[i]):a,r?this.observersForEvent(s.on[i]):a,this.observersForEvent(s.onAfter.transition),this.observersForEvent(s.onAfter[t]),this.observersForEvent(s.on[t])],e)):this.context.onInvalidTransition(t,n,i)},beginTransit:function(){this.pending=!0},endTransit:function(t){return this.pending=!1,t},failTransit:function(t){throw this.pending=!1,t},doTransit:function(t){this.state=t.to},observe:function(t){if(2===t.length){var n={};n[t[0]]=t[1],this.observers.push(n)}else this.observers.push(t[0])},observersForEvent:function(t){for(var n,i=0,e=this.observers.length,s=[];i0)throw new Error('Undefined states in transitions: "'+f.join(", ")+'"')},transitionFor:function(t,n){var i=this.defaults.wildcard;return this.map[t][n]||this.map[i][n]},transitionsFor:function(t){var n=this.defaults.wildcard;return Object.keys(this.map[t]).concat(Object.keys(this.map[n]))},allStates:function(){return this.states},allTransitions:function(){return this.transitions}}),t.exports=e},function(t,n,i){function e(t,n){this.context=t,this.config=n,this.state=n.init.from,this.observers=[t]}var s=i(0),r=i(6),o=i(1),a=[null,[]];s(e.prototype,{init:function(t){if(s(this.context,this.config.data.apply(this.context,t)),o.hook(this,"init"),this.config.init.active)return this.fire(this.config.init.name,[])},is:function(t){return Array.isArray(t)?t.indexOf(this.state)>=0:this.state===t},isPending:function(){return this.pending},can:function(t){return!this.isPending()&&!!this.seek(t)},cannot:function(t){return!this.can(t)},allStates:function(){return this.config.allStates()},allTransitions:function(){return this.config.allTransitions()},transitions:function(){return this.config.transitionsFor(this.state)},seek:function(t,n){var i=this.config.defaults.wildcard,e=this.config.transitionFor(this.state,t),s=e&&e.to;return"function"==typeof s?s.apply(this.context,n):s===i?this.state:s},fire:function(t,n){return this.transit(t,this.state,this.seek(t,n),n)},transit:function(t,n,i,e){var s=this.config.lifecycle,r=this.config.options.observeUnchangedState||n!==i;return i?this.isPending()?this.context.onPendingTransition(t,n,i):(this.config.addState(i),this.beginTransit(),e.unshift({transition:t,from:n,to:i,fsm:this.context}),this.observeEvents([this.observersForEvent(s.onBefore.transition),this.observersForEvent(s.onBefore[t]),r?this.observersForEvent(s.onLeave.state):a,r?this.observersForEvent(s.onLeave[n]):a,this.observersForEvent(s.on.transition),r?["doTransit",[this]]:a,r?this.observersForEvent(s.onEnter.state):a,r?this.observersForEvent(s.onEnter[i]):a,r?this.observersForEvent(s.on[i]):a,this.observersForEvent(s.onAfter.transition),this.observersForEvent(s.onAfter[t]),this.observersForEvent(s.on[t])],e)):this.context.onInvalidTransition(t,n,i)},beginTransit:function(){this.pending=!0},endTransit:function(t){return this.pending=!1,t},failTransit:function(t){throw this.pending=!1,t},doTransit:function(t){this.state=t.to},observe:function(t){if(2===t.length){var n={};n[t[0]]=t[1],this.observers.push(n)}else this.observers.push(t[0])},observersForEvent:function(t){for(var n,i=0,e=this.observers.length,s=[];i - - - + + ATM - + -ready - -ready + +ready + +ready -pin - -pin + +pin + +pin -ready->pin - - - insert-card + +ready->pin + + + insert-card -action - -action + +action + +action -pin->action - - - confirm + +pin->action + + + confirm -return-card - -return-card + +return-card + +return-card -pin->return-card - - - reject + +pin->return-card + + + reject -deposit-account - -deposit-account + +deposit-account + +deposit-account -action->deposit-account - - - deposit + +action->deposit-account + + + deposit -withdrawal-account - -withdrawal-account + +withdrawal-account + +withdrawal-account -action->withdrawal-account - - - withdraw + +action->withdrawal-account + + + withdraw -return-card->ready - - - withdraw + +return-card->ready + + + withdraw -deposit-amount - -deposit-amount + +deposit-amount + +deposit-amount -deposit-account->deposit-amount - - - provide + +deposit-account->deposit-amount + + + provide -confirm-deposit - -confirm-deposit + +confirm-deposit + +confirm-deposit -deposit-amount->confirm-deposit - - - provide + +deposit-amount->confirm-deposit + + + provide -collect-envelope - -collect-envelope + +collect-envelope + +collect-envelope -confirm-deposit->collect-envelope - - - confirm + +confirm-deposit->collect-envelope + + + confirm -continue - -continue + +continue + +continue -collect-envelope->continue - - - provide + +collect-envelope->continue + + + provide -continue->action - - - continue + +continue->action + + + continue -continue->return-card - - - finish + +continue->return-card + + + finish -withdrawal-amount - -withdrawal-amount + +withdrawal-amount + +withdrawal-amount -withdrawal-account->withdrawal-amount - - - provide + +withdrawal-account->withdrawal-amount + + + provide -confirm-withdrawal - -confirm-withdrawal + +confirm-withdrawal + +confirm-withdrawal -withdrawal-amount->confirm-withdrawal - - - provide + +withdrawal-amount->confirm-withdrawal + + + provide -dispense-cash - -dispense-cash + +dispense-cash + +dispense-cash -confirm-withdrawal->dispense-cash - - - confirm + +confirm-withdrawal->dispense-cash + + + confirm -dispense-cash->continue - - - withdraw + +dispense-cash->continue + + + withdraw diff --git a/examples/demo/demo.js b/examples/demo/demo.js index f2b2b95..5a5b46e 100644 --- a/examples/demo/demo.js +++ b/examples/demo/demo.js @@ -25,7 +25,6 @@ Demo = function() { }; var fsm = new StateMachine({ - transitions: [ { name: 'start', from: 'none', to: 'green' }, { name: 'warn', from: 'green', to: 'yellow' }, diff --git a/examples/horizontal_door.dot b/examples/horizontal_door.dot index 008113f..ef63350 100644 --- a/examples/horizontal_door.dot +++ b/examples/horizontal_door.dot @@ -1,5 +1,8 @@ digraph "door" { rankdir=LR; + graph [ fontcolor="dimgray", fontname="Helvetica", splines="spline" ]; + node [ color="dimgray", fontcolor="dimgray", fontname="Helvetica", fontsize="13" ]; + edge [ fontcolor="dimgray", fontname="Arial", fontsize="10" ]; "closed"; "open"; "closed" -> "open" [ color="blue" ; headport="n" ; label=" open " ; tailport="n" ]; diff --git a/examples/horizontal_door.png b/examples/horizontal_door.png index 65d8ddb..be2235d 100644 Binary files a/examples/horizontal_door.png and b/examples/horizontal_door.png differ diff --git a/examples/horizontal_door.svg b/examples/horizontal_door.svg index 217e038..25b8254 100644 --- a/examples/horizontal_door.svg +++ b/examples/horizontal_door.svg @@ -1,35 +1,39 @@ - - - + + door - + -closed - -closed + +closed + +closed -open - -open + +open + +open -closed:n->open:n - - - open + +closed:n->open:n + + + open -open:s->closed:s - - - close + +open:s->closed:s + + + close diff --git a/examples/matter.dot b/examples/matter.dot index 9a5b12e..adb077c 100644 --- a/examples/matter.dot +++ b/examples/matter.dot @@ -1,5 +1,8 @@ digraph "matter" { rankdir=LR; + graph [ fontcolor="dimgray", fontname="Helvetica", splines="spline" ]; + node [ color="dimgray", fontcolor="dimgray", fontname="Helvetica", fontsize="13" ]; + edge [ fontcolor="dimgray", fontname="Arial", fontsize="10" ]; "solid"; "liquid"; "gas"; diff --git a/examples/matter.js b/examples/matter.js index c3b960f..b1a8892 100644 --- a/examples/matter.js +++ b/examples/matter.js @@ -2,6 +2,11 @@ var StateMachine = require('../src/app'), visualize = require('../src/plugin/visualize'); var Matter = StateMachine.factory({ + statedefs: [ + {name: 'solid' }, + {name: 'liquid'}, + {name: 'gas' } + ], init: 'solid', transitions: [ { name: 'melt', from: 'solid', to: 'liquid', dot: { headport: 'nw' } }, diff --git a/examples/matter.png b/examples/matter.png index cde3b89..b492e6e 100644 Binary files a/examples/matter.png and b/examples/matter.png differ diff --git a/examples/matter.svg b/examples/matter.svg index 9ccd86f..ca5dbd7 100644 --- a/examples/matter.svg +++ b/examples/matter.svg @@ -1,52 +1,59 @@ - - - + + matter - + -solid - -solid + +solid + +solid -liquid - -liquid + +liquid + +liquid -solid->liquid:nw - - - melt + +solid->liquid:nw + + + melt -liquid->solid:se - - - freeze + +liquid->solid:se + + + freeze -gas - -gas + +gas + +gas -liquid->gas:nw - - - vaporize + +liquid->gas:nw + + + vaporize -gas->liquid:se - - - condense + +gas->liquid:se + + + condense diff --git a/examples/traffic_light_statedefs.dot b/examples/traffic_light_statedefs.dot new file mode 100644 index 0000000..7b34274 --- /dev/null +++ b/examples/traffic_light_statedefs.dot @@ -0,0 +1,15 @@ +digraph "Traffic_Light" { + graph [ fontcolor="dimgray", fontname="Helvetica", splines="spline" ]; + node [ color="dimgray", fontcolor="dimgray", fontname="Helvetica", fontsize="13" ]; + edge [ fontcolor="dimgray", fontname="Arial", fontsize="10" ]; + "green" [ fillcolor="green", style="filled" ]; + "yellow" [ fillcolor="yellow", shape="rect", style="filled" ]; + "red" [ fillcolor="red", style="filled" ]; + "none" -> "green" [ color="blue" ; label=" start " ]; + "green" -> "yellow" [ color="blue" ; label=" warn " ]; + "green" -> "red" [ color="red" ; label=" panic " ]; + "yellow" -> "red" [ color="red" ; label=" panic " ]; + "red" -> "yellow" [ color="blue" ; label=" calm " ]; + "red" -> "green" [ color="green" ; label=" clear " ]; + "yellow" -> "green" [ color="green" ; label=" clear " ]; +} \ No newline at end of file diff --git a/examples/traffic_light_statedefs.js b/examples/traffic_light_statedefs.js new file mode 100644 index 0000000..d89fb72 --- /dev/null +++ b/examples/traffic_light_statedefs.js @@ -0,0 +1,25 @@ +var StateMachine = require('../src/app'), + visualize = require('../src/plugin/visualize'); + +var fsm = new StateMachine({ + statedefs: [ + {name: 'green', dot: {style: "filled", fillcolor: "green"}}, + {name: 'yellow', dot: {shape: "rect", style: "filled", fillcolor: "yellow"}}, + {name: 'red', dot: {style: "filled", fillcolor: "red"}} + ], + transitions: [ + { name: 'start', from: 'none', to: 'green', dot: {color:'blue'}}, + { name: 'warn', from: 'green', to: 'yellow', dot: {color:'blue'}}, + { name: 'panic', from: 'green', to: 'red', dot: {color:'red'} }, + { name: 'panic', from: 'yellow', to: 'red', dot: {color:'red'} }, + { name: 'calm', from: 'red', to: 'yellow', dot: {color:'blue'} }, + { name: 'clear', from: 'red', to: 'green', dot: {color:'green'} }, + { name: 'clear', from: 'yellow', to: 'green', dot: {color:'green'} }, + ] +}); + +fsm.visualize = function() { + return visualize(fsm, { name: 'Traffic_Light' }); +} + +module.exports = fsm; diff --git a/examples/traffic_light_statedefs.png b/examples/traffic_light_statedefs.png new file mode 100644 index 0000000..c8c181b Binary files /dev/null and b/examples/traffic_light_statedefs.png differ diff --git a/examples/traffic_light_statedefs.svg b/examples/traffic_light_statedefs.svg new file mode 100644 index 0000000..1de51d2 --- /dev/null +++ b/examples/traffic_light_statedefs.svg @@ -0,0 +1,86 @@ + + + + + + +Traffic_Light + + + +green + +green + + + +yellow + +yellow + + + +green->yellow + + + warn + + + +red + +red + + + +green->red + + + panic + + + +yellow->green + + + clear + + + +yellow->red + + + panic + + + +red->green + + + clear + + + +red->yellow + + + calm + + + +none + +none + + + +none->green + + + start + + + diff --git a/examples/vertical_door.dot b/examples/vertical_door.dot index 822dad8..15fb9c5 100644 --- a/examples/vertical_door.dot +++ b/examples/vertical_door.dot @@ -1,4 +1,7 @@ digraph "fsm" { + graph [ fontcolor="dimgray", fontname="Helvetica", splines="spline" ]; + node [ color="dimgray", fontcolor="dimgray", fontname="Helvetica", fontsize="13" ]; + edge [ fontcolor="dimgray", fontname="Arial", fontsize="10" ]; "closed"; "open"; "closed" -> "open" [ label=" open " ]; diff --git a/examples/vertical_door.png b/examples/vertical_door.png index c29023d..278dd31 100644 Binary files a/examples/vertical_door.png and b/examples/vertical_door.png differ diff --git a/examples/vertical_door.svg b/examples/vertical_door.svg index 12dc09b..91ebf30 100644 --- a/examples/vertical_door.svg +++ b/examples/vertical_door.svg @@ -1,35 +1,39 @@ - - - + + fsm - + -closed - -closed + +closed + +closed -open - -open + +open + +open -closed->open - - - open + +closed->open + + + open -open->closed - - - close + +open->closed + + + close diff --git a/examples/wizard.dot b/examples/wizard.dot index c7e6f25..d80e63a 100644 --- a/examples/wizard.dot +++ b/examples/wizard.dot @@ -1,5 +1,8 @@ digraph "wizard" { rankdir=LR; + graph [ fontcolor="dimgray", fontname="Helvetica", splines="spline" ]; + node [ color="dimgray", fontcolor="dimgray", fontname="Helvetica", fontsize="13" ]; + edge [ fontcolor="dimgray", fontname="Arial", fontsize="10" ]; "A"; "B"; "C"; diff --git a/examples/wizard.png b/examples/wizard.png index 74945ca..6eb51db 100644 Binary files a/examples/wizard.png and b/examples/wizard.png differ diff --git a/examples/wizard.svg b/examples/wizard.svg index 46edc96..865d6ae 100644 --- a/examples/wizard.svg +++ b/examples/wizard.svg @@ -1,69 +1,79 @@ - - - + + wizard - + -A - -A + +A + +A -B - -B + +B + +B -A:ne->B:w - - - step + +A:ne->B:w + + + step -B:s->A:se - - - reset + +B:s->A:se + + + reset -C - -C + +C + +C -B:e->C:w - - - step + +B:e->C:w + + + step -C:s->A:se - - - reset + +C:s->A:se + + + reset -D - -D + +D + +D -C:e->D:w - - - step + +C:e->D:w + + + step -D:s->A:se - - - reset + +D:s->A:se + + + reset diff --git a/lib/state-machine.js b/lib/state-machine.js index b8b9e37..d6843f8 100644 --- a/lib/state-machine.js +++ b/lib/state-machine.js @@ -145,216 +145,14 @@ module.exports = { /***/ }), /* 2 */ -/***/ (function(module, exports, __webpack_require__) { - -"use strict"; - - -//------------------------------------------------------------------------------------------------- - -function camelize(label) { - - if (label.length === 0) - return label; - - var n, result, word, words = label.split(/[_-]/); - - // single word with first character already lowercase, return untouched - if ((words.length === 1) && (words[0][0].toLowerCase() === words[0][0])) - return label; - - result = words[0].toLowerCase(); - for(n = 1 ; n < words.length ; n++) { - result = result + words[n].charAt(0).toUpperCase() + words[n].substring(1).toLowerCase(); - } - - return result; -} - -//------------------------------------------------------------------------------------------------- - -camelize.prepended = function(prepend, label) { - label = camelize(label); - return prepend + label[0].toUpperCase() + label.substring(1); -} - -//------------------------------------------------------------------------------------------------- - -module.exports = camelize; +/***/ (function(module, exports) { +throw new Error("Module parse failed: /Users/Shared/dev/web/javascript-state-machine/src/config.js Unexpected token (171:72)\nYou may need an appropriate loader to handle this file type.\n| if (errStr.length > 0)\n| errStr += '\\n';\n| errStr += 'Transition name same as state: \"' + sameAsTrans.join(, ) + '\"';\n| }\n| throw new Error(errStr);"); /***/ }), /* 3 */ /***/ (function(module, exports, __webpack_require__) { -"use strict"; - - -//------------------------------------------------------------------------------------------------- - -var mixin = __webpack_require__(0), - camelize = __webpack_require__(2); - -//------------------------------------------------------------------------------------------------- - -function Config(options, StateMachine) { - - options = options || {}; - - this.options = options; // preserving original options can be useful (e.g visualize plugin) - this.defaults = StateMachine.defaults; - this.states = []; - this.transitions = []; - this.map = {}; - this.lifecycle = this.configureLifecycle(); - this.init = this.configureInitTransition(options.init); - this.data = this.configureData(options.data); - this.methods = this.configureMethods(options.methods); - - this.map[this.defaults.wildcard] = {}; - - this.configureTransitions(options.transitions || []); - - this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin); - -} - -//------------------------------------------------------------------------------------------------- - -mixin(Config.prototype, { - - addState: function(name) { - if (!this.map[name]) { - this.states.push(name); - this.addStateLifecycleNames(name); - this.map[name] = {}; - } - }, - - addStateLifecycleNames: function(name) { - this.lifecycle.onEnter[name] = camelize.prepended('onEnter', name); - this.lifecycle.onLeave[name] = camelize.prepended('onLeave', name); - this.lifecycle.on[name] = camelize.prepended('on', name); - }, - - addTransition: function(name) { - if (this.transitions.indexOf(name) < 0) { - this.transitions.push(name); - this.addTransitionLifecycleNames(name); - } - }, - - addTransitionLifecycleNames: function(name) { - this.lifecycle.onBefore[name] = camelize.prepended('onBefore', name); - this.lifecycle.onAfter[name] = camelize.prepended('onAfter', name); - this.lifecycle.on[name] = camelize.prepended('on', name); - }, - - mapTransition: function(transition) { - var name = transition.name, - from = transition.from, - to = transition.to; - this.addState(from); - if (typeof to !== 'function') - this.addState(to); - this.addTransition(name); - this.map[from][name] = transition; - return transition; - }, - - configureLifecycle: function() { - return { - onBefore: { transition: 'onBeforeTransition' }, - onAfter: { transition: 'onAfterTransition' }, - onEnter: { state: 'onEnterState' }, - onLeave: { state: 'onLeaveState' }, - on: { transition: 'onTransition' } - }; - }, - - configureInitTransition: function(init) { - if (typeof init === 'string') { - return this.mapTransition(mixin({}, this.defaults.init, { to: init, active: true })); - } - else if (typeof init === 'object') { - return this.mapTransition(mixin({}, this.defaults.init, init, { active: true })); - } - else { - this.addState(this.defaults.init.from); - return this.defaults.init; - } - }, - - configureData: function(data) { - if (typeof data === 'function') - return data; - else if (typeof data === 'object') - return function() { return data; } - else - return function() { return {}; } - }, - - configureMethods: function(methods) { - return methods || {}; - }, - - configurePlugins: function(plugins, builtin) { - plugins = plugins || []; - var n, max, plugin; - for(n = 0, max = plugins.length ; n < max ; n++) { - plugin = plugins[n]; - if (typeof plugin === 'function') - plugins[n] = plugin = plugin() - if (plugin.configure) - plugin.configure(this); - } - return plugins - }, - - configureTransitions: function(transitions) { - var i, n, transition, from, to, wildcard = this.defaults.wildcard; - for(n = 0 ; n < transitions.length ; n++) { - transition = transitions[n]; - from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] - to = transition.to || wildcard; - for(i = 0 ; i < from.length ; i++) { - this.mapTransition({ name: transition.name, from: from[i], to: to }); - } - } - }, - - transitionFor: function(state, transition) { - var wildcard = this.defaults.wildcard; - return this.map[state][transition] || - this.map[wildcard][transition]; - }, - - transitionsFor: function(state) { - var wildcard = this.defaults.wildcard; - return Object.keys(this.map[state]).concat(Object.keys(this.map[wildcard])); - }, - - allStates: function() { - return this.states; - }, - - allTransitions: function() { - return this.transitions; - } - -}); - -//------------------------------------------------------------------------------------------------- - -module.exports = Config; - -//------------------------------------------------------------------------------------------------- - - -/***/ }), -/* 4 */ -/***/ (function(module, exports, __webpack_require__) { - var mixin = __webpack_require__(0), Exception = __webpack_require__(6), @@ -539,6 +337,46 @@ module.exports = JSM; //------------------------------------------------------------------------------------------------- +/***/ }), +/* 4 */ +/***/ (function(module, exports, __webpack_require__) { + +"use strict"; + + +//------------------------------------------------------------------------------------------------- + +function camelize(label) { + + if (label.length === 0) + return label; + + var n, result, word, words = label.split(/[_-]/); + + // single word with first character already lowercase, return untouched + if ((words.length === 1) && (words[0][0].toLowerCase() === words[0][0])) + return label; + + result = words[0].toLowerCase(); + for(n = 1 ; n < words.length ; n++) { + result = result + words[n].charAt(0).toUpperCase() + words[n].substring(1).toLowerCase(); + } + + return result; +} + +//------------------------------------------------------------------------------------------------- + +camelize.prepended = function(prepend, label) { + label = camelize(label); + return prepend + label[0].toUpperCase() + label.substring(1); +} + +//------------------------------------------------------------------------------------------------- + +module.exports = camelize; + + /***/ }), /* 5 */ /***/ (function(module, exports, __webpack_require__) { @@ -549,10 +387,10 @@ module.exports = JSM; //----------------------------------------------------------------------------------------------- var mixin = __webpack_require__(0), - camelize = __webpack_require__(2), + camelize = __webpack_require__(4), plugin = __webpack_require__(1), - Config = __webpack_require__(3), - JSM = __webpack_require__(4); + Config = __webpack_require__(2), + JSM = __webpack_require__(3); //----------------------------------------------------------------------------------------------- @@ -640,6 +478,11 @@ StateMachine.defaults = { init: { name: 'init', from: 'none' + }, + dotPrefix: { + graph: { fontcolor: "dimgray", fontname:"Helvetica", splines: "spline"}, + node: {color: "dimgray", fontsize: 13, fontcolor: "dimgray", fontname: "Helvetica"}, + edge: { fontcolor: "dimgray", fontsize: 10, fontname: "Arial"} } } diff --git a/lib/visualize.js b/lib/visualize.js index 9c18e13..1458878 100644 --- a/lib/visualize.js +++ b/lib/visualize.js @@ -123,7 +123,9 @@ function dotcfg(fsm, options) { name = options.name, rankdir = dotcfg.rankdir(options.orientation), states = dotcfg.states(config, options), + statedefs = dotcfg.statedefs(config, options), transitions = dotcfg.transitions(config, options), + dotPrefix = config.dotPrefix, result = { } if (name) @@ -135,9 +137,15 @@ function dotcfg(fsm, options) { if (states && states.length > 0) result.states = states + if (statedefs && statedefs.length > 0) + result.statedefs = statedefs; + if (transitions && transitions.length > 0) result.transitions = transitions + if (dotPrefix) + result.dotPrefix = dotPrefix; + return result } @@ -164,6 +172,11 @@ dotcfg.states = function(config, options) { return states; } +dotcfg.statedefs = function(config, options) { + return config.options['statedefs']; + // can be null +} + dotcfg.transitions = function(config, options) { var n, max, transition, init = config.init, @@ -198,7 +211,6 @@ dotcfg.transition = function(name, from, to, dot, config, options, output) { else { output.push(mixin({}, { from: from, to: to, label: pad(name) }, dot || {})) } - } //------------------------------------------------------------------------------------------------- @@ -218,6 +230,8 @@ function dotify(dotcfg) { var name = dotcfg.name || 'fsm', states = dotcfg.states || [], transitions = dotcfg.transitions || [], + statedefs = dotcfg.statedefs, + dotPrefix = dotcfg.dotPrefix, rankdir = dotcfg.rankdir, output = [], n, max; @@ -225,8 +239,20 @@ function dotify(dotcfg) { output.push("digraph " + quote(name) + " {") if (rankdir) output.push(" rankdir=" + rankdir + ";") - for(n = 0, max = states.length ; n < max ; n++) - output.push(dotify.state(states[n])) + + if (dotPrefix) { + output.push(dotify.dotPrefix(dotPrefix)); + } + + if (statedefs) { + for(n = 0, max = statedefs.length ; n < max ; n++) + output.push(dotify.statedef(statedefs[n])) + } + else { + for(n = 0, max = states.length ; n < max ; n++) + output.push(dotify.gen(states[n])) + } + for(n = 0, max = transitions.length ; n < max ; n++) output.push(dotify.edge(transitions[n])) output.push("}") @@ -234,14 +260,34 @@ function dotify(dotcfg) { } -dotify.state = function(state) { +dotify.gen = function(state) { return " " + quote(state) + ";" } +dotify.statedef = function(statedef) { + var retstr = " " + quote(statedef.name); + if (statedef.dot) { + retstr += dotify.gen.attr(statedef.dot); + } + return retstr + ";"; +} + dotify.edge = function(edge) { return " " + quote(edge.from) + " -> " + quote(edge.to) + dotify.edge.attr(edge) + ";" } +dotify.dotPrefix = function(dotPrefix) { + var prefixStrAry = []; + var ix, key; + var keys = Object.keys(dotPrefix); + for (ix = 0; ix < keys.length; ix++) { + key = keys[ix]; + prefixStrAry.push(' ' + key + ' ' + dotify.gen.attr(dotPrefix[key]) + ";" ); + } + + return prefixStrAry.join("\n"); +} + dotify.edge.attr = function(edge) { var n, max, key, keys = Object.keys(edge).sort(), output = []; for(n = 0, max = keys.length ; n < max ; n++) { @@ -252,6 +298,15 @@ dotify.edge.attr = function(edge) { return output.length > 0 ? " [ " + output.join(" ; ") + " ]" : "" } +dotify.gen.attr = function(attrs) { + var n, max, key, keys = Object.keys(attrs).sort(), output = []; + for(n = 0, max = keys.length ; n < max ; n++) { + key = keys[n]; + output.push(key + "=" + quote(attrs[key])) + } + return output.length > 0 ? " [ " + output.join(", ") + " ]" : "" +} + //------------------------------------------------------------------------------------------------- visualize.dotcfg = dotcfg; diff --git a/src/app.js b/src/app.js index 786e7f7..f8ae1b0 100644 --- a/src/app.js +++ b/src/app.js @@ -94,6 +94,11 @@ StateMachine.defaults = { init: { name: 'init', from: 'none' + }, + dotPrefix: { + graph: { fontcolor: "dimgray", fontname:"Helvetica", splines: "spline"}, + node: {color: "dimgray", fontsize: 13, fontcolor: "dimgray", fontname: "Helvetica"}, + edge: { fontcolor: "dimgray", fontsize: 10, fontname: "Arial"} } } diff --git a/src/config.js b/src/config.js index 21601df..dd23e67 100644 --- a/src/config.js +++ b/src/config.js @@ -20,9 +20,26 @@ function Config(options, StateMachine) { this.init = this.configureInitTransition(options.init); this.data = this.configureData(options.data); this.methods = this.configureMethods(options.methods); + this.hasStateDefs = false; + if (Object.keys(options).indexOf('dotPrefix') >=0 ) { + this.dotPrefix = options['dotPrefix']; + if (this.dotPrefix !== null && + typeof this.dotPrefix === 'object' && + Object.keys(this.dotPrefix).length === 0) { + this.dotPrefix = null; + } + } + else { + this.dotPrefix = this.defaults.dotPrefix; + } this.map[this.defaults.wildcard] = {}; + if (options['statedefs']) { + this.hasStateDefs = true; + this.configureStateDefs(options['statedefs']); + } + this.configureTransitions(options.transitions || []); this.plugins = this.configurePlugins(options.plugins, StateMachine.plugin); @@ -121,15 +138,49 @@ mixin(Config.prototype, { return plugins }, + configureStateDefs: function(statedefs) { + var n; + for (n = 0; n < statedefs.length; n++) { + this.addState(statedefs[n].name); + } + }, + configureTransitions: function(transitions) { - var i, n, transition, from, to, wildcard = this.defaults.wildcard; + var i, n, transition, fromStates, from, to, wildcard = this.defaults.wildcard; + var undefinedStates = [], sameAsTrans = []; for(n = 0 ; n < transitions.length ; n++) { transition = transitions[n]; - from = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] + fromStates = Array.isArray(transition.from) ? transition.from : [transition.from || wildcard] to = transition.to || wildcard; - for(i = 0 ; i < from.length ; i++) { - this.mapTransition({ name: transition.name, from: from[i], to: to }); + if (this.hasStateDefs) { + if (this.states.indexOf(transition.name) >= 0) { + sameAsTrans.push(transition.name); + } + if (to !== wildcard && this.states.indexOf(to) === -1) { + undefinedStates.push(to); + } + } + + for(i = 0 ; i < fromStates.length ; i++) { + from = fromStates[i]; + if (this.hasStateDefs && from !== wildcard && from !== 'none' && + this.states.indexOf(from) === -1) { + undefinedStates.push(from); + } + this.mapTransition({ name: transition.name, from: from, to: to }); + } + } + if (undefinedStates.length > 0 || sameAsTrans.length > 0) { + var errStr = ''; + if (undefinedStates.length > 0) { + errStr += 'Undefined states in transitions: "' + undefinedStates.join(', ') + '"'; + } + if (sameAsTrans.length > 0) { + if (errStr.length > 0) + errStr += '\n'; + errStr += 'Transition name same as state: "' + sameAsTrans.join(', ') + '"'; } + throw new Error(errStr); } }, diff --git a/src/plugin/visualize.js b/src/plugin/visualize.js index 0bee32b..d6974cf 100644 --- a/src/plugin/visualize.js +++ b/src/plugin/visualize.js @@ -20,7 +20,9 @@ function dotcfg(fsm, options) { name = options.name, rankdir = dotcfg.rankdir(options.orientation), states = dotcfg.states(config, options), + statedefs = dotcfg.statedefs(config, options), transitions = dotcfg.transitions(config, options), + dotPrefix = config.dotPrefix, result = { } if (name) @@ -32,9 +34,15 @@ function dotcfg(fsm, options) { if (states && states.length > 0) result.states = states + if (statedefs && statedefs.length > 0) + result.statedefs = statedefs; + if (transitions && transitions.length > 0) result.transitions = transitions + if (dotPrefix) + result.dotPrefix = dotPrefix; + return result } @@ -61,6 +69,11 @@ dotcfg.states = function(config, options) { return states; } +dotcfg.statedefs = function(config, options) { + return config.options['statedefs']; + // can be null +} + dotcfg.transitions = function(config, options) { var n, max, transition, init = config.init, @@ -95,7 +108,6 @@ dotcfg.transition = function(name, from, to, dot, config, options, output) { else { output.push(mixin({}, { from: from, to: to, label: pad(name) }, dot || {})) } - } //------------------------------------------------------------------------------------------------- @@ -115,6 +127,8 @@ function dotify(dotcfg) { var name = dotcfg.name || 'fsm', states = dotcfg.states || [], transitions = dotcfg.transitions || [], + statedefs = dotcfg.statedefs, + dotPrefix = dotcfg.dotPrefix, rankdir = dotcfg.rankdir, output = [], n, max; @@ -122,8 +136,20 @@ function dotify(dotcfg) { output.push("digraph " + quote(name) + " {") if (rankdir) output.push(" rankdir=" + rankdir + ";") - for(n = 0, max = states.length ; n < max ; n++) - output.push(dotify.state(states[n])) + + if (dotPrefix) { + output.push(dotify.dotPrefix(dotPrefix)); + } + + if (statedefs) { + for(n = 0, max = statedefs.length ; n < max ; n++) + output.push(dotify.statedef(statedefs[n])) + } + else { + for(n = 0, max = states.length ; n < max ; n++) + output.push(dotify.gen(states[n])) + } + for(n = 0, max = transitions.length ; n < max ; n++) output.push(dotify.edge(transitions[n])) output.push("}") @@ -131,14 +157,34 @@ function dotify(dotcfg) { } -dotify.state = function(state) { +dotify.gen = function(state) { return " " + quote(state) + ";" } +dotify.statedef = function(statedef) { + var retstr = " " + quote(statedef.name); + if (statedef.dot) { + retstr += dotify.gen.attr(statedef.dot); + } + return retstr + ";"; +} + dotify.edge = function(edge) { return " " + quote(edge.from) + " -> " + quote(edge.to) + dotify.edge.attr(edge) + ";" } +dotify.dotPrefix = function(dotPrefix) { + var prefixStrAry = []; + var ix, key; + var keys = Object.keys(dotPrefix); + for (ix = 0; ix < keys.length; ix++) { + key = keys[ix]; + prefixStrAry.push(' ' + key + ' ' + dotify.gen.attr(dotPrefix[key]) + ";" ); + } + + return prefixStrAry.join("\n"); +} + dotify.edge.attr = function(edge) { var n, max, key, keys = Object.keys(edge).sort(), output = []; for(n = 0, max = keys.length ; n < max ; n++) { @@ -149,6 +195,15 @@ dotify.edge.attr = function(edge) { return output.length > 0 ? " [ " + output.join(" ; ") + " ]" : "" } +dotify.gen.attr = function(attrs) { + var n, max, key, keys = Object.keys(attrs).sort(), output = []; + for(n = 0, max = keys.length ; n < max ; n++) { + key = keys[n]; + output.push(key + "=" + quote(attrs[key])) + } + return output.length > 0 ? " [ " + output.join(", ") + " ]" : "" +} + //------------------------------------------------------------------------------------------------- visualize.dotcfg = dotcfg; diff --git a/test/helpers/dotprefix.js b/test/helpers/dotprefix.js new file mode 100644 index 0000000..22cd6a6 --- /dev/null +++ b/test/helpers/dotprefix.js @@ -0,0 +1,25 @@ + +/* + | dot prefix string will be appended to all dot output + */ +const PFXSTR = '\ + graph [ fontcolor="dimgray", fontname="Helvetica", splines="spline" ];\n\ + node [ color="dimgray", fontcolor="dimgray", fontname="Helvetica", fontsize="13" ];\n\ + edge [ fontcolor="dimgray", fontname="Arial", fontsize="10" ];\ +'; + +/* +| dot default pfx object will always be appended to dot output object +*/ +const PFXOBJ = { dotPrefix: { +graph: { fontcolor: 'dimgray', + fontname: 'Helvetica', + splines: 'spline' }, +node: { color: 'dimgray', + fontsize: 13, + fontcolor: 'dimgray', + fontname: 'Helvetica' }, +edge: { fontcolor: 'dimgray', fontsize: 10, fontname: 'Arial' } +}}; + +module.exports = { PFXSTR, PFXOBJ } \ No newline at end of file diff --git a/test/plugin/visualize.js b/test/plugin/visualize.js index a2b9388..3ba3906 100644 --- a/test/plugin/visualize.js +++ b/test/plugin/visualize.js @@ -1,10 +1,13 @@ import test from 'ava' import StateMachine from '../../src/app' import visualize from '../../src/plugin/visualize' +import {PFXSTR,PFXOBJ} from '../helpers/dotprefix' var dotcfg = visualize.dotcfg, // converts FSM to DOT CONFIG dotify = visualize.dotify; // converts DOT CONFIG to DOT OUTPUT + + //------------------------------------------------------------------------------------------------- test('visualize state machine', t => { @@ -20,6 +23,7 @@ test('visualize state machine', t => { }) t.is(visualize(fsm), `digraph "fsm" { +${PFXSTR} "solid"; "liquid"; "gas"; @@ -45,6 +49,7 @@ test('visualize state machine factory', t => { }) t.is(visualize(FSM), `digraph "fsm" { +${PFXSTR} "solid"; "liquid"; "gas"; @@ -71,6 +76,7 @@ test('visualize with custom .dot markup', t => { t.is(visualize(fsm, { name: 'matter', orientation: 'horizontal' }), `digraph "matter" { rankdir=LR; +${PFXSTR} "solid"; "liquid"; "gas"; @@ -97,7 +103,7 @@ test('dotcfg simple state machine', t => { ] }) - t.deepEqual(dotcfg(fsm), { + t.deepEqual(dotcfg(fsm), Object.assign({ states: [ 'solid', 'liquid', 'gas' ], transitions: [ { from: 'solid', to: 'liquid', label: ' melt ' }, @@ -105,7 +111,7 @@ test('dotcfg simple state machine', t => { { from: 'liquid', to: 'gas', label: ' vaporize ' }, { from: 'gas', to: 'liquid', label: ' condense ' } ] - }) + }, PFXOBJ)) }) @@ -117,17 +123,14 @@ test('dotcfg for state machine - optionally include :init transition', t => { init: { name: 'boot', from: 'booting', to: 'ready', dot: { color: 'red' } } }) - t.deepEqual(dotcfg(fsm, { init: false }), { - states: [ 'ready' ] - }) + t.deepEqual(dotcfg(fsm, { init: false }), Object.assign({ + states: [ 'ready' ]}, PFXOBJ)); - t.deepEqual(dotcfg(fsm, { init: true }), { + t.deepEqual(dotcfg(fsm, { init: true }), Object.assign({ states: [ 'booting', 'ready' ], transitions: [ { from: 'booting', to: 'ready', label: ' boot ', color: 'red' } - ] - }) - + ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -143,15 +146,13 @@ test('dotcfg for fsm with multiple transitions with same :name', t => { ] }) - t.deepEqual(dotcfg(fsm), { + t.deepEqual(dotcfg(fsm), Object.assign({ states: [ 'A', 'B', 'C', 'D' ], transitions: [ { from: 'A', to: 'B', label: ' step ' }, { from: 'B', to: 'C', label: ' step ' }, { from: 'C', to: 'D', label: ' step ' } - ] - }) - + ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -168,7 +169,7 @@ test('dotcfg for fsm transition with multiple :from', t => { ] }) - t.deepEqual(dotcfg(fsm), { + t.deepEqual(dotcfg(fsm), Object.assign({ states: [ 'A', 'B', 'C', 'D' ], transitions: [ { from: 'A', to: 'B', label: ' step ' }, @@ -176,15 +177,12 @@ test('dotcfg for fsm transition with multiple :from', t => { { from: 'C', to: 'D', label: ' step ' }, { from: 'A', to: 'A', label: ' reset ' }, { from: 'B', to: 'A', label: ' reset ' } - ] - }) - + ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- test('dotcfg for fsm with wildcard/missing :from', t => { - var fsm = new StateMachine({ init: 'A', transitions: [ @@ -196,7 +194,7 @@ test('dotcfg for fsm with wildcard/missing :from', t => { ] }) - t.deepEqual(dotcfg(fsm), { + t.deepEqual(dotcfg(fsm), Object.assign({ states: [ 'A', 'B', 'C', 'D', 'X' ], transitions: [ { from: 'A', to: 'B', label: ' step ' }, @@ -214,9 +212,7 @@ test('dotcfg for fsm with wildcard/missing :from', t => { { from: 'C', to: 'X', label: ' finish ' }, { from: 'D', to: 'X', label: ' finish ' }, { from: 'X', to: 'X', label: ' finish ' } - ] - }) - + ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -236,7 +232,7 @@ test('dotcfg for fsm with wildcard/missing :to', t => { ] }) - t.deepEqual(dotcfg(fsm), { + t.deepEqual(dotcfg(fsm), Object.assign({ states: [ 'A', 'B', 'C', 'D' ], transitions: [ { from: 'A', to: 'B', label: ' step ' }, @@ -250,9 +246,7 @@ test('dotcfg for fsm with wildcard/missing :to', t => { { from: 'B', to: 'B', label: ' noop ' }, { from: 'C', to: 'C', label: ' noop ' }, { from: 'D', to: 'D', label: ' noop ' } - ] - }) - + ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -272,10 +266,8 @@ test('dotcfg for fsm - conditional transition is not displayed', t => { } }); - t.deepEqual(dotcfg(fsm), { - states: [ 'A' ] - }) - + t.deepEqual(dotcfg(fsm), Object.assign({ + states: [ 'A' ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -290,14 +282,12 @@ test('dotcfg with custom transition .dot edge markup', t => { ] }) - t.deepEqual(dotcfg(fsm), { + t.deepEqual(dotcfg(fsm), Object.assign({ states: [ 'A', 'B', 'C' ], transitions: [ { from: 'A', to: 'B', label: 'A2B', color: "red", headport: "nw", tailport: "ne" }, { from: 'B', to: 'C', label: 'B2C', color: "green", headport: "sw", tailport: "se" } - ] - }) - + ]}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -306,10 +296,8 @@ test('dotcfg with custom name', t => { var fsm = new StateMachine(); - t.deepEqual(dotcfg(fsm, { name: 'bob' }), { - name: 'bob', - }) - + t.deepEqual(dotcfg(fsm, { name: 'bob' }), Object.assign({ + name: 'bob'}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -318,14 +306,11 @@ test('dotcfg with custom orientation', t => { var fsm = new StateMachine(); - t.deepEqual(dotcfg(fsm, { orientation: 'horizontal' }), { - rankdir: 'LR', - }) - - t.deepEqual(dotcfg(fsm, { orientation: 'vertical' }), { - rankdir: 'TB', - }) + t.deepEqual(dotcfg(fsm, { orientation: 'horizontal' }), Object.assign({ + rankdir: 'LR'}, PFXOBJ)); + t.deepEqual(dotcfg(fsm, { orientation: 'vertical' }), Object.assign({ + rankdir: 'TB'}, PFXOBJ)); }) //------------------------------------------------------------------------------------------------- @@ -334,8 +319,7 @@ test('dotcfg for empty state machine', t => { var fsm = new StateMachine(); - t.deepEqual(dotcfg(fsm), {}) - + t.deepEqual(dotcfg(fsm), PFXOBJ); }) //================================================================================================= @@ -344,9 +328,9 @@ test('dotcfg for empty state machine', t => { test('dotify empty', t => { var expected = `digraph "fsm" { -}` - t.is(dotify(), expected) - t.is(dotify({}), expected) +}`; + t.is(dotify(), expected); + t.is(dotify({}), expected); }) //------------------------------------------------------------------------------------------------- diff --git a/test/singletest.js b/test/singletest.js new file mode 100644 index 0000000..917acea --- /dev/null +++ b/test/singletest.js @@ -0,0 +1,19 @@ +// Ava doesn't appear to be able to run a single test (that I've found), +// which makes it pretty hard to debug a single test +// +// Use this to run a single test as follows: +// +// node_modules/ava/cli.js test/singletest.js +// +// Copy and paste the test code after the 'test body' section +// +// 10-Nov-2017 -- rickb +// +import test from 'ava' +import StateMachine from '../src/app' +import visualize from '../src/plugin/visualize' +import {PFXSTR,PFXOBJ} from './helpers/dotprefix' + +/** test body - paste test function in here */ + +test('dummy', t => {}); \ No newline at end of file diff --git a/test/statedefs.js b/test/statedefs.js new file mode 100644 index 0000000..5ed1325 --- /dev/null +++ b/test/statedefs.js @@ -0,0 +1,158 @@ +import test from 'ava'; +import StateMachine from '../src/app'; +import visualize from '../src/plugin/visualize'; +import {PFXSTR,PFXOBJ} from './helpers/dotprefix'; + +test('states defined', t => { + var fsm = new StateMachine({ + init: 'green', + statedefs: [ + { name: 'green' }, + { name: 'red' }, + { name: 'yellow' }, + { name: 'green' } + ], + transitions: [ + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ] + }); + + t.is(fsm.state, 'green'); + + fsm.warn(); t.is(fsm.state, 'yellow'); + fsm.panic(); t.is(fsm.state, 'red'); + fsm.calm(); t.is(fsm.state, 'yellow'); + fsm.clear(); t.is(fsm.state, 'green'); + // all works as expected + +}); + +test('states defined with dot', t => { + var fsm = new StateMachine({ + init: 'green', + statedefs: [ + { name: 'green', dot: { fillcolor: "green" } }, + { name: 'red' , dot: { fillcolor: "red" } }, + { name: 'yellow', dot: { fillcolor: "yellow"} }, + { name: 'green', dot: { fillcolor: "green", shape: "rect" } } + ], + transitions: [ + { name: 'warn', from: 'green', to: 'yellow' }, + { name: 'panic', from: 'yellow', to: 'red' }, + { name: 'calm', from: 'red', to: 'yellow' }, + { name: 'clear', from: 'yellow', to: 'green' } + ] + }); + + t.is(visualize(fsm),`digraph "fsm" { +${PFXSTR} + "green" [ fillcolor="green" ]; + "red" [ fillcolor="red" ]; + "yellow" [ fillcolor="yellow" ]; + "green" [ fillcolor="green", shape="rect" ]; + "green" -> "yellow" [ label=" warn " ]; + "yellow" -> "red" [ label=" panic " ]; + "red" -> "yellow" [ label=" calm " ]; + "yellow" -> "green" [ label=" clear " ]; +}`); +}); + +test('clear dotPrefix (empty)', t => { + var fsm = new StateMachine({ + dotPrefix: {}, + statedefs: [ + {name: "roger"}, + {name: "wilco"} + ], + transitions: [ + { name: 'affirm', from: 'roger', to: 'wilco' }, + ] + }); + t.is(visualize(fsm),`digraph "fsm" { + "roger"; + "wilco"; + "roger" -> "wilco" [ label=" affirm " ]; +}`); +}); + +test('clear dotPrefix (null)', t => { + var fsm = new StateMachine({ + dotPrefix: null, + statedefs: [ + {name: "roger"}, + {name: "wilco"} + ], + transitions: [ + { name: 'affirm', from: 'roger', to: 'wilco' }, + ] + }); + t.is(visualize(fsm),`digraph "fsm" { + "roger"; + "wilco"; + "roger" -> "wilco" [ label=" affirm " ]; +}`); +}); + + +test('redefine dotPrefix', t => { + var fsm = new StateMachine({ + dotPrefix: { + node: { fillColor: 'red' } + }, + statedefs: [ + {name: "roger"}, + {name: "wilco"} + ], + transitions: [ + { name: 'affirm', from: 'roger', to: 'wilco' }, + ] + }); + t.is(visualize(fsm),`digraph "fsm" { + node [ fillColor="red" ]; + "roger"; + "wilco"; + "roger" -> "wilco" [ label=" affirm " ]; +}`); +}); + +test('transition uses undefined state', t => { + var error = t.throws(function() { new StateMachine({ + statedefs: [ + {name: "roger"}, + {name: "wilco"} + ], + transitions: [ + { name: 'response', from: 'roger', to: 'negative' }, + { name: 'ack', from: 'positive', to: 'wilco' } + ] + })}); + t.is(error.message, 'Undefined states in transitions: "negative, positive"'); +}); + +test('transition name contained in states', t => { + var error = t.throws(function() { new StateMachine({ + statedefs: [ + {name: "roger"}, + {name: "wilco"} + ], + transitions: [ + { name: 'roger', from: 'roger', to: 'wilco' }, + ] + })}); + t.is(error.message, 'Transition name same as state: "roger"'); +}); + +test('transition name in states AND undefined state in transition', t => { + var error = t.throws(function() { new StateMachine({ + statedefs: [ + {name: "roger"}, + ], + transitions: [ + { name: 'roger', from: 'roger', to: 'negative' }, + ] + })}); + t.is(error.message, 'Undefined states in transitions: "negative"\nTransition name same as state: "roger"'); +}); \ No newline at end of file