Repository: dominikbraun/graph
Pull request #19 (2022-07-22 12:55:42)

Export `Traits` struct


Patch:
diff --git a/directed.go b/directed.go
index 0b6c92e..8d49ca3 100644
--- a/directed.go
+++ b/directed.go
@@ -7,14 +7,14 @@ import (
 
 type directed[K comparable, T any] struct {
 	hash     Hash[K, T]
-	traits   *traits
+	traits   *Traits
 	vertices map[K]T
 	edges    map[K]map[K]Edge[T]
 	outEdges map[K]map[K]Edge[T]
 	inEdges  map[K]map[K]Edge[T]
 }
 
-func newDirected[K comparable, T any](hash Hash[K, T], traits *traits) *directed[K, T] {
+func newDirected[K comparable, T any](hash Hash[K, T], traits *Traits) *directed[K, T] {
 	return &directed[K, T]{
 		hash:     hash,
 		traits:   traits,
@@ -25,6 +25,10 @@ func newDirected[K comparable, T any](hash Hash[K, T], traits *traits) *directed
 	}
 }
 
+func (d *directed[K, T]) Traits() *Traits {
+	return d.traits
+}
+
 func (d *directed[K, T]) Vertex(value T) {
 	hash := d.hash(value)
 	d.vertices[hash] = value
@@ -61,7 +65,7 @@ func (d *directed[K, T]) WeightedEdgeByHashes(sourceHash, targetHash K, weight i
 	}
 
 	// If the graph was declared to be acyclic, permit the creation of a cycle.
-	if d.traits.isAcyclic {
+	if d.traits.IsAcyclic {
 		createsCycle, err := d.CreatesCycleByHashes(sourceHash, targetHash)
 		if err != nil {
 			return fmt.Errorf("failed to check for cycles: %w", err)
diff --git a/graph.go b/graph.go
index c53d371..2a860a1 100644
--- a/graph.go
+++ b/graph.go
@@ -4,6 +4,9 @@ package graph
 // are of type T, and each vertex is identified by a hash of type K.
 type Graph[K comparable, T any] interface {
 
+	// Traits returns the graph's traits. Those traits must be set when creating a graph using New.
+	Traits() *Traits
+
 	// Vertex creates a new vertex in the graph, which won't be connected to another vertex yet.
 	// This function is idempotent, but overwrites an existing vertex if the hash already exists.
 	Vertex(value T)
@@ -231,14 +234,14 @@ type Hash[K comparable, T any] func(T) K
 //	g := graph.New(graph.IntHash, graph.Directed(), graph.Acyclic())
 //
 // The obtained Graph implementation is depends on these traits.
-func New[K comparable, T any](hash Hash[K, T], options ...func(*traits)) Graph[K, T] {
-	var p traits
+func New[K comparable, T any](hash Hash[K, T], options ...func(*Traits)) Graph[K, T] {
+	var p Traits
 
 	for _, option := range options {
 		option(&p)
 	}
 
-	if p.isDirected {
+	if p.IsDirected {
 		return newDirected(hash, &p)
 	}
 
diff --git a/traits.go b/traits.go
index f7eb8cf..cd34be3 100644
--- a/traits.go
+++ b/traits.go
@@ -1,45 +1,52 @@
 package graph
 
-type traits struct {
-	isDirected bool
-	isAcyclic  bool
-	isWeighted bool
-	isRooted   bool
+// Traits represents a set of graph traits and types, such as directedness or acyclicness. These
+// traits can be set when creating a graph by passing the corresponding functional options, for
+// example:
+//
+//	g := graph.New(graph.IntHash, graph.Directed())
+//
+// This will set the IsDirected field to true.
+type Traits struct {
+	IsDirected bool
+	IsAcyclic  bool
+	IsWeighted bool
+	IsRooted   bool
 }
 
 // Directed creates a directed graph. This has implications on graph traversal and the order of
 // arguments of the Edge and EdgeByHashes functions.
-func Directed() func(*traits) {
-	return func(t *traits) {
-		t.isDirected = true
+func Directed() func(*Traits) {
+	return func(t *Traits) {
+		t.IsDirected = true
 	}
 }
 
 // Acyclic creates an acyclic graph, which won't allow adding directed edges resulting in a cycle.
-func Acyclic() func(*traits) {
-	return func(t *traits) {
-		t.isAcyclic = true
+func Acyclic() func(*Traits) {
+	return func(t *Traits) {
+		t.IsAcyclic = true
 	}
 }
 
 // Weighted creates a weighted graph. To set weights, use the WeightedEdge and WeightedEdgeByHashes
 // functions.
-func Weighted() func(*traits) {
-	return func(t *traits) {
-		t.isWeighted = true
+func Weighted() func(*Traits) {
+	return func(t *Traits) {
+		t.IsWeighted = true
 	}
 }
 
 // Rooted creates a rooted graph. This is particularly common for building tree data structures.
-func Rooted() func(*traits) {
-	return func(t *traits) {
-		t.isRooted = true
+func Rooted() func(*Traits) {
+	return func(t *Traits) {
+		t.IsRooted = true
 	}
 }
 
 // Tree is an alias for Acyclic and Rooted, since most trees in Computer Science are rooted trees.
-func Tree() func(*traits) {
-	return func(t *traits) {
+func Tree() func(*Traits) {
+	return func(t *Traits) {
 		Acyclic()(t)
 		Rooted()(t)
 	}
diff --git a/undirected.go b/undirected.go
index 994f874..124f81f 100644
--- a/undirected.go
+++ b/undirected.go
@@ -8,14 +8,14 @@ import (
 
 type undirected[K comparable, T any] struct {
 	hash     Hash[K, T]
-	traits   *traits
+	traits   *Traits
 	vertices map[K]T
 	edges    map[K]map[K]Edge[T]
 	outEdges map[K]map[K]Edge[T]
 	inEdges  map[K]map[K]Edge[T]
 }
 
-func newUndirected[K comparable, T any](hash Hash[K, T], traits *traits) *undirected[K, T] {
+func newUndirected[K comparable, T any](hash Hash[K, T], traits *Traits) *undirected[K, T] {
 	return &undirected[K, T]{
 		hash:     hash,
 		traits:   traits,
@@ -26,6 +26,10 @@ func newUndirected[K comparable, T any](hash Hash[K, T], traits *traits) *undire
 	}
 }
 
+func (u *undirected[K, T]) Traits() *Traits {
+	return u.traits
+}
+
 func (u *undirected[K, T]) Vertex(value T) {
 	hash := u.hash(value)
 	u.vertices[hash] = value
@@ -62,7 +66,7 @@ func (u *undirected[K, T]) WeightedEdgeByHashes(sourceHash, targetHash K, weight
 	}
 
 	// If the graph was declared to be acyclic, permit the creation of a cycle.
-	if u.traits.isAcyclic {
+	if u.traits.IsAcyclic {
 		createsCycle, err := u.CreatesCycleByHashes(sourceHash, targetHash)
 		if err != nil {
 			return fmt.Errorf("failed to check for cycles: %w", err)
@@ -361,7 +365,7 @@ func (u *undirected[K, T]) edgesAreEqual(a, b Edge[T]) bool {
 		return true
 	}
 
-	if !u.traits.isDirected {
+	if !u.traits.IsDirected {
 		return aSourceHash == bTargetHash && aTargetHash == bSourceHash
 	}
 


Test patch:
diff --git a/directed_test.go b/directed_test.go
index f4d9c1d..d123ca5 100644
--- a/directed_test.go
+++ b/directed_test.go
@@ -4,6 +4,31 @@ import (
 	"testing"
 )
 
+func TestDirected_Traits(t *testing.T) {
+	tests := map[string]struct {
+		traits   *Traits
+		expected *Traits
+	}{
+		"default traits": {
+			traits:   &Traits{},
+			expected: &Traits{},
+		},
+		"directed": {
+			traits:   &Traits{IsDirected: true},
+			expected: &Traits{IsDirected: true},
+		},
+	}
+
+	for name, test := range tests {
+		g := newDirected(IntHash, test.traits)
+		traits := g.Traits()
+
+		if !traitsAreEqual(traits, test.expected) {
+			t.Errorf("%s: traits expectancy doesn't match: expected %v, got %v", name, test.expected, traits)
+		}
+	}
+}
+
 func TestDirected_Vertex(t *testing.T) {
 	tests := map[string]struct {
 		vertices         []int
@@ -20,7 +45,7 @@ func TestDirected_Vertex(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -51,7 +76,7 @@ func TestDirected_WeightedEdgeByHashes(t *testing.T) {
 	tests := map[string]struct {
 		vertices      []int
 		edgeHashes    [][3]int
-		traits        *traits
+		traits        *Traits
 		expectedEdges []Edge[int]
 		// Even though some of the WeightedEdgeByHashes calls might work, at least one of them
 		// could fail - for example if the last call would introduce a cycle.
@@ -60,7 +85,7 @@ func TestDirected_WeightedEdgeByHashes(t *testing.T) {
 		"graph with 2 edges": {
 			vertices:   []int{1, 2, 3},
 			edgeHashes: [][3]int{{1, 2, 10}, {1, 3, 20}},
-			traits:     &traits{},
+			traits:     &Traits{},
 			expectedEdges: []Edge[int]{
 				{Source: 1, Target: 2, Weight: 10},
 				{Source: 1, Target: 3, Weight: 20},
@@ -69,14 +94,14 @@ func TestDirected_WeightedEdgeByHashes(t *testing.T) {
 		"hashes for non-existent vertices": {
 			vertices:          []int{1, 2},
 			edgeHashes:        [][3]int{{1, 3, 20}},
-			traits:            &traits{},
+			traits:            &Traits{},
 			shouldFinallyFail: true,
 		},
 		"edge introducing a cycle in an acyclic graph": {
 			vertices:   []int{1, 2, 3},
 			edgeHashes: [][3]int{{1, 2, 0}, {2, 3, 0}, {3, 1, 0}},
-			traits: &traits{
-				isAcyclic: true,
+			traits: &Traits{
+				IsAcyclic: true,
 			},
 			shouldFinallyFail: true,
 		},
@@ -147,7 +172,7 @@ func TestDirected_GetEdgeByHashes(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
 		}
@@ -222,7 +247,7 @@ func TestDirected_DFSByHash(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -309,7 +334,7 @@ func TestDirected_BFSByHash(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -402,7 +427,7 @@ func TestDirected_CreatesCycleByHashes(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -439,7 +464,7 @@ func TestDirected_DegreeByHash(t *testing.T) {
 	}{}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -492,7 +517,7 @@ func TestDirected_StronglyConnectedComponents(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -590,7 +615,7 @@ func TestDirected_ShortestPathByHashes(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(StringHash, &traits{})
+		graph := newDirected(StringHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -671,7 +696,7 @@ func TestDirected_AdjacencyList(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -722,7 +747,7 @@ func TestDirected_edgesAreEqual(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 		actual := graph.edgesAreEqual(test.a, test.b)
 
 		if actual != test.edgesAreEqual {
@@ -745,7 +770,7 @@ func TestDirected_addEdge(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, edge := range test.edges {
 			sourceHash := graph.hash(edge.Source)
@@ -806,7 +831,7 @@ func TestDirected_predecessors(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newDirected(IntHash, &traits{})
+		graph := newDirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
diff --git a/graph_test.go b/graph_test.go
index b6c9e6b..0685c1b 100644
--- a/graph_test.go
+++ b/graph_test.go
@@ -10,15 +10,15 @@ func TestNew(t *testing.T) {
 	undirectedType := reflect.TypeOf(&undirected[int, int]{})
 
 	tests := map[string]struct {
-		options      []func(*traits)
+		options      []func(*Traits)
 		expectedType reflect.Type
 	}{
 		"no options": {
-			options:      []func(*traits){},
+			options:      []func(*Traits){},
 			expectedType: undirectedType,
 		},
 		"directed option": {
-			options:      []func(*traits){Directed()},
+			options:      []func(*Traits){Directed()},
 			expectedType: directedType,
 		},
 	}
diff --git a/traits_test.go b/traits_test.go
index c978fa8..0fb31ee 100644
--- a/traits_test.go
+++ b/traits_test.go
@@ -4,17 +4,17 @@ import "testing"
 
 func TestDirected(t *testing.T) {
 	tests := map[string]struct {
-		expected *traits
+		expected *Traits
 	}{
 		"directed graph": {
-			expected: &traits{
-				isDirected: true,
+			expected: &Traits{
+				IsDirected: true,
 			},
 		},
 	}
 
 	for name, test := range tests {
-		p := &traits{}
+		p := &Traits{}
 
 		Directed()(p)
 
@@ -26,17 +26,17 @@ func TestDirected(t *testing.T) {
 
 func TestAcyclic(t *testing.T) {
 	tests := map[string]struct {
-		expected *traits
+		expected *Traits
 	}{
 		"acyclic graph": {
-			expected: &traits{
-				isAcyclic: true,
+			expected: &Traits{
+				IsAcyclic: true,
 			},
 		},
 	}
 
 	for name, test := range tests {
-		p := &traits{}
+		p := &Traits{}
 
 		Acyclic()(p)
 
@@ -48,17 +48,17 @@ func TestAcyclic(t *testing.T) {
 
 func TestWeighted(t *testing.T) {
 	tests := map[string]struct {
-		expected *traits
+		expected *Traits
 	}{
 		"weighted graph": {
-			expected: &traits{
-				isWeighted: true,
+			expected: &Traits{
+				IsWeighted: true,
 			},
 		},
 	}
 
 	for name, test := range tests {
-		p := &traits{}
+		p := &Traits{}
 
 		Weighted()(p)
 
@@ -70,17 +70,17 @@ func TestWeighted(t *testing.T) {
 
 func TestRooted(t *testing.T) {
 	tests := map[string]struct {
-		expected *traits
+		expected *Traits
 	}{
 		"rooted graph": {
-			expected: &traits{
-				isRooted: true,
+			expected: &Traits{
+				IsRooted: true,
 			},
 		},
 	}
 
 	for name, test := range tests {
-		p := &traits{}
+		p := &Traits{}
 
 		Rooted()(p)
 
@@ -92,18 +92,18 @@ func TestRooted(t *testing.T) {
 
 func TestTree(t *testing.T) {
 	tests := map[string]struct {
-		expected *traits
+		expected *Traits
 	}{
 		"tree graph": {
-			expected: &traits{
-				isAcyclic: true,
-				isRooted:  true,
+			expected: &Traits{
+				IsAcyclic: true,
+				IsRooted:  true,
 			},
 		},
 	}
 
 	for name, test := range tests {
-		p := &traits{}
+		p := &Traits{}
 
 		Tree()(p)
 
@@ -113,9 +113,9 @@ func TestTree(t *testing.T) {
 	}
 }
 
-func traitsAreEqual(a, b *traits) bool {
-	return a.isAcyclic == b.isAcyclic &&
-		a.isDirected == b.isDirected &&
-		a.isRooted == b.isRooted &&
-		a.isWeighted == b.isWeighted
+func traitsAreEqual(a, b *Traits) bool {
+	return a.IsAcyclic == b.IsAcyclic &&
+		a.IsDirected == b.IsDirected &&
+		a.IsRooted == b.IsRooted &&
+		a.IsWeighted == b.IsWeighted
 }
diff --git a/undirected_test.go b/undirected_test.go
index eece8e3..585b1d6 100644
--- a/undirected_test.go
+++ b/undirected_test.go
@@ -4,6 +4,31 @@ import (
 	"testing"
 )
 
+func TestUndirected_Traits(t *testing.T) {
+	tests := map[string]struct {
+		traits   *Traits
+		expected *Traits
+	}{
+		"default traits": {
+			traits:   &Traits{},
+			expected: &Traits{},
+		},
+		"directed": {
+			traits:   &Traits{IsDirected: true},
+			expected: &Traits{IsDirected: true},
+		},
+	}
+
+	for name, test := range tests {
+		g := newUndirected(IntHash, test.traits)
+		traits := g.Traits()
+
+		if !traitsAreEqual(traits, test.expected) {
+			t.Errorf("%s: traits expectancy doesn't match: expected %v, got %v", name, test.expected, traits)
+		}
+	}
+}
+
 func TestUndirected_Vertex(t *testing.T) {
 	tests := map[string]struct {
 		vertices         []int
@@ -20,7 +45,7 @@ func TestUndirected_Vertex(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -51,7 +76,7 @@ func TestUndirected_WeightedEdgeByHashes(t *testing.T) {
 	tests := map[string]struct {
 		vertices      []int
 		edgeHashes    [][3]int
-		traits        *traits
+		traits        *Traits
 		expectedEdges []Edge[int]
 		// Even though some of the WeightedEdgeByHashes calls might work, at least one of them
 		// could fail - for example if the last call would introduce a cycle.
@@ -60,7 +85,7 @@ func TestUndirected_WeightedEdgeByHashes(t *testing.T) {
 		"graph with 2 edges": {
 			vertices:   []int{1, 2, 3},
 			edgeHashes: [][3]int{{1, 2, 10}, {1, 3, 20}},
-			traits:     &traits{},
+			traits:     &Traits{},
 			expectedEdges: []Edge[int]{
 				{Source: 1, Target: 2, Weight: 10},
 				{Source: 1, Target: 3, Weight: 20},
@@ -69,14 +94,14 @@ func TestUndirected_WeightedEdgeByHashes(t *testing.T) {
 		"hashes for non-existent vertices": {
 			vertices:          []int{1, 2},
 			edgeHashes:        [][3]int{{1, 3, 20}},
-			traits:            &traits{},
+			traits:            &Traits{},
 			shouldFinallyFail: true,
 		},
 		"edge introducing a cycle in an acyclic graph": {
 			vertices:   []int{1, 2, 3},
 			edgeHashes: [][3]int{{1, 2, 0}, {2, 3, 0}, {3, 1, 0}},
-			traits: &traits{
-				isAcyclic: true,
+			traits: &Traits{
+				IsAcyclic: true,
 			},
 			shouldFinallyFail: true,
 		},
@@ -147,7 +172,7 @@ func TestUndirected_GetEdgeByHashes(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -267,7 +292,7 @@ func TestUndirected_DFSByHash(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -362,7 +387,7 @@ func TestUndirected_BFSByHash(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -453,7 +478,7 @@ func TestUndirected_CreatesCycleByHashes(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -490,7 +515,7 @@ func TestUndirected_DegreeByHash(t *testing.T) {
 	}{}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -526,7 +551,7 @@ func TestUndirected_StronglyConnectedComponents(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		sccs, err := graph.StronglyConnectedComponents()
 
@@ -609,7 +634,7 @@ func TestUndirected_ShortestPathByHashes(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(StringHash, &traits{})
+		graph := newUndirected(StringHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -699,7 +724,7 @@ func TestUndirected_AdjacencyList(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)
@@ -755,7 +780,7 @@ func TestUndirected_edgesAreEqual(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 		actual := graph.edgesAreEqual(test.a, test.b)
 
 		if actual != test.edgesAreEqual {
@@ -778,7 +803,7 @@ func TestUndirected_addEdge(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, edge := range test.edges {
 			sourceHash := graph.hash(edge.Source)
@@ -871,7 +896,7 @@ func TestUndirected_adjacencies(t *testing.T) {
 	}
 
 	for name, test := range tests {
-		graph := newUndirected(IntHash, &traits{})
+		graph := newUndirected(IntHash, &Traits{})
 
 		for _, vertex := range test.vertices {
 			graph.Vertex(vertex)