/* 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 Affero 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 Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see */ package utils import ( "strconv" "strings" ) // CompilePathSlice returns the path as a slice accepted by DataNode structure // field1[field2][index], field3 // will become: // field1 field2 index field3 func CompilePathSlice(spath []string) (path []string) { path = make([]string, 0, len(spath)) // in most cases the length will be the same for _, p := range spath { idxStart := strings.Index(p, IdxStart) if idxStart == -1 || !strings.HasSuffix(p, IdxEnd) { // no index in current path path = append(path, p) // append the path as it is continue } path = append(path, p[:idxStart]) // append first part of path path = append(path, strings.Split(p[idxStart+1:len(p)-1], IdxCombination)...) // append the indexes } return } // CompilePath returns the path as a slice func CompilePath(spath string) (path []string) { return CompilePathSlice(SplitPath(spath, NestingSep[0], -1)) } // NewDataNode using the compiled path creates a node func NewDataNode(path []string) (n *DataNode) { n = new(DataNode) if len(path) == 0 { // is most probably a leaf return // so return it as it is } obj := NewDataNode(path[1:]) // create the next node if path[0] == "0" { // only support the 0 index when creating new array n.Type = NMSliceType // is a slice so put the first element as the obj node n.Slice = []*DataNode{obj} return } n.Type = NMMapType // consider it a map otherwise n.Map = map[string]*DataNode{path[0]: obj} // and the first element from path is the key in map return } // NewLeafNode creates a leaf node with given value func NewLeafNode(val any) *DataNode { return &DataNode{ Type: NMDataType, Value: &DataLeaf{ Data: val, }, } } // DataNode a structure for storing data type DataNode struct { Type NMType `json:"-"` Map map[string]*DataNode `json:",omitempty"` Slice []*DataNode `json:",omitempty"` Value *DataLeaf `json:",omitempty"` } // DataLeaf is an item in the DataNode type DataLeaf struct { Data any // value of the element // the config of the leaf NewBranch bool `json:",omitempty"` AttributeID string `json:",omitempty"` } // String returs the value of the leaf as string func (dl *DataLeaf) String() string { return IfaceAsString(dl.Data) } // Field returns the DataLeaf found at path // if at the end of the path is not a DataLeaf return an error func (n *DataNode) Field(path []string) (*DataLeaf, error) { switch n.Type { // based on current type return the value case NMDataType: if len(path) != 0 { // only return if the path is empty return nil, ErrNotFound } return n.Value, nil case NMMapType: if len(path) == 0 { // do not return in this function the n.Map return nil, ErrWrongPath } node, has := n.Map[path[0]] if !has { // not found in map return nil, ErrNotFound } return node.Field(path[1:]) // let the next node handle the value case NMSliceType: if len(path) == 0 { // do not return in this function the n.Slice return nil, ErrWrongPath } if path[0] == Length { // special case for slice to return the Length of the slice return &DataLeaf{Data: len(n.Slice)}, nil } idx, err := strconv.Atoi(path[0]) // convert the path to index if err != nil { return nil, err } if idx < 0 { // in case the index is negative add the slice length idx += len(n.Slice) } if idx < 0 || idx >= len(n.Slice) { // check if the index is in range [0,len(slice)) return nil, ErrNotFound } return n.Slice[idx].Field(path[1:]) // let the next node handle the value } // this is possible if the node was created but no value was assigned to it return nil, ErrWrongPath } // FieldAsInterface will compile the given path // and return the node value at the end of the path // this function is used most probably by filters so expect the path to not be compiled func (n *DataNode) FieldAsInterface(path []string) (any, error) { return n.fieldAsInterface(CompilePathSlice(path)) } // fieldAsInterface return ill the node value at the end of the path // but will not compile the path func (n *DataNode) fieldAsInterface(path []string) (any, error) { switch n.Type { // based on current type return the value case NMDataType: if len(path) != 0 { // only return if the path is empty return nil, ErrNotFound } return n.Value.Data, nil case NMMapType: if len(path) == 0 { // last element form path so return the n.Map return n.Map, nil } node, has := n.Map[path[0]] if !has { // not found in map return nil, ErrNotFound } return node.fieldAsInterface(path[1:]) // let the next node handle the value case NMSliceType: if len(path) == 0 { // last element form path so return the n.Slice return n.Slice, nil } if path[0] == Length { // special case for slice to return the Length of the slice return len(n.Slice), nil } idx, err := strconv.Atoi(path[0]) // convert the path to index if err != nil { return nil, err } if idx < 0 { // in case the index is negative add the slice length idx += len(n.Slice) } if idx < 0 || idx >= len(n.Slice) { // check if the index is in range [0,len(slice)) return nil, ErrNotFound } return n.Slice[idx].fieldAsInterface(path[1:]) // let the next node handle the value } // this is possible if the node was created but no value was assigned to it return nil, ErrWrongPath } // Set will set the value at de specified path // the path should be in the same format as the path given to Field func (n *DataNode) Set(path []string, val any) (addedNew bool, err error) { if len(path) == 0 { // the path is empty so overwrite curent node data switch v := val.(type) { // cast the value to see if is a supported type for node case map[string]*DataNode: n.Type = NMMapType n.Map = v case []*DataNode: // need this to not parse the node path for each element in slice n.Type = NMSliceType n.Slice = v case *DataLeaf: n.Type = NMDataType n.Value = v case *DataNode: n.Type = v.Type n.Map = v.Map n.Slice = v.Slice n.Value = v.Value default: n.Type = NMDataType n.Value = &DataLeaf{ Data: val, } } return } switch n.Type { // the path has more elements so parse the nodes based on type case NMDataType: // node is an leaf but the path expects a slice or a map return false, ErrWrongPath case NMMapType: node, has := n.Map[path[0]] if !has { // create the node if not exists node = NewDataNode(path[1:]) n.Map[path[0]] = node } addedNew, err = node.Set(path[1:], val) // set the value in the node return addedNew || !has, err case NMSliceType: idx, err := strconv.Atoi(path[0]) // convert the path to int if err != nil { return false, err } if idx == len(n.Slice) { // special case when the index is the length so append node := NewDataNode(path[1:]) // create the node n.Slice = append(n.Slice, node) _, err = node.Set(path[1:], val) // set the value in the node return true, err } // try dynamic path instead // if idx < 0 { // in case the index is negative add the slice length // idx += len(n.Slice) // path[0] = strconv.Itoa(idx) // update the slice to reflect on orderNavMap // } if idx < 0 || idx >= len(n.Slice) { // check if the index is in range [0,len(slice)) return false, ErrNotFound } return n.Slice[idx].Set(path[1:], val) // set the value in the next node } // this is possible if the node was created but no value was assigned to it return false, ErrWrongPath } // IsEmpty return if the node is empty/ has no data func (n DataNode) IsEmpty() bool { return n.Value == nil && len(n.Map) == 0 && len(n.Slice) == 0 } // Remove will remove the value at given path // the path should be in the same format as the path given to Field // also removes the empty nodes found in path func (n *DataNode) Remove(path []string) error { if len(path) == 0 { // the path is empty so make the node empty n.Map = nil n.Slice = nil n.Value = nil return nil } switch n.Type { // the path has more elements so parse the nodes based on type case NMDataType: // no remove for data type return ErrWrongPath case NMMapType: node, has := n.Map[path[0]] if !has { // the element doesn't exist so ignore return nil } err := node.Remove(path[1:]) // remove the element in next path if node.IsEmpty() { // remove the current element if empty delete(n.Map, path[0]) } return err case NMSliceType: idx, err := strconv.Atoi(path[0]) // convert the path to int if err != nil { return err // the only error is when we expect an index but is not int } if idx < 0 { // in case the index is negative add the slice length idx += len(n.Slice) path[0] = strconv.Itoa(idx) // update the path for OrdNavMap } if idx < 0 || idx >= len(n.Slice) { // the index is not in range so ignore return nil } err = n.Slice[idx].Remove(path[1:]) // remove the element in next path if n.Slice[idx].IsEmpty() { // remove the current element if empty n.Slice = append(n.Slice[:idx], n.Slice[idx+1:]...) // slow but needed } return err } // this is possible if the node was created but no value was assigned to it return ErrWrongPath } // Append will append the value at de specified path // the path should be in the same format as the path given to Field func (n *DataNode) Append(path []string, val *DataLeaf) (idx int, err error) { if len(path) == 0 { // the path is empty so overwrite curent node data switch n.Type { case NMMapType: // append only works for slice return -1, ErrWrongPath case NMDataType: if n.Value != nil && n.Value.Data != nil { // it has alerady data so can not append return -1, ErrWrongPath } // is empty so make a slice to be compatible with append n.Type = NMSliceType n.Value = nil n.Slice = []*DataNode{{Type: NMDataType, Value: val}} return 0, nil default: // is a slice so append the leaf as a DataNode n.Type = NMSliceType n.Slice = append(n.Slice, &DataNode{Type: NMDataType, Value: val}) return len(n.Slice) - 1, nil } } switch n.Type { // the path has more elements so parse the nodes based on type case NMDataType: // node is an leaf but the path expects a slice or a map return -1, ErrWrongPath case NMMapType: node, has := n.Map[path[0]] if !has { // create the node if not exists node = NewDataNode(path[1:]) n.Map[path[0]] = node } return node.Append(path[1:], val) // append the value in the next node case NMSliceType: idx, err := strconv.Atoi(path[0]) // convert the path to int if err != nil { return -1, err } if idx == len(n.Slice) { // special case when the index is the length so append node := NewDataNode(path[1:]) n.Slice = append(n.Slice, node) return node.Append(path[1:], val) } // try dynamic path instead // if idx < 0 { // in case the index is negative add the slice length // idx += len(n.Slice) // path[0] = strconv.Itoa(idx) // update the slice to reflect on orderNavMap // } if idx < 0 || idx >= len(n.Slice) { // check if the index is in range [0,len(slice)) return -1, ErrNotFound } return n.Slice[idx].Append(path[1:], val) // append the value in the next node } // this is possible if the node was created but no value was assigned to it return -1, ErrWrongPath } // Compose will string compose the value at de specified path with the given value // the path should be in the same format as the path given to Field func (n *DataNode) Compose(path []string, val *DataLeaf) (err error) { if len(path) == 0 { // the path is empty so overwrite curent node data switch n.Type { case NMMapType: // compose only works for slice and dataType return ErrWrongPath case NMDataType: if n.Value == nil || n.Value.Data == nil { // is empty so make a slice to be compatible with append n.Type = NMSliceType n.Value = nil n.Slice = []*DataNode{{Type: NMDataType, Value: val}} return } n.Value.Data = n.Value.String() + val.String() // has already data so do a simple compose default: // is a slice if len(n.Slice) == 0 { // empty so add the first element n.Type = NMSliceType n.Slice = []*DataNode{{Type: NMDataType, Value: val}} return } n.Slice[len(n.Slice)-1].Value.Data = n.Slice[len(n.Slice)-1].Value.String() + val.String() // compose the last element from slice } return } switch n.Type { // the path has more elements so parse the nodes based on type case NMDataType: // node is an leaf but the path expects a slice or a map return ErrWrongPath case NMMapType: node, has := n.Map[path[0]] if !has { // create the node if not exists node = NewDataNode(path[1:]) n.Map[path[0]] = node } return node.Compose(path[1:], val) // compose the value in the next node case NMSliceType: idx, err := strconv.Atoi(path[0]) // convert the path to int if err != nil { return err } if idx == len(n.Slice) { // special case when the index is the length so append node := NewDataNode(path[1:]) n.Slice = append(n.Slice, node) return node.Compose(path[1:], val) } // try dynamic path instead // if idx < 0 { // in case the index is negative add the slice length // idx += len(n.Slice) // path[0] = strconv.Itoa(idx) // update the slice to reflect on orderNavMap // } if idx < 0 || idx >= len(n.Slice) { // check if the index is in range [0,len(slice)) return ErrNotFound } return n.Slice[idx].Compose(path[1:], val) // compose the value in the next node } // this is possible if the node was created but no value was assigned to it return ErrWrongPath }