veylant/internal/routing/condition.go
2026-02-23 13:35:04 +01:00

212 lines
5.5 KiB
Go

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
}
}