Files
cgrates/engine/dynamicdp.go
2025-05-03 21:42:36 +02:00

295 lines
10 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 engine
import (
"fmt"
"github.com/nyaruka/phonenumbers"
"github.com/cgrates/birpc/context"
"github.com/cgrates/cgrates/utils"
)
func NewDynamicDP(ctx *context.Context, resConns, stsConns, actsConns, trdConns, rnkCOnns []string,
tenant string, initialDP utils.DataProvider) *DynamicDP {
return &DynamicDP{
resConns: resConns,
stsConns: stsConns,
actsConns: actsConns,
trdConns: trdConns,
rnkConns: rnkCOnns,
tenant: tenant,
initialDP: initialDP,
cache: utils.MapStorage{},
ctx: ctx,
}
}
type DynamicDP struct {
resConns []string
stsConns []string
actsConns []string
trdConns []string
rnkConns []string
tenant string
initialDP utils.DataProvider
cache utils.MapStorage
ctx *context.Context
}
func (dDP *DynamicDP) String() string { return dDP.initialDP.String() }
func (dDP *DynamicDP) FieldAsString(fldPath []string) (string, error) {
val, err := dDP.FieldAsInterface(fldPath)
if err != nil {
return "", err
}
return utils.IfaceAsString(val), nil
}
var initialDPPrefixes = utils.NewStringSet([]string{
utils.MetaReq, utils.MetaVars, utils.MetaCgreq,
utils.MetaCgrep, utils.MetaRep, utils.MetaAct,
utils.MetaEC, utils.MetaUCH, utils.MetaOpts,
utils.MetaHdr, utils.MetaTrl, utils.MetaCfg,
utils.MetaTenant, utils.MetaTmp})
func (dDP *DynamicDP) FieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) == 0 {
return nil, utils.ErrNotFound
}
if initialDPPrefixes.Has(fldPath[0]) {
return dDP.initialDP.FieldAsInterface(fldPath)
}
val, err = dDP.cache.FieldAsInterface(fldPath)
if err == utils.ErrNotFound { // in case not found in cache try to populate it
return dDP.fieldAsInterface(fldPath)
}
return
}
func (dDP *DynamicDP) fieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) < 2 {
return nil, fmt.Errorf("invalid fieldname <%s>", fldPath)
}
switch fldPath[0] {
case utils.MetaAccounts:
// sample of fieldName : ~*accounts.1001.Balances[Concrete1].Units
// split the field name in 3 parts
// fieldNameType (~*accounts), accountID(1001) and queried part (Balances.Balances[Concrete1].Units)
var account utils.Account
if err = connMgr.Call(dDP.ctx, dDP.actsConns, utils.AccountSv1GetAccount,
&utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &account); err != nil {
return
}
//construct dataProvider from account and set it further
dp := utils.NewObjectDP(account)
dDP.cache.Set(fldPath[:2], dp)
return dp.FieldAsInterface(fldPath[2:])
case utils.MetaResources:
// sample of fieldName : ~*resources.ResourceID.Field
var reply utils.ResourceWithConfig
if err := connMgr.Call(dDP.ctx, dDP.resConns, utils.ResourceSv1GetResourceWithConfig,
&utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &reply); err != nil {
return nil, err
}
dp := utils.NewObjectDP(&reply)
dDP.cache.Set(fldPath[:2], dp)
return dp.FieldAsInterface(fldPath[2:])
case utils.MetaStats:
// sample of fieldName : ~*stats.StatID.*acd
var statValues map[string]*utils.Decimal
if err := connMgr.Call(dDP.ctx, dDP.stsConns, utils.StatSv1GetQueueDecimalMetrics,
&utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}},
&statValues); err != nil {
return nil, err
}
for k, v := range statValues {
dDP.cache.Set([]string{utils.MetaStats, fldPath[1], k}, v)
}
return dDP.cache.FieldAsInterface(fldPath)
case utils.MetaTrends:
//sample of fieldName : ~*trends.TrendID.Metrics.*acd.Value
var trendSum utils.TrendSummary
if err := connMgr.Call(context.TODO(), dDP.trdConns, utils.TrendSv1GetTrendSummary, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &trendSum); err != nil {
return nil, err
}
dp := utils.NewObjectDP(trendSum)
dDP.cache.Set(fldPath[:2], dp)
return dp.FieldAsInterface(fldPath[2:])
case utils.MetaRankings:
// sample of fieldName : ~*rankings.RankingID.SortedStatIDs[0]
var rankingSum utils.RankingSummary
if err := connMgr.Call(context.TODO(), dDP.rnkConns, utils.RankingSv1GetRankingSummary, &utils.TenantIDWithAPIOpts{TenantID: &utils.TenantID{Tenant: dDP.tenant, ID: fldPath[1]}}, &rankingSum); err != nil {
return nil, err
}
dp := utils.NewObjectDP(rankingSum)
dDP.cache.Set(fldPath[:2], dp)
return dp.FieldAsInterface(fldPath[2:])
case utils.MetaLibPhoneNumber:
// sample of fieldName ~*libphonenumber.<~*req.Destination>
// or ~*libphonenumber.<~*req.Destination>.Carrier
dp, err := newLibPhoneNumberDP(fldPath[1])
if err != nil {
return nil, err
}
dDP.cache.Set(fldPath[:2], dp)
return dp.FieldAsInterface(fldPath[2:])
default: // in case of constant we give an empty DataProvider ( empty navigable map )
}
return nil, utils.ErrNotFound
}
func newLibPhoneNumberDP(number string) (dp utils.DataProvider, err error) {
num, err := phonenumbers.ParseAndKeepRawInput(number, utils.EmptyString)
if err != nil {
return nil, err
}
return &libphonenumberDP{pNumber: num, cache: make(utils.MapStorage)}, nil
}
type libphonenumberDP struct {
pNumber *phonenumbers.PhoneNumber
cache utils.MapStorage
}
func (dDP *libphonenumberDP) String() string { return dDP.pNumber.String() }
func (dDP *libphonenumberDP) FieldAsString(fldPath []string) (string, error) {
val, err := dDP.FieldAsInterface(fldPath)
if err != nil {
return "", err
}
return utils.IfaceAsString(val), nil
}
func (dDP *libphonenumberDP) FieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) == 0 {
dDP.setDefaultFields()
val = dDP.cache
return
}
if val, err = dDP.cache.FieldAsInterface(fldPath); err == utils.ErrNotFound { // in case not found in cache try to populate it
return dDP.fieldAsInterface(fldPath)
}
return
}
func (dDP *libphonenumberDP) fieldAsInterface(fldPath []string) (val any, err error) {
if len(fldPath) != 1 {
return nil, fmt.Errorf("invalid field path <%+v> for libphonenumberDP", fldPath)
}
switch fldPath[0] {
case "CountryCode":
val = dDP.pNumber.GetCountryCode()
case "NationalNumber":
val = dDP.pNumber.GetNationalNumber()
case "Region":
val = phonenumbers.GetRegionCodeForNumber(dDP.pNumber)
case "NumberType":
val = phonenumbers.GetNumberType(dDP.pNumber)
case "GeoLocation":
regCode := phonenumbers.GetRegionCodeForNumber(dDP.pNumber)
geoLocation, err := phonenumbers.GetGeocodingForNumber(dDP.pNumber, regCode)
if err != nil {
utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting GeoLocation for number %+v", err, dDP.pNumber))
}
val = geoLocation
case "Carrier":
carrier, err := phonenumbers.GetCarrierForNumber(dDP.pNumber, phonenumbers.GetRegionCodeForNumber(dDP.pNumber))
if err != nil {
utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting Carrier for number %+v", err, dDP.pNumber))
}
val = carrier
case "LengthOfNationalDestinationCode":
val = phonenumbers.GetLengthOfNationalDestinationCode(dDP.pNumber)
case "RawInput":
val = dDP.pNumber.GetRawInput()
case "Extension":
val = dDP.pNumber.GetExtension()
case "NumberOfLeadingZeros":
val = dDP.pNumber.GetNumberOfLeadingZeros()
case "ItalianLeadingZero":
val = dDP.pNumber.GetItalianLeadingZero()
case "PreferredDomesticCarrierCode":
val = dDP.pNumber.GetPreferredDomesticCarrierCode()
case "CountryCodeSource":
val = dDP.pNumber.GetCountryCodeSource()
}
dDP.cache[fldPath[0]] = val
return
}
func (dDP *libphonenumberDP) setDefaultFields() {
if _, has := dDP.cache["CountryCode"]; !has {
dDP.cache["CountryCode"] = dDP.pNumber.GetCountryCode()
}
if _, has := dDP.cache["NationalNumber"]; !has {
dDP.cache["NationalNumber"] = dDP.pNumber.GetNationalNumber()
}
if _, has := dDP.cache["Region"]; !has {
dDP.cache["Region"] = phonenumbers.GetRegionCodeForNumber(dDP.pNumber)
}
if _, has := dDP.cache["NumberType"]; !has {
dDP.cache["NumberType"] = phonenumbers.GetNumberType(dDP.pNumber)
}
if _, has := dDP.cache["GeoLocation"]; !has {
geoLocation, err := phonenumbers.GetGeocodingForNumber(dDP.pNumber, phonenumbers.GetRegionCodeForNumber(dDP.pNumber))
if err != nil {
utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting GeoLocation for number %+v", err, dDP.pNumber))
}
dDP.cache["GeoLocation"] = geoLocation
}
if _, has := dDP.cache["Carrier"]; !has {
carrier, err := phonenumbers.GetCarrierForNumber(dDP.pNumber, phonenumbers.GetRegionCodeForNumber(dDP.pNumber))
if err != nil {
utils.Logger.Warning(fmt.Sprintf("Received error: <%+v> when getting Carrier for number %+v", err, dDP.pNumber))
}
dDP.cache["Carrier"] = carrier
}
if _, has := dDP.cache["LengthOfNationalDestinationCode"]; !has {
dDP.cache["LengthOfNationalDestinationCode"] = phonenumbers.GetLengthOfNationalDestinationCode(dDP.pNumber)
}
if _, has := dDP.cache["RawInput"]; !has {
dDP.cache["RawInput"] = dDP.pNumber.GetRawInput()
}
if _, has := dDP.cache["Extension"]; !has {
dDP.cache["Extension"] = dDP.pNumber.GetExtension()
}
if _, has := dDP.cache["NumberOfLeadingZeros"]; !has {
dDP.cache["NumberOfLeadingZeros"] = dDP.pNumber.GetNumberOfLeadingZeros()
}
if _, has := dDP.cache["ItalianLeadingZero"]; !has {
dDP.cache["ItalianLeadingZero"] = dDP.pNumber.GetItalianLeadingZero()
}
if _, has := dDP.cache["PreferredDomesticCarrierCode"]; !has {
dDP.cache["PreferredDomesticCarrierCode"] = dDP.pNumber.GetPreferredDomesticCarrierCode()
}
if _, has := dDP.cache["CountryCodeSource"]; !has {
dDP.cache["CountryCodeSource"] = dDP.pNumber.GetCountryCodeSource()
}
}