package routing import ( "fmt" "regexp" "strings" ) // JSONPathResolver resolves JSONPath expressions like ${input.repo}, ${Clone.output.path} type JSONPathResolver struct { input map[string]interface{} stepResults map[string]interface{} } // NewJSONPathResolver creates a new resolver with input and step results func NewJSONPathResolver(input map[string]interface{}, stepResults map[string]interface{}) *JSONPathResolver { return &JSONPathResolver{ input: input, stepResults: stepResults, } } // Resolve resolves a single JSONPath expression // Supports: ${input.field}, ${StepName.output.field}, ${StepName.output.nested.field} func (r *JSONPathResolver) Resolve(expr string) (interface{}, error) { if expr == "" { return nil, fmt.Errorf("expression cannot be empty") } // Check if it's a template expression (starts with ${ and ends with }) if !strings.HasPrefix(expr, "${") || !strings.HasSuffix(expr, "}") { // Return as-is if not a template return expr, nil } // Extract the path from ${...} path := strings.TrimPrefix(expr, "${") path = strings.TrimSuffix(path, "}") return r.resolvePath(path) } // ResolveString resolves a string that may contain multiple JSONPath expressions // Example: "Analysis at ${Clone.output.path} completed" func (r *JSONPathResolver) ResolveString(str string) (string, error) { // Find all ${...} patterns pattern := regexp.MustCompile(`\$\{[^}]+\}`) result := str matches := pattern.FindAllString(str, -1) for _, match := range matches { value, err := r.Resolve(match) if err != nil { return "", err } // Convert value to string strValue := fmt.Sprintf("%v", value) result = strings.ReplaceAll(result, match, strValue) } return result, nil } // ResolvePaths resolves all JSONPath expressions in a map recursively func (r *JSONPathResolver) ResolvePaths(data map[string]interface{}) (map[string]interface{}, error) { result := make(map[string]interface{}) for key, value := range data { resolved, err := r.resolveValue(value) if err != nil { return nil, fmt.Errorf("failed to resolve key '%s': %w", key, err) } result[key] = resolved } return result, nil } // resolvePath resolves a dot-separated path // Paths can be: input.field, StepName.output.field, etc. func (r *JSONPathResolver) resolvePath(path string) (interface{}, error) { parts := strings.Split(path, ".") if len(parts) == 0 { return nil, fmt.Errorf("invalid path: %s", path) } // Check if first part is "input" if parts[0] == "input" { return r.resolveFromInput(parts[1:]) } // Otherwise, assume it's a step name stepName := parts[0] stepData, ok := r.stepResults[stepName] if !ok { return nil, fmt.Errorf("step '%s' not found in results", stepName) } // Navigate through remaining parts return r.navigateObject(stepData, parts[1:]) } // resolveFromInput resolves path from input data func (r *JSONPathResolver) resolveFromInput(parts []string) (interface{}, error) { if len(parts) == 0 { return r.input, nil } return r.navigateObject(r.input, parts) } // navigateObject navigates through an object using path parts func (r *JSONPathResolver) navigateObject(obj interface{}, parts []string) (interface{}, error) { current := obj for i, part := range parts { if current == nil { return nil, fmt.Errorf("cannot navigate through nil at part %d (%s)", i, part) } // Handle map if mapObj, ok := current.(map[string]interface{}); ok { value, exists := mapObj[part] if !exists { return nil, fmt.Errorf("key '%s' not found in object", part) } current = value continue } // Handle map[string]string if strMap, ok := current.(map[string]string); ok { value, exists := strMap[part] if !exists { return nil, fmt.Errorf("key '%s' not found in string map", part) } current = value continue } // Cannot navigate further return nil, fmt.Errorf("cannot navigate through non-object type at part %d (%s)", i, part) } return current, nil } // resolveValue recursively resolves values (strings, maps, slices) func (r *JSONPathResolver) resolveValue(value interface{}) (interface{}, error) { if value == nil { return nil, nil } switch v := value.(type) { case string: // Try to resolve as JSONPath if strings.Contains(v, "${") { // Check if it's a pure template (only one expression filling the whole string) if strings.HasPrefix(v, "${") && strings.HasSuffix(v, "}") && strings.Count(v, "${") == 1 { // Pure template - resolve as object resolved, err := r.Resolve(v) if err != nil { return nil, err } return resolved, nil } // String with embedded expressions - resolve as string resolved, err := r.ResolveString(v) if err != nil { return nil, err } return resolved, nil } return v, nil case map[string]interface{}: // Recursively resolve map return r.ResolvePaths(v) case []interface{}: // Recursively resolve slice result := make([]interface{}, len(v)) for i, item := range v { resolved, err := r.resolveValue(item) if err != nil { return nil, err } result[i] = resolved } return result, nil default: // Return as-is for other types return v, nil } } // ValidatePath checks if a path is valid (doesn't guarantee it resolves) func (r *JSONPathResolver) ValidatePath(path string) error { if !strings.Contains(path, ".") && path != "input" { return fmt.Errorf("invalid path: must contain '.' or be 'input'") } parts := strings.Split(path, ".") if len(parts) == 0 { return fmt.Errorf("invalid path: no parts") } return nil } // GetAvailableSteps returns list of available steps in step results func (r *JSONPathResolver) GetAvailableSteps() []string { steps := make([]string, 0, len(r.stepResults)) for step := range r.stepResults { steps = append(steps, step) } return steps } // GetInputFields returns list of available input fields func (r *JSONPathResolver) GetInputFields() []string { fields := make([]string, 0, len(r.input)) for field := range r.input { fields = append(fields, field) } return fields }