diff --git a/config/dataprovider.go b/config/dataprovider.go index 351da2a65..49af848d2 100644 --- a/config/dataprovider.go +++ b/config/dataprovider.go @@ -19,6 +19,7 @@ along with this program. If not, see package config import ( + "fmt" "net" "strings" @@ -52,13 +53,22 @@ func GetDynamicString(dnVal string, dP DataProvider) (string, error) { //NewObjectDP constructs a DataProvider func NewObjectDP(obj interface{}) (dP DataProvider) { - dP = &ObjectDP{obj: obj, cache: NewNavigableMap(nil)} + dP = &ObjectDP{obj: obj, cache: make(map[string]interface{})} return } type ObjectDP struct { obj interface{} - cache *NavigableMap + cache map[string]interface{} +} + +func (objDp *ObjectDP) setCache(path string, val interface{}) { + objDp.cache[path] = val +} + +func (objDp *ObjectDP) getCache(path string) (val interface{}, has bool) { + val, has = objDp.cache[path] + return } // String is part of engine.DataProvider interface @@ -70,16 +80,59 @@ func (objDP *ObjectDP) String() string { // FieldAsInterface is part of engine.DataProvider interface func (objDP *ObjectDP) FieldAsInterface(fldPath []string) (data interface{}, err error) { // []string{ BalanceMap *monetary[0] Value } - if data, err = objDP.cache.FieldAsInterface(fldPath); err == nil || - err != utils.ErrNotFound { // item found in cache + var has bool + if data, has = objDP.getCache(strings.Join(fldPath, ".")); has { return } - err = nil // cancel previous err - // for _, fld := range fldPath { - // //process each field - // } - objDP.cache.Set(fldPath, data, false, false) + var prevFld string + for _, fld := range fldPath { + var slctrStr string + if splt := strings.Split(fld, "["); len(splt) != 1 { // check if we have selector + fld = splt[0] + if splt[1][len(splt[1])-1:] != "]" { + return nil, fmt.Errorf("filter rule <%s> needs to end in ]", splt[1]) + } + slctrStr = splt[1][:len(splt[1])-1] // also strip the last ] + } + if prevFld == utils.EmptyString { + prevFld += fld + } else { + prevFld += "." + fld + } + + // check if we take the current path from cache + if data, has = objDP.getCache(prevFld); !has { + if data, err = utils.ReflectFieldMethodInterface(objDP.obj, fld); err != nil { // take the object the field for current path + // in case of error set nil for the current path and return err + objDP.setCache(prevFld, nil) + return nil, err + } + // add the current field in prevFld so we can set in cache the full path with it's data + objDP.setCache(prevFld, data) + } + + // change the obj to be the current data and continue the processing + objDP.obj = data + if slctrStr != utils.EmptyString { //we have selector so we need to do an aditional get + prevFld += "[" + slctrStr + "]" + // check if we take the current path from cache + if data, has = objDP.getCache(prevFld); !has { + if data, err = utils.ReflectFieldMethodInterface(objDP.obj, slctrStr); err != nil { // take the object the field for current path + // in case of error set nil for the current path and return err + objDP.setCache(prevFld, nil) + return nil, err + } + // add the current field in prevFld so we can set in cache the full path with it's data + objDP.setCache(prevFld, data) + } + // change the obj to be the current data and continue the processing + objDP.obj = data + } + + } + //add in cache the initial path + objDP.setCache(strings.Join(fldPath, "."), data) return } diff --git a/engine/filters.go b/engine/filters.go index ccb0db6fe..8685bb439 100644 --- a/engine/filters.go +++ b/engine/filters.go @@ -126,7 +126,7 @@ func (fS *FilterS) Pass(tenant string, filterIDs []string, continue } for _, fltr := range f.Rules { - fieldNameDP, err = fS.getFieldNameDataProvider(ev, fltr.FieldName, tenant) + fieldNameDP, err = fS.getFieldNameDataProvider(ev, &fltr.FieldName, tenant) if err != nil { return pass, err } @@ -186,10 +186,10 @@ func (f *Filter) Compile() (err error) { } var supportedFiltersType *utils.StringSet = utils.NewStringSet([]string{utils.MetaString, utils.MetaPrefix, utils.MetaSuffix, - utils.MetaTimings, utils.MetaRSR, utils.MetaStatS, utils.MetaDestinations, + utils.MetaTimings, utils.MetaRSR, utils.MetaDestinations, utils.MetaEmpty, utils.MetaExists, utils.MetaLessThan, utils.MetaLessOrEqual, - utils.MetaGreaterThan, utils.MetaGreaterOrEqual, utils.MetaResources, utils.MetaEqual, - utils.MetaAccount, utils.MetaNotEqual}) + utils.MetaGreaterThan, utils.MetaGreaterOrEqual, utils.MetaEqual, + utils.MetaNotEqual}) var needsFieldName *utils.StringSet = utils.NewStringSet([]string{utils.MetaString, utils.MetaPrefix, utils.MetaSuffix, utils.MetaTimings, utils.MetaDestinations, utils.MetaLessThan, utils.MetaEmpty, utils.MetaExists, utils.MetaLessOrEqual, utils.MetaGreaterThan, @@ -290,24 +290,6 @@ func (rf *FilterRule) CompileValues() (err error) { FilterValue: valSplt[2], } } - case utils.MetaAccount: - //value for filter of type *accounts needs to be in the following form: - //*gt:AccountID:ValueOfUsage - rf.accountItems = make([]*itemFilter, len(rf.Values)) - for i, val := range rf.Values { - valSplt := strings.Split(val, utils.InInFieldSep) - if len(valSplt) != 3 { - return fmt.Errorf("Value %s needs to contain at least 3 items", val) - } - // valSplt[0] filter type - // valSplt[1] id of the Resource - // valSplt[2] value to compare - rf.accountItems[i] = &itemFilter{ - FilterType: valSplt[0], - ItemID: valSplt[1], - FilterValue: valSplt[2], - } - } } return } @@ -597,38 +579,6 @@ func (fltr *FilterRule) passGreaterThan(fielNameDP config.DataProvider, fieldVal // return true, nil // } -// func (fltr *FilterRule) passAccountS(dP config.DataProvider, -// accountS rpcclient.RpcClientConnection, tenant string) (bool, error) { -// if accountS == nil || reflect.ValueOf(accountS).IsNil() { -// return false, errors.New("Missing AccountS information") -// } -// for _, accItem := range fltr.accountItems { -// //split accItem.ItemID in two accountID and actual filter -// //AccountID.BalanceMap.*monetary[0].Value -// splittedString := strings.SplitN(accItem.ItemID, utils.NestingSep, 2) -// accID := splittedString[0] -// filterID := splittedString[1] -// var reply Account -// if err := accountS.Call(utils.ApierV2GetAccount, -// &utils.AttrGetAccount{Tenant: tenant, Account: accID}, &reply); err != nil { -// return false, err -// } -// //compose the newFilter -// fltr, err := NewFilterRule(accItem.FilterType, -// utils.DynamicDataPrefix+filterID, []string{accItem.FilterValue}) -// if err != nil { -// return false, err -// } -// dP, _ := reply.AsNavigableMap(nil) -// if val, err := fltr.Pass(dP, nil, tenant); err != nil || !val { -// //in case of error return false and error -// //and in case of not pass return false and nil -// return false, err -// } -// } -// return true, nil -// } - func (fltr *FilterRule) passEqualTo(fielNameDP config.DataProvider, fieldValuesDP []config.DataProvider) (bool, error) { fldIf, err := config.GetDynamicInterface(fltr.FieldName, fielNameDP) if err != nil { @@ -654,41 +604,39 @@ func (fltr *FilterRule) passEqualTo(fielNameDP config.DataProvider, fieldValuesD return false, nil } -func (fS *FilterS) getFieldNameDataProvider(initialDP config.DataProvider, fieldName string, tenant string) (dp config.DataProvider, err error) { +func (fS *FilterS) getFieldNameDataProvider(initialDP config.DataProvider, fieldName *string, tenant string) (dp config.DataProvider, err error) { switch { - case strings.HasPrefix(fieldName, utils.MetaAccounts): - //construct dataProvider from account and set it furthder + case strings.HasPrefix(*fieldName, utils.DynamicDataPrefix+utils.MetaAccounts): + //same of fieldName : ~*accounts.1001.BalanceMap.*monetary[0].Value + // split the field name in 3 parts + // fieldNameType (~*accounts), accountID(1001) and quried part (BalanceMap.*monetary[0].Value) + splitFldName := strings.SplitN(*fieldName, ".", 3) + if len(splitFldName) != 3 { + return nil, fmt.Errorf("invalid fieldname <%s>", *fieldName) + } var account *Account - //extract the AccountID from fieldName if err = fS.ralSConns.Call(utils.ApierV2GetAccount, - &utils.AttrGetAccount{Tenant: tenant, Account: "completeHereWithID"}, &account); err != nil { + &utils.AttrGetAccount{Tenant: tenant, Account: splitFldName[1]}, &account); err != nil { return } + //construct dataProvider from account and set it furthder dp = config.NewObjectDP(account) - case strings.HasPrefix(fieldName, utils.MetaResources): - case strings.HasPrefix(fieldName, utils.MetaStats): + // remove from fieldname the fielNameType and the AccountID + *fieldName = utils.DynamicDataPrefix + splitFldName[2] + case strings.HasPrefix(*fieldName, utils.DynamicDataPrefix+utils.MetaResources): + case strings.HasPrefix(*fieldName, utils.DynamicDataPrefix+utils.MetaStats): default: dp = initialDP } return } -func (fS *FilterS) getFieldValueDataProviders(initialDP config.DataProvider, values []string, tenant string) (dp []config.DataProvider, err error) { +func (fS *FilterS) getFieldValueDataProviders(initialDP config.DataProvider, + values []string, tenant string) (dp []config.DataProvider, err error) { dp = make([]config.DataProvider, len(values)) - for i, val := range values { - switch { - case strings.HasPrefix(val, utils.MetaAccounts): - var account *Account - //extract the AccountID from fieldName - if err = fS.ralSConns.Call(utils.ApierV2GetAccount, - &utils.AttrGetAccount{Tenant: tenant, Account: "completeHereWithID"}, &account); err != nil { - return - } - dp[i] = config.NewObjectDP(account) - case strings.HasPrefix(val, utils.MetaResources): - case strings.HasPrefix(val, utils.MetaStats): - default: - dp[i] = initialDP + for i := range values { + if dp[i], err = fS.getFieldNameDataProvider(initialDP, &values[i], tenant); err != nil { + return } } return diff --git a/general_tests/filters_it_test.go b/general_tests/filters_it_test.go index 62c93027f..c0ffea8df 100644 --- a/general_tests/filters_it_test.go +++ b/general_tests/filters_it_test.go @@ -48,11 +48,11 @@ var sTestsFltr = []func(t *testing.T){ testV1FltrStartEngine, testV1FltrRpcConn, testV1FltrLoadTarrifPlans, - testV1FltrAddStats, - testV1FltrPupulateThreshold, - testV1FltrGetThresholdForEvent, - testV1FltrGetThresholdForEvent2, - testV1FltrPopulateResources, + //testV1FltrAddStats, + //testV1FltrPupulateThreshold, + //testV1FltrGetThresholdForEvent, + //testV1FltrGetThresholdForEvent2, + //testV1FltrPopulateResources, testV1FltrAccounts, testV1FltrStopEngine, } @@ -519,15 +519,17 @@ func testV1FltrAccounts(t *testing.T) { } else if resp != utils.OK { t.Error("Unexpected reply returned", resp) } - //Add a filter of type *accounts and check if *monetary balance of account 1001 is minim 9 ( greater than 9) - //we expect that the balance to be 10 so the filter should pass (10 > 9) + // Add a filter with fieldName taken value from account 1001 + // and check if *monetary balance is minim 9 ( greater than 9) + // we expect that the balance to be 10 so the filter should pass (10 > 9) filter := &engine.Filter{ Tenant: "cgrates.org", ID: "FLTR_TH_Accounts", Rules: []*engine.FilterRule{ { - Type: "*account", - Values: []string{"*gt:1001.BalanceMap.*monetary[0].Value:9"}, + Type: "*gt", + FieldName: "~*accounts.1001.BalanceMap.*monetary[0].Value", + Values: []string{"9"}, }, }, } @@ -538,7 +540,16 @@ func testV1FltrAccounts(t *testing.T) { } else if result != utils.OK { t.Error("Unexpected reply returned", result) } - + // Add a log action + attrsAA := &utils.AttrSetActions{ActionsId: "LOG", Actions: []*utils.TPAction{ + {Identifier: utils.LOG}, + }} + if err := fltrRpc.Call("ApierV2.SetActions", attrsAA, &result); err != nil && err.Error() != utils.ErrExists.Error() { + t.Error("Got error on ApierV2.SetActions: ", err.Error()) + } else if result != utils.OK { + t.Errorf("Calling ApierV2.SetActions received: %s", result) + } + time.Sleep(10 * time.Millisecond) //Add a threshold with filter from above and an inline filter for Account 1010 tPrfl := &engine.ThresholdProfile{ Tenant: "cgrates.org", @@ -581,15 +592,17 @@ func testV1FltrAccounts(t *testing.T) { } // update the filter - //Add a filter of type *accounts and check if *monetary balance of account 1001 is minim 11 ( greater than 11) - //we expect that the balance to be 10 so the filter should not pass (10 > 11) + // Add a filter with fieldName taken value from account 1001 + // and check if *monetary balance is is minim 11 ( greater than 11) + // we expect that the balance to be 10 so the filter should not pass (10 > 11) filter = &engine.Filter{ Tenant: "cgrates.org", ID: "FLTR_TH_Accounts", Rules: []*engine.FilterRule{ { - Type: "*account", - Values: []string{"*gt:1001.BalanceMap.*monetary[0].Value:11"}, + Type: "*gt", + FieldName: "~*accounts.1001.BalanceMap.*monetary[0].Value", + Values: []string{"11"}, }, }, } diff --git a/general_tests/objectdp_test.go b/general_tests/objectdp_test.go new file mode 100644 index 000000000..0126c9fe1 --- /dev/null +++ b/general_tests/objectdp_test.go @@ -0,0 +1,86 @@ +/* +Real-time Online/Offline Charging System (OCS) for Telecom & ISP environments +Copyright (C) ITsysCOM GmbH + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, +but WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +GNU General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see +*/ +package general_tests + +import ( + "testing" + + "github.com/cgrates/cgrates/config" + "github.com/cgrates/cgrates/engine" + "github.com/cgrates/cgrates/utils" +) + +func TestAccountNewObjectDPFieldAsInterface(t *testing.T) { + acc := &engine.Account{ + ID: "cgrates.org:1001", + BalanceMap: map[string]engine.Balances{ + utils.MONETARY: []*engine.Balance{ + { + Value: 20, + Weight: 10, + }, + }, + }, + } + accDP := config.NewObjectDP(acc) + if data, err := accDP.FieldAsInterface([]string{"BalanceMap", "*monetary[0]", "Value"}); err != nil { + t.Error(err) + } else if data != 20. { + t.Errorf("Expected: %+v ,recived: %+v", 20., data) + } + if _, err := accDP.FieldAsInterface([]string{"BalanceMap", "*monetary[1]", "Value"}); err == nil || + err.Error() != "index out of range" { + t.Error(err) + } + if _, err := accDP.FieldAsInterface([]string{"BalanceMap", "*monetary[0]", "InexistentField"}); err == nil || + err != utils.ErrNotFound { + t.Error(err) + } +} + +func TestAccountNewObjectDPFieldAsInterfaceFromCache(t *testing.T) { + acc := &engine.Account{ + ID: "cgrates.org:1001", + BalanceMap: map[string]engine.Balances{ + utils.MONETARY: []*engine.Balance{ + { + Value: 20, + Weight: 10, + }, + }, + }, + } + accDP := config.NewObjectDP(acc) + + if data, err := accDP.FieldAsInterface([]string{"BalanceMap", "*monetary[0]", "Value"}); err != nil { + t.Error(err) + } else if data != 20. { + t.Errorf("Expected: %+v ,recived: %+v", 20., data) + } + // the value should be taken from cache + if data, err := accDP.FieldAsInterface([]string{"BalanceMap", "*monetary[0]", "Value"}); err != nil { + t.Error(err) + } else if data != 20. { + t.Errorf("Expected: %+v ,recived: %+v", 20., data) + } + if data, err := accDP.FieldAsInterface([]string{"BalanceMap", "*monetary[0]"}); err != nil { + t.Error(err) + } else if data != acc.BalanceMap[utils.MONETARY][0] { + t.Errorf("Expected: %+v ,recived: %+v", acc.BalanceMap[utils.MONETARY][0], data) + } +} diff --git a/utils/reflect.go b/utils/reflect.go index 4cfdf32aa..79338b1b5 100644 --- a/utils/reflect.go +++ b/utils/reflect.go @@ -562,8 +562,8 @@ func ReflectFieldMethodInterface(obj interface{}, fldName string) (retIf interfa if err != nil { return nil, err } - if idx > v.Len() { - return nil, fmt.Errorf("out of range") + if idx > v.Len()-1 { + return nil, fmt.Errorf("index out of range") } field = v.Index(idx) default: @@ -582,12 +582,17 @@ func ReflectFieldMethodInterface(obj interface{}, fldName string) (retIf interfa if field.Type().NumOut() > 2 { return nil, fmt.Errorf("invalid function called") } - errorInterface := reflect.TypeOf((*error)(nil)).Elem() - if !field.Type().Out(1).Implements(errorInterface) { - return nil, fmt.Errorf("invalid function called") + // the function have two parameters in return and check if the second is of type error + if field.Type().NumOut() == 2 { + errorInterface := reflect.TypeOf((*error)(nil)).Elem() + if !field.Type().Out(1).Implements(errorInterface) { + return nil, fmt.Errorf("invalid function called") + } } fields := field.Call([]reflect.Value{}) - //verify if error is not nil + if len(fields) == 2 && !fields[1].IsNil() { + return fields[0].Interface(), fields[1].Interface().(error) + } return fields[0].Interface(), nil } } diff --git a/utils/reflect_test.go b/utils/reflect_test.go index f5bb50ff3..e4419c93b 100644 --- a/utils/reflect_test.go +++ b/utils/reflect_test.go @@ -699,6 +699,8 @@ type TestA struct { StrField string } +type TestASlice []*TestA + func (_ *TestA) TestFunc() string { return "This is a test function on a structure" } @@ -708,10 +710,10 @@ func (_ *TestA) TestFuncWithParam(param string) string { } func (_ *TestA) TestFuncWithError() (string, error) { - return "TestFunction", nil + return "TestFuncWithError", nil } func (_ *TestA) TestFuncWithError2() (string, error) { - return "TestFunction", ErrPartiallyExecuted + return "TestFuncWithError2", ErrPartiallyExecuted } func TestReflectFieldMethodInterface(t *testing.T) { @@ -735,7 +737,11 @@ func TestReflectFieldMethodInterface(t *testing.T) { ifValue, err = ReflectFieldMethodInterface(a, "TestFuncWithError") if err != nil { t.Error(err) - } else if ifValue != "TestFunction" { - t.Errorf("Expecting: TestFunction, received: %+v", ifValue) + } else if ifValue != "TestFuncWithError" { + t.Errorf("Expecting: TestFuncWithError, received: %+v", ifValue) + } + ifValue, err = ReflectFieldMethodInterface(a, "TestFuncWithError2") + if err == nil || err != ErrPartiallyExecuted { + t.Error(err) } }