231 lines
6.0 KiB
Go
231 lines
6.0 KiB
Go
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
|
||
|
|
}
|