File: composite.go

package info (click to toggle)
golang-github-emicklei-dot 1.6.2-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid
  • size: 300 kB
  • sloc: makefile: 9
file content (145 lines) | stat: -rw-r--r-- 4,547 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
package dotx

import (
	"errors"
	"log"
	"os"
	"strings"

	"github.com/emicklei/dot"
)

type compositeGraphKind int

// Connectable is a dot.Node or a *dotx.Composite
type Connectable interface {
	Attr(label string, value interface{}) dot.Node
}

const (
	// SameGraph means that the composite graph will be a cluster within the graph.
	SameGraph compositeGraphKind = iota
	// ExternalGraph means the composite graph will be exported on its own, linked by the node within the graph
	ExternalGraph
)

// Composite is a graph and node to create abstractions in graphs.
type Composite struct {
	*dot.Graph
	outerNode   dot.Node
	outerGraph  *dot.Graph
	dotFilename string
	kind        compositeGraphKind
}

// NewComposite creates a Composite abstraction that is represented as a Node (box3d shape) in the graph.
// The kind determines whether the graph of the composite is embedded (same graph) or external.
func NewComposite(id string, g *dot.Graph, kind compositeGraphKind) *Composite {
	var innerGraph *dot.Graph
	if kind == SameGraph {
		innerGraph = g.Subgraph(id, dot.ClusterOption{})
	} else {
		innerGraph = dot.NewGraph(dot.Directed)
	}
	sub := &Composite{
		Graph:      innerGraph,
		outerNode:  g.Node(id).Attr("shape", "box3d"),
		outerGraph: g,
		kind:       kind,
	}
	sub.ExportName(id)
	return sub
}

// ExportFilename returns the name of the file used by ExportFile. Override it using ExportName.
func (s *Composite) ExportFilename() string {
	return s.dotFilename
}

// Attr sets label=value and returns the Node in the graph
func (s *Composite) Attr(label string, value interface{}) dot.Node {
	return s.outerNode.Attr(label, value)
}

// ExportName argument name will be used for the .dot export and the HREF link using svg
// So if name = "my example" then export will create "my_example.dot" and the link will be "my_example.svg"
func (s *Composite) ExportName(name string) {
	hrefFile := strings.ReplaceAll(name, " ", "_") + ".svg"
	dotFile := strings.ReplaceAll(name, " ", "_") + ".dot"
	s.outerNode.Attr("href", hrefFile)
	s.dotFilename = dotFile
}

// Input creates an edge.
// If the from Connectable is part of the parent graph then the edge is added to the parent graph.
// If the from Connectable is part of the composite then the edge is added to the inner graph.
func (s *Composite) Input(id string, from Connectable) dot.Edge {
	var fromNode dot.Node
	if n, ok := from.(dot.Node); ok {
		fromNode = n
	} else {
		if c, ok := from.(*Composite); ok {
			fromNode = c.outerNode
		}
	}
	if s.Graph.HasNode(fromNode) {
		// edge on innergraph
		return s.connect(id, true, fromNode)
	}
	// ensure input node in innergraph
	s.Node(id).Attr("shape", "point")
	// edge on outergraph
	return fromNode.Edge(s.outerNode).Label(id)
}

// Output creates an edge.
// If the to Connectable is part of the parent graph then the edge is added to the parent graph.
// If the to Connectable is part of the composite then the edge is added to the inner graph.
func (s *Composite) Output(id string, to Connectable) dot.Edge {
	var toNode dot.Node
	if n, ok := to.(dot.Node); ok {
		toNode = n
	} else {
		if c, ok := to.(*Composite); ok {
			toNode = c.outerNode
		}
	}
	if s.Graph.HasNode(toNode) {
		// edge on innergraph
		return s.connect(id, false, toNode)
	}
	// ensure output node in innergraph
	s.Node(id).Attr("shape", "point")
	// edge on outergraph
	return s.outerNode.Edge(toNode).Label(id)
}

func (s *Composite) connect(portName string, isInput bool, inner dot.Node) dot.Edge {
	// node creation is idempotent
	port := s.Node(portName).Attr("shape", "point")
	if isInput {
		return s.EdgeWithPorts(port, inner, "s", "n").Attr("taillabel", portName)
	} else {
		// is output
		return s.EdgeWithPorts(inner, port, "s", "n").Attr("headlabel", portName)
	}
}

// ExportFile creates a DOT file using the default name (based on name) or overridden using ExportName().
func (s *Composite) ExportFile() error {
	if s.kind != ExternalGraph {
		return errors.New("ExportFile is only applicable to a ExternalGraph Composite")
	}
	return os.WriteFile(s.ExportFilename(), []byte(s.Graph.String()), 0666)
}

// Export writes the DOT file for a Composite after building the content (child) graph using the build function.
// Use ExportName() on the Composite to modify the filename used.
// If writing of the file fails then a warning is logged.
func (s *Composite) Export(build func(g *dot.Graph)) *Composite {
	build(s.Graph)
	if err := s.ExportFile(); err != nil {
		log.Println("WARN: dotx.Composite.Export failed", err)
	}
	return s
}