github.com/alkemics/goflow@v0.2.1/checkers/cycles/check.go (about) 1 package cycles 2 3 import ( 4 "fmt" 5 6 "github.com/alkemics/goflow" 7 ) 8 9 type CycleError struct { 10 NID string 11 } 12 13 func (e CycleError) Error() string { 14 return fmt.Sprintf("node visited multiple times: %s (cycles)", e.NID) 15 } 16 17 type OrphanError struct { 18 NID string 19 } 20 21 func (e OrphanError) Error() string { 22 return fmt.Sprintf("orphan node: %s (cycles)", e.NID) 23 } 24 25 // Check checks that no cycles exist in the graph as well as for 26 // orphan nodes, i.e. nodes that are never visited 27 func Check(graph goflow.GraphRenderer) error { 28 // Map nodes by ID. 29 nodeMap := make(map[string]goflow.NodeRenderer) 30 for _, n := range graph.Nodes() { 31 nodeMap[n.ID()] = n 32 } 33 34 // Construct a map of edges and find the nodes without parents. 35 firstNodeIDs := make([]string, 0) 36 edges := make(map[string][]string) 37 for nID, n := range nodeMap { 38 previous := n.Previous() 39 if len(previous) == 0 { 40 firstNodeIDs = append(firstNodeIDs, nID) 41 } 42 for _, prev := range previous { 43 edges[prev] = append(edges[prev], nID) 44 } 45 } 46 47 // v, ok: 48 // ok == false -> not visited 49 // v == true -> permanent mark 50 // v == false -> temporary mark 51 visited := make(map[string]bool) 52 53 for _, nID := range firstNodeIDs { 54 if err := visit(nID, edges, visited); err != nil { 55 return err 56 } 57 } 58 59 // Check that all nodes have been visited. 60 orphanErrs := make([]error, 0) 61 for nID := range nodeMap { 62 permanent := visited[nID] 63 if !permanent { 64 orphanErrs = append(orphanErrs, OrphanError{NID: nID}) 65 } 66 } 67 if len(orphanErrs) > 0 { 68 return goflow.MultiError{Errs: orphanErrs} 69 } 70 71 return nil 72 } 73 74 // visit implements the DFS algorithm presented here 75 // https://en.wikipedia.org/wiki/Topological_sorting#Depth-first_search 76 func visit(nodeID string, edges map[string][]string, visited map[string]bool) error { 77 if visited == nil { 78 return nil 79 } 80 permanent, marked := visited[nodeID] 81 if marked { 82 if permanent { 83 return nil 84 } 85 return CycleError{NID: nodeID} 86 } 87 88 visited[nodeID] = false 89 90 for _, nextID := range edges[nodeID] { 91 if err := visit(nextID, edges, visited); err != nil { 92 return err 93 } 94 } 95 96 visited[nodeID] = true 97 return nil 98 }