Any good tools for building finite state machine diagrams from a declarative description?

Question

I’m looking for a way to turn a declarative textual description of a finite state machine into a diagram, and I ask myself: are there any tools or methods that could help in this task?

Although my code is in Javascript, it is not a runtime tool, and any reasonable technology would be acceptable.

The business rules for the application subsystem that I support are supported by rather complicated ones, we considered it necessary to create an infrastructure for state machines that subscribed to topics published on our global event bus. They gave developers a much simpler system for work and more confidence that we are doing the right thing. But it is difficult to share the results of this with our business people.

If we had simple diagrams, that would be much clearer. But this is a constantly evolving code base. I don’t even want to contemplate creating these diagrams manually. Are there tools that could create them from a reasonable text description? I am ready to take on the task of converting my declarative textual configuration to any textual format, if the creation of the image can be performed using the tool. I don't care about precise control over the layout or anything like that. If he used some kind of power directed algorithm to place things, I would be more than happy.

The Wikipedia article contains a pretty good description of such machines and uses a simple subway-style turnstile. A simple example.

Turnstile diagram

My code uses state descriptors like

{
    id: 'Locked',
    data: {canOpen: false},
    transitions: {
        'turnstile/coin/inserted': 'Unlocked',
        'turnstile/handle/pushed': 'Locked'
    }
}

data : data , data , . , -, id transitions .

, : ?


. . , , , .


, , . . , .

, , , , .

Jasmine, , ​​ :

describe('turnstile', function() {
    var states = [
        {
            id: 'Locked',
            data: {canOpen: false},
            transitions: {
                'turnstile/coin/inserted': 'Unlocked',
                'turnstile/handle/pushed': 'Locked'
            }
        },
        {
            id: 'Unlocked',
            data: {canOpen: true},
            transitions: {
                'turnstile/coin/inserted': 'Unlocked',
                'turnstile/handle/pushed': 'Locked'
            }
        }
    ];

    // 2nd param is id of start state
    var ts = new StateMachine(states, "Locked");

    beforeEach(function() {
        ts.setState('Locked');
    });
    it('starts in the "Locked" state', function() {
        expect(ts.currentState.id).toBe('Locked');
    });
    it('when locked, remains locked on "handle/pushed"', function() {
        EventBus.publish('turnstile/handle/pushed');
        expect(ts.currentState.id).toBe('Locked');
    });
    it('when locked, moves to unlocked on "coin/inserted"', function() {
        EventBus.publish('turnstile/coin/inserted');
        expect(ts.currentState.id).toBe('Unlocked');
    });
    it('when unlocked, remains unlocked on "coin/inserted"', function() {
        EventBus.publish('turnstile/coin/inserted');
        expect(ts.currentState.id).toBe('Unlocked');
        EventBus.publish('turnstile/coin/inserted');
        expect(ts.currentState.id).toBe('Unlocked');
    });
    it('when unlocked, moves to locked on "handle/pushed"', function() {
        EventBus.publish('turnstile/coin/inserted');
        expect(ts.currentState.id).toBe('Unlocked');
        EventBus.publish('turnstile/handle/pushed');
        expect(ts.currentState.id).toBe('Locked');
    });
});

, . , . :

    var coinCount, personCount;

, :

    var onChange = function(newState, oldState, event) {
        if (event === 'turnstile/coin/inserted') {
           coinCount++
        }
        if (oldState.canOpen && event === 'turnstile/handle/pushed') {
            personCount++
        }
    };

:

    beforeEach(function() {
        ts.setState('Locked');
        coinCount = 0;
        personCount = 0;
    });

    it('updates the coin count on any "coin/inserted", regardless of state', function() {
        expect(coinCount).toBe(0);
        EventBus.publish('turnstile/coin/inserted');
        expect(coinCount).toBe(1);
        EventBus.publish('turnstile/coin/inserted');
        expect(coinCount).toBe(2);
        EventBus.publish('turnstile/handle/pushed');
        expect(coinCount).toBe(2);
        EventBus.publish('turnstile/handle/pushed');
        expect(coinCount).toBe(2);
        EventBus.publish('turnstile/coin/inserted');
        expect(coinCount).toBe(3);
    });
    it('updates the person count on "handle/pushed" only when unlocked', function() {
        expect(personCount).toBe(0);
        EventBus.publish('turnstile/handle/pushed');
        expect(personCount).toBe(0);
        EventBus.publish('turnstile/coin/inserted');
        expect(personCount).toBe(0);
        EventBus.publish('turnstile/handle/pushed');
        expect(personCount).toBe(1);
        EventBus.publish('turnstile/coin/inserted');
        expect(personCount).toBe(1);
        EventBus.publish('turnstile/handle/pushed');
        expect(personCount).toBe(2);
    });
+4
4

JavaScript. , GoJS: http://gojs.net/latest/samples/stateChart.html.

