package routing import ( "fmt" "time" ) // ValidationError represents a single validation error type ValidationError struct { Path string // JSONPath where error occurred Message string // Error message } // ValidationResult contains all validation errors type ValidationResult struct { Valid bool Errors []ValidationError } // Validator validates WorkflowSpec and CronWorkflowSpec type Validator struct { kb *KnowledgeBase } // NewValidator creates a new validator with knowledge base func NewValidator(kb *KnowledgeBase) *Validator { return &Validator{kb: kb} } // ValidateWorkflowSpec validates a one-time workflow spec func (v *Validator) ValidateWorkflowSpec(spec *WorkflowSpec) *ValidationResult { result := &ValidationResult{ Valid: true, Errors: []ValidationError{}, } if spec == nil { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec", Message: "workflow spec cannot be nil", }) return result } // Validate name var errs []ValidationError if spec.Name == "" { result.Valid = false errs = append(errs, ValidationError{ Path: "spec.name", Message: "workflow name is required", }) } // Validate states if len(spec.States) == 0 { result.Valid = false errs = append(errs, ValidationError{ Path: "spec.states", Message: "at least one state is required", }) } // Validate each state stateNames := make(map[string]bool) for i, state := range spec.States { path := fmt.Sprintf("spec.states[%d]", i) if state.Name == "" { result.Valid = false errs = append(errs, ValidationError{ Path: path + ".name", Message: "state name is required", }) continue } if stateNames[state.Name] { result.Valid = false errs = append(errs, ValidationError{ Path: path + ".name", Message: fmt.Sprintf("duplicate state name: %s", state.Name), }) } stateNames[state.Name] = true // Validate state type stateErrs := v.validateState(state, path) if len(stateErrs) > 0 { result.Valid = false errs = append(errs, stateErrs...) } } // Validate state transitions for i, state := range spec.States { path := fmt.Sprintf("spec.states[%d]", i) // Check that Next state exists (if specified) if state.Next != "" && !stateNames[state.Next] { result.Valid = false errs = append(errs, ValidationError{ Path: path + ".next", Message: fmt.Sprintf("state '%s' does not exist", state.Next), }) } // Check that Catch targets exist for j, catchClause := range state.Catch { if catchClause.Next != "" && !stateNames[catchClause.Next] { result.Valid = false errs = append(errs, ValidationError{ Path: path + fmt.Sprintf(".catch[%d].next", j), Message: fmt.Sprintf("state '%s' does not exist", catchClause.Next), }) } } } result.Errors = errs return result } // ValidateCronWorkflowSpec validates a scheduled workflow spec func (v *Validator) ValidateCronWorkflowSpec(spec *CronWorkflowSpec) *ValidationResult { result := &ValidationResult{ Valid: true, Errors: []ValidationError{}, } if spec == nil { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec", Message: "cron workflow spec cannot be nil", }) return result } // Validate name if spec.Name == "" { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.name", Message: "workflow name is required", }) } // Validate type if spec.Type != "CronWorkflow" { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.type", Message: "type must be 'CronWorkflow'", }) } // Validate cron expression if spec.Schedule == "" { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.schedule", Message: "cron schedule is required", }) } else { if err := validateCronExpression(spec.Schedule); err != nil { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.schedule", Message: err.Error(), }) } } // Validate timezone if spec.Timezone == "" { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.timezone", Message: "timezone is required", }) } else { if _, err := time.LoadLocation(spec.Timezone); err != nil { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.timezone", Message: fmt.Sprintf("invalid timezone: %s", spec.Timezone), }) } } // Validate maxConcurrent if spec.MaxConcurrent < 1 { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.maxConcurrent", Message: "maxConcurrent must be >= 1", }) } // Validate states (same as one-time workflow) if len(spec.States) == 0 { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: "spec.states", Message: "at least one state is required", }) } stateNames := make(map[string]bool) for i, state := range spec.States { path := fmt.Sprintf("spec.states[%d]", i) if state.Name == "" { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: path + ".name", Message: "state name is required", }) continue } if stateNames[state.Name] { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: path + ".name", Message: fmt.Sprintf("duplicate state name: %s", state.Name), }) } stateNames[state.Name] = true stateErrs := v.validateState(state, path) if len(stateErrs) > 0 { result.Valid = false result.Errors = append(result.Errors, stateErrs...) } } // Validate state transitions for i, state := range spec.States { path := fmt.Sprintf("spec.states[%d]", i) if state.Next != "" && !stateNames[state.Next] { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: path + ".next", Message: fmt.Sprintf("state '%s' does not exist", state.Next), }) } for j, catchClause := range state.Catch { if catchClause.Next != "" && !stateNames[catchClause.Next] { result.Valid = false result.Errors = append(result.Errors, ValidationError{ Path: path + fmt.Sprintf(".catch[%d].next", j), Message: fmt.Sprintf("state '%s' does not exist", catchClause.Next), }) } } } return result } // validateState validates a single state func (v *Validator) validateState(state State, path string) []ValidationError { var errs []ValidationError switch state.Type { case StateTypeTask: if state.Resource == "" { errs = append(errs, ValidationError{ Path: path + ".resource", Message: "resource is required for Task state", }) } else { // Check if activity exists in knowledge base if v.kb != nil && !v.kb.HasActivity(state.Resource) { errs = append(errs, ValidationError{ Path: path + ".resource", Message: fmt.Sprintf("activity '%s' not found in knowledge base", state.Resource), }) } } // Validate timeout format if state.Timeout != "" { if err := validateDuration(state.Timeout); err != nil { errs = append(errs, ValidationError{ Path: path + ".timeout", Message: err.Error(), }) } } case StateTypePass: // Pass state is valid with just a result if state.Result == nil { errs = append(errs, ValidationError{ Path: path + ".result", Message: "result is required for Pass state", }) } case StateTypeFail: // Fail state requires error if state.Error == "" { errs = append(errs, ValidationError{ Path: path + ".error", Message: "error is required for Fail state", }) } default: errs = append(errs, ValidationError{ Path: path + ".type", Message: fmt.Sprintf("invalid state type: %s", state.Type), }) } return errs } // validateDuration validates Go duration string (e.g., "5m", "30s") func validateDuration(dur string) error { _, err := time.ParseDuration(dur) if err != nil { return fmt.Errorf("invalid duration: %s", dur) } return nil } // String returns a human-readable validation result func (vr *ValidationResult) String() string { if vr.Valid { return "Valid ✓" } msg := fmt.Sprintf("Invalid ✗ (%d errors):\n", len(vr.Errors)) for _, err := range vr.Errors { msg += fmt.Sprintf(" %s: %s\n", err.Path, err.Message) } return msg }