Files
cgrates/utils/routes.go

465 lines
12 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 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 <http://www.gnu.org/licenses/>
*/
package utils
import (
"maps"
"strconv"
"strings"
)
// RoutesDefaultRatio is the default ratio value for routes when none is
// explicitly specified in the profile. Defined here to avoid circular
// dependencies with the config package.
var RoutesDefaultRatio = 1
// RouteProfile represents the configuration of a Route profile.
type RouteProfile struct {
Tenant string
ID string // LCR Profile ID
FilterIDs []string
Weights DynamicWeights
Blockers DynamicBlockers
Sorting string // Sorting strategy
SortingParameters []string
Routes []*Route
}
// Clone method for RouteProfile
func (rp *RouteProfile) Clone() *RouteProfile {
if rp == nil {
return nil
}
clone := &RouteProfile{
Tenant: rp.Tenant,
ID: rp.ID,
Sorting: rp.Sorting,
}
if rp.FilterIDs != nil {
clone.FilterIDs = make([]string, len(rp.FilterIDs))
copy(clone.FilterIDs, rp.FilterIDs)
}
if rp.SortingParameters != nil {
clone.SortingParameters = make([]string, len(rp.SortingParameters))
copy(clone.SortingParameters, rp.SortingParameters)
}
if rp.Routes != nil {
clone.Routes = make([]*Route, len(rp.Routes))
for i, route := range rp.Routes {
clone.Routes[i] = route.Clone()
}
}
if rp.Weights != nil {
clone.Weights = rp.Weights.Clone()
}
if rp.Blockers != nil {
clone.Blockers = rp.Blockers.Clone()
}
return clone
}
// CacheClone returns a clone of RouteProfile used by ltcache CacheCloner
func (rp *RouteProfile) CacheClone() any {
return rp.Clone()
}
// RouteProfileWithAPIOpts wraps RouteProfile with APIOpts.
type RouteProfileWithAPIOpts struct {
*RouteProfile
APIOpts map[string]any
}
// compileCacheParameters prepares route ratios for MetaLoad sorting by parsing the
// SortingParameters and applying appropriate ratio values to each route.
func (rp *RouteProfile) compileCacheParameters() error {
if rp.Sorting != MetaLoad {
return nil
}
// Parse route ID to ratio mappings.
ratioMap := make(map[string]int)
for _, param := range rp.SortingParameters {
parts := strings.Split(param, ConcatenatedKeySep)
ratio, err := strconv.Atoi(parts[1])
if err != nil {
return err
}
ratioMap[parts[0]] = ratio
}
// Get default ratio (from map or config).
defaultRatio := RoutesDefaultRatio
if mapDefault, exists := ratioMap[MetaDefault]; exists {
defaultRatio = mapDefault
}
// Apply appropriate ratio to each route.
for _, route := range rp.Routes {
route.cacheRoute = make(map[string]any)
ratio, exists := ratioMap[route.ID]
if !exists {
ratio = defaultRatio
}
route.cacheRoute[MetaRatio] = ratio
}
return nil
}
// Compile is a wrapper for convenience setting up the RouteProfile.
func (rp *RouteProfile) Compile() error {
return rp.compileCacheParameters()
}
// TenantID returns unique identifier of the LCRProfile in a multi-tenant environment.
func (rp *RouteProfile) TenantID() string {
return ConcatenatedKey(rp.Tenant, rp.ID)
}
// Set implements the profile interface, setting values in RouteProfile based on path.
func (rp *RouteProfile) Set(path []string, val any, newBranch bool) (err error) {
switch len(path) {
default:
return ErrWrongPath
case 1:
switch path[0] {
default:
return ErrWrongPath
case Tenant:
rp.Tenant = IfaceAsString(val)
case ID:
rp.ID = IfaceAsString(val)
case FilterIDs:
var valA []string
valA, err = IfaceAsStringSlice(val)
rp.FilterIDs = append(rp.FilterIDs, valA...)
case SortingParameters:
var valA []string
valA, err = IfaceAsStringSlice(val)
rp.SortingParameters = append(rp.SortingParameters, valA...)
case Sorting:
if valStr := IfaceAsString(val); len(valStr) != 0 {
rp.Sorting = valStr
}
case Weights:
if val != EmptyString {
rp.Weights, err = NewDynamicWeightsFromString(IfaceAsString(val), InfieldSep, ANDSep)
}
case Blockers:
if val != EmptyString {
rp.Blockers, err = NewDynamicBlockersFromString(IfaceAsString(val), InfieldSep, ANDSep)
}
}
case 2:
if val == EmptyString {
return
}
if path[0] != Routes {
return ErrWrongPath
}
if len(rp.Routes) == 0 || newBranch {
rp.Routes = append(rp.Routes, new(Route))
}
rt := rp.Routes[len(rp.Routes)-1]
switch path[1] {
case ID:
rt.ID = IfaceAsString(val)
case FilterIDs:
var valA []string
valA, err = IfaceAsStringSlice(val)
rt.FilterIDs = append(rt.FilterIDs, valA...)
case AccountIDs:
var valA []string
valA, err = IfaceAsStringSlice(val)
rt.AccountIDs = append(rt.AccountIDs, valA...)
case RateProfileIDs:
var valA []string
valA, err = IfaceAsStringSlice(val)
rt.RateProfileIDs = append(rt.RateProfileIDs, valA...)
case ResourceIDs:
var valA []string
valA, err = IfaceAsStringSlice(val)
rt.ResourceIDs = append(rt.ResourceIDs, valA...)
case StatIDs:
var valA []string
valA, err = IfaceAsStringSlice(val)
rt.StatIDs = append(rt.StatIDs, valA...)
case Weights:
if val != EmptyString {
rt.Weights, err = NewDynamicWeightsFromString(IfaceAsString(val), InfieldSep, ANDSep)
}
case Blockers:
if val != EmptyString {
rt.Blockers, err = NewDynamicBlockersFromString(IfaceAsString(val), InfieldSep, ANDSep)
}
case RouteParameters:
rt.RouteParameters = IfaceAsString(val)
default:
return ErrWrongPath
}
}
return
}
// Merge implements the profile interface, merging values from another RouteProfile.
func (rp *RouteProfile) Merge(v2 any) {
vi := v2.(*RouteProfile)
if len(vi.Tenant) != 0 {
rp.Tenant = vi.Tenant
}
if len(vi.ID) != 0 {
rp.ID = vi.ID
}
rp.FilterIDs = append(rp.FilterIDs, vi.FilterIDs...)
rp.SortingParameters = append(rp.SortingParameters, vi.SortingParameters...)
var equal bool
for _, routeV2 := range vi.Routes {
for _, route := range rp.Routes {
if route.ID == routeV2.ID {
route.Merge(routeV2)
equal = true
break
}
}
if !equal {
rp.Routes = append(rp.Routes, routeV2)
}
equal = false
}
rp.Weights = append(rp.Weights, vi.Weights...)
rp.Blockers = append(rp.Blockers, vi.Blockers...)
if len(vi.Sorting) != 0 {
rp.Sorting = vi.Sorting
}
}
// String implements the DataProvider interface, returning the RouteProfile in JSON format.
func (rp *RouteProfile) String() string { return ToJSON(rp) }
// FieldAsString implements the DataProvider interface, retrieving field value as string.
func (rp *RouteProfile) FieldAsString(fldPath []string) (_ string, err error) {
var val any
if val, err = rp.FieldAsInterface(fldPath); err != nil {
return
}
return IfaceAsString(val), nil
}
// FieldAsInterface implements the DataProvider interface, retrieving field value as interface.
func (rp *RouteProfile) FieldAsInterface(fldPath []string) (_ any, err error) {
if len(fldPath) == 1 {
switch fldPath[0] {
default:
fld, idx := GetPathIndex(fldPath[0])
if idx != nil {
switch fld {
case SortingParameters:
if *idx < len(rp.SortingParameters) {
return rp.SortingParameters[*idx], nil
}
case FilterIDs:
if *idx < len(rp.FilterIDs) {
return rp.FilterIDs[*idx], nil
}
case Routes:
if *idx < len(rp.Routes) {
return rp.Routes[*idx], nil
}
}
}
return nil, ErrNotFound
case Tenant:
return rp.Tenant, nil
case ID:
return rp.ID, nil
case FilterIDs:
return rp.FilterIDs, nil
case Weights:
return rp.Weights.String(InfieldSep, ANDSep), nil
case SortingParameters:
return rp.SortingParameters, nil
case Sorting:
return rp.Sorting, nil
case Blockers:
return rp.Blockers.String(InfieldSep, ANDSep), nil
case Routes:
return rp.Routes, nil
}
}
if len(fldPath) == 0 {
return nil, ErrNotFound
}
fld, idx := GetPathIndex(fldPath[0])
if fld != Routes ||
idx == nil {
return nil, ErrNotFound
}
if *idx >= len(rp.Routes) {
return nil, ErrNotFound
}
return rp.Routes[*idx].FieldAsInterface(fldPath[1:])
}
// Route defines a single route within a RouteProfile.
type Route struct {
ID string // RouteID
FilterIDs []string
AccountIDs []string
RateProfileIDs []string // used when computing price
ResourceIDs []string // queried in some strategies
StatIDs []string // queried in some strategies
Weights DynamicWeights
Blockers DynamicBlockers // if true, stops processing further routes
RouteParameters string
// Internal cache for route properties
// Example: cacheRoute["*ratio"] contains the route's ratio value
cacheRoute map[string]any
}
// Clone method for Route
func (r *Route) Clone() *Route {
if r == nil {
return nil
}
clone := &Route{
ID: r.ID,
RouteParameters: r.RouteParameters,
}
if r.FilterIDs != nil {
clone.FilterIDs = make([]string, len(r.FilterIDs))
copy(clone.FilterIDs, r.FilterIDs)
}
if r.Weights != nil {
clone.Weights = r.Weights.Clone()
}
if r.Blockers != nil {
clone.Blockers = r.Blockers.Clone()
}
if r.AccountIDs != nil {
clone.AccountIDs = make([]string, len(r.AccountIDs))
copy(clone.AccountIDs, r.AccountIDs)
}
if r.RateProfileIDs != nil {
clone.RateProfileIDs = make([]string, len(r.RateProfileIDs))
copy(clone.RateProfileIDs, r.RateProfileIDs)
}
if r.ResourceIDs != nil {
clone.ResourceIDs = make([]string, len(r.ResourceIDs))
copy(clone.ResourceIDs, r.ResourceIDs)
}
if r.StatIDs != nil {
clone.StatIDs = make([]string, len(r.StatIDs))
copy(clone.StatIDs, r.StatIDs)
}
if r.cacheRoute != nil {
clone.cacheRoute = make(map[string]any)
maps.Copy(clone.cacheRoute, r.cacheRoute)
}
return clone
}
// Ratio returns the cached ratio value for this route.
func (r *Route) Ratio() any {
return r.cacheRoute[MetaRatio]
}
// Merge implements the merge interface, merging values from another Route.
func (r *Route) Merge(v2 *Route) {
if len(v2.ID) != 0 {
r.ID = v2.ID
}
if len(v2.RouteParameters) != 0 {
r.RouteParameters = v2.RouteParameters
}
r.Weights = append(r.Weights, v2.Weights...)
r.Blockers = append(r.Blockers, v2.Blockers...)
r.FilterIDs = append(r.FilterIDs, v2.FilterIDs...)
r.AccountIDs = append(r.AccountIDs, v2.AccountIDs...)
r.RateProfileIDs = append(r.RateProfileIDs, v2.RateProfileIDs...)
r.ResourceIDs = append(r.ResourceIDs, v2.ResourceIDs...)
r.StatIDs = append(r.StatIDs, v2.StatIDs...)
}
// String implements the DataProvider interface, returning the Route in JSON format.
func (r *Route) String() string { return ToJSON(r) }
// FieldAsString implements the DataProvider interface, retrieving field value as string.
func (r *Route) FieldAsString(fldPath []string) (_ string, err error) {
var val any
if val, err = r.FieldAsInterface(fldPath); err != nil {
return
}
return IfaceAsString(val), nil
}
// FieldAsInterface implements the DataProvider interface, retrieving field value as interface.
func (r *Route) FieldAsInterface(fldPath []string) (_ any, err error) {
if len(fldPath) != 1 {
return nil, ErrNotFound
}
switch fldPath[0] {
default:
fld, idx := GetPathIndex(fldPath[0])
if idx != nil {
switch fld {
case AccountIDs:
if *idx < len(r.AccountIDs) {
return r.AccountIDs[*idx], nil
}
case FilterIDs:
if *idx < len(r.FilterIDs) {
return r.FilterIDs[*idx], nil
}
case RateProfileIDs:
if *idx < len(r.RateProfileIDs) {
return r.RateProfileIDs[*idx], nil
}
case ResourceIDs:
if *idx < len(r.ResourceIDs) {
return r.ResourceIDs[*idx], nil
}
case StatIDs:
if *idx < len(r.StatIDs) {
return r.StatIDs[*idx], nil
}
}
}
return nil, ErrNotFound
case ID:
return r.ID, nil
case FilterIDs:
return r.FilterIDs, nil
case AccountIDs:
return r.AccountIDs, nil
case RateProfileIDs:
return r.RateProfileIDs, nil
case ResourceIDs:
return r.ResourceIDs, nil
case StatIDs:
return r.StatIDs, nil
case Weights:
return r.Weights.String(InfieldSep, ANDSep), nil
case Blockers:
return r.Blockers.String(InfieldSep, ANDSep), nil
case RouteParameters:
return r.RouteParameters, nil
}
}