From 97f387b38f8e1baed300c7cc3d4b2dd9938820da Mon Sep 17 00:00:00 2001 From: Dan Kortschak Date: Thu, 27 May 2021 07:18:04 +0930 Subject: [PATCH] graph/encoding: provide attribute handling out of the box --- graph/encoding/dot/decode_test.go | 32 ++++---- graph/encoding/dot/encode_test.go | 8 +- graph/encoding/encoding.go | 33 ++++++++ graph/encoding/encoding_test.go | 121 ++++++++++++++++++++++++++++++ 4 files changed, 175 insertions(+), 19 deletions(-) create mode 100644 graph/encoding/encoding_test.go diff --git a/graph/encoding/dot/decode_test.go b/graph/encoding/dot/decode_test.go index 9d1e880f..fe3ee165 100644 --- a/graph/encoding/dot/decode_test.go +++ b/graph/encoding/dot/decode_test.go @@ -532,7 +532,13 @@ type dotDirectedGraph struct { // newDotDirectedGraph returns a new directed capable of creating user-defined // nodes and edges. func newDotDirectedGraph() *dotDirectedGraph { - return &dotDirectedGraph{DirectedGraph: simple.NewDirectedGraph()} + return &dotDirectedGraph{ + DirectedGraph: simple.NewDirectedGraph(), + + graph: &encoding.Attributes{}, + node: &encoding.Attributes{}, + edge: &encoding.Attributes{}, + } } // NewNode returns a new node with a unique node ID for the graph. @@ -552,7 +558,7 @@ func (g *dotDirectedGraph) DOTAttributers() (graph, node, edge encoding.Attribut // DOTAttributeSetters implements the dot.AttributeSetters interface. func (g *dotDirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) { - return &g.graph, &g.node, &g.edge + return g.graph, g.node, g.edge } // SetDOTID sets the DOT ID of the graph. @@ -579,7 +585,13 @@ type dotUndirectedGraph struct { // newDotUndirectedGraph returns a new undirected capable of creating user- // defined nodes and edges. func newDotUndirectedGraph() *dotUndirectedGraph { - return &dotUndirectedGraph{UndirectedGraph: simple.NewUndirectedGraph()} + return &dotUndirectedGraph{ + UndirectedGraph: simple.NewUndirectedGraph(), + + graph: &encoding.Attributes{}, + node: &encoding.Attributes{}, + edge: &encoding.Attributes{}, + } } // NewNode adds a new node with a unique node ID to the graph. @@ -599,7 +611,7 @@ func (g *dotUndirectedGraph) DOTAttributers() (graph, node, edge encoding.Attrib // DOTUnmarshalerAttrs implements the dot.UnmarshalerAttrs interface. func (g *dotUndirectedGraph) DOTAttributeSetters() (graph, node, edge encoding.AttributeSetter) { - return &g.graph, &g.node, &g.edge + return g.graph, g.node, g.edge } // SetDOTID sets the DOT ID of the graph. @@ -705,15 +717,9 @@ 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 - -func (a attributes) Attributes() []encoding.Attribute { - return []encoding.Attribute(a) -} -func (a *attributes) SetAttribute(attr encoding.Attribute) error { - *a = append(*a, attr) - return nil +type attributes interface { + encoding.Attributer + encoding.AttributeSetter } const undirectedSelfLoopGraph = `graph { diff --git a/graph/encoding/dot/encode_test.go b/graph/encoding/dot/encode_test.go index 8c6f51a0..afec9e94 100644 --- a/graph/encoding/dot/encode_test.go +++ b/graph/encoding/dot/encode_test.go @@ -321,10 +321,6 @@ type graphAttributer struct { edge encoding.Attributer } -type attributer []encoding.Attribute - -func (a attributer) Attributes() []encoding.Attribute { return a } - func (g graphAttributer) DOTAttributers() (graph, node, edge encoding.Attributer) { return g.graph, g.node, g.edge } @@ -1192,8 +1188,8 @@ var encodeTests = []struct { {from: 2, to: 4}: {}, {from: 3, to: 4}: {{Key: "color", Value: "red"}}, }), - graph: attributer{{Key: "rankdir", Value: `"LR"`}}, - node: attributer{{Key: "fontsize", Value: "16"}, {Key: "shape", Value: "ellipse"}}, + graph: &encoding.Attributes{{Key: "rankdir", Value: `"LR"`}}, + node: &encoding.Attributes{{Key: "fontsize", Value: "16"}, {Key: "shape", Value: "ellipse"}}, edge: nil, }, diff --git a/graph/encoding/encoding.go b/graph/encoding/encoding.go index 53ef0d56..d2037f64 100644 --- a/graph/encoding/encoding.go +++ b/graph/encoding/encoding.go @@ -34,3 +34,36 @@ type Attributer interface { type Attribute struct { Key, Value string } + +// Attributes is a helper type providing simple attribute handling. +type Attributes []Attribute + +// Attributes returns all of the receiver's attributes. +func (a *Attributes) Attributes() []Attribute { + return *a +} + +// SetAttribute sets attr in the receiver. Calling SetAttribute with an +// Attribute with a Key that is in the collection replaces the existing +// value and calling with an empty Value removes the attribute from the +// collection if it exists. SetAttribute always returns nil. +func (a *Attributes) SetAttribute(attr Attribute) error { + if attr.Key == "" { + return nil + } + for i, v := range *a { + if v.Key == attr.Key { + if attr.Value == "" { + (*a)[i] = (*a)[len(*a)-1] + *a = (*a)[:len(*a)-1] + return nil + } + (*a)[i].Value = attr.Value + return nil + } + } + if attr.Value != "" { + *a = append(*a, attr) + } + return nil +} diff --git a/graph/encoding/encoding_test.go b/graph/encoding/encoding_test.go new file mode 100644 index 00000000..4aed09f0 --- /dev/null +++ b/graph/encoding/encoding_test.go @@ -0,0 +1,121 @@ +// Copyright ©2021 The Gonum Authors. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package encoding + +import ( + "sort" + "testing" +) + +var setAttributesTests = []struct { + attr *Attributes + opName string + op func(AttributeSetter) error + want *Attributes +}{ + { + attr: &Attributes{}, + opName: "noop", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "", Value: "bar"}) + }, + want: &Attributes{}, + }, + { + attr: &Attributes{}, + opName: "add attr to empty", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "foo", Value: "bar"}) + }, + want: &Attributes{{Key: "foo", Value: "bar"}}, + }, + { + attr: &Attributes{}, + opName: "remove attr from empty", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "foo", Value: ""}) + }, + want: &Attributes{}, + }, + { + attr: &Attributes{{Key: "foo", Value: "bar"}}, + opName: "add attr to non-empty", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "bif", Value: "fud"}) + }, + want: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}}, + }, + { + attr: &Attributes{{Key: "foo", Value: "bar"}}, + opName: "remove attr from singleton", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "foo", Value: ""}) + }, + want: &Attributes{}, + }, + { + attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}}, + opName: "remove first attr from pair", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "foo", Value: ""}) + }, + want: &Attributes{{Key: "bif", Value: "fud"}}, + }, + { + attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}}, + opName: "remove second attr from pair", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "bif", Value: ""}) + }, + want: &Attributes{{Key: "foo", Value: "bar"}}, + }, + { + attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}}, + opName: "replace first attr in pair", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "foo", Value: "not bar"}) + }, + want: &Attributes{{Key: "foo", Value: "not bar"}, {Key: "bif", Value: "fud"}}, + }, + { + attr: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "fud"}}, + opName: "replace second attr in pair", + op: func(a AttributeSetter) error { + return a.SetAttribute(Attribute{Key: "bif", Value: "not fud"}) + }, + want: &Attributes{{Key: "foo", Value: "bar"}, {Key: "bif", Value: "not fud"}}, + }, +} + +func TestSetAttributes(t *testing.T) { + for _, test := range setAttributesTests { + err := test.op(test.attr) + if err != nil { + t.Errorf("unexpected error for %q: %v", test.opName, err) + continue + } + if !sameAttributes(test.attr, test.want) { + t.Errorf("unexpected result from %q:\ngot: %+v\nwant:%+v", test.opName, test.attr, test.want) + } + } +} + +func sameAttributes(a, b Attributer) bool { + aAttr := a.Attributes() + bAttr := b.Attributes() + if len(aAttr) != len(bAttr) { + return false + } + aAttr = append(aAttr[:0:0], aAttr...) + sort.Slice(aAttr, func(i, j int) bool { return aAttr[i].Key < aAttr[j].Key }) + bAttr = append(bAttr[:0:0], bAttr...) + sort.Slice(bAttr, func(i, j int) bool { return bAttr[i].Key < bAttr[j].Key }) + for i, a := range aAttr { + if bAttr[i] != a { + return false + } + } + return true +}