Skip to content

Commit

Permalink
Add labels for exit conditions to edges between nodes
Browse files Browse the repository at this point in the history
  • Loading branch information
PeterJCLaw committed Apr 3, 2022
1 parent f717068 commit 277c214
Show file tree
Hide file tree
Showing 5 changed files with 58 additions and 19 deletions.
33 changes: 33 additions & 0 deletions routemaster/config/model.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Loading and validation of config files."""

import datetime
import collections
from typing import (
TYPE_CHECKING,
Any,
Expand Down Expand Up @@ -103,6 +104,10 @@ def all_destinations(self) -> Collection[str]:
"""Returns the constant next state."""
return [self.state]

def destinations_for_render(self) -> Mapping[str, str]:
"""Returns the constant next state."""
return {self.state: ""}


class ContextNextStatesOption(NamedTuple):
"""Represents an option for a context conditional next state."""
Expand All @@ -128,6 +133,30 @@ def all_destinations(self) -> Collection[str]:
"""Returns all possible destination states."""
return [x.state for x in self.destinations] + [self.default]

def destinations_for_render(self) -> Mapping[str, str]:
"""
Returns destination states and a summary of how each might be reached.
This is intended for use in visualisations, so while the description of
how to reach each state is somewhat Pythonic its focus is being
human-readable.
"""
destination_reasons = [
(x.state, f"{self.path} == {x.value}")
for x in self.destinations
] + [
(self.default, "default"),
]

collected = collections.defaultdict(list)
for destination, raeson in destination_reasons:
collected[destination].append(raeson)

return {
destination: " or ".join(reasons)
for destination, reasons in collected.items()
}


class NoNextStates(NamedTuple):
"""Represents the lack of a next state to progress to."""
Expand All @@ -142,6 +171,10 @@ def all_destinations(self) -> Collection[str]:
"""Returns no states."""
return []

def destinations_for_render(self) -> Mapping[str, str]:
"""Returns no states."""
return {}


NextStates = Union[ConstantNextState, ContextNextStates, NoNextStates]

Expand Down
7 changes: 7 additions & 0 deletions routemaster/config/tests/test_next_states.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,14 @@
def test_constant_next_state():
next_states = ConstantNextState(state='foo')
assert next_states.all_destinations() == ['foo']
assert next_states.destinations_for_render() == {'foo': ""}
assert next_states.next_state_for_label(None) == 'foo'


def test_no_next_states_must_not_be_called():
next_states = NoNextStates()
assert next_states.all_destinations() == []
assert next_states.destinations_for_render() == {}
with pytest.raises(RuntimeError):
next_states.next_state_for_label(None)

Expand All @@ -34,6 +36,11 @@ def test_context_next_states(make_context):
context = make_context(label='label1', metadata={'foo': True})

assert next_states.all_destinations() == ['1', '2', '3']
assert next_states.destinations_for_render() == {
'1': "metadata.foo == True",
'2': "metadata.foo == False",
'3': "default",
}
assert next_states.next_state_for_label(context) == '1'


Expand Down
7 changes: 6 additions & 1 deletion routemaster/config/visualisation.jinja
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,13 @@
'target-arrow-shape': 'triangle',
'target-arrow-color': '#ccc',
'line-color': '#ccc',
'text-wrap': 'wrap',
'text-max-width': 80,
'width': 1
})
.style({
'label': 'data(label)',
})
.selector(':selected')
.css({
'background-color': 'black',
Expand Down Expand Up @@ -157,7 +162,7 @@
cy.layout(layoutOptions).run();
}
var initialSpacingFactor = 0.4 + cy.nodes().length * 0.04;
var initialSpacingFactor = 0.5 + cy.nodes().length * 0.05;
var spacingFactorElem = document.getElementById('spacing-factor');
spacingFactorElem.addEventListener('change', function() {
changeSpacing(spacingFactorElem.value / 10);
Expand Down
25 changes: 9 additions & 16 deletions routemaster/state_machine/tests/test_visualisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,54 +12,47 @@
'data': {
'source': 'start',
'target': 'perform_action',
'label': 'feeds.tests.should_do_alternate_action == False or default',
},
},
{
'data': {
'source': 'start',
'target': 'perform_alternate_action',
'label': 'feeds.tests.should_do_alternate_action == True',
},
},
# We emit duplicate edges when the destination is duplicated; this seems to
# be fine though.
{
'data': {
'source': 'start',
'target': 'perform_action',
'id': 'perform_action',
},
},
{
'data': {'id': 'perform_action'},
'classes': 'action',
},
{
'data': {
'source': 'perform_action',
'target': 'end',
'label': '',
},
},
{
'data': {'id': 'perform_alternate_action'},
'data': {
'id': 'perform_alternate_action',
},
'classes': 'action',
},
{
'data': {
'source': 'perform_alternate_action',
'target': 'end',
'label': 'feeds.tests.should_loop == False or default',
},
},
# We emit duplicate edges when the destination is duplicated; this seems to
# be fine though.
{
'data': {
'source': 'perform_alternate_action',
'target': 'start',
},
},
{
'data': {
'source': 'perform_alternate_action',
'target': 'end',
'label': 'feeds.tests.should_loop == True',
},
},
{
Expand Down
5 changes: 3 additions & 2 deletions routemaster/state_machine/visualisation.py
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,12 @@ def nodes_for_cytoscape(
'classes': node_kind,
})

all_destinations = state.next_states.all_destinations()
for destination_name in all_destinations:
destinations = state.next_states.destinations_for_render()
for destination_name, reason in destinations.items():
elements.append({'data': {
'source': state.name,
'target': destination_name,
'label': reason,
}})

return elements

0 comments on commit 277c214

Please sign in to comment.