diff --git a/graph/encoding/dot/decode.go b/graph/encoding/dot/decode.go index b00097d0..4361c8f0 100644 --- a/graph/encoding/dot/decode.go +++ b/graph/encoding/dot/decode.go @@ -26,6 +26,18 @@ type DOTIDSetter interface { 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. func Unmarshal(data []byte, dst encoding.Builder) error { 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. func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) { fs := gen.addVertex(dst, stmt.From) @@ -177,6 +213,8 @@ func (gen *generator) addEdgeStmt(dst encoding.Builder, stmt *ast.EdgeStmt) { for _, t := range ts { edge := dst.NewEdge(f, t) dst.SetEdge(edge) + applyPortsToEdge(stmt.From, stmt.To, edge) + e, ok := edge.(encoding.AttributeSetter) if !ok { continue @@ -223,6 +261,7 @@ func (gen *generator) addEdge(dst encoding.Builder, to *ast.Edge) []graph.Node { for _, t := range ts { edge := dst.NewEdge(f, t) dst.SetEdge(edge) + applyPortsToEdge(to.Vertex, to.To, edge) } } } diff --git a/graph/encoding/dot/decode_test.go b/graph/encoding/dot/decode_test.go index fad5047a..13f4002a 100644 --- a/graph/encoding/dot/decode_test.go +++ b/graph/encoding/dot/decode_test.go @@ -34,6 +34,14 @@ func TestRoundTrip(t *testing.T) { want: undirectedID, directed: false, }, + { + want: directedWithPorts, + directed: true, + }, + { + want: undirectedWithPorts, + directed: false, + }, } for i, g := range golden { var dst encoding.Builder @@ -120,6 +128,42 @@ const undirectedID = `graph H { 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 // round-trip encoding and decoding of DOT graphs with nodes and edges // 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 // decoding of edge DOT label attributes. type dotEdge struct { graph.Edge // Edge label. - Label string + Label string + FromPortLabels dotPortLabels + ToPortLabels dotPortLabels } // 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. type attributes []encoding.Attribute diff --git a/graph/formats/dot/ast/ast.go b/graph/formats/dot/ast/ast.go index 58bcd81b..4ed00d70 100644 --- a/graph/formats/dot/ast/ast.go +++ b/graph/formats/dot/ast/ast.go @@ -351,7 +351,7 @@ func (p *Port) String() string { if len(p.ID) > 0 { fmt.Fprintf(buf, ":%s", p.ID) } - if p.CompassPoint != CompassPointDefault { + if p.CompassPoint != CompassPointNone { fmt.Fprintf(buf, ":%s", p.CompassPoint) } return buf.String() @@ -362,7 +362,7 @@ type CompassPoint uint // Compass points. const ( - CompassPointDefault CompassPoint = iota // _ + CompassPointNone CompassPoint = iota // CompassPointNorth // n CompassPointNorthEast // ne CompassPointEast // e @@ -372,13 +372,14 @@ const ( CompassPointWest // w CompassPointNorthWest // nw CompassPointCenter // c + CompassPointDefault // _ ) // String returns the string representation of the compass point. func (c CompassPoint) String() string { switch c { - case CompassPointDefault: - return "_" + case CompassPointNone: + return "" case CompassPointNorth: return "n" case CompassPointNorthEast: @@ -397,6 +398,8 @@ func (c CompassPoint) String() string { return "nw" case CompassPointCenter: return "c" + case CompassPointDefault: + return "_" } panic(fmt.Sprintf("invalid compass point (%d)", uint(c))) } diff --git a/graph/formats/dot/ast/ast_test.go b/graph/formats/dot/ast/ast_test.go index 655e934b..246c24ad 100644 --- a/graph/formats/dot/ast/ast_test.go +++ b/graph/formats/dot/ast/ast_test.go @@ -55,10 +55,7 @@ func TestParseFile(t *testing.T) { out: "../internal/testdata/attr_sep.golden", }, {in: "../internal/testdata/subgraph_vertex.dot"}, - { - in: "../internal/testdata/port.dot", - out: "../internal/testdata/port.golden", - }, + {in: "../internal/testdata/port.dot"}, } for _, g := range golden { file, err := dot.ParseFile(g.in) diff --git a/graph/formats/dot/internal/astx/astx.go b/graph/formats/dot/internal/astx/astx.go index 1e3a13a3..4e370670 100644 --- a/graph/formats/dot/internal/astx/astx.go +++ b/graph/formats/dot/internal/astx/astx.go @@ -299,7 +299,7 @@ func getCompassPoint(s string) (ast.CompassPoint, bool) { case "c": return ast.CompassPointCenter, true } - return ast.CompassPointDefault, false + return ast.CompassPointNone, false } // === [ Identifiers ] ========================================================= diff --git a/graph/formats/dot/internal/astx/astx_test.go b/graph/formats/dot/internal/astx/astx_test.go index 69e30205..7e9df2c4 100644 --- a/graph/formats/dot/internal/astx/astx_test.go +++ b/graph/formats/dot/internal/astx/astx_test.go @@ -54,10 +54,7 @@ func TestParseFile(t *testing.T) { out: "../testdata/attr_sep.golden", }, {in: "../testdata/subgraph_vertex.dot"}, - { - in: "../testdata/port.dot", - out: "../testdata/port.golden", - }, + {in: "../testdata/port.dot"}, {in: "../testdata/quoted_id.dot"}, { in: "../testdata/backslash_newline_id.dot", diff --git a/graph/formats/dot/internal/parser/parser_test.go b/graph/formats/dot/internal/parser/parser_test.go index 5edd4006..d743c5eb 100644 --- a/graph/formats/dot/internal/parser/parser_test.go +++ b/graph/formats/dot/internal/parser/parser_test.go @@ -54,10 +54,7 @@ func TestParseFile(t *testing.T) { out: "../testdata/attr_sep.golden", }, {in: "../testdata/subgraph_vertex.dot"}, - { - in: "../testdata/port.dot", - out: "../testdata/port.golden", - }, + {in: "../testdata/port.dot"}, {in: "../testdata/quoted_id.dot"}, { in: "../testdata/backslash_newline_id.dot", diff --git a/graph/formats/dot/internal/testdata/port.golden b/graph/formats/dot/internal/testdata/port.golden deleted file mode 100644 index 18ca1f30..00000000 --- a/graph/formats/dot/internal/testdata/port.golden +++ /dev/null @@ -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 -}