feat: add DAG core functions — parse, validate, topo sort, resolve env, cron match (0007a, 0007d)
Pure functions for parsing dagu-compatible YAML, validating DAG structure, topological sorting with parallel levels (Kahn's algorithm), and env variable resolution. Also adds cron_match for schedule matching. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package core
|
||||
|
||||
import "fmt"
|
||||
|
||||
// DagValidate validates a DagDefinition for structural correctness.
|
||||
// Checks: steps have name/ID, no duplicate names/IDs, all depends reference
|
||||
// existing steps, no dependency cycles. On success, computes topological levels.
|
||||
// Returns warnings for steps with both command and script set.
|
||||
func DagValidate(dag DagDefinition) DagValidationResult {
|
||||
result := DagValidationResult{Valid: true}
|
||||
|
||||
// Build name/ID sets and check for missing identifiers and duplicates.
|
||||
seen := make(map[string]bool)
|
||||
for i, step := range dag.Steps {
|
||||
ref := stepRef(step)
|
||||
if ref == "" {
|
||||
result.Errors = append(result.Errors,
|
||||
fmt.Sprintf("step[%d]: must have name or id", i))
|
||||
result.Valid = false
|
||||
continue
|
||||
}
|
||||
if seen[ref] {
|
||||
result.Errors = append(result.Errors,
|
||||
fmt.Sprintf("step[%d]: duplicate name/id %q", i, ref))
|
||||
result.Valid = false
|
||||
}
|
||||
seen[ref] = true
|
||||
|
||||
// Warning: command and script both set.
|
||||
if step.Command != "" && step.Script != "" {
|
||||
result.Warnings = append(result.Warnings,
|
||||
fmt.Sprintf("step %q: has both command and script", ref))
|
||||
}
|
||||
}
|
||||
|
||||
if !result.Valid {
|
||||
return result
|
||||
}
|
||||
|
||||
// Check that all depends reference existing steps.
|
||||
for _, step := range dag.Steps {
|
||||
for _, dep := range step.Depends {
|
||||
if !seen[dep] {
|
||||
result.Errors = append(result.Errors,
|
||||
fmt.Sprintf("step %q: depends on unknown step %q", stepRef(step), dep))
|
||||
result.Valid = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !result.Valid {
|
||||
return result
|
||||
}
|
||||
|
||||
// Topological sort with Kahn's — detects cycles and computes levels.
|
||||
levels, err := DagTopoSort(dag.Steps)
|
||||
if err != nil {
|
||||
result.Errors = append(result.Errors, fmt.Sprintf("cycle detected: %v", err))
|
||||
result.Valid = false
|
||||
return result
|
||||
}
|
||||
|
||||
// Convert [][]DagStep to [][]string for the result.
|
||||
strLevels := make([][]string, len(levels))
|
||||
for i, level := range levels {
|
||||
refs := make([]string, len(level))
|
||||
for j, s := range level {
|
||||
refs[j] = stepRef(s)
|
||||
}
|
||||
strLevels[i] = refs
|
||||
}
|
||||
result.Levels = strLevels
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// stepRef returns the canonical reference for a step (ID preferred, then Name).
|
||||
func stepRef(s DagStep) string {
|
||||
if s.ID != "" {
|
||||
return s.ID
|
||||
}
|
||||
return s.Name
|
||||
}
|
||||
Reference in New Issue
Block a user