mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-11 18:16:24 +05:00
416 lines
14 KiB
Go
416 lines
14 KiB
Go
/*
|
|
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 <https://www.gnu.org/licenses/>
|
|
*/
|
|
|
|
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
|
|
}
|