diff --git a/engine/account.go b/engine/account.go index e539721f0..d733af1c8 100644 --- a/engine/account.go +++ b/engine/account.go @@ -697,17 +697,24 @@ func (acc *Account) DebitConnectionFee(cc *CallCost, usefulMoneyBalances Balance } } -func (acc *Account) matchConditions(condition, balanceType string) (bool, error) { - +func (acc *Account) matchConditions(condition 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 + for balanceType, balanceChain := range acc.BalanceMap { + for _, b := range balanceChain { + check, err := cl.Check(&struct { + Type string + *Balance + }{ + Type: balanceType, + Balance: b, + }) + if err != nil { + return false, err + } + if check { + return true, nil + } } } return false, nil diff --git a/engine/action.go b/engine/action.go index 97a1b807b..fdf0eaff1 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, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); 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, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); 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, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); 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, a.BalanceType); matched { + if matched, err := acc.matchConditions(a.ExtraParameters); matched { return topupResetAction(acc, sq, a, acs) } else { return err diff --git a/utils/cond_loader.go b/utils/cond_loader.go index 08695f8f1..8fdfca867 100644 --- a/utils/cond_loader.go +++ b/utils/cond_loader.go @@ -2,7 +2,7 @@ package utils import ( "encoding/json" - "errors" + "fmt" "reflect" "strings" ) @@ -16,11 +16,12 @@ const ( CondEXP = "*exp" CondOR = "*or" CondAND = "*and" + CondHAS = "*has" ) -var ( - ErrNotNumerical = errors.New("NOT_NUMERICAL") -) +func NewErrInvalidArgument(arg interface{}) error { + return fmt.Errorf("INVALID_ARGUMENT: %v", arg) +} type condElement interface { addChild(condElement) error @@ -87,16 +88,36 @@ type operatorValue struct { func (ov *operatorValue) addChild(condElement) error { return ErrNotImplemented } func (ov *operatorValue) checkStruct(o interface{}) (bool, error) { + // no conversion if ov.operator == CondEQ { return ov.value == o, nil } + // StringMap conversion + if ov.operator == CondHAS { + var strMap StringMap + var ok bool + if strMap, ok = o.(StringMap); !ok { + return false, NewErrInvalidArgument(o) + } + var strSlice []interface{} + if strSlice, ok = ov.value.([]interface{}); !ok { + return false, NewErrInvalidArgument(ov.value) + } + for _, str := range strSlice { + if !strMap[str.(string)] { + return false, nil + } + } + return true, nil + } + // float conversion var of, vf float64 var ok bool if of, ok = o.(float64); !ok { - return false, ErrNotNumerical + return false, NewErrInvalidArgument(o) } if vf, ok = ov.value.(float64); !ok { - return false, ErrNotNumerical + return false, NewErrInvalidArgument(ov.value) } switch ov.operator { case CondGT: @@ -140,9 +161,13 @@ func (cp *CondLoader) load(a map[string]interface{}, parentElement condElement) var currentElement condElement switch t := value.(type) { case []interface{}: - currentElement = &operatorSlice{operator: key} - for _, e := range t { - cp.load(e.(map[string]interface{}), currentElement) + if key == CondHAS { + currentElement = &operatorValue{operator: key, value: t} + } else { + currentElement = &operatorSlice{operator: key} + for _, e := range t { + cp.load(e.(map[string]interface{}), currentElement) + } } case map[string]interface{}: currentElement = &keyStruct{key: key} @@ -177,5 +202,8 @@ func (cp *CondLoader) Parse(s string) (err error) { } func (cp *CondLoader) Check(o interface{}) (bool, error) { + if cp.rootElement == nil { + return false, ErrParserError + } return cp.rootElement.checkStruct(o) } diff --git a/utils/cond_loader_test.go b/utils/cond_loader_test.go index e5ef1c42f..c0e9930ff 100644 --- a/utils/cond_loader_test.go +++ b/utils/cond_loader_test.go @@ -1,6 +1,9 @@ package utils -import "testing" +import ( + "strings" + "testing" +) func TestCondLoader(t *testing.T) { cl := &CondLoader{} @@ -8,6 +11,15 @@ func TestCondLoader(t *testing.T) { if err != nil { t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) } + + err = cl.Parse(`{"*has":["NAT","RET","EUR"]}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } + err = cl.Parse(`{"Field":7, "Other":true}`) + if err != nil { + t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) + } } func TestCondKeyValue(t *testing.T) { @@ -42,6 +54,20 @@ func TestCondKeyValue(t *testing.T) { 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, "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(`{"Field":7, "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) { @@ -79,22 +105,26 @@ func TestCondKeyValuePointer(t *testing.T) { } func TestCondOperatorValue(t *testing.T) { - root := &operatorValue{operator: "*gt", value: 3.4} + root := &operatorValue{operator: CondGT, 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} + root = &operatorValue{operator: CondEQ, 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} + root = &operatorValue{operator: CondEQ, 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"} + root = &operatorValue{operator: CondEQ, value: "zinc"} if check, err := root.checkStruct("zinc"); !check || err != nil { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(root)) } + root = &operatorValue{operator: CondHAS, value: []interface{}{"NAT", "RET", "EUR"}} + if check, err := root.checkStruct(StringMap{"WOR": true, "EUR": true, "NAT": true, "RET": true, "ROM": true}); !check || err != nil { + t.Errorf("Error checking struct: %v %v (%v)", !check, err, ToIJSON(root)) + } } func TestCondKeyStruct(t *testing.T) { @@ -119,7 +149,7 @@ func TestCondKeyStruct(t *testing.T) { if err != nil { t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) } - if check, err := cl.Check(o); check || err != ErrNotNumerical { + if check, err := cl.Check(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } err = cl.Parse(`{"Field":{"*gte": 6}}`) @@ -181,7 +211,7 @@ func TestCondKeyStructPointer(t *testing.T) { if err != nil { t.Errorf("Error loading structure: %+v (%v)", ToIJSON(cl.rootElement), err) } - if check, err := cl.Check(o); check || err != ErrNotNumerical { + if check, err := cl.Check(o); check || !strings.HasPrefix(err.Error(), "INVALID_ARGUMENT") { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } err = cl.Parse(`{"Field":{"*gte": 6}}`) @@ -261,3 +291,47 @@ func TestCondOperatorSlice(t *testing.T) { t.Errorf("Error checking struct: %v %v (%v)", check, err, ToIJSON(cl.rootElement)) } } + +func TestCondMixed(t *testing.T) { + o := &struct { + Test string + Field float64 + Categories StringMap + Other bool + }{ + Test: "test", + Field: 6.0, + Categories: StringMap{"call": true, "data": true, "voice": true}, + Other: true, + } + cl := &CondLoader{} + err := cl.Parse(`{"*and":[{"Test":"test"},{"Field":{"*gt":5}},{"Other":true},{"Categories":{"*has":["data", "call"]}}]}`) + 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 TestCondBalanceType(t *testing.T) { + type Balance struct { + Value float64 + } + + o := &struct { + BalanceType string + Balance + }{ + BalanceType: MONETARY, + Balance: Balance{Value: 10}, + } + cl := &CondLoader{} + err := cl.Parse(`{"BalanceType":"*monetary","Value":10}`) + 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)) + } +}