diff --git a/engine/account.go b/engine/account.go index ec486c756..e539721f0 100644 --- a/engine/account.go +++ b/engine/account.go @@ -697,17 +697,20 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } -func (acc *Account) matchConditions(condition string) (bool, error) { - condMap := make(map[string]interface{}) - if err := json.Unmarshal([]byte(condition), condMap); err != nil { - return false, err +func (acc *Account) matchConditions(condition, balanceType string) (bool, error) { + + cl := &utils.CondLoader{} + cl.Parse(condition) + for _, b := range acc.BalanceMap[balanceType] { + check, err := cl.Check(b) + if err != nil { + return false, err + } + if check { + return true, nil + } } - operator, found := condMap["Operator"] - if !found { - operator = utils.COND_EQ - } - _ = operator - return true, nil + return false, nil } // used in some api for transition diff --git a/engine/action.go b/engine/action.go index fdf0eaff1..97a1b807b 100644 --- a/engine/action.go +++ b/engine/action.go @@ -628,7 +628,7 @@ func transferMonetaryDefault(acc *Account, sq *StatsQueueTriggered, a *Action, a } func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return debitAction(acc, sq, a, acs) } else { return err @@ -636,7 +636,7 @@ func conditionalDebitAction(acc *Account, sq *StatsQueueTriggered, a *Action, ac } func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return debitResetAction(acc, sq, a, acs) } else { return err @@ -644,7 +644,7 @@ func conditionalDebitResetAction(acc *Account, sq *StatsQueueTriggered, a *Actio } func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return topupAction(acc, sq, a, acs) } else { return err @@ -652,7 +652,7 @@ func conditionalTopupAction(acc *Account, sq *StatsQueueTriggered, a *Action, ac } func conditionalTopupResetAction(acc *Account, sq *StatsQueueTriggered, a *Action, acs Actions) error { - if matched, err := acc.matchConditions(a.ExtraParameters); matched { + if matched, err := acc.matchConditions(a.ExtraParameters, a.BalanceType); matched { return topupResetAction(acc, sq, a, acs) } else { return err diff --git a/utils/cond_loader.go b/utils/cond_loader.go index a7bcd617d..08695f8f1 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -2,88 +2,164 @@ package utils import ( "encoding/json" + "errors" + "reflect" "strings" ) const ( - COND_EQ = "*eq" - COND_GT = "*gt" - COND_LT = "*lt" - COND_EXP = "*exp" + CondEQ = "*eq" + CondGT = "*gt" + CondGTE = "*gte" + CondLT = "*lt" + CondLTE = "*lte" + CondEXP = "*exp" + CondOR = "*or" + CondAND = "*and" ) -type CondElement interface { - AddChild(CondElement) - CheckStruct(interface{}) (bool, error) +var ( + ErrNotNumerical = errors.New("NOT_NUMERICAL") +) + +type condElement interface { + addChild(condElement) error + checkStruct(interface{}) (bool, error) } -type OperatorSlice struct { - Operator string - Slice []CondElement +type operatorSlice struct { + operator string + slice []condElement } -func (os *OperatorSlice) AddChild(ce CondElement) { - os.Slice = append(os.Slice, ce) +func (os *operatorSlice) addChild(ce condElement) error { + os.slice = append(os.slice, ce) + return nil } -func (os *OperatorSlice) CheckStruct(o interface{}) (bool, error) { return true, nil } - -type KeyStruct struct { - Key string - Struct CondElement +func (os *operatorSlice) checkStruct(o interface{}) (bool, error) { + switch os.operator { + case CondOR: + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + if check { + return true, nil + } + } + case CondAND: + accumulator := true + for _, cond := range os.slice { + check, err := cond.checkStruct(o) + if err != nil { + return false, err + } + accumulator = accumulator && check + } + return accumulator, nil + } + return false, nil } -func (ks *KeyStruct) AddChild(ce CondElement) { - ks.Struct = ce -} -func (ks *KeyStruct) CheckStruct(o interface{}) (bool, error) { return true, nil } - -type OperatorValue struct { - Operator string - Value interface{} +type keyStruct struct { + key string + elem condElement } -func (ov *OperatorValue) AddChild(CondElement) {} -func (ov *OperatorValue) CheckStruct(o interface{}) (bool, error) { return true, nil } - -type KeyValue struct { - Key string - Value interface{} +func (ks *keyStruct) addChild(ce condElement) error { + ks.elem = ce + return nil +} +func (ks *keyStruct) checkStruct(o interface{}) (bool, error) { + obj := reflect.ValueOf(o) + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + value := obj.FieldByName(ks.key) + return ks.elem.checkStruct(value.Interface()) } -func (os *KeyValue) AddChild(CondElement) {} -func (os *KeyValue) CheckStruct(o interface{}) (bool, error) { return true, nil } +type operatorValue struct { + operator string + value interface{} +} + +func (ov *operatorValue) addChild(condElement) error { return ErrNotImplemented } +func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { + if ov.operator == CondEQ { + return ov.value == o, nil + } + var of, vf float64 + var ok bool + if of, ok = o.(float64); !ok { + return false, ErrNotNumerical + } + if vf, ok = ov.value.(float64); !ok { + return false, ErrNotNumerical + } + switch ov.operator { + case CondGT: + return of > vf, nil + case CondGTE: + return of >= vf, nil + case CondLT: + return of < vf, nil + case CondLTE: + return of <= vf, nil + + } + return true, nil +} + +type keyValue struct { + key string + value interface{} +} + +func (kv *keyValue) addChild(condElement) error { return ErrNotImplemented } +func (kv *keyValue) checkStruct(o interface{}) (bool, error) { + obj := reflect.ValueOf(o) + if obj.Kind() == reflect.Ptr { + obj = obj.Elem() + } + value := obj.FieldByName(kv.key) + return value.Interface() == kv.value, nil +} func isOperator(s string) bool { return strings.HasPrefix(s, "*") } -type CondLoader struct{} +type CondLoader struct { + rootElement condElement +} -func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) (CondElement, error) { +func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) (condElement, error) { for key, value := range a { - var currentElement CondElement + var currentElement condElement switch t := value.(type) { case []interface{}: - currentElement = &OperatorSlice{Operator: key} + currentElement = &operatorSlice{operator: key} for _, e := range t { - cp.Load(e.(map[string]interface{}), currentElement) + cp.load(e.(map[string]interface{}), currentElement) } case map[string]interface{}: - currentElement = &KeyStruct{Key: key} + currentElement = &keyStruct{key: key} //log.Print("map: ", t) - cp.Load(t, currentElement) + cp.load(t, currentElement) case interface{}: if isOperator(key) { - currentElement = &OperatorValue{Operator: key, Value: t} + currentElement = &operatorValue{operator: key, value: t} } else { - currentElement = &KeyValue{Key: key, Value: t} + currentElement = &keyValue{key: key, value: t} } //log.Print("generic interface: ", t) default: return nil, ErrParserError } if parentElement != nil { - parentElement.AddChild(currentElement) + parentElement.addChild(currentElement) } else { return currentElement, nil } @@ -91,10 +167,15 @@ func (cp *CondLoader) Load(a map[string]interface{}, parentElement CondElement) return nil, nil } -func (cp *CondLoader) Parse(s string) (root CondElement, err error) { +func (cp *CondLoader) Parse(s string) (err error) { a := make(map[string]interface{}) if err := json.Unmarshal([]byte([]byte(s)), &a); err != nil { - return nil, err + return err } - return cp.Load(a, nil) + cp.rootElement, err = cp.load(a, nil) + return +} + +func (cp *CondLoader) Check(o interface{}) (bool, error) { + return cp.rootElement.checkStruct(o) } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index 70b0382f8..e5ef1c42f 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -4,8 +4,260 @@ import "testing" func TestCondLoader(t *testing.T) { cl := &CondLoader{} - root, err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) - if err != nil || root == nil { - t.Errorf("Error loading structure: %+v (%v)", ToIJSON(root), err) + err := cl.Parse(`{"*or":[{"test":1},{"field":{"*gt":1}},{"best":"coco"}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } +} + +func TestCondKeyValue(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"Test":"test"}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } +} + +func TestCondKeyValuePointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"Test":"test"}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":6}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } +} + +func TestCondOperatorValue(t *testing.T) { + root := &operatorValue{operator: "*gt", value: 3.4} + if check, err := root.checkStruct(3.5); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } + root = &operatorValue{operator: "*eq", value: 3.4} + if check, err := root.checkStruct(3.5); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } + root = &operatorValue{operator: "*eq", value: 3.4} + if check, err := root.checkStruct(3.4); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } + root = &operatorValue{operator: "*eq", value: "zinc"} + if check, err := root.checkStruct("zinc"); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) + } +} + +func TestCondKeyStruct(t *testing.T) { + o := struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"Field":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); check || err != ErrNotNumerical { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lt": 7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*eq": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*eq": "test"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } +} + +func TestCondKeyStructPointer(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"Field":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*gt": 5}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); check || err != ErrNotNumerical { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*gte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lt": 7}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*lte": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Field":{"*eq": 6}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"Test":{"*eq": "test"}}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } +} + +func TestCondOperatorSlice(t *testing.T) { + o := &struct { + Test string + Field float64 + Other bool + }{ + Test: "test", + Field: 6.0, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"*or":[{"Test":"test"},{"Field":{"*gt":7}},{"Other":false}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) + } + err = cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":7}},{"Other":false}]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + if check, err := cl.Check(o); check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } }