graph/encoding: provide attribute handling out of the box

This commit is contained in:
Dan Kortschak
2021-05-27 07:18:04 +09:30
parent c8613b2ab9
commit 97f387b38f
4 changed files with 175 additions and 19 deletions

View File

@@ -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 {

View File

@@ -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,
},

View File

@@ -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
}

View File

@@ -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
}