-
Notifications
You must be signed in to change notification settings - Fork 1
/
condition.go
236 lines (216 loc) · 6.87 KB
/
condition.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
package rulesengine
import (
"encoding/json"
"errors"
"fmt"
"reflect"
)
// Condition represents an individual condition within a rule in the rules engine.
// Conditions can compare facts to values using operators, and they can also nest other conditions.
// Fields:
// - Priority: Optional priority of the condition, must be greater than zero if set.
// - Name: The name of the condition.
// - Operator: The operator to be applied for comparison (e.g., equals, greaterThan).
// - Value: The value to compare the fact to.
// - Fact: The fact that is being evaluated in the condition.
// - FactResult: The result of fact evaluation.
// - Result: The evaluation result of the condition (true/false).
// - Params: Additional parameters that may affect the condition's evaluation.
// - Condition: Raw condition string (for debugging or custom use cases).
// - All, Any: Nested conditions that require all or any of the sub-conditions to be true.
// - Not: A nested condition that negates its result.
type Condition struct {
Priority *int
Name string
Operator string
Value ValueNode
Fact string
FactResult Fact
Result bool
Params map[string]interface{}
Condition string
All []*Condition
Any []*Condition
Not *Condition
}
// Validate checks if the Condition is valid based on business rules.
// It verifies that if a value, fact, or operator are set, all three must be set.
// It also ensures that if nested conditions (Any, All, Not) are provided, no value, fact, or operator is set.
// Returns an error if the condition is invalid
func (c *Condition) Validate() error {
// Validate priority (must be greater than 0 if set)
if c.Priority != nil && *c.Priority <= 0 {
return errors.New("priority must be greater than zero")
}
valueExists := c.Value.Type != Null || (c.Value.Type != String && c.Value.String != "")
// Validate that if any of Value, Fact, or Operator are set, all three must be set
if valueExists || c.Operator != "" || c.Fact != "" {
if !valueExists || c.Operator == "" || c.Fact == "" {
return errors.New("if value, operator, or fact are set, all three must be provided")
}
}
// If Any, All, or Not are set, Value, Operator, and Fact must not be set
if (len(c.Any) > 0 || len(c.All) > 0 || c.Not != nil) && (valueExists || c.Operator != "" || c.Fact != "") {
return errors.New("value, operator, and fact must not be set if any, all, or not conditions are provided")
}
return nil
}
// UnmarshalJSON is a custom JSON unmarshaller for the Condition struct.
// It validates the condition after unmarshalling to ensure it adheres to the rules.
// Params:
// - data: JSON data representing the condition.
// Returns an error if the condition is invalid after unmarshalling.
func (c *Condition) UnmarshalJSON(data []byte) error {
// Create a temporary struct to hold the incoming data
type Alias Condition // Alias to avoid infinite recursion inEvaluator UnmarshalJSON
temp := &struct {
*Alias
}{
Alias: (*Alias)(c),
}
// Unmarshal the JSON data into the temp struct
if err := json.Unmarshal(data, &temp); err != nil {
return err
}
// Validate the condition after unmarshaling
if err := c.Validate(); err != nil {
return err
}
return nil
}
// ToJSON converts the Condition instance to a JSON string representation.
// Useful for serializing the condition for storage or transmission.
func (c *Condition) ToJSON(stringify bool) (interface{}, error) {
props := map[string]interface{}{}
if c.Priority != nil {
props["priority"] = *c.Priority
}
if c.Name != "" {
props["name"] = c.Name
}
if oper := c.booleanOperator(); oper != "" {
if c.All != nil {
allConditions := make([]interface{}, len(c.All))
for i, condition := range c.All {
jsonCondition, err := condition.ToJSON(false)
if err != nil {
return nil, err
}
allConditions[i] = jsonCondition
}
props["all"] = allConditions
}
if c.Any != nil {
anyConditions := make([]interface{}, len(c.Any))
for i, condition := range c.Any {
jsonCondition, err := condition.ToJSON(false)
if err != nil {
return nil, err
}
anyConditions[i] = jsonCondition
}
props["any"] = anyConditions
}
if c.Not != nil {
jsonCondition, err := c.Not.ToJSON(false)
if err != nil {
return nil, err
}
props["not"] = jsonCondition
}
} else if c.IsConditionReference() {
props["condition"] = c.Condition
} else {
props["operator"] = c.Operator
props["value"] = c.Value
props["fact"] = c.Fact
props["factResult"] = c.FactResult
props["result"] = c.Result
if c.Params != nil {
props["params"] = c.Params
}
}
if stringify {
jsonStr, err := json.Marshal(props)
if err != nil {
return nil, err
}
return string(jsonStr), nil
}
return props, nil
}
// Evaluate evaluates the condition against the given almanac and operator map
func (c *Condition) Evaluate(almanac *Almanac, operatorMap map[string]Operator) (*EvaluationResult, error) {
if reflect.ValueOf(almanac).IsZero() {
return nil, errors.New("almanac required")
}
if reflect.ValueOf(operatorMap).IsZero() {
return nil, errors.New("operatorMap required")
}
if c.IsBooleanOperator() {
return nil, errors.New("Cannot evaluate() a boolean condition")
}
op, ok := operatorMap[c.Operator]
if !ok {
return nil, fmt.Errorf("Unknown operator: %s", c.Operator)
}
rightHandSideValue := c.Value
leftHandSideValue, err := almanac.FactValue(c.Fact)
if err != nil {
return nil, err
}
var result bool
if leftHandSideValue != nil && leftHandSideValue.Value != nil {
result = op.Evaluate(leftHandSideValue.Value, &rightHandSideValue)
// TODO VALUE
Debug(fmt.Sprintf(`condition::evaluate <%v %s %v?> (%v)`, leftHandSideValue.Value.Raw(), c.Operator, rightHandSideValue, result))
}
res := &EvaluationResult{
Result: result,
RightHandSideValue: rightHandSideValue,
Operator: c.Operator,
}
if leftHandSideValue != nil {
res.LeftHandSideValue = *leftHandSideValue
}
return res, nil
}
// booleanOperator returns the boolean operator for the condition
func booleanOperator(condition *Condition) string {
if len(condition.Any) > 0 {
return "any"
} else if len(condition.All) > 0 {
return "all"
} else if condition.Not != nil {
return "not"
}
return ""
}
// booleanOperator returns the condition's boolean operator
func (c *Condition) booleanOperator() string {
if c == nil {
return ""
}
if c.All != nil {
return "all"
}
if c.Any != nil {
return "any"
}
if c.Not != nil {
return "not"
}
return ""
}
// IsBooleanOperator returns whether the operator is boolean ('all', 'any', 'not')
func (c *Condition) IsBooleanOperator() bool {
return c.booleanOperator() != ""
}
// isConditionReference returns whether the condition represents a reference to a condition
func (c *Condition) IsConditionReference() bool {
if c == nil {
return false
}
_, ok := reflect.TypeOf(*c).FieldByName("Condition")
return ok && c.Condition != ""
}