feat: condition evaluator for rule filtering
This commit is contained in:
@@ -0,0 +1,98 @@
|
|||||||
|
package condition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"aiaa-notification-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Evaluate checks all conditions against data. Returns true if ALL conditions pass.
|
||||||
|
// An empty conditions slice always returns true.
|
||||||
|
func Evaluate(conditions []model.Condition, data map[string]interface{}) bool {
|
||||||
|
if len(conditions) == 0 {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
for _, c := range conditions {
|
||||||
|
if !evaluateOne(c, data) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func evaluateOne(c model.Condition, data map[string]interface{}) bool {
|
||||||
|
fieldVal, fieldExists := data[c.Field]
|
||||||
|
|
||||||
|
switch c.Op {
|
||||||
|
case "exists":
|
||||||
|
return fieldExists
|
||||||
|
case "not_exists":
|
||||||
|
return !fieldExists
|
||||||
|
case "eq":
|
||||||
|
if !fieldExists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", fieldVal) == c.Value
|
||||||
|
case "ne":
|
||||||
|
if !fieldExists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("%v", fieldVal) != c.Value
|
||||||
|
case "contains":
|
||||||
|
if !fieldExists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return strings.Contains(fmt.Sprintf("%v", fieldVal), c.Value)
|
||||||
|
case "gt", "gte", "lt", "lte":
|
||||||
|
if !fieldExists {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
return compareNumeric(fieldVal, c.Value, c.Op)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func compareNumeric(fieldVal interface{}, value string, op string) bool {
|
||||||
|
fv, err := toFloat64(fieldVal)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
cv, err := strconv.ParseFloat(value, 64)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch op {
|
||||||
|
case "gt":
|
||||||
|
return fv > cv
|
||||||
|
case "gte":
|
||||||
|
return fv >= cv
|
||||||
|
case "lt":
|
||||||
|
return fv < cv
|
||||||
|
case "lte":
|
||||||
|
return fv <= cv
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func toFloat64(v interface{}) (float64, error) {
|
||||||
|
switch val := v.(type) {
|
||||||
|
case float64:
|
||||||
|
return val, nil
|
||||||
|
case float32:
|
||||||
|
return float64(val), nil
|
||||||
|
case int:
|
||||||
|
return float64(val), nil
|
||||||
|
case int64:
|
||||||
|
return float64(val), nil
|
||||||
|
case string:
|
||||||
|
return strconv.ParseFloat(val, 64)
|
||||||
|
case json.Number:
|
||||||
|
return val.Float64()
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("cannot convert %T to float64", v)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,56 @@
|
|||||||
|
package condition
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"aiaa-notification-service/internal/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEvaluate_Empty(t *testing.T) {
|
||||||
|
if !Evaluate(nil, nil) {
|
||||||
|
t.Error("empty conditions should pass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluate_Exists(t *testing.T) {
|
||||||
|
conds := []model.Condition{{Field: "symbol", Op: "exists"}}
|
||||||
|
data := map[string]interface{}{"symbol": "BTC"}
|
||||||
|
if !Evaluate(conds, data) {
|
||||||
|
t.Error("symbol exists, should pass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluate_NotExists(t *testing.T) {
|
||||||
|
conds := []model.Condition{{Field: "symbol", Op: "not_exists"}}
|
||||||
|
data := map[string]interface{}{"price": 100}
|
||||||
|
if !Evaluate(conds, data) {
|
||||||
|
t.Error("symbol not exists, should pass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluate_Gt(t *testing.T) {
|
||||||
|
conds := []model.Condition{{Field: "price", Op: "gt", Value: "100"}}
|
||||||
|
data := map[string]interface{}{"price": float64(200)}
|
||||||
|
if !Evaluate(conds, data) {
|
||||||
|
t.Error("200 > 100, should pass")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluate_Fail(t *testing.T) {
|
||||||
|
conds := []model.Condition{
|
||||||
|
{Field: "symbol", Op: "exists"},
|
||||||
|
{Field: "price", Op: "lt", Value: "100"},
|
||||||
|
}
|
||||||
|
data := map[string]interface{}{"symbol": "BTC", "price": float64(200)}
|
||||||
|
if Evaluate(conds, data) {
|
||||||
|
t.Error("200 < 100 is false, should fail")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestEvaluate_Contains(t *testing.T) {
|
||||||
|
conds := []model.Condition{{Field: "msg", Op: "contains", Value: "error"}}
|
||||||
|
data := map[string]interface{}{"msg": "connection error occurred"}
|
||||||
|
if !Evaluate(conds, data) {
|
||||||
|
t.Error("msg contains 'error', should pass")
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user