package workflow import ( "encoding/json" "fmt" "reflect" "strconv" "strings" ) // ExpressionEvaluator evaluates workflow expressions. type ExpressionEvaluator struct { context ContextAccessor } // NewExpressionEvaluator creates a new expression evaluator func NewExpressionEvaluator(ctx ContextAccessor) *ExpressionEvaluator { return &ExpressionEvaluator{ context: ctx, } } // EvaluateValue evaluates an arbitrary JSON value using the = prefix convention: // - Non-string types (bool, number, etc.): returned as-is (literal value) // - String not starting with "=": returned as literal string // - String starting with "=": remainder evaluated as expression (e.g. "=$name") // - String starting with "==": leading "=" removed, rest returned as literal string (e.g. "==foo" → "=foo") func (e *ExpressionEvaluator) EvaluateValue(value interface{}) (interface{}, error) { str, ok := value.(string) if !ok { // Non-string types are literal values return value, nil } if !strings.HasPrefix(str, "=") { // No = prefix: literal string return str, nil } if strings.HasPrefix(str, "==") { // == prefix: escaped literal, strip one leading = return str[1:], nil } // Single = prefix: evaluate the rest as expression return e.Evaluate(str[1:]) } // Evaluate evaluates an expression string and returns the result func (e *ExpressionEvaluator) Evaluate(expr string) (interface{}, error) { if expr == "" { return nil, nil } expr = strings.TrimSpace(expr) // Handle string literals (must be a simple quoted string, not a complex expression) if isSimpleStringLiteral(expr) { return expr[1 : len(expr)-1], nil } // Handle boolean literals if expr == "true" { return true, nil } if expr == "false" { return false, nil } // Handle null/nil if expr == "null" || expr == "nil" { return nil, nil } // Handle numeric literals if num, err := strconv.ParseInt(expr, 10, 64); err == nil { return num, nil } if num, err := strconv.ParseFloat(expr, 64); err == nil { return num, nil } // Handle JSON literal arrays and objects if strings.HasPrefix(expr, "[") || strings.HasPrefix(expr, "{") { var parsed interface{} if err := json.Unmarshal([]byte(expr), &parsed); err == nil { return parsed, nil } } // Handle logical operators (check before comparison operators) if strings.Contains(expr, "&&") { return e.evaluateLogicalAnd(expr) } if strings.Contains(expr, "||") { return e.evaluateLogicalOr(expr) } // Handle comparison operators (check before variable references) if strings.Contains(expr, "==") { return e.evaluateBinaryOp(expr, "==") } if strings.Contains(expr, "!=") { return e.evaluateBinaryOp(expr, "!=") } if strings.Contains(expr, ">=") { return e.evaluateBinaryOp(expr, ">=") } if strings.Contains(expr, "<=") { return e.evaluateBinaryOp(expr, "<=") } if strings.Contains(expr, ">") { return e.evaluateBinaryOp(expr, ">") } if strings.Contains(expr, "<") { return e.evaluateBinaryOp(expr, "<") } // Handle negation if strings.HasPrefix(expr, "!") { val, err := e.Evaluate(strings.TrimSpace(expr[1:])) if err != nil { return nil, err } return !toBool(val), nil } // Handle arithmetic operators if e.findOperatorIndex(expr, "+") != -1 { return e.evaluateArithmetic(expr, "+") } if e.findOperatorIndex(expr, "-") != -1 && !e.isSimpleNegativeNumber(expr) { return e.evaluateArithmetic(expr, "-") } if strings.Contains(expr, "*") { return e.evaluateArithmetic(expr, "*") } if strings.Contains(expr, "/") { return e.evaluateArithmetic(expr, "/") } // Handle variable references if strings.HasPrefix(expr, "$") || strings.HasPrefix(expr, "_") || strings.HasPrefix(expr, "SYSVAR.") { return e.evaluateVariableReference(expr) } // Try to resolve as a path (could be param reference or variable reference) return e.evaluateVariableReference(expr) } // isSimpleStringLiteral checks if expr is a simple quoted string (not a complex expression) func isSimpleStringLiteral(expr string) bool { if len(expr) < 2 { return false } quote := expr[0] if quote != '"' && quote != '\'' { return false } if expr[len(expr)-1] != quote { return false } // Check that there are no unescaped quotes in the middle for i := 1; i < len(expr)-1; i++ { if expr[i] == quote && (i == 0 || expr[i-1] != '\\') { return false } } return true } // isSimpleNegativeNumber checks if expr is just a negative number like "-5" func (e *ExpressionEvaluator) isSimpleNegativeNumber(expr string) bool { if !strings.HasPrefix(expr, "-") { return false } rest := strings.TrimSpace(expr[1:]) if _, err := strconv.ParseInt(rest, 10, 64); err == nil { return true } if _, err := strconv.ParseFloat(rest, 64); err == nil { return true } return false } // evaluateVariableReference resolves variable references and paths func (e *ExpressionEvaluator) evaluateVariableReference(path string) (interface{}, error) { parts := e.parsePath(path) if len(parts) == 0 { return nil, fmt.Errorf("empty variable path") } // Get the root variable var root interface{} var err error startIndex := 1 rootName := parts[0] if strings.HasPrefix(rootName, "$") { // Global variable root, err = e.getGlobalVariable(rootName) } else if rootName == "SYSVAR" { // System variable - combine SYSVAR with next part for lookup if len(parts) < 2 { return nil, fmt.Errorf("incomplete SYSVAR reference") } sysvarKey := "SYSVAR." + parts[1] root, err = e.getSystemVariable(sysvarKey) startIndex = 2 } else if strings.HasPrefix(rootName, "_") { // Local variable root, err = e.getLocalVariable(rootName) } else { // Try as parameter reference root, err = e.getParameter(rootName) if err != nil { return nil, err } } if err != nil { return nil, err } // Navigate the path current := root for i := startIndex; i < len(parts); i++ { part := parts[i] current, err = e.navigatePath(current, part) if err != nil { return nil, err } } return current, nil } // parsePath parses a variable path into segments // Handles: $var.field, $var[index], $var["literal"], SYSVAR.xxx func (e *ExpressionEvaluator) parsePath(path string) []string { var parts []string current := "" inBracket := false bracketContent := "" for i := 0; i < len(path); i++ { ch := path[i] switch ch { case '.': if inBracket { bracketContent += string(ch) } else { if current != "" { parts = append(parts, current) current = "" } } case '[': if current != "" { parts = append(parts, current) current = "" } inBracket = true bracketContent = "" case ']': if inBracket { // Evaluate bracket content as expression val, err := e.Evaluate(bracketContent) if err == nil { parts = append(parts, fmt.Sprintf("[%v]", val)) } inBracket = false bracketContent = "" } default: if inBracket { bracketContent += string(ch) } else { current += string(ch) } } } if current != "" { parts = append(parts, current) } return parts } // navigatePath navigates one level in a nested structure func (e *ExpressionEvaluator) navigatePath(obj interface{}, key string) (interface{}, error) { if obj == nil { return nil, fmt.Errorf("cannot navigate path on nil object") } // Handle .length property on arrays, slices, and strings if key == "length" { val := reflect.ValueOf(obj) switch val.Kind() { case reflect.Slice, reflect.Array: return int64(val.Len()), nil case reflect.String: // Return the number of Unicode code points (runes), not bytes return int64(len([]rune(val.String()))), nil } // Fall through to map lookup so user-defined "length" keys still work } // Handle array/slice index: [index] if strings.HasPrefix(key, "[") && strings.HasSuffix(key, "]") { indexStr := key[1 : len(key)-1] index, err := strconv.Atoi(indexStr) if err != nil { return nil, fmt.Errorf("invalid array index: %s", indexStr) } val := reflect.ValueOf(obj) if val.Kind() == reflect.Slice || val.Kind() == reflect.Array { if index < 0 || index >= val.Len() { return nil, fmt.Errorf("index out of range: %d", index) } return val.Index(index).Interface(), nil } return nil, fmt.Errorf("cannot index non-array type") } // Handle map/struct field access val := reflect.ValueOf(obj) if val.Kind() == reflect.Map { mapVal := val.MapIndex(reflect.ValueOf(key)) if mapVal.IsValid() { return mapVal.Interface(), nil } return nil, nil } if val.Kind() == reflect.Struct { field := val.FieldByName(key) if field.IsValid() { return field.Interface(), nil } return nil, fmt.Errorf("field not found: %s", key) } return nil, fmt.Errorf("cannot access field on type: %T", obj) } // getParameter retrieves a parameter func (e *ExpressionEvaluator) getParameter(name string) (interface{}, error) { if val, ok := e.context.GetParam(name); ok { return val, nil } return nil, fmt.Errorf("parameter not found: %s", name) } // getGlobalVariable retrieves a global variable func (e *ExpressionEvaluator) getGlobalVariable(name string) (interface{}, error) { if val, ok := e.context.GetVariable(name); ok { return val, nil } return nil, nil // Undefined variables return nil } // getSystemVariable retrieves a system variable func (e *ExpressionEvaluator) getSystemVariable(name string) (interface{}, error) { if val, ok := e.context.GetSystemVar(name); ok { return val, nil } return nil, fmt.Errorf("system variable not found: %s", name) } // getLocalVariable retrieves a local variable func (e *ExpressionEvaluator) getLocalVariable(name string) (interface{}, error) { if val, ok := e.context.GetLocalVar(name); ok { return val, nil } return nil, fmt.Errorf("local variable not found: %s", name) } // evaluateBinaryOp evaluates binary comparison operations func (e *ExpressionEvaluator) evaluateBinaryOp(expr string, op string) (interface{}, error) { parts := strings.SplitN(expr, op, 2) if len(parts) != 2 { return nil, fmt.Errorf("invalid binary operation: %s", expr) } left, err := e.Evaluate(strings.TrimSpace(parts[0])) if err != nil { return nil, err } right, err := e.Evaluate(strings.TrimSpace(parts[1])) if err != nil { return nil, err } return e.compare(left, right, op) } // compare compares two values based on the operator func (e *ExpressionEvaluator) compare(left, right interface{}, op string) (bool, error) { switch op { case "==": return e.equals(left, right), nil case "!=": return !e.equals(left, right), nil case ">", ">=", "<", "<=": return e.compareNumeric(left, right, op) default: return false, fmt.Errorf("unknown operator: %s", op) } } // equals compares two values for equality, normalizing numeric types func (e *ExpressionEvaluator) equals(left, right interface{}) bool { // Handle nil cases if left == nil && right == nil { return true } if left == nil || right == nil { return false } // Normalize numeric types for comparison if isNumeric(left) && isNumeric(right) { return toFloat64(left) == toFloat64(right) } return reflect.DeepEqual(left, right) } func isNumeric(val interface{}) bool { switch val.(type) { case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64, float32, float64: return true default: return false } } // compareNumeric compares numeric values func (e *ExpressionEvaluator) compareNumeric(left, right interface{}, op string) (bool, error) { l := toFloat64(left) r := toFloat64(right) switch op { case ">": return l > r, nil case ">=": return l >= r, nil case "<": return l < r, nil case "<=": return l <= r, nil default: return false, fmt.Errorf("unknown numeric operator: %s", op) } } // evaluateLogicalAnd evaluates logical AND func (e *ExpressionEvaluator) evaluateLogicalAnd(expr string) (interface{}, error) { parts := strings.Split(expr, "&&") for _, part := range parts { val, err := e.Evaluate(strings.TrimSpace(part)) if err != nil { return nil, err } if !toBool(val) { return false, nil } } return true, nil } // evaluateLogicalOr evaluates logical OR func (e *ExpressionEvaluator) evaluateLogicalOr(expr string) (interface{}, error) { parts := strings.Split(expr, "||") for _, part := range parts { val, err := e.Evaluate(strings.TrimSpace(part)) if err != nil { return nil, err } if toBool(val) { return true, nil } } return false, nil } // evaluateArithmetic evaluates arithmetic operations func (e *ExpressionEvaluator) evaluateArithmetic(expr string, op string) (interface{}, error) { // Find the operator (avoiding string literals) opIndex := e.findOperatorIndex(expr, op) if opIndex == -1 { return nil, fmt.Errorf("operator not found: %s", op) } leftExpr := strings.TrimSpace(expr[:opIndex]) rightExpr := strings.TrimSpace(expr[opIndex+len(op):]) left, err := e.Evaluate(leftExpr) if err != nil { return nil, err } right, err := e.Evaluate(rightExpr) if err != nil { return nil, err } // Handle string concatenation with + if op == "+" { if isString(left) || isString(right) { return toString(left) + toString(right), nil } } // Numeric operations l := toFloat64(left) r := toFloat64(right) switch op { case "+": return l + r, nil case "-": return l - r, nil case "*": return l * r, nil case "/": if r == 0 { return nil, fmt.Errorf("division by zero") } return l / r, nil default: return nil, fmt.Errorf("unknown arithmetic operator: %s", op) } } // findOperatorIndex finds the index of an operator, avoiding string literals func (e *ExpressionEvaluator) findOperatorIndex(expr string, op string) int { inString := false stringChar := rune(0) for i := 0; i < len(expr)-len(op)+1; i++ { ch := rune(expr[i]) if ch == '"' || ch == '\'' { if !inString { inString = true stringChar = ch } else if ch == stringChar { inString = false } } if !inString && strings.HasPrefix(expr[i:], op) { return i } } return -1 } // Helper functions func toBool(val interface{}) bool { if val == nil { return false } switch v := val.(type) { case bool: return v case int, int64, float64: return toFloat64(v) != 0 case string: return v != "" default: return true } } func toFloat64(val interface{}) float64 { if val == nil { return 0 } switch v := val.(type) { case int: return float64(v) case int64: return float64(v) case float64: return v case float32: return float64(v) case string: f, _ := strconv.ParseFloat(v, 64) return f default: return 0 } } func toString(val interface{}) string { if val == nil { return "" } return fmt.Sprintf("%v", val) } func isString(val interface{}) bool { _, ok := val.(string) return ok } // SetVariable sets a variable value using a path expression func (e *ExpressionEvaluator) SetVariable(path string, value interface{}) error { parts := e.parsePath(path) if len(parts) == 0 { return fmt.Errorf("empty variable path") } rootName := parts[0] // Check if trying to set a parameter (read-only) if !strings.HasPrefix(rootName, "$") && !strings.HasPrefix(rootName, "_") { // This is a parameter reference - check if it exists if _, ok := e.context.GetParam(rootName); ok { return fmt.Errorf("cannot modify parameter: %s (parameters are read-only)", rootName) } } // Simple case: direct assignment if len(parts) == 1 { if strings.HasPrefix(rootName, "$") { // Apply type conversion if needed convertedValue, err := e.applyTypeConversion(rootName, value) if err != nil { return err } e.context.SetVariable(rootName, convertedValue) return nil } return fmt.Errorf("cannot set non-global variable: %s", rootName) } // Special case: $var[index] - direct slice index assignment with auto-grow if len(parts) == 2 && strings.HasPrefix(rootName, "$") { lastKey := parts[1] if strings.HasPrefix(lastKey, "[") && strings.HasSuffix(lastKey, "]") { indexStr := lastKey[1 : len(lastKey)-1] index, err := strconv.Atoi(indexStr) if err == nil { // Use atomic SetArrayIndex to avoid race conditions e.context.SetArrayIndex(rootName, index, value) return nil } } } // Deep set with auto-creation of intermediates if !strings.HasPrefix(rootName, "$") { return fmt.Errorf("cannot set non-global variable: %s", rootName) } root, _ := e.context.GetVariable(rootName) if root == nil { // Auto-create root: slice if next part is [N], map otherwise if isArrayIndex(parts[1]) { root = make([]interface{}, 0) } else { root = make(map[string]interface{}) } e.context.SetVariable(rootName, root) } // Navigate to parent of the final field, auto-creating intermediates. // We track (parent, key) so we can propagate slice growth back up. current := root for i := 1; i < len(parts)-1; i++ { key := parts[i] // Determine what type the next level should be nextIsArray := i+1 < len(parts) && isArrayIndex(parts[i+1]) if isArrayIndex(key) { idx, _ := strconv.Atoi(key[1 : len(key)-1]) slice, ok := current.([]interface{}) if !ok { return fmt.Errorf("cannot index non-array type %T at path segment %s", current, key) } // Auto-grow slice if idx >= len(slice) { grown := make([]interface{}, idx+1) copy(grown, slice) slice = grown // Propagate grown slice back: if root level, update variable if i == 1 { e.context.SetVariable(rootName, slice) } } // Auto-create element if nil if slice[idx] == nil { if nextIsArray { slice[idx] = make([]interface{}, 0) } else { slice[idx] = make(map[string]interface{}) } } current = slice[idx] } else { m, ok := current.(map[string]interface{}) if !ok { return fmt.Errorf("cannot access field %s on type %T", key, current) } if m[key] == nil { if nextIsArray { m[key] = make([]interface{}, 0) } else { m[key] = make(map[string]interface{}) } } current = m[key] } } // Set the final field lastKey := parts[len(parts)-1] return e.setField(current, lastKey, value) } // setField sets a field on an object func (e *ExpressionEvaluator) setField(obj interface{}, key string, value interface{}) error { if obj == nil { return fmt.Errorf("cannot set field on nil object") } // Handle map if m, ok := obj.(map[string]interface{}); ok { m[key] = value return nil } // Handle slice index assignment (key is "[N]" format) if strings.HasPrefix(key, "[") && strings.HasSuffix(key, "]") { indexStr := key[1 : len(key)-1] index, err := strconv.Atoi(indexStr) if err != nil { return fmt.Errorf("invalid array index: %s", indexStr) } rv := reflect.ValueOf(obj) if rv.Kind() == reflect.Slice { // Grow slice if necessary if index >= rv.Len() { // We need to get the pointer to the slice in the parent to grow it // For now, return an error if index is out of bounds return fmt.Errorf("array index %d out of bounds (length %d)", index, rv.Len()) } rv.Index(index).Set(reflect.ValueOf(value)) return nil } } return fmt.Errorf("cannot set field on type: %T", obj) } // isArrayIndex returns true if the path segment is an array index like "[0]", "[2]" func isArrayIndex(segment string) bool { return strings.HasPrefix(segment, "[") && strings.HasSuffix(segment, "]") } // EvaluateDeep recursively evaluates expressions in nested structures (maps, arrays) // It walks through the entire structure and evaluates any string values that look like expressions func (e *ExpressionEvaluator) EvaluateDeep(value interface{}) (interface{}, error) { switch v := value.(type) { case string: // Evaluate string values using EvaluateValue (handles = prefix convention) return e.EvaluateValue(v) case map[string]interface{}: // Recursively evaluate all values in the map result := make(map[string]interface{}) for key, val := range v { evaluated, err := e.EvaluateDeep(val) if err != nil { return nil, fmt.Errorf("failed to evaluate map key %s: %w", key, err) } result[key] = evaluated } return result, nil case []interface{}: // Recursively evaluate all elements in the array result := make([]interface{}, len(v)) for i, val := range v { evaluated, err := e.EvaluateDeep(val) if err != nil { return nil, fmt.Errorf("failed to evaluate array index %d: %w", i, err) } result[i] = evaluated } return result, nil default: // For other types (int, bool, float, etc.), return as-is return value, nil } } // applyTypeConversion applies type conversion based on variable type declaration // If the variable is declared as OBJECT and the value is a string, it parses the string as JSON // If the variable is declared as an array type (e.g., [OBJECT], [STRING]) and the value is a string, it parses as JSON array // If the variable is declared as STRING and the value is not a string, it marshals the value to JSON func (e *ExpressionEvaluator) applyTypeConversion(varName string, value interface{}) (interface{}, error) { // Get base context to access VarTypes baseCtx := e.context.GetBaseContext() if baseCtx.VarTypes == nil { return value, nil } // Check if variable has a type declaration varType, ok := baseCtx.VarTypes[varName] if !ok { return value, nil } // Check if type is an array type like [OBJECT], [STRING], etc. if strings.HasPrefix(varType, "[") && strings.HasSuffix(varType, "]") { // Array type if str, ok := value.(string); ok { var result []interface{} if err := json.Unmarshal([]byte(str), &result); err != nil { return nil, fmt.Errorf("failed to convert string to %s for variable %s (original string: %q): %w", varType, varName, str, err) } return result, nil } } else if varType == "OBJECT" { // If type is OBJECT and value is string, parse as JSON if str, ok := value.(string); ok { var result map[string]interface{} if err := json.Unmarshal([]byte(str), &result); err != nil { return nil, fmt.Errorf("failed to convert string to OBJECT for variable %s (original string: %q): %w", varName, str, err) } return result, nil } } else if varType == "INT" { // If type is INT and value is a string, parse as integer if str, ok := value.(string); ok { if n, err := strconv.ParseInt(str, 10, 64); err == nil { return n, nil } // Also try parsing as float then truncating if f, err := strconv.ParseFloat(str, 64); err == nil { return int64(f), nil } return nil, fmt.Errorf("failed to convert string %q to INT for variable %s", str, varName) } // If value is float64, convert to int64 if f, ok := value.(float64); ok { return int64(f), nil } } else if varType == "FLOAT" { // If type is FLOAT and value is a string, parse as float if str, ok := value.(string); ok { if f, err := strconv.ParseFloat(str, 64); err == nil { return f, nil } return nil, fmt.Errorf("failed to convert string %q to FLOAT for variable %s", str, varName) } // If value is int64, convert to float64 if n, ok := value.(int64); ok { return float64(n), nil } } else if varType == "STRING" { // If type is STRING and value is not a string, marshal to JSON if _, ok := value.(string); !ok { // For non-string values (objects, arrays, etc.), marshal to JSON string jsonBytes, err := json.Marshal(value) if err != nil { return nil, fmt.Errorf("failed to convert %T to STRING for variable %s: %w", value, varName, err) } return string(jsonBytes), nil } } return value, nil }