c3dfc9315f
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>
84 lines
2.1 KiB
Go
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
|
|
}
|