package graph import ( "fmt" "os" "strings" "gopkg.in/yaml.v3" ) // yamlNode is the intermediate YAML representation of a node. type yamlNode struct { Services map[string]yamlService `yaml:"services"` } // yamlService is the intermediate YAML representation of a service. type yamlService struct { DependsOn yamlDependsOn `yaml:"depends_on"` } // yamlDependsOn supports both short form (list of strings) and long form (map with conditions). type yamlDependsOn struct { simple []string detail map[string]yamlDepCondition } type yamlDepCondition struct { Condition DepCondition `yaml:"condition"` } func (d *yamlDependsOn) UnmarshalYAML(value *yaml.Node) error { switch value.Kind { case yaml.SequenceNode: var names []string if err := value.Decode(&names); err != nil { return err } d.simple = names return nil case yaml.MappingNode: d.detail = make(map[string]yamlDepCondition) if err := value.Decode(&d.detail); err != nil { return err } return nil default: return fmt.Errorf("depends_on must be a list or mapping, got %v", value.Kind) } } // parseServiceRef parses a reference like "redis" or "infra:redis". func parseServiceRef(ref string) ServiceRef { parts := strings.SplitN(ref, ":", 2) if len(parts) == 2 { return ServiceRef{NodeID: parts[0], Name: parts[1]} } return ServiceRef{Name: parts[0]} } // ParseYAML parses a node/service dependency graph from YAML bytes. // // Example: // // nodes: // server1: // services: // web: // agent_id: agent-1 // depends_on: // - redis // - infra:cache // api: // depends_on: // redis: // condition: healthy // infra: // services: // cache: // db: func ParseYAML(data []byte) (*Graph, error) { var raw struct { Nodes map[string]yamlNode `yaml:"nodes"` } if err := yaml.Unmarshal(data, &raw); err != nil { return nil, fmt.Errorf("parse yaml: %w", err) } g := New() // Phase 1: register all nodes and services for nodeID, yn := range raw.Nodes { g.AddNode(nodeID) for svcName := range yn.Services { g.AddService(nodeID, &Service{Name: svcName}) } } // Phase 2: wire dependencies for nodeID, yn := range raw.Nodes { for svcName, ys := range yn.Services { // Short form for _, ref := range ys.DependsOn.simple { target := parseServiceRef(ref) if err := g.AddDependency(nodeID, svcName, Dependency{ Target: target, Condition: Started, }); err != nil { return nil, err } } // Long form for ref, cond := range ys.DependsOn.detail { target := parseServiceRef(ref) if err := g.AddDependency(nodeID, svcName, Dependency{ Target: target, Condition: cond.Condition, }); err != nil { return nil, err } } } } return g, nil } // ParseYAMLFile reads and parses from a file. func ParseYAMLFile(path string) (*Graph, error) { data, err := os.ReadFile(path) if err != nil { return nil, err } return ParseYAML(data) }