172 lines
4.4 KiB
Go
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
|
|
}
|
|
}
|