diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..e2fa125 --- /dev/null +++ b/.gitignore @@ -0,0 +1,4 @@ +.idea +.zed + +*.dot diff --git a/.golangci.yaml b/.golangci.yaml index cbef4ea..75fa91f 100644 --- a/.golangci.yaml +++ b/.golangci.yaml @@ -39,6 +39,14 @@ linters: deny: - pkg: github.com/pkg/errors desc: Should be replaced by standard lib errors package. + errcheck: + exclude-functions: + - (*os.File).Close + - (*bufio.Writer).Flush + - (*bufio.Reader).Close + - io.Copy + - fmt.Print.* + - log.Print.* dupl: threshold: 150 funlen: diff --git a/README.md b/README.md index f015981..586f596 100644 --- a/README.md +++ b/README.md @@ -104,7 +104,7 @@ func (c *Container) MustResolve(target interface{}) - [x] Interface binding support - [x] Error handling - [ ] Named dependency resolution -- [ ] Dependency graph visualization +- [x] Dependency graph visualization ## 📊 Benefits diff --git a/container.go b/container.go index 5819b13..12ad6a2 100644 --- a/container.go +++ b/container.go @@ -2,7 +2,9 @@ package compoapp import ( + "bufio" "fmt" + "os" "reflect" "sync" ) @@ -443,3 +445,86 @@ func (c *Container) validateDependencies() error { } return nil } + +const dotHeader string = `digraph DependencyGraph { + rankdir=LR; + node [shape=box, style=rounded, fontname="Arial"]; + edge [fontname="Arial"]; + +` + +// Visualize creates .dot file for graphviz visualization +func (c *Container) Visualize(filepath string) error { + c.mu.RLock() + defer c.mu.RUnlock() + //nolint:gosec + f, err := os.Create(filepath) + if err != nil { + return fmt.Errorf("failed to create file: %w", err) + } + defer f.Close() + + writer := bufio.NewWriter(f) + defer writer.Flush() + + // Write DOT header + if _, err := writer.WriteString(dotHeader); err != nil { + return fmt.Errorf("failed to write digraph header: %w", err) + } + + nodes := make(map[string]struct{}) + edges := make(map[string][]string) + + // Process dependencies + for componentName, deps := range c.graph.dependencies { + from := componentName.String() + nodes[from] = struct{}{} + for _, dep := range deps { + to := dep.String() + nodes[to] = struct{}{} + edges[from] = append(edges[from], to) + } + } + + // Process dependents (reverse dependencies) + for componentName, dependents := range c.graph.dependents { + to := componentName.String() + nodes[to] = struct{}{} + for _, dep := range dependents { + from := dep.String() + nodes[from] = struct{}{} + edges[from] = append(edges[from], to) + } + } + + for nodeName := range nodes { + if _, err := fmt.Fprintf(writer, " %q;\n", nodeName); err != nil { + return fmt.Errorf("failed to write node name: %w", err) + } + } + + if _, err := writer.WriteString("\n"); err != nil { + return fmt.Errorf("failed to write newline: %w", err) + } + + addedEdges := make(map[string]struct{}) + for from, toList := range edges { + for _, to := range toList { + edgeKey := from + "->" + to + if _, exists := addedEdges[edgeKey]; exists { + continue + } + if _, err := fmt.Fprintf(writer, " %q -> %q;\n", from, to); err != nil { + return fmt.Errorf("failed to write edge: %w", err) + } + addedEdges[edgeKey] = struct{}{} + } + } + + // Close DOT graph + if _, err := writer.WriteString("}\n"); err != nil { + return fmt.Errorf("failed to write graph closure: %w", err) + } + + return nil +} diff --git a/samples/interfaces/main.go b/samples/interfaces/main.go index 5314042..800f0d4 100644 --- a/samples/interfaces/main.go +++ b/samples/interfaces/main.go @@ -46,4 +46,8 @@ func main() { var app *App container.MustResolve(&app) + + if err := container.Visualize("graph.dot"); err != nil { + panic(err) + } }