encoding/dot: support edge ports on Unmarshal/decode

This commit is contained in:
J. Holmes
2018-04-15 23:01:32 -06:00
committed by Dan Kortschak
parent 46ea2fd92c
commit cb2511e4c8
8 changed files with 121 additions and 29 deletions

View File

@@ -26,6 +26,18 @@ type DOTIDSetter interface {
SetDOTID(id string) SetDOTID(id string)
} }
// PortSetter is implemented by graph.Edge and graph.Line that can set
// the DOT port and compass directions of an edge.
type PortSetter interface {
// SetFromPort sets the From port and
// compass direction of the receiver.
SetFromPort(port, compass string) error
// SetToPort sets the To port and compass
// direction of the receiver.
SetToPort(port, compass string) error
}
// Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst. // Unmarshal parses the Graphviz DOT-encoded data and stores the result in dst.
func Unmarshal(data []byte, dst encoding.Builder) error { func Unmarshal(data []byte, dst encoding.Builder) error {
file, err := dot.ParseBytes(data) file, err := dot.ParseBytes(data)
@@ -169,6 +181,30 @@ func (gen *generator) addStmt(dst encoding.Builder, stmt ast.Stmt) {
} }
} }
// applyPortsToEdge applies the available port metadata from an ast.Edge
// to a graph.Edge
func applyPortsToEdge(from ast.Vertex, to *ast.Edge, edge graph.Edge) {
if ps, isPortSetter := edge.(PortSetter); isPortSetter {
if n, vertexIsNode := from.(*ast.Node); vertexIsNode {
if n.Port != nil {
err := ps.SetFromPort(n.Port.ID, n.Port.CompassPoint.String())
if err != nil {
panic(fmt.Errorf("unable to unmarshal edge port (:%s:%s)", n.Port.ID, n.Port.CompassPoint.String()))
}
}
}
if n, vertexIsNode := to.Vertex.(*ast.Node); vertexIsNode {
if n.Port != nil {
err := ps.SetToPort(n.Port.ID, n.Port.CompassPoint.String())
if err != nil {
panic(fmt.Errorf("unable to unmarshal edge DOT port (:%s:%s)", n.Port.ID, n.Port.CompassPoint.String()))
}
}
}
}
}
// addEdgeStmt adds the given edge statement to the graph. // addEdgeStmt adds the given edge statement to the graph.
func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) { func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
fs := gen.addVertex(dst, stmt.From) fs := gen.addVertex(dst, stmt.From)
@@ -177,6 +213,8 @@ func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) {
for _, t := range ts { for _, t := range ts {
edge := dst.NewEdge(f, t) edge := dst.NewEdge(f, t)
dst.SetEdge(edge) dst.SetEdge(edge)
applyPortsToEdge(stmt.From, stmt.To, edge)
e, ok := edge.(encoding.AttributeSetter) e, ok := edge.(encoding.AttributeSetter)
if !ok { if !ok {
continue continue
@@ -223,6 +261,7 @@ func (gen *generator) addEdge(dst encoding.Builder, to *ast.Edge) []graph.Node {
for _, t := range ts { for _, t := range ts {
edge := dst.NewEdge(f, t) edge := dst.NewEdge(f, t)
dst.SetEdge(edge) dst.SetEdge(edge)
applyPortsToEdge(to.Vertex, to.To, edge)
} }
} }
} }

View File

@@ -34,6 +34,14 @@ func TestRoundTrip(t *testing.T) {
want: undirectedID, want: undirectedID,
directed: false, directed: false,
}, },
{
want: directedWithPorts,
directed: true,
},
{
want: undirectedWithPorts,
directed: false,
},
} }
for i, g := range golden { for i, g := range golden {
var dst encoding.Builder var dst encoding.Builder
@@ -120,6 +128,42 @@ const undirectedID = `graph H {
A -- B; A -- B;
}` }`
const directedWithPorts = `digraph {
// Node definitions.
A;
B;
C;
D;
E;
F;
// Edge definitions.
A:foo -> B:bar;
A -> C:bar;
B:foo -> C;
D:foo:n -> E:bar:s;
D:e -> F:bar:w;
E:_ -> F:c;
}`
const undirectedWithPorts = `graph {
// Node definitions.
A;
B;
C;
D;
E;
F;
// Edge definitions.
A:foo -- B:bar;
A -- C:bar;
B:foo -- C;
D:foo:n -- E:bar:s;
D:e -- F:bar:w;
E:_ -- F:c;
}`
// Below follows a minimal implementation of a graph capable of validating the // Below follows a minimal implementation of a graph capable of validating the
// round-trip encoding and decoding of DOT graphs with nodes and edges // round-trip encoding and decoding of DOT graphs with nodes and edges
// containing DOT attributes. // containing DOT attributes.
@@ -257,12 +301,18 @@ func (n *dotNode) Attributes() []encoding.Attribute {
}} }}
} }
type dotPortLabels struct {
Port, Compass string
}
// dotEdge extends simple.Edge with a label field to test round-trip encoding and // dotEdge extends simple.Edge with a label field to test round-trip encoding and
// decoding of edge DOT label attributes. // decoding of edge DOT label attributes.
type dotEdge struct { type dotEdge struct {
graph.Edge graph.Edge
// Edge label. // Edge label.
Label string Label string
FromPortLabels dotPortLabels
ToPortLabels dotPortLabels
} }
// SetAttribute sets a DOT attribute. // SetAttribute sets a DOT attribute.
@@ -285,6 +335,26 @@ func (e *dotEdge) Attributes() []encoding.Attribute {
}} }}
} }
func (e *dotEdge) SetFromPort(port, compass string) error {
e.FromPortLabels.Port = port
e.FromPortLabels.Compass = compass
return nil
}
func (e *dotEdge) SetToPort(port, compass string) error {
e.ToPortLabels.Port = port
e.ToPortLabels.Compass = compass
return nil
}
func (e *dotEdge) FromPort() (port, compass string) {
return e.FromPortLabels.Port, e.FromPortLabels.Compass
}
func (e *dotEdge) ToPort() (port, compass string) {
return e.ToPortLabels.Port, e.ToPortLabels.Compass
}
// attributes is a helper for global attributes. // attributes is a helper for global attributes.
type attributes []encoding.Attribute type attributes []encoding.Attribute

View File

@@ -351,7 +351,7 @@ func (p *Port) String() string {
if len(p.ID) > 0 { if len(p.ID) > 0 {
fmt.Fprintf(buf, ":%s", p.ID) fmt.Fprintf(buf, ":%s", p.ID)
} }
if p.CompassPoint != CompassPointDefault { if p.CompassPoint != CompassPointNone {
fmt.Fprintf(buf, ":%s", p.CompassPoint) fmt.Fprintf(buf, ":%s", p.CompassPoint)
} }
return buf.String() return buf.String()
@@ -362,7 +362,7 @@ type CompassPoint uint
// Compass points. // Compass points.
const ( const (
CompassPointDefault CompassPoint = iota // _ CompassPointNone CompassPoint = iota //
CompassPointNorth // n CompassPointNorth // n
CompassPointNorthEast // ne CompassPointNorthEast // ne
CompassPointEast // e CompassPointEast // e
@@ -372,13 +372,14 @@ const (
CompassPointWest // w CompassPointWest // w
CompassPointNorthWest // nw CompassPointNorthWest // nw
CompassPointCenter // c CompassPointCenter // c
CompassPointDefault // _
) )
// String returns the string representation of the compass point. // String returns the string representation of the compass point.
func (c CompassPoint) String() string { func (c CompassPoint) String() string {
switch c { switch c {
case CompassPointDefault: case CompassPointNone:
return "_" return ""
case CompassPointNorth: case CompassPointNorth:
return "n" return "n"
case CompassPointNorthEast: case CompassPointNorthEast:
@@ -397,6 +398,8 @@ func (c CompassPoint) String() string {
return "nw" return "nw"
case CompassPointCenter: case CompassPointCenter:
return "c" return "c"
case CompassPointDefault:
return "_"
} }
panic(fmt.Sprintf("invalid compass point (%d)", uint(c))) panic(fmt.Sprintf("invalid compass point (%d)", uint(c)))
} }

View File

@@ -55,10 +55,7 @@ func TestParseFile(t *testing.T) {
out: "../internal/testdata/attr_sep.golden", out: "../internal/testdata/attr_sep.golden",
}, },
{in: "../internal/testdata/subgraph_vertex.dot"}, {in: "../internal/testdata/subgraph_vertex.dot"},
{ {in: "../internal/testdata/port.dot"},
in: "../internal/testdata/port.dot",
out: "../internal/testdata/port.golden",
},
} }
for _, g := range golden { for _, g := range golden {
file, err := dot.ParseFile(g.in) file, err := dot.ParseFile(g.in)

View File

@@ -299,7 +299,7 @@ func getCompassPoint(s string) (ast.CompassPoint, bool) {
case "c": case "c":
return ast.CompassPointCenter, true return ast.CompassPointCenter, true
} }
return ast.CompassPointDefault, false return ast.CompassPointNone, false
} }
// === [ Identifiers ] ========================================================= // === [ Identifiers ] =========================================================

View File

@@ -54,10 +54,7 @@ func TestParseFile(t *testing.T) {
out: "../testdata/attr_sep.golden", out: "../testdata/attr_sep.golden",
}, },
{in: "../testdata/subgraph_vertex.dot"}, {in: "../testdata/subgraph_vertex.dot"},
{ {in: "../testdata/port.dot"},
in: "../testdata/port.dot",
out: "../testdata/port.golden",
},
{in: "../testdata/quoted_id.dot"}, {in: "../testdata/quoted_id.dot"},
{ {
in: "../testdata/backslash_newline_id.dot", in: "../testdata/backslash_newline_id.dot",

View File

@@ -54,10 +54,7 @@ func TestParseFile(t *testing.T) {
out: "../testdata/attr_sep.golden", out: "../testdata/attr_sep.golden",
}, },
{in: "../testdata/subgraph_vertex.dot"}, {in: "../testdata/subgraph_vertex.dot"},
{ {in: "../testdata/port.dot"},
in: "../testdata/port.dot",
out: "../testdata/port.golden",
},
{in: "../testdata/quoted_id.dot"}, {in: "../testdata/quoted_id.dot"},
{ {
in: "../testdata/backslash_newline_id.dot", in: "../testdata/backslash_newline_id.dot",

View File

@@ -1,11 +0,0 @@
digraph {
A:ne -> B:sw
C:foo -> D:bar:se
E -> F
G:n
H:e
I:s
J:w
K:nw
L:c
}