: // The nodes are all of the states. var nodeDataArray = [ { id: 'Locked', data: { canOpen: false }, transitions: { 'turnstile/coin/inserted': 'Unlocked', 'turnstile/handle/pushed': 'Locked' } }, { id: 'Unlocked', data: { canOpen: true }, transitions: { 'turnstile/coin/inserted': 'Unlocked', 'turnstile/handle/pushed': 'Locked' } } ]; :

simple node state diagram

, http://gojs.net/temp/stateChartGeneration.html. , , . , . , , . , , , GoJS: http://gojs.net.

, , , . a/b/c.

FSM, ​​. . : http://gojs.net/latest/samples/logicCircuit.html.

, HTML:

function init() {
var $ = go.GraphObject.make;  // for conciseness in defining templates

myDiagram =
  $(go.Diagram, "myDiagram",  // must name or refer to the DIV HTML element
    {
      // start everything in the middle of the viewport
      initialContentAlignment: go.Spot.Center,
      layout: $(go.ForceDirectedLayout),
      // have mouse wheel events zoom in and out instead of scroll up and down
      "toolManager.mouseWheelBehavior": go.ToolManager.WheelZoom,
      // support double-click in background creating a new node
      "clickCreatingTool.archetypeNodeData": { text: "new node" },
      // enable undo & redo
      "undoManager.isEnabled": true
    });

// when the document is modified, add a "*" to the title and enable the "Save" button
myDiagram.addDiagramListener("Modified", function(e) {
  var button = document.getElementById("SaveButton");
  if (button) button.disabled = !myDiagram.isModified;
  var idx = document.title.indexOf("*");
  if (myDiagram.isModified) {
    if (idx < 0) document.title += "*";
  } else {
    if (idx >= 0) document.title = document.title.substr(0, idx);
  }
});

// define the Node template
myDiagram.nodeTemplate =
  $(go.Node, "Auto",
    new go.Binding("location", "loc", go.Point.parse).makeTwoWay(go.Point.stringify),
    // define the node outer shape, which will surround the TextBlock
    $(go.Shape, "Circle",
      {
        parameter1: 20,  // the corner has a large radius
        fill: $(go.Brush, "Linear", { 0: "rgb(254, 201, 0)", 1: "rgb(254, 162, 0)" }),
        stroke: "black",
        portId: "",
        fromLinkable: true,
        fromLinkableSelfNode: true,
        fromLinkableDuplicates: true,
        toLinkable: true,
        toLinkableSelfNode: true,
        toLinkableDuplicates: true,
        cursor: "pointer"
      }),
    $(go.TextBlock,
      {
        font: "bold 11pt helvetica, bold arial, sans-serif",
        editable: true  // editing the text automatically updates the model data
      },
      new go.Binding("text", "id").makeTwoWay())
  );

// unlike the normal selection Adornment, this one includes a Button
myDiagram.nodeTemplate.selectionAdornmentTemplate =
  $(go.Adornment, "Spot",
    $(go.Panel, "Auto",
      $(go.Shape, { fill: null, stroke: "blue", strokeWidth: 2 }),
      $(go.Placeholder)  // this represents the selected Node
    ),
    // the button to create a "next" node, at the top-right corner
    $("Button",
      {
        alignment: go.Spot.TopRight,
        click: addNodeAndLink  // this function is defined below
      },
      $(go.Shape, "PlusLine", { desiredSize: new go.Size(6, 6) })
    ) // end button
  ); // end Adornment

// clicking the button inserts a new node to the right of the selected node,
// and adds a link to that new node
function addNodeAndLink(e, obj) {
  var adorn = obj.part;
  e.handled = true;
  var diagram = adorn.diagram;
  diagram.startTransaction("Add State");

  // get the node data for which the user clicked the button
  var fromNode = adorn.adornedPart;
  var fromData = fromNode.data;
  // create a new "State" data object, positioned off to the right of the adorned Node
  var toData = { text: "new" };
  var p = fromNode.location.copy();
  p.x += 200;
  toData.loc = go.Point.stringify(p);  // the "loc" property is a string, not a Point object
  // add the new node data to the model
  var model = diagram.model;
  model.addNodeData(toData);

  // create a link data from the old node data to the new node data
  var linkdata = {
    from: model.getKeyForNodeData(fromData),  // or just: fromData.id
    to: model.getKeyForNodeData(toData),
    text: "transition"
  };
  // and add the link data to the model
  model.addLinkData(linkdata);

  // select the new Node
  var newnode = diagram.findNodeForData(toData);
  diagram.select(newnode);

  diagram.commitTransaction("Add State");

  // if the new node is off-screen, scroll the diagram to show the new node
  diagram.scrollToRect(newnode.actualBounds);
}

// replace the default Link template in the linkTemplateMap
myDiagram.linkTemplate =
  $(go.Link,  // the whole link panel
    {
      curve: go.Link.Bezier, curviness: 40, adjusting: go.Link.Stretch,
      reshapable: true, relinkableFrom: true, relinkableTo: true
    },
    new go.Binding("points").makeTwoWay(),
    new go.Binding("curviness", "curviness"),
    $(go.Shape,  // the link shape
      { strokeWidth: 1.5 }),
    $(go.Shape,  // the arrowhead
      { toArrow: "standard", stroke: null }),
    $(go.Panel, "Auto",
      $(go.Shape,  // the link shape
        {
          fill: $(go.Brush, "Radial",
                  { 0: "rgb(240, 240, 240)", 0.3: "rgb(240, 240, 240)", 1: "rgba(240, 240, 240, 0)" }),
          stroke: null
        }),
      $(go.TextBlock, "transition",  // the label
        {
          textAlign: "center",
          font: "10pt helvetica, arial, sans-serif",
          stroke: "black",
          margin: 4,
          editable: true  // editing the text automatically updates the model data
        },
        new go.Binding("text", "text").makeTwoWay())
    )
  );

// The nodes are all of the states.
var nodeDataArray = [
  {
    id: 'Locked',
    data: { canOpen: false },
    transitions: {
      'turnstile/coin/inserted': 'Unlocked',
      'turnstile/handle/pushed': 'Locked'
    }
  },
  {
    id: 'Unlocked',
    data: { canOpen: true },
    transitions: {
      'turnstile/coin/inserted': 'Unlocked',
      'turnstile/handle/pushed': 'Locked'
    }
  }
];

// The links are all of the transitions, which are held in the "transitions" object
// of the "from" node/state.
var linkDataArray = [];
nodeDataArray.forEach(function(d) {
  if (d.transitions) {
    for (var t in d.transitions) {
      linkDataArray.push({
        from: d.id,
        to: d.transitions[t],
        text: t
      })
    }
  }
})

// The model uses "id", not "key", as the property holding the identifier.
myDiagram.model = $(go.GraphLinksModel,
  {
    nodeKeyProperty: "id",
    nodeDataArray: nodeDataArray,
    linkDataArray: linkDataArray
  });
}
+2

