diff --git a/config/navigablemap.go b/config/navigablemap.go index 20a647a9f..5f41ae25a 100644 --- a/config/navigablemap.go +++ b/config/navigablemap.go @@ -23,6 +23,7 @@ import ( "errors" "fmt" "net" + "reflect" "strconv" "strings" "time" @@ -101,54 +102,125 @@ func (nM *NavigableMap) FieldAsInterface(fldPath []string) (fldVal interface{}, return nil, errors.New("empty field path") } lastMp := nM.data // last map when layered - var canCast bool for i, spath := range fldPath { if i == lenPath-1 { // lastElement - var idx *int - if idxStart := strings.Index(spath, utils.IdxStart); idxStart != -1 && - strings.HasSuffix(spath, utils.IdxEnd) { - slctr := spath[idxStart+1 : len(spath)-1] - if !strings.HasPrefix(slctr, utils.DynamicDataPrefix) { - if idxVal, err := strconv.Atoi(slctr); err != nil { - return nil, err - } else { - idx = utils.IntPointer(idxVal) - } - } - spath = spath[:idxStart] // ignore the selector for now since it is processed in other places - } - var has bool - fldVal, has = lastMp[spath] - if !has { - return nil, utils.ErrNotFound - } - if valItms, isItms := fldVal.([]*NMItem); isItms && idx != nil { - if *idx >= len(valItms) { - return nil, fmt.Errorf("selector index %d out of range", *idx) - } - fldVal = valItms[*idx].Data - } + return nM.getLastItem(lastMp, spath) + } + if lastMp, err = nM.getNextMap(lastMp, spath); err != nil { return } - elmnt, has := lastMp[spath] - if !has { - return nil, utils.ErrNotFound - } - lastMp, canCast = elmnt.(map[string]interface{}) - if !canCast { - lastMpNM, canCast := elmnt.(*NavigableMap) // attempt to cast into NavigableMap - if !canCast { - err = fmt.Errorf("cannot cast field: <%+v> type: %T with path: <%s> to map[string]interface{}", - elmnt, elmnt, spath) - return - } - lastMp = lastMpNM.data - } } err = errors.New("end of function") return } +// getLastItem returns the item from the map +// checking if it needs to return the item or an element of him if the item is a slice +func (nM *NavigableMap) getLastItem(mp map[string]interface{}, spath string) (val interface{}, err error) { + var idx *int + spath, idx = nM.getIndex(spath) + var has bool + val, has = mp[spath] + if !has { + return nil, utils.ErrNotFound + } + if idx == nil { + return val, nil + } + switch vt := val.(type) { + case []string: + if *idx > len(vt) { + return nil, utils.ErrNotFound + // return nil, fmt.Errorf("selector index %d out of range", *idx) + } + return vt[*idx], nil + case []*NMItem: + if *idx > len(vt) { + return nil, utils.ErrNotFound + // return nil, fmt.Errorf("selector index %d out of range", *idx) + } + return vt[*idx].Data, nil + default: + } + // only if all above fails use reflect: + vr := reflect.ValueOf(val) + if vr.Kind() == reflect.Ptr { + vr = vr.Elem() + } + if vr.Kind() != reflect.Slice && vr.Kind() != reflect.Array { + return nil, utils.ErrNotFound + // return nil, fmt.Errorf("selector index used on non slice type(%T)", val) + } + if *idx > vr.Len() { + return nil, utils.ErrNotFound + // return nil, fmt.Errorf("selector index %d out of range", *idx) + } + return vr.Index(*idx).Interface(), nil +} + +// getNextMap returns the next map from the given map +// used only for path parsing +func (nM *NavigableMap) getNextMap(mp map[string]interface{}, spath string) (map[string]interface{}, error) { + var idx *int + spath, idx = nM.getIndex(spath) + mi, has := mp[spath] + if !has { + return nil, utils.ErrNotFound + } + if idx == nil { + switch mv := mi.(type) { + case map[string]interface{}: + return mv, nil + case *map[string]interface{}: + return *mv, nil + case NavigableMap: + return mv.data, nil + case *NavigableMap: + return mv.data, nil + default: + } + } else { + switch mv := mi.(type) { + case []map[string]interface{}: + if *idx < len(mv) { + return mv[*idx], nil + } + case []NavigableMap: + if *idx < len(mv) { + return mv[*idx].data, nil + } + case []*NavigableMap: + if *idx < len(mv) { + return mv[*idx].data, nil + } + default: + } + return nil, utils.ErrNotFound // xml compatible + } + return nil, fmt.Errorf("cannot cast field: <%+v> type: %T with path: <%s> to map[string]interface{}", + mi, mi, spath) +} + +// getIndex returns the path and index if index present +// path[index]=>path,index +// path=>path,nil +func (nM *NavigableMap) getIndex(spath string) (opath string, idx *int) { + idxStart := strings.Index(spath, utils.IdxStart) + if idxStart == -1 || !strings.HasSuffix(spath, utils.IdxEnd) { + return spath, nil + } + slctr := spath[idxStart+1 : len(spath)-1] + opath = spath[:idxStart] + if strings.HasPrefix(slctr, utils.DynamicDataPrefix) { + return + } + idxVal, err := strconv.Atoi(slctr) + if err != nil { + return spath, nil + } + return opath, &idxVal +} + // FieldAsString returns the field value as string for the path specified // implements DataProvider func (nM *NavigableMap) FieldAsString(fldPath []string) (fldVal string, err error) { diff --git a/config/navigablemap_test.go b/config/navigablemap_test.go index ddcdca390..db3e7cb35 100644 --- a/config/navigablemap_test.go +++ b/config/navigablemap_test.go @@ -19,6 +19,7 @@ package config import ( "encoding/xml" + "fmt" "reflect" "strings" "testing" @@ -873,3 +874,185 @@ func TestNavMapMerge(t *testing.T) { t.Errorf("expecting: %+v, received: %+v", nM2, nM) } } + +func TestNavMapGetIndex(t *testing.T) { + nM := NewNavigableMap(nil) + var expIndx *int + var expPath string + var path string = "Fld1" + expPath = path + if rplyPath, rplyIndx := nM.getIndex(path); rplyPath != expPath && rplyIndx != expIndx { + t.Errorf("Expected: path=%s ,indx=%v, received: path=%s ,indx=%v", expPath, expIndx, rplyPath, rplyIndx) + } + + path = "slice[5]" + expPath = "slice" + expIndx = utils.IntPointer(5) + if rplyPath, rplyIndx := nM.getIndex(path); rplyPath != expPath && rplyIndx != expIndx { + t.Errorf("Expected: path=%s ,indx=%v, received: path=%s ,indx=%v", expPath, expIndx, rplyPath, rplyIndx) + } + + path = "slice[~cgreq.Count]" + expPath = "slice" + expIndx = nil + if rplyPath, rplyIndx := nM.getIndex(path); rplyPath != expPath && rplyIndx != expIndx { + t.Errorf("Expected: path=%s ,indx=%v, received: path=%s ,indx=%v", expPath, expIndx, rplyPath, rplyIndx) + } +} + +func TestNavMapGetNextMap(t *testing.T) { + nM := NewNavigableMap(nil) + mp := map[string]interface{}{ + "field1": 10, + "field2": []string{"val1", "val2"}, + "field3": []int{1, 2, 3}, + "map1": map[string]interface{}{"field1": 100000}, + "map2": []map[string]interface{}{map[string]interface{}{"field2": 11}}, + "map3": []NavigableMap{NavigableMap{data: map[string]interface{}{"field4": 112}}}, + } + path := "map4" + if _, err := nM.getNextMap(mp, path); err != nil && err.Error() != utils.ErrNotFound.Error() { + t.Errorf("Expected error: %s , received error %v", utils.ErrNotFound.Error(), err) + } + path = "map2[10]" + if _, err := nM.getNextMap(mp, path); err != nil && err.Error() != utils.ErrNotFound.Error() { + t.Errorf("Expected error: %s , received error %v", utils.ErrNotFound.Error(), err) + } + path = "field1" + experr := fmt.Errorf("cannot cast field: <%+v> type: %T with path: <%s> to map[string]interface{}", + mp[path], mp[path], path) + if _, err := nM.getNextMap(mp, path); err != nil && err.Error() != experr.Error() { + t.Errorf("Expected error: %s , received error %v", experr.Error(), err) + } + path = "map1" + expMap := map[string]interface{}{"field1": 100000} + if rm, err := nM.getNextMap(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(rm, expMap) { + t.Errorf("Expected: %s, received: %s", utils.ToJSON(expMap), utils.ToJSON(rm)) + } + path = "map2[0]" + expMap = map[string]interface{}{"field2": 11} + if rm, err := nM.getNextMap(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(rm, expMap) { + t.Errorf("Expected: %s, received: %s", utils.ToJSON(expMap), utils.ToJSON(rm)) + } + path = "map3[0]" + expMap = map[string]interface{}{"field4": 112} + if rm, err := nM.getNextMap(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(rm, expMap) { + t.Errorf("Expected: %s, received: %s", utils.ToJSON(expMap), utils.ToJSON(rm)) + } +} + +func TestNavMapgetLastItem(t *testing.T) { + nM := NewNavigableMap(nil) + mp := map[string]interface{}{ + "field1": 10, + "field2": []string{"val1", "val2"}, + "field3": []int{1, 2, 3}, + "map1": map[string]interface{}{"field1": 100000}, + "map2": []map[string]interface{}{map[string]interface{}{"field2": 11}}, + "map3": []NavigableMap{NavigableMap{data: map[string]interface{}{"field4": 112}}}, + } + path := "map4" + if _, err := nM.getLastItem(mp, path); err != nil && err.Error() != utils.ErrNotFound.Error() { + t.Errorf("Expected error: %s , received error %v", utils.ErrNotFound.Error(), err) + } + path = "map2[10]" + if _, err := nM.getLastItem(mp, path); err != nil && err.Error() != utils.ErrNotFound.Error() { + t.Errorf("Expected error: %s , received error %v", utils.ErrNotFound.Error(), err) + } + path = "field1" + var expVal interface{} = 10 + if rplyVal, err := nM.getLastItem(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(expVal, rplyVal) { + t.Errorf("Expected: %v ,received: %v", expVal, rplyVal) + } + + path = "field2[1]" + expVal = "val2" + if rplyVal, err := nM.getLastItem(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(expVal, rplyVal) { + t.Errorf("Expected: %v ,received: %v", expVal, rplyVal) + } + path = "field3[2]" + expVal = 3 + if rplyVal, err := nM.getLastItem(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(expVal, rplyVal) { + t.Errorf("Expected: %v ,received: %v", expVal, rplyVal) + } + path = "field2" + expVal = []string{"val1", "val2"} + if rplyVal, err := nM.getLastItem(mp, path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(expVal, rplyVal) { + t.Errorf("Expected: %v ,received: %v", expVal, rplyVal) + } +} + +func TestNavMapFieldAsInterface(t *testing.T) { + nM := &NavigableMap{ + data: map[string]interface{}{ + "FirstLevel": map[string]interface{}{ + "SecondLevel": []map[string]interface{}{ + map[string]interface{}{ + "ThirdLevel": map[string]interface{}{ + "Fld1": "Val1", + }, + }, + map[string]interface{}{ + "Count": 10, + "ThirdLevel2": map[string]interface{}{ + "Fld2": []string{"Val1", "Val2", "Val3"}, + }, + }, + }, + }, + "AnotherFirstLevel": "ValAnotherFirstLevel", + }, + } + + path := []string{"FirstLevel", "SecondLevel[0]", "Count"} + expErr := utils.ErrNotFound + var eVal interface{} = nil + if _, err := nM.FieldAsInterface(path); err != nil && err.Error() != expErr.Error() { + t.Errorf("Expected error: %s, received error: %v", expErr.Error(), err) + } + + path = []string{"AnotherFirstLevel", "SecondLevel", "Count"} + expErr = fmt.Errorf("cannot cast field: <%+v> type: %T with path: <%s> to map[string]interface{}", + nM.data["AnotherFirstLevel"], nM.data["AnotherFirstLevel"], "AnotherFirstLevel") + if _, err := nM.FieldAsInterface(path); err != nil && err.Error() != expErr.Error() { + t.Errorf("Expected error: %s, received error: %v", expErr.Error(), err) + } + + path = []string{"FirstLevel", "SecondLevel[1]", "Count"} + eVal = 10 + if rplyVal, err := nM.FieldAsInterface(path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eVal, rplyVal) { + t.Errorf("Expected: %s , received: %s", utils.ToJSON(eVal), utils.ToJSON(rplyVal)) + } + + path = []string{"FirstLevel", "SecondLevel[1]", "ThirdLevel2", "Fld2"} + eVal = []string{"Val1", "Val2", "Val3"} + if rplyVal, err := nM.FieldAsInterface(path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eVal, rplyVal) { + t.Errorf("Expected: %s , received: %s", utils.ToJSON(eVal), utils.ToJSON(rplyVal)) + } + + path = []string{"FirstLevel", "SecondLevel[1]", "ThirdLevel2", "Fld2[2]"} + eVal = "Val3" + if rplyVal, err := nM.FieldAsInterface(path); err != nil { + t.Error(err) + } else if !reflect.DeepEqual(eVal, rplyVal) { + t.Errorf("Expected: %s , received: %s", utils.ToJSON(eVal), utils.ToJSON(rplyVal)) + } +} diff --git a/engine/libsuppliers.go b/engine/libsuppliers.go index b50c317f1..18feda731 100644 --- a/engine/libsuppliers.go +++ b/engine/libsuppliers.go @@ -23,6 +23,7 @@ import ( "sort" "strings" + "github.com/cgrates/cgrates/config" "github.com/cgrates/cgrates/utils" ) @@ -148,6 +149,24 @@ func (sSpls *SortedSuppliers) Digest() string { return strings.Join(sSpls.SuppliersWithParams(), utils.FIELDS_SEP) } +func (sSpls *SortedSuppliers) AsNavigableMap() (nm *config.NavigableMap) { + mp := map[string]interface{}{ + "ProfileID": sSpls.ProfileID, + "Sorting": sSpls.Sorting, + "Count": sSpls.Count, + } + sm := make([]map[string]interface{}, len(sSpls.SortedSuppliers)) + for i, ss := range sSpls.SortedSuppliers { + sm[i] = map[string]interface{}{ + "SupplierID": ss.SupplierID, + "SupplierParameters": ss.SupplierParameters, + "SortingData": ss.SortingData, + } + } + mp["SortedSuppliers"] = sm + return config.NewNavigableMap(mp) +} + type SupplierWithParams struct { SupplierName string SupplierParams string diff --git a/sessions/sessions.go b/sessions/sessions.go index 2bde8462b..134e6353b 100644 --- a/sessions/sessions.go +++ b/sessions/sessions.go @@ -1714,7 +1714,7 @@ func (v1AuthReply *V1AuthorizeReply) AsNavigableMap( cgrReply[utils.CapMaxUsage] = *v1AuthReply.MaxUsage } if v1AuthReply.Suppliers != nil { - cgrReply[utils.CapSuppliers] = *v1AuthReply.Suppliers + cgrReply[utils.CapSuppliers] = v1AuthReply.Suppliers.AsNavigableMap() } if v1AuthReply.ThresholdIDs != nil { cgrReply[utils.CapThresholds] = *v1AuthReply.ThresholdIDs @@ -2604,7 +2604,7 @@ func (v1Rply *V1ProcessEventReply) AsNavigableMap( cgrReply[utils.CapAttributes] = attrs } if v1Rply.Suppliers != nil { - cgrReply[utils.CapSuppliers] = *v1Rply.Suppliers + cgrReply[utils.CapSuppliers] = v1Rply.Suppliers.AsNavigableMap() } } return config.NewNavigableMap(cgrReply), nil diff --git a/sessions/sessions_test.go b/sessions/sessions_test.go index c021a618a..68a5f96a0 100644 --- a/sessions/sessions_test.go +++ b/sessions/sessions_test.go @@ -1007,7 +1007,7 @@ func TestSessionSV1AuthorizeReplyAsNavigableMap(t *testing.T) { utils.CapAttributes: map[string]interface{}{"OfficeGroup": "Marketing"}, utils.CapResourceAllocation: "ResGr1", utils.CapMaxUsage: 5 * time.Minute, - utils.CapSuppliers: *splrs, + utils.CapSuppliers: splrs.AsNavigableMap(), utils.CapThresholds: *thIDs, utils.CapStatQueues: *statIDs, })