package routing import ( "fmt" "strings" ) // SupportedFields lists the RoutingContext attributes that can be tested. var SupportedFields = []string{ "user.role", "user.department", "request.sensitivity", "request.model", "request.use_case", "request.token_estimate", } // SupportedOperators lists the comparison operators for conditions. var SupportedOperators = []string{ "eq", "neq", // equality "in", "nin", // set membership "gte", "lte", // ordered comparison (sensitivity ordinal, token_estimate numeric) "contains", "matches", // string operations } // IsValidField returns true if field is a known context attribute. func IsValidField(field string) bool { for _, f := range SupportedFields { if f == field { return true } } return false } // IsValidOperator returns true if op is a known operator. func IsValidOperator(op string) bool { for _, o := range SupportedOperators { if o == op { return true } } return false } // Evaluate returns true when the condition holds for rctx. // Unknown fields or operators default to false (fail-safe). func (c Condition) Evaluate(rctx *RoutingContext) bool { switch c.Field { case "user.role": return evalString(c.Operator, rctx.UserRole, c.Value) case "user.department": return evalString(c.Operator, rctx.Department, c.Value) case "request.sensitivity": return evalSensitivity(c.Operator, rctx.Sensitivity, c.Value) case "request.model": return evalString(c.Operator, rctx.Model, c.Value) case "request.use_case": return evalString(c.Operator, rctx.UseCase, c.Value) case "request.token_estimate": return evalInt(c.Operator, rctx.TokenEstimate, c.Value) default: return false } } // ValidateConditions checks that all conditions in a slice have valid field+operator pairs. // Returns a descriptive error for the first invalid condition found. func ValidateConditions(conditions []Condition) error { for i, c := range conditions { if !IsValidField(c.Field) { return fmt.Errorf("condition[%d]: unknown field %q", i, c.Field) } if !IsValidOperator(c.Operator) { return fmt.Errorf("condition[%d]: unknown operator %q", i, c.Operator) } } return nil } // ─── String field evaluator ─────────────────────────────────────────────────── func evalString(op, fieldVal string, value interface{}) bool { switch op { case "eq": return fieldVal == fmt.Sprintf("%v", value) case "neq": return fieldVal != fmt.Sprintf("%v", value) case "contains": return strings.Contains(fieldVal, fmt.Sprintf("%v", value)) case "matches": // prefix match return strings.HasPrefix(fieldVal, fmt.Sprintf("%v", value)) case "in": return inSlice(fieldVal, value) case "nin": return !inSlice(fieldVal, value) default: return false } } // ─── Sensitivity (ordinal) evaluator ───────────────────────────────────────── func evalSensitivity(op string, fieldVal Sensitivity, value interface{}) bool { // in/nin don't need a single parsed level — they work on a slice directly. switch op { case "in": return inSensitivitySlice(fieldVal, value) case "nin": return !inSensitivitySlice(fieldVal, value) } target, ok := ParseSensitivity(fmt.Sprintf("%v", value)) if !ok { return false } switch op { case "eq": return fieldVal == target case "neq": return fieldVal != target case "gte": return fieldVal >= target case "lte": return fieldVal <= target default: return false } } // ─── Integer (token_estimate) evaluator ────────────────────────────────────── func evalInt(op string, fieldVal int, value interface{}) bool { target, ok := toInt(value) if !ok { return false } switch op { case "eq": return fieldVal == target case "neq": return fieldVal != target case "gte": return fieldVal >= target case "lte": return fieldVal <= target default: return false } } // ─── Helpers ───────────────────────────────────────────────────────────────── // inSlice returns true if s is in the JSON array represented by value. func inSlice(s string, value interface{}) bool { arr, ok := toStringSlice(value) if !ok { return false } for _, v := range arr { if v == s { return true } } return false } func inSensitivitySlice(s Sensitivity, value interface{}) bool { arr, ok := toStringSlice(value) if !ok { return false } for _, v := range arr { if lv, ok2 := ParseSensitivity(v); ok2 && lv == s { return true } } return false } // toStringSlice tries to convert value (JSON deserialized as []interface{}) to []string. func toStringSlice(value interface{}) ([]string, bool) { raw, ok := value.([]interface{}) if !ok { // Also handle []string directly (from Go code, not JSON) if ss, ok2 := value.([]string); ok2 { return ss, true } return nil, false } out := make([]string, 0, len(raw)) for _, v := range raw { out = append(out, fmt.Sprintf("%v", v)) } return out, true } // toInt tries to convert a JSON-decoded value to int. // JSON numbers arrive as float64. func toInt(value interface{}) (int, bool) { switch v := value.(type) { case int: return v, true case int64: return int(v), true case float64: return int(v), true default: return 0, false } }