package run import ( "encoding/json" "fmt" "reflect" "strings" ) func matchCondition(raw json.RawMessage, answers map[string]interface{}) (bool, error) { if len(raw) == 0 || string(raw) == "{}" || string(raw) == "null" { return true, nil } var rule interface{} if err := json.Unmarshal(raw, &rule); err != nil { return false, err } return matchConditionValue(rule, answers) } func matchConditionValue(value interface{}, answers map[string]interface{}) (bool, error) { rule, ok := value.(map[string]interface{}) if !ok { return false, fmt.Errorf("invalid condition") } all, hasAll := rule["all"] any, hasAny := rule["any"] if hasAll && hasAny { return false, fmt.Errorf("condition cannot contain both all and any") } if hasAll { items, ok := all.([]interface{}) if !ok || len(items) == 0 { return false, fmt.Errorf("invalid all condition") } for _, item := range items { matched, err := matchConditionValue(item, answers) if err != nil || !matched { return false, err } } return true, nil } if hasAny { items, ok := any.([]interface{}) if !ok || len(items) == 0 { return false, fmt.Errorf("invalid any condition") } for _, item := range items { matched, err := matchConditionValue(item, answers) if err != nil { return false, err } if matched { return true, nil } } return false, nil } return matchRule(rule, answers) } func matchRule(rule map[string]interface{}, answers map[string]interface{}) (bool, error) { field, _ := rule["field"].(string) operator, _ := rule["operator"].(string) if field == "" || operator == "" { return false, fmt.Errorf("condition field and operator are required") } actual, exists := lookupContextValue(answers, field) expected := rule["value"] switch operator { case "exists": return exists && actual != nil && fmt.Sprint(actual) != "", nil case "not_exists": return !exists || actual == nil || fmt.Sprint(actual) == "", nil case "equals": return reflect.DeepEqual(normalizeValue(actual), normalizeValue(expected)), nil case "not_equals": return !reflect.DeepEqual(normalizeValue(actual), normalizeValue(expected)), nil case "contains": return strings.Contains(strings.ToLower(fmt.Sprint(actual)), strings.ToLower(fmt.Sprint(expected))), nil case "greater_than", "less_than": left, leftOK := toFloat(actual) right, rightOK := toFloat(expected) if !leftOK || !rightOK { return false, nil } if operator == "greater_than" { return left > right, nil } return left < right, nil case "in": values, ok := expected.([]interface{}) if !ok { return false, nil } for _, value := range values { if reflect.DeepEqual(normalizeValue(actual), normalizeValue(value)) { return true, nil } } return false, nil default: return false, fmt.Errorf("unsupported operator: %s", operator) } } // lookupContextValue accepts the public namespaced form (input.foo/derived.foo) // and the legacy bare form used by SOP edge conditions. func lookupContextValue(values map[string]interface{}, field string) (interface{}, bool) { if value, ok := values[field]; ok { return value, true } for _, prefix := range []string{"input.", "derived.", "form."} { if strings.HasPrefix(field, prefix) { key := strings.TrimPrefix(field, prefix) if namespace, ok := values[strings.TrimSuffix(prefix, ".")].(map[string]interface{}); ok { value, exists := namespace[key] return value, exists } value, ok := values[key] return value, ok } } return nil, false } func runtimeContext(input, derived, form map[string]interface{}) map[string]interface{} { context := mergeValues(input, derived) context = mergeValues(context, form) context["input"] = input context["derived"] = derived context["form"] = form return context } func normalizeValue(value interface{}) interface{} { switch typed := value.(type) { case json.Number: if number, err := typed.Float64(); err == nil { return number } case int: return float64(typed) case int64: return float64(typed) case uint64: return float64(typed) } return value } func toFloat(value interface{}) (float64, bool) { switch typed := value.(type) { case float64: return typed, true case float32: return float64(typed), true case int: return float64(typed), true case int64: return float64(typed), true case uint64: return float64(typed), true case json.Number: result, err := typed.Float64() return result, err == nil default: return 0, false } }