212 lines
5.5 KiB
Go
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
|
|
}
|
|
}
|