Files
iqudo-top1/internal/run/engine.go
2026-08-18 16:27:02 +08:00

172 lines
4.4 KiB
Go

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