mirror of
https://github.com/cgrates/cgrates.git
synced 2026-02-12 18:46:24 +05:00
Git 2.45+ introduced a backward incompatible change in the iso-strict date format, showing time in the Zulu timezone with "Z" suffix instead of "+00:00". This commit adds parsing for the new date format before falling back to the old format. Revise GetCGRVersion error messages. Revise GetCGRVersion unit test.
943 lines
25 KiB
Go
943 lines
25 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 (
|
|
"archive/zip"
|
|
"bytes"
|
|
"crypto/rand"
|
|
"crypto/sha1"
|
|
"encoding/gob"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"log"
|
|
"math"
|
|
math_rand "math/rand"
|
|
"os"
|
|
"path/filepath"
|
|
"reflect"
|
|
"regexp"
|
|
"strconv"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
|
|
"github.com/cgrates/rpcclient"
|
|
)
|
|
|
|
var (
|
|
startCGRateSTime time.Time
|
|
|
|
rfc3339Rule = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}.+$`)
|
|
sqlRule = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}$`)
|
|
utcFormat = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}[T]\d{2}:\d{2}:\d{2}$`)
|
|
gotimeRule = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.?\d*\s[+,-]\d+\s\w+$`)
|
|
gotimeRule2 = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}\s\d{2}:\d{2}:\d{2}\.?\d*\s[+,-]\d+\s[+,-]\d+$`)
|
|
fsTimestamp = regexp.MustCompile(`^\d{16}$`)
|
|
astTimestamp = regexp.MustCompile(`^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d*[+,-]\d+$`)
|
|
unixTimestampRule = regexp.MustCompile(`^\d{10}$`)
|
|
unixTimestampMilisecondsRule = regexp.MustCompile(`^\d{13}$`)
|
|
unixTimestampNanosecondsRule = regexp.MustCompile(`^\d{19}$`)
|
|
oneLineTimestampRule = regexp.MustCompile(`^\d{14}$`)
|
|
oneSpaceTimestampRule = regexp.MustCompile(`^\d{2}\.\d{2}.\d{4}\s{1}\d{2}:\d{2}:\d{2}$`)
|
|
eamonTimestampRule = regexp.MustCompile(`^\d{2}/\d{2}/\d{4}\s{1}\d{2}:\d{2}:\d{2}$`)
|
|
broadsoftTimestampRule = regexp.MustCompile(`^\d{14}\.\d{3}`)
|
|
)
|
|
|
|
func init() {
|
|
startCGRateSTime = time.Now()
|
|
math_rand.Seed(startCGRateSTime.UnixNano())
|
|
}
|
|
|
|
// GetStartTime return the Start time of engine (in UNIX format)
|
|
func GetStartTime() string {
|
|
return startCGRateSTime.Format(time.UnixDate)
|
|
}
|
|
|
|
func NewCounter(start, limit int64) *Counter {
|
|
return &Counter{
|
|
value: start,
|
|
limit: limit,
|
|
}
|
|
}
|
|
|
|
type Counter struct {
|
|
value, limit int64
|
|
sync.Mutex
|
|
}
|
|
|
|
func (c *Counter) Next() int64 {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
c.value += 1
|
|
if c.limit > 0 && c.value > c.limit {
|
|
c.value = 0
|
|
}
|
|
return c.value
|
|
}
|
|
|
|
func (c *Counter) Value() int64 {
|
|
c.Lock()
|
|
defer c.Unlock()
|
|
return c.value
|
|
}
|
|
|
|
// Returns first non empty string out of vals. Useful to extract defaults
|
|
func FirstNonEmpty(vals ...string) string {
|
|
for _, val := range vals {
|
|
if len(val) != 0 {
|
|
return val
|
|
}
|
|
}
|
|
return EmptyString
|
|
}
|
|
|
|
// Sha1 generate the SHA1 hash from any string
|
|
func Sha1(attrs ...string) string {
|
|
hasher := sha1.New()
|
|
for _, attr := range attrs {
|
|
hasher.Write([]byte(attr))
|
|
}
|
|
return fmt.Sprintf("%x", hasher.Sum(nil))
|
|
}
|
|
|
|
// helper function for uuid generation
|
|
func GenUUID() string {
|
|
b := make([]byte, 16)
|
|
_, err := io.ReadFull(rand.Reader, b)
|
|
if err != nil {
|
|
log.Fatal(err)
|
|
}
|
|
b[6] = (b[6] & 0x0F) | 0x40
|
|
b[8] = (b[8] &^ 0x40) | 0x80
|
|
return fmt.Sprintf("%x-%x-%x-%x-%x", b[:4], b[4:6], b[6:8], b[8:10],
|
|
b[10:])
|
|
}
|
|
|
|
// UUIDSha1Prefix generates a prefix of the sha1 applied to an UUID
|
|
// prefix 8 is chosen since the probability of colision starts being minimal after 7 characters (see git commits)
|
|
func UUIDSha1Prefix() string {
|
|
return Sha1(GenUUID())[:7]
|
|
}
|
|
|
|
// Round return rounded version of x with prec precision.
|
|
//
|
|
// Special cases are:
|
|
//
|
|
// Round(±0) = ±0
|
|
// Round(±Inf) = ±Inf
|
|
// Round(NaN) = NaN
|
|
func Round(x float64, prec int, method string) float64 {
|
|
var rounder float64
|
|
maxPrec := 7 // define a max precison to cut float errors
|
|
if maxPrec < prec {
|
|
maxPrec = prec
|
|
}
|
|
pow := math.Pow(10, float64(prec))
|
|
intermed := x * pow
|
|
_, frac := math.Modf(intermed)
|
|
|
|
switch method {
|
|
case ROUNDING_UP:
|
|
if frac >= math.Pow10(-maxPrec) { // Max precision we go, rest is float chaos
|
|
rounder = math.Ceil(intermed)
|
|
} else {
|
|
rounder = math.Floor(intermed)
|
|
}
|
|
case ROUNDING_DOWN:
|
|
rounder = math.Floor(intermed)
|
|
case ROUNDING_MIDDLE:
|
|
if frac >= 0.5 {
|
|
rounder = math.Ceil(intermed)
|
|
} else {
|
|
rounder = math.Floor(intermed)
|
|
}
|
|
default:
|
|
rounder = intermed
|
|
}
|
|
|
|
return rounder / pow
|
|
}
|
|
|
|
func getAddDuration(tmStr string) (addDur time.Duration, err error) {
|
|
eDurIdx := strings.Index(tmStr, "+")
|
|
if eDurIdx == -1 {
|
|
return
|
|
}
|
|
return time.ParseDuration(tmStr[eDurIdx+1:])
|
|
}
|
|
|
|
// ParseTimeDetectLayout returns the time from string
|
|
func ParseTimeDetectLayout(tmStr string, timezone string) (time.Time, error) {
|
|
tmStr = strings.TrimSpace(tmStr)
|
|
var nilTime time.Time
|
|
if len(tmStr) == 0 || tmStr == UNLIMITED {
|
|
return nilTime, nil
|
|
}
|
|
loc, err := time.LoadLocation(timezone)
|
|
if err != nil {
|
|
return nilTime, err
|
|
}
|
|
switch {
|
|
case tmStr == UNLIMITED || tmStr == "":
|
|
// leave it at zero
|
|
case tmStr == "*daily":
|
|
return time.Now().AddDate(0, 0, 1), nil // add one day
|
|
case tmStr == "*monthly":
|
|
return time.Now().AddDate(0, 1, 0), nil // add one month
|
|
case tmStr == "*yearly":
|
|
return time.Now().AddDate(1, 0, 0), nil // add one year
|
|
case strings.HasPrefix(tmStr, "*month_end"):
|
|
expDate := GetEndOfMonth(time.Now())
|
|
extraDur, err := getAddDuration(tmStr)
|
|
if err != nil {
|
|
return nilTime, err
|
|
}
|
|
expDate = expDate.Add(extraDur)
|
|
return expDate, nil
|
|
case strings.HasPrefix(tmStr, "*mo"): // add one month and extra duration
|
|
extraDur, err := getAddDuration(tmStr)
|
|
if err != nil {
|
|
return nilTime, err
|
|
}
|
|
return time.Now().AddDate(0, 1, 0).Add(extraDur), nil
|
|
case astTimestamp.MatchString(tmStr):
|
|
return time.Parse("2006-01-02T15:04:05.999999999-0700", tmStr)
|
|
case rfc3339Rule.MatchString(tmStr):
|
|
return time.Parse(time.RFC3339, tmStr)
|
|
case gotimeRule.MatchString(tmStr):
|
|
return time.Parse("2006-01-02 15:04:05.999999999 -0700 MST", tmStr)
|
|
case gotimeRule2.MatchString(tmStr):
|
|
return time.Parse("2006-01-02 15:04:05.999999999 -0700 -0700", tmStr)
|
|
case sqlRule.MatchString(tmStr):
|
|
return time.ParseInLocation("2006-01-02 15:04:05", tmStr, loc)
|
|
case fsTimestamp.MatchString(tmStr):
|
|
if tmstmp, err := strconv.ParseInt(tmStr+"000", 10, 64); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Unix(0, tmstmp).In(loc), nil
|
|
}
|
|
case unixTimestampRule.MatchString(tmStr):
|
|
if tmstmp, err := strconv.ParseInt(tmStr, 10, 64); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Unix(tmstmp, 0).In(loc), nil
|
|
}
|
|
case unixTimestampMilisecondsRule.MatchString(tmStr):
|
|
if tmstmp, err := strconv.ParseInt(tmStr, 10, 64); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Unix(0, tmstmp*int64(time.Millisecond)).In(loc), nil
|
|
}
|
|
case unixTimestampNanosecondsRule.MatchString(tmStr):
|
|
if tmstmp, err := strconv.ParseInt(tmStr, 10, 64); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Unix(0, tmstmp).In(loc), nil
|
|
}
|
|
case tmStr == "0" || len(tmStr) == 0: // Time probably missing from request
|
|
return nilTime, nil
|
|
case oneLineTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("20060102150405", tmStr, loc)
|
|
case oneSpaceTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("02.01.2006 15:04:05", tmStr, loc)
|
|
case eamonTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("02/01/2006 15:04:05", tmStr, loc)
|
|
case broadsoftTimestampRule.MatchString(tmStr):
|
|
return time.ParseInLocation("20060102150405.999", tmStr, loc)
|
|
case tmStr == "*now":
|
|
return time.Now(), nil
|
|
case strings.HasPrefix(tmStr, "+"):
|
|
tmStr = strings.TrimPrefix(tmStr, "+")
|
|
if tmStrTmp, err := time.ParseDuration(tmStr); err != nil {
|
|
return nilTime, err
|
|
} else {
|
|
return time.Now().Add(tmStrTmp), nil
|
|
}
|
|
case utcFormat.MatchString(tmStr):
|
|
return time.ParseInLocation("2006-01-02T15:04:05", tmStr, loc)
|
|
|
|
}
|
|
return nilTime, errors.New("Unsupported time format")
|
|
}
|
|
|
|
// RoundDuration returns a number equal or larger than the amount that exactly
|
|
// is divisible to whole
|
|
func RoundDuration(whole, amount time.Duration) time.Duration {
|
|
a, w := float64(amount), float64(whole)
|
|
if math.Mod(a, w) == 0 {
|
|
return amount
|
|
}
|
|
return time.Duration((w - math.Mod(a, w) + a))
|
|
}
|
|
|
|
func SplitPrefix(prefix string, minLength int) []string {
|
|
length := int(math.Max(float64(len(prefix)-(minLength-1)), 0))
|
|
subs := make([]string, length)
|
|
max := len(prefix)
|
|
for i := 0; i < length; i++ {
|
|
subs[i] = prefix[:max-i]
|
|
}
|
|
return subs
|
|
}
|
|
|
|
func CopyHour(src, dest time.Time) time.Time {
|
|
if src.Hour() == 0 && src.Minute() == 0 && src.Second() == 0 {
|
|
return src
|
|
}
|
|
return time.Date(dest.Year(), dest.Month(), dest.Day(), src.Hour(), src.Minute(), src.Second(), src.Nanosecond(), src.Location())
|
|
}
|
|
|
|
// Parses duration, considers s as time unit if not provided, seconds as float to specify subunits
|
|
func ParseDurationWithSecs(durStr string) (d time.Duration, err error) {
|
|
if durStr == "" {
|
|
return
|
|
}
|
|
if _, err = strconv.ParseFloat(durStr, 64); err == nil { // Seconds format considered
|
|
durStr += "s"
|
|
}
|
|
return time.ParseDuration(durStr)
|
|
}
|
|
|
|
// Parses duration, considers s as time unit if not provided, seconds as float to specify subunits
|
|
func ParseDurationWithNanosecs(durStr string) (d time.Duration, err error) {
|
|
if durStr == "" {
|
|
return
|
|
}
|
|
if durStr == UNLIMITED {
|
|
durStr = "-1"
|
|
}
|
|
if _, err = strconv.ParseFloat(durStr, 64); err == nil { // Seconds format considered
|
|
durStr += "ns"
|
|
}
|
|
return time.ParseDuration(durStr)
|
|
}
|
|
|
|
// returns the minimum duration between the two
|
|
func MinDuration(d1, d2 time.Duration) time.Duration {
|
|
if d1 < d2 {
|
|
return d1
|
|
}
|
|
return d2
|
|
}
|
|
|
|
// ParseZeroRatingSubject will parse the subject in the balance
|
|
// returns duration if able to extract it from subject
|
|
// returns error if not able to parse duration (ie: if ratingSubject is standard one)
|
|
func ParseZeroRatingSubject(tor, rateSubj string, defaultRateSubj map[string]string) (time.Duration, error) {
|
|
rateSubj = strings.TrimSpace(rateSubj)
|
|
if rateSubj == "" || rateSubj == ANY {
|
|
var hasToR bool
|
|
if rateSubj, hasToR = defaultRateSubj[tor]; !hasToR {
|
|
rateSubj = defaultRateSubj[META_ANY]
|
|
}
|
|
}
|
|
if !strings.HasPrefix(rateSubj, ZERO_RATING_SUBJECT_PREFIX) {
|
|
return 0, errors.New("malformed rating subject: " + rateSubj)
|
|
}
|
|
durStr := rateSubj[len(ZERO_RATING_SUBJECT_PREFIX):]
|
|
if _, err := strconv.ParseFloat(durStr, 64); err == nil { // No time unit, postpend
|
|
durStr += "ns"
|
|
}
|
|
return time.ParseDuration(durStr)
|
|
}
|
|
|
|
func ConcatenatedKey(keyVals ...string) string {
|
|
return strings.Join(keyVals, CONCATENATED_KEY_SEP)
|
|
}
|
|
|
|
func SplitConcatenatedKey(key string) []string {
|
|
return strings.Split(key, CONCATENATED_KEY_SEP)
|
|
}
|
|
|
|
func InfieldJoin(vals ...string) string {
|
|
return strings.Join(vals, INFIELD_SEP)
|
|
}
|
|
|
|
func InfieldSplit(val string) []string {
|
|
return strings.Split(val, INFIELD_SEP)
|
|
}
|
|
|
|
func Unzip(src, dest string) error {
|
|
r, err := zip.OpenReader(src)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer r.Close()
|
|
|
|
for _, f := range r.File {
|
|
rc, err := f.Open()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer rc.Close()
|
|
|
|
path := filepath.Join(dest, f.Name)
|
|
if f.FileInfo().IsDir() {
|
|
os.MkdirAll(path, f.Mode())
|
|
} else {
|
|
f, err := os.OpenFile(
|
|
path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer f.Close()
|
|
|
|
_, err = io.Copy(f, rc)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// Fib returns successive Fibonacci numbers.
|
|
func Fib() func() int {
|
|
a, b := 0, 1
|
|
return func() int {
|
|
if b > 0 { // only increment Fibonacci numbers while b doesn't overflow
|
|
a, b = b, a+b
|
|
}
|
|
return a
|
|
}
|
|
}
|
|
|
|
// FibDuration returns successive Fibonacci numbers as time.Duration with the
|
|
// unit specified by durationUnit or maxDuration if it is exceeded
|
|
func FibDuration(durationUnit, maxDuration time.Duration) func() time.Duration {
|
|
fib := Fib()
|
|
return func() time.Duration {
|
|
fibNrAsDuration := time.Duration(fib())
|
|
if fibNrAsDuration > (AbsoluteMaxDuration / durationUnit) { // check if the current fibonacci nr. in the sequence would exceed the absolute maximum duration if multiplied by the duration unit value
|
|
fibNrAsDuration = AbsoluteMaxDuration
|
|
} else {
|
|
fibNrAsDuration *= durationUnit
|
|
}
|
|
if maxDuration > 0 && maxDuration < fibNrAsDuration {
|
|
return maxDuration
|
|
}
|
|
return fibNrAsDuration
|
|
}
|
|
}
|
|
|
|
// Utilities to provide pointers where we need to define ad-hoc
|
|
func StringPointer(str string) *string {
|
|
if str == ZERO {
|
|
str = EmptyString
|
|
return &str
|
|
}
|
|
return &str
|
|
}
|
|
|
|
func IntPointer(i int) *int {
|
|
return &i
|
|
}
|
|
|
|
func Int64Pointer(i int64) *int64 {
|
|
return &i
|
|
}
|
|
|
|
func Float64Pointer(f float64) *float64 {
|
|
return &f
|
|
}
|
|
|
|
func BoolPointer(b bool) *bool {
|
|
return &b
|
|
}
|
|
|
|
func StringMapPointer(sm StringMap) *StringMap {
|
|
return &sm
|
|
}
|
|
|
|
func MapStringStringPointer(mp map[string]string) *map[string]string {
|
|
return &mp
|
|
}
|
|
|
|
func TimePointer(t time.Time) *time.Time {
|
|
return &t
|
|
}
|
|
|
|
func DurationPointer(d time.Duration) *time.Duration {
|
|
return &d
|
|
}
|
|
|
|
func ToIJSON(v any) string {
|
|
b, _ := json.MarshalIndent(v, "", " ")
|
|
return string(b)
|
|
}
|
|
|
|
func ToJSON(v any) string {
|
|
b, _ := json.Marshal(v)
|
|
return string(b)
|
|
}
|
|
|
|
func LogFull(v any) {
|
|
log.Print(ToIJSON(v))
|
|
}
|
|
|
|
// Simple object cloner, b should be a pointer towards a value into which we want to decode
|
|
func Clone(a, b any) error {
|
|
buff := new(bytes.Buffer)
|
|
enc := gob.NewEncoder(buff)
|
|
dec := gob.NewDecoder(buff)
|
|
if err := enc.Encode(a); err != nil {
|
|
return err
|
|
}
|
|
if err := dec.Decode(b); err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// Used as generic function logic for various fields
|
|
|
|
// Attributes
|
|
//
|
|
// source - the base source
|
|
// width - the field width
|
|
// strip - if present it will specify the strip strategy, when missing strip will not be allowed
|
|
// padding - if present it will specify the padding strategy to use, left, right, zeroleft, zeroright
|
|
func FmtFieldWidth(fieldID, source string, width int, strip, padding string, mandatory bool) (string, error) {
|
|
if mandatory && len(source) == 0 {
|
|
return "", fmt.Errorf("Empty source value for fieldID: <%s>", fieldID)
|
|
}
|
|
if width == 0 { // Disable width processing if not defined
|
|
return source, nil
|
|
}
|
|
if len(source) == width { // the source is exactly the maximum length
|
|
return source, nil
|
|
}
|
|
if len(source) > width { //the source is bigger than allowed
|
|
if len(strip) == 0 {
|
|
return "", fmt.Errorf("Source %s is bigger than the width %d, no strip defied, fieldID: <%s>", source, width, fieldID)
|
|
}
|
|
if strip == MetaRight {
|
|
return source[:width], nil
|
|
} else if strip == MetaXRight {
|
|
return source[:width-1] + "x", nil // Suffix with x to mark prefix
|
|
} else if strip == MetaLeft {
|
|
diffIndx := len(source) - width
|
|
return source[diffIndx:], nil
|
|
} else if strip == MetaXLeft { // Prefix one x to mark stripping
|
|
diffIndx := len(source) - width
|
|
return "x" + source[diffIndx+1:], nil
|
|
}
|
|
} else { //the source is smaller as the maximum allowed
|
|
if len(padding) == 0 {
|
|
return "", fmt.Errorf("Source %s is smaller than the width %d, no padding defined, fieldID: <%s>", source, width, fieldID)
|
|
}
|
|
var paddingFmt string
|
|
switch padding {
|
|
case MetaRight:
|
|
paddingFmt = fmt.Sprintf("%%-%ds", width)
|
|
case MetaLeft:
|
|
paddingFmt = fmt.Sprintf("%%%ds", width)
|
|
case MetaZeroLeft:
|
|
paddingFmt = fmt.Sprintf("%%0%ds", width)
|
|
}
|
|
if len(paddingFmt) != 0 {
|
|
return fmt.Sprintf(paddingFmt, source), nil
|
|
}
|
|
}
|
|
return source, nil
|
|
}
|
|
|
|
// Returns the string representation of iface or error if not convertible
|
|
func CastIfToString(iface any) (strVal string, casts bool) {
|
|
switch rawVal := iface.(type) {
|
|
case string:
|
|
strVal = rawVal
|
|
casts = true
|
|
case int:
|
|
strVal = strconv.Itoa(rawVal)
|
|
casts = true
|
|
case int64:
|
|
strVal = strconv.FormatInt(rawVal, 10)
|
|
casts = true
|
|
case float64:
|
|
strVal = strconv.FormatFloat(rawVal, 'f', -1, 64)
|
|
casts = true
|
|
case bool:
|
|
strVal = strconv.FormatBool(rawVal)
|
|
casts = true
|
|
case []uint8:
|
|
var byteVal []byte
|
|
if byteVal, casts = iface.([]byte); casts {
|
|
strVal = string(byteVal)
|
|
}
|
|
default: // Maybe we are lucky and the value converts to string
|
|
strVal, casts = iface.(string)
|
|
}
|
|
return strVal, casts
|
|
}
|
|
|
|
func GetEndOfMonth(ref time.Time) time.Time {
|
|
if ref.IsZero() {
|
|
return time.Now()
|
|
}
|
|
year, month, _ := ref.Date()
|
|
if month == time.December {
|
|
year++
|
|
month = time.January
|
|
} else {
|
|
month++
|
|
}
|
|
eom := time.Date(year, month, 1, 0, 0, 0, 0, ref.Location())
|
|
return eom.Add(-time.Second)
|
|
}
|
|
|
|
// formats number in K,M,G, etc.
|
|
func SizeFmt(num float64, suffix string) string {
|
|
if suffix == EmptyString {
|
|
suffix = "B"
|
|
}
|
|
for _, unit := range []string{"", "Ki", "Mi", "Gi", "Ti", "Pi", "Ei", "Zi"} {
|
|
if math.Abs(num) < 1024.0 {
|
|
return fmt.Sprintf("%3.1f%s%s", num, unit, suffix)
|
|
}
|
|
num /= 1024.0
|
|
}
|
|
return fmt.Sprintf("%.1f%s%s", num, "Yi", suffix)
|
|
}
|
|
|
|
func TimeIs0h(t time.Time) bool {
|
|
return t.Hour() == 0 && t.Minute() == 0 && t.Second() == 0
|
|
}
|
|
|
|
func ParseHierarchyPath(path string, sep string) HierarchyPath {
|
|
if path == EmptyString {
|
|
return nil
|
|
}
|
|
if sep == EmptyString {
|
|
for _, sep = range []string{"/", NestingSep} {
|
|
if strings.Contains(path, sep) {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
path = strings.Trim(path, sep) // Need to strip if prefix of suffiy (eg: paths with /) so we can properly split
|
|
return HierarchyPath(strings.Split(path, sep))
|
|
}
|
|
|
|
// HierarchyPath is used in various places to represent various path hierarchies (eg: in Diameter groups, XML trees)
|
|
type HierarchyPath []string
|
|
|
|
// AsString converts HierarchyPath to a string.
|
|
func (hP HierarchyPath) AsString(sep string, isAbsolute bool) string {
|
|
var strHP strings.Builder
|
|
|
|
// If isAbsolute is true and the HierarchyPath slice is empty, sep will be returned.
|
|
// This will indicate the start of the absolute path.
|
|
if isAbsolute {
|
|
strHP.WriteString(sep)
|
|
}
|
|
|
|
if len(hP) == 0 {
|
|
// If isAbsolute is false and HierarchyPath is empty, return '.' to represent the current directory in a relative path.
|
|
// This convention avoids errors (e.g., "expr expression is nil") when retrieving elements from an empty path.
|
|
if !isAbsolute {
|
|
return "."
|
|
}
|
|
return strHP.String()
|
|
}
|
|
|
|
for i, elem := range hP {
|
|
if i != 0 {
|
|
strHP.WriteString(sep)
|
|
}
|
|
strHP.WriteString(elem)
|
|
}
|
|
return strHP.String()
|
|
}
|
|
|
|
// Mask a number of characters in the suffix of the destination
|
|
func MaskSuffix(dest string, maskLen int) string {
|
|
destLen := len(dest)
|
|
if maskLen < 0 {
|
|
return dest
|
|
} else if maskLen > destLen {
|
|
maskLen = destLen
|
|
}
|
|
dest = dest[:destLen-maskLen]
|
|
for i := 0; i < maskLen; i++ {
|
|
dest += MASK_CHAR
|
|
}
|
|
return dest
|
|
}
|
|
|
|
// Sortable Int64Slice
|
|
type Int64Slice []int64
|
|
|
|
func (slc Int64Slice) Len() int {
|
|
return len(slc)
|
|
}
|
|
func (slc Int64Slice) Swap(i, j int) {
|
|
slc[i], slc[j] = slc[j], slc[i]
|
|
}
|
|
func (slc Int64Slice) Less(i, j int) bool {
|
|
return slc[i] < slc[j]
|
|
}
|
|
|
|
// CapitalizeErrorMessage returns the capitalized version of an error, useful in APIs
|
|
func CapitalizedMessage(errMessage string) (capStr string) {
|
|
capStr = strings.ToUpper(errMessage)
|
|
capStr = strings.Replace(capStr, " ", "_", -1)
|
|
return
|
|
}
|
|
|
|
func GetCGRVersion() (vers string, err error) {
|
|
vers = fmt.Sprintf("%s@%s", CGRateS, VERSION)
|
|
if GitCommitDate == "" || GitCommitHash == "" {
|
|
return vers, nil
|
|
}
|
|
var matched bool
|
|
|
|
/*
|
|
Git v2.45 relevant release note:
|
|
* The output format for dates "iso-strict" has been tweaked to show
|
|
a time in the Zulu timezone with "Z" suffix, instead of "+00:00".
|
|
*/
|
|
|
|
// Parse the Git commit date, which might be in different formats depending on the Git version
|
|
trimmedCommitDate := strings.TrimSpace(GitCommitDate)
|
|
commitDate, err := time.Parse("2006-01-02T15:04:05Z", trimmedCommitDate)
|
|
if err != nil {
|
|
// Failed to parse iso-strict date format for git version 2.45+. Try to parse with the previous format.
|
|
var fallbackErr error
|
|
commitDate, fallbackErr = time.Parse("2006-01-02T15:04:05-07:00", trimmedCommitDate)
|
|
if fallbackErr != nil {
|
|
// Both parsing attempts failed, group the errors together.
|
|
err = fmt.Errorf(
|
|
"failed to parse date:\ngit2.45+ iso-strict format: %w\nprevious iso-strict format: %w",
|
|
err, fallbackErr)
|
|
} else {
|
|
err = nil // successfully parsed with fallback format
|
|
}
|
|
}
|
|
if err != nil {
|
|
return vers, fmt.Errorf("version build error: %w", err)
|
|
}
|
|
matched, err = regexp.MatchString("^[0-9a-f]{12,}$", GitCommitHash)
|
|
if err != nil {
|
|
return vers, fmt.Errorf("version build error: commit hash compilation failed: %v", err)
|
|
} else if !matched {
|
|
return vers, fmt.Errorf("version build error: commit hash does not match expected format")
|
|
}
|
|
commitHash := GitCommitHash
|
|
//CGRateS@v0.10.1~dev-20200110075344-7572e7b11e00
|
|
return fmt.Sprintf("%s@%s-%s-%s", CGRateS, VERSION, commitDate.UTC().Format("20060102150405"), commitHash[:12]), nil
|
|
}
|
|
|
|
func NewTenantID(tntID string) *TenantID {
|
|
if !strings.Contains(tntID, CONCATENATED_KEY_SEP) { // no :, ID without Tenant
|
|
return &TenantID{ID: tntID}
|
|
}
|
|
tIDSplt := strings.Split(tntID, CONCATENATED_KEY_SEP)
|
|
return &TenantID{Tenant: tIDSplt[0], ID: ConcatenatedKey(tIDSplt[1:]...)}
|
|
}
|
|
|
|
type TenantArg struct {
|
|
Tenant string
|
|
}
|
|
|
|
type TenantArgWithPaginator struct {
|
|
TenantArg
|
|
Paginator
|
|
}
|
|
|
|
type TenantWithArgDispatcher struct {
|
|
*TenantArg
|
|
*ArgDispatcher
|
|
}
|
|
|
|
type TenantID struct {
|
|
Tenant string
|
|
ID string
|
|
}
|
|
|
|
type TenantIDWithArgDispatcher struct {
|
|
*TenantID
|
|
*ArgDispatcher
|
|
}
|
|
|
|
func (tID *TenantID) TenantID() string {
|
|
return ConcatenatedKey(tID.Tenant, tID.ID)
|
|
}
|
|
|
|
type TenantIDWithCache struct {
|
|
Tenant string
|
|
ID string
|
|
Cache *string
|
|
}
|
|
|
|
func (tID *TenantIDWithCache) TenantID() string {
|
|
return ConcatenatedKey(tID.Tenant, tID.ID)
|
|
}
|
|
|
|
// RPCCall is a generic method calling RPC on a struct instance
|
|
// serviceMethod is assumed to be in the form InstanceV1.Method
|
|
// where V1Method will become RPC method called on instance
|
|
func RPCCall(inst any, serviceMethod string, args any, reply any) error {
|
|
methodSplit := strings.Split(serviceMethod, ".")
|
|
if len(methodSplit) != 2 {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
method := reflect.ValueOf(inst).MethodByName(
|
|
strings.ToUpper(methodSplit[0][len(methodSplit[0])-2:]) + methodSplit[1])
|
|
if !method.IsValid() {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
params := []reflect.Value{reflect.ValueOf(args), reflect.ValueOf(reply)}
|
|
ret := method.Call(params)
|
|
if len(ret) != 1 {
|
|
return ErrServerError
|
|
}
|
|
if ret[0].Interface() == nil {
|
|
return nil
|
|
}
|
|
err, ok := ret[0].Interface().(error)
|
|
if !ok {
|
|
return ErrServerError
|
|
}
|
|
return err
|
|
}
|
|
|
|
// ApierRPCCall implements generic RPCCall for APIer instances
|
|
func APIerRPCCall(inst any, serviceMethod string, args any, reply any) error {
|
|
methodSplit := strings.Split(serviceMethod, ".")
|
|
if len(methodSplit) != 2 {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
method := reflect.ValueOf(inst).MethodByName(methodSplit[1])
|
|
if !method.IsValid() {
|
|
return rpcclient.ErrUnsupporteServiceMethod
|
|
}
|
|
params := []reflect.Value{reflect.ValueOf(args), reflect.ValueOf(reply)}
|
|
ret := method.Call(params)
|
|
if len(ret) != 1 {
|
|
return ErrServerError
|
|
}
|
|
if ret[0].Interface() == nil {
|
|
return nil
|
|
}
|
|
err, ok := ret[0].Interface().(error)
|
|
if !ok {
|
|
return ErrServerError
|
|
}
|
|
return err
|
|
}
|
|
|
|
// CachedRPCResponse is used to cache a RPC response
|
|
type CachedRPCResponse struct {
|
|
Result any
|
|
Error error
|
|
}
|
|
|
|
func ReverseString(s string) string {
|
|
r := []rune(s)
|
|
for i, j := 0, len(r)-1; i < len(r)/2; i, j = i+1, j-1 {
|
|
r[i], r[j] = r[j], r[i]
|
|
}
|
|
return string(r)
|
|
}
|
|
|
|
func GetUrlRawArguments(dialURL string) (out map[string]string) {
|
|
out = make(map[string]string)
|
|
idx := strings.IndexRune(dialURL, '?')
|
|
if idx == -1 {
|
|
return
|
|
}
|
|
strParams := dialURL[idx+1:]
|
|
if len(strParams) == 0 {
|
|
return
|
|
}
|
|
vecParams := strings.Split(strParams, "&")
|
|
for _, paramPair := range vecParams {
|
|
idx := strings.IndexRune(paramPair, '=')
|
|
if idx == -1 {
|
|
continue
|
|
}
|
|
out[paramPair[:idx]] = paramPair[idx+1:]
|
|
}
|
|
return
|
|
}
|
|
|
|
// WarnExecTime is used when we need to meassure the execution of specific functions
|
|
// and warn when the total duration is higher than expected
|
|
// should be usually called with defer, ie: defer WarnExecTime(time.Now(), "MyTestFunc", time.Duration(2*time.Second))
|
|
func WarnExecTime(startTime time.Time, logID string, maxDur time.Duration) {
|
|
totalDur := time.Since(startTime)
|
|
if totalDur > maxDur {
|
|
Logger.Warning(fmt.Sprintf("<%s> execution took: <%s>", logID, totalDur))
|
|
}
|
|
}
|
|
|
|
// endchan := LongExecTimeDetector("mesaj", 5*time.Second)
|
|
// defer func() { close(endchan) }()
|
|
func LongExecTimeDetector(logID string, maxDur time.Duration) (endchan chan struct{}) {
|
|
endchan = make(chan struct{}, 1)
|
|
go func() {
|
|
select {
|
|
case <-time.After(maxDur):
|
|
Logger.Warning(fmt.Sprintf("<%s> execution more than: <%s>", logID, maxDur))
|
|
case <-endchan:
|
|
}
|
|
}()
|
|
return
|
|
}
|
|
|
|
type GetFilterIndexesArg struct {
|
|
CacheID string
|
|
ItemIDPrefix string
|
|
FilterType string
|
|
FldNameVal map[string]string
|
|
}
|
|
|
|
type MatchFilterIndexArg struct {
|
|
CacheID string
|
|
ItemIDPrefix string
|
|
FilterType string
|
|
FieldName string
|
|
FieldVal string
|
|
}
|
|
|
|
type SetFilterIndexesArg struct {
|
|
CacheID string
|
|
ItemIDPrefix string
|
|
Indexes map[string]StringMap
|
|
}
|
|
|
|
func CastRPCErr(err error) error {
|
|
if err != nil {
|
|
if _, has := ErrMap[err.Error()]; has {
|
|
return ErrMap[err.Error()]
|
|
}
|
|
}
|
|
return err
|
|
}
|
|
|
|
// RandomInteger returns a random 64-bit integer between min and max values
|
|
func RandomInteger(min, max int64) int64 {
|
|
return math_rand.Int63n(max-min) + min
|
|
}
|