, .

andrei noam, , , DOT. , - noam, , , , .

, :

digraph finite_state_machine {
  rankdir=LR;
  node [shape = doublecircle]; Locked Unlocked;
  node [shape = circle];
  secret_node [style=bold, shape=point];
  secret_node -> Locked;
  Locked [ label = "Locked" ]
  Unlocked [ label = "Unlocked" ]
  Locked -> Unlocked [ label = "turnstile/coin/inserted" ];
  Locked -> Locked [ label = "turnstile/handle/pushed" ];
  Unlocked -> Unlocked [ label = "turnstile/coin/inserted" ];
  Unlocked -> Locked [ label = "turnstile/handle/pushed" ];
}

:

http://sds.sauyet.com/images/turnstile.svg

, , , .

, , , , , . . , - , Ramda Vis.js, Javascript Graphviz:

var Viz = require('viz.js');
var fs = require('fs');
var R = require('ramda');
var machine = require('./machine'); // TODO: better import of data

var states = machine.states; // :: [State]
var start = machine.start; // :: State

var label = R.prop('id'); // :: State -> String
var names = R.pluck('id', states); // :; [String]

// :: [Transition]
var transitions = R.unnest(R.map(state =>
    R.map(pair => ({
        from: state.id,
        to: pair[1],
        label: pair[0]
    }), R.toPairs(state.transitions))
, states));

// :: String
var results = [
    'digraph finite_state_machine {',
    '  rankdir=LR;',
    '  node [shape = doublecircle]; ' + R.join(' ', names) + ';',
    '  node [shape = circle];', // if there were non-terminal states
    '  secret_node [style=bold, shape=point];',
    '  secret_node -> ' + start + ';'
].concat(R.map(state => 
    '  ' + state.id + ' [ label = "' + label(state) + '" ]', states
)).concat(R.map(trans => '  ' + trans.from + ' -> ' + trans.to + 
         ' [ label = "' + trans.label + '" ];', transitions
)).concat(
    '}'
).join('\n');

console.log('Building diagram from following .dot format:' + '\n\n');
console.log(results);
console.log('\n\n');

fs.writeFile('diagram.svg', Viz(results), function(err) {
    if (err) {
        return console.log(err);
    }
    console.log('File written successfully');
});

label, , .

Walter Northwoods . . , . , , script, , , , , . , . .

, , .


machine.js:

module.exports = {
    start: 'Locked',
    states: [
      {
        id: 'Locked',
        data: { canOpen: false },
        transitions: {
          'turnstile/coin/inserted': 'Unlocked',
          'turnstile/handle/pushed': 'Locked'
        }
      },
      {
        id: 'Unlocked',
        data: { canOpen: true },
        transitions: {
          'turnstile/coin/inserted': 'Unlocked',
          'turnstile/handle/pushed': 'Locked'
        }
      }
    ]
}
+1

D3.JS

* Although this is probably the most difficult solution that will be proposed here because of the huge learning curve that you will need to build in order to crack something in order to use it, a wonderful, but already too complicated advantage chart, sub> but it worth it !!!

0
source

Source: https://habr.com/ru/post/1617682/


All Articles