Files
fn_registry/functions/core/dag_validate.go
T
egutierrez 3136eb862f 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>
2026-04-12 13:05:05 +02:00

84 lines
2.1 KiB
Go

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
}