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  }