package workflow import ( "encoding/json" "fmt" ) // NodeType represents the type of a workflow node type NodeType string const ( NodeTypeTask NodeType = "task" NodeTypeCondition NodeType = "condition" NodeTypeParallel NodeType = "parallel" NodeTypeJoin NodeType = "join" NodeTypeTrigger NodeType = "trigger" ) // NodeStatus represents the status of a workflow node type NodeStatus string const ( NodeStatusPending NodeStatus = "pending" NodeStatusReady NodeStatus = "ready" NodeStatusRunning NodeStatus = "running" NodeStatusCompleted NodeStatus = "completed" NodeStatusSkipped NodeStatus = "skipped" NodeStatusFailed NodeStatus = "failed" ) // WorkflowNode represents a node in the workflow DAG type WorkflowNode struct { Key string `json:"key"` Type NodeType `json:"type"` Title string `json:"title"` Content string `json:"content"` Assignee *string `json:"assignee,omitempty"` DependsOn []string `json:"dependsOn,omitempty"` Config string `json:"config,omitempty"` // JSON config for task nodes } // WorkflowDefinition represents the complete workflow DAG definition type WorkflowDefinition struct { Nodes map[string]*WorkflowNode `json:"nodes"` } // ParseWorkflowDefinition parses a JSON string into a WorkflowDefinition func ParseWorkflowDefinition(jsonDef string) (*WorkflowDefinition, error) { var def WorkflowDefinition if err := json.Unmarshal([]byte(jsonDef), &def); err != nil { return nil, fmt.Errorf("failed to parse workflow definition: %w", err) } return &def, nil } // Validate validates the workflow definition func (d *WorkflowDefinition) Validate() error { if d.Nodes == nil || len(d.Nodes) == 0 { return fmt.Errorf("workflow definition must have at least one node") } // Validate each node for key, node := range d.Nodes { if node.Type == "" { return fmt.Errorf("node '%s' must have a type", key) } // Validate dependsOn references exist for _, depKey := range node.DependsOn { if _, exists := d.Nodes[depKey]; !exists { return fmt.Errorf("node '%s' depends on non-existent node '%s'", key, depKey) } } // Validate node type switch node.Type { case NodeTypeTask, NodeTypeCondition, NodeTypeParallel, NodeTypeJoin, NodeTypeTrigger: // Valid types default: return fmt.Errorf("node '%s' has invalid type: %s", key, node.Type) } } // Check for cycles in the DAG if err := d.validateNoCycles(); err != nil { return err } return nil } // validateNoCycles checks for cycles in the workflow DAG using DFS func (d *WorkflowDefinition) validateNoCycles() error { visited := make(map[string]bool) recStack := make(map[string]bool) var dfs func(string) error dfs = func(nodeKey string) error { visited[nodeKey] = true recStack[nodeKey] = true node := d.Nodes[nodeKey] for _, depKey := range node.DependsOn { if !visited[depKey] { if err := dfs(depKey); err != nil { return err } } else if recStack[depKey] { return fmt.Errorf("cycle detected: node '%s' creates a dependency loop", depKey) } } recStack[nodeKey] = false return nil } // Run DFS from each node for key := range d.Nodes { if !visited[key] { if err := dfs(key); err != nil { return err } } } return nil } // GetRootNodes returns nodes that have no dependencies (starting points) func (d *WorkflowDefinition) GetRootNodes() []string { var roots []string for key, node := range d.Nodes { if len(node.DependsOn) == 0 { roots = append(roots, key) } } return roots } // GetDependentNodes returns nodes that depend on the given node func (d *WorkflowDefinition) GetDependentNodes(nodeKey string) []string { var dependents []string for key, node := range d.Nodes { for _, dep := range node.DependsOn { if dep == nodeKey { dependents = append(dependents, key) break } } } return dependents } // GetReadyNodes returns nodes that are ready to execute (all dependencies satisfied) func (d *WorkflowDefinition) GetReadyNodes(executedNodes map[string]bool) []string { var ready []string for key, node := range d.Nodes { if executedNodes[key] { continue // Already executed } allDepsSatisfied := true for _, depKey := range node.DependsOn { if !executedNodes[depKey] { allDepsSatisfied = false break } } if allDepsSatisfied { ready = append(ready, key) } } return ready }