diff --git a/engine/action.go b/engine/action.go index dab1b2ec1..a6f343092 100644 --- a/engine/action.go +++ b/engine/action.go @@ -201,6 +201,7 @@ func init() { actionFuncMap[utils.MetaDynamicRoute] = dynamicRoute actionFuncMap[utils.MetaDynamicRanking] = dynamicRanking actionFuncMap[utils.MetaDynamicRatingProfile] = dynamicRatingProfile + actionFuncMap[utils.MetaDynamicRanking] = dynamicTrend } func getActionFunc(typ string) (f actionTypeFunc, exists bool) { @@ -1535,7 +1536,7 @@ func dynamicThreshold(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, } // populate Threshold's MinSleep if params[6] != utils.EmptyString { - thProf.MinSleep, err = time.ParseDuration(params[6]) + thProf.MinSleep, err = utils.ParseDurationWithNanosecs(params[6]) if err != nil { return err } @@ -1659,9 +1660,9 @@ func dynamicStats(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, return err } } - // populate Stat's QueueLeTTLngh + // populate Stat's TTL if params[5] != utils.EmptyString { - stQProf.TTL, err = time.ParseDuration(params[5]) + stQProf.TTL, err = utils.ParseDurationWithNanosecs(params[5]) if err != nil { return err } @@ -2549,3 +2550,111 @@ func dynamicRatingProfile(_ *Account, act *Action, _ Actions, _ *FilterS, ev any var reply string return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetRatingProfile, ratingProf, &reply) } + +// dynamicTrend processes the `ExtraParameters` field from the action to +// construct a TrendProfile +// +// The ExtraParameters field format is expected as follows: +// +// 0 Tenant: string +// 1 ID: string +// 2 Schedule: string +// 3 StatID: string +// 4 Metrics: strings separated by "&". +// 5 TTL: duration +// 6 QueueLength: integer +// 7 MinItems: integer +// 8 CorrelationType: string +// 9 Tolerance: float +// 10 Stored: bool +// 11 ThresholdIDs: strings separated by "&". +// 12 APIOpts: set of key-value pairs (separated by "&"). +// +// Parameters are separated by ";" and must be provided in the specified order. +func dynamicTrend(_ *Account, act *Action, _ Actions, _ *FilterS, ev any, + _ SharedActionsData, connCfg ActionConnCfg) (err error) { + cgrEv, canCast := ev.(*utils.CGREvent) + if !canCast { + return errors.New("Couldn't cast event to CGREvent") + } + dP := utils.MapStorage{ // create DataProvider from event + utils.MetaReq: cgrEv.Event, + utils.MetaTenant: cgrEv.Tenant, + utils.MetaNow: time.Now(), + utils.MetaOpts: cgrEv.APIOpts, + } + // Parse action parameters based on the predefined format. + params := strings.Split(act.ExtraParameters, utils.InfieldSep) + if len(params) != 13 { + return errors.New(fmt.Sprintf("invalid number of parameters <%d> expected 13", len(params))) + } + // parse dynamic parameters + for i := range params { + if params[i], err = utils.ParseParamForDataProvider(params[i], dP, false); err != nil { + return err + } + } + // Prepare request arguments based on provided parameters. + trend := &TrendProfileWithAPIOpts{ + TrendProfile: &TrendProfile{ + Tenant: params[0], + ID: params[1], + Schedule: params[2], + StatID: params[3], + CorrelationType: params[8], + }, + APIOpts: make(map[string]any), + } + // populate Trend's Metrics + if params[4] != utils.EmptyString { + trend.Metrics = strings.Split(params[4], utils.ANDSep) + } + // populate Trend's TTL + if params[5] != utils.EmptyString { + trend.TTL, err = utils.ParseDurationWithNanosecs(params[5]) + if err != nil { + return err + } + } + // populate Trend's QueueLength + if params[6] != utils.EmptyString { + trend.QueueLength, err = strconv.Atoi(params[6]) + if err != nil { + return err + } + } + // populate Trend's MinItems + if params[7] != utils.EmptyString { + trend.MinItems, err = strconv.Atoi(params[7]) + if err != nil { + return err + } + } + // populate Trend's Tolerance + if params[9] != utils.EmptyString { + trend.Tolerance, err = strconv.ParseFloat(params[9], 64) + if err != nil { + return err + } + } + // populate Trend's Stored + if params[10] != utils.EmptyString { + trend.Stored, err = strconv.ParseBool(params[10]) + if err != nil { + return err + } + } + // populate Trend's ThresholdIDs + if params[11] != utils.EmptyString { + trend.ThresholdIDs = strings.Split(params[11], utils.ANDSep) + } + // populate Trend's APIOpts + if params[12] != utils.EmptyString { + if err := parseParamStringToMap(params[12], trend.APIOpts); err != nil { + return err + } + } + // create the TrendProfile based on the populated parameters + var reply string + return connMgr.Call(context.Background(), connCfg.ConnIDs, utils.APIerSv1SetTrendProfile, trend, &reply) +} diff --git a/engine/actions_test.go b/engine/actions_test.go index df5927db6..a05190f32 100644 --- a/engine/actions_test.go +++ b/engine/actions_test.go @@ -6942,3 +6942,182 @@ func TestDynamicRatingProfile(t *testing.T) { }) } } + +func TestDynamicTrend(t *testing.T) { + tempConn := connMgr + tmpDm := dm + tmpCache := Cache + defer func() { + config.SetCgrConfig(config.NewDefaultCGRConfig()) + SetConnManager(tempConn) + dm = tmpDm + Cache = tmpCache + }() + Cache.Clear(nil) + var tpwo *TrendProfileWithAPIOpts + ccMock := &ccMock{ + calls: map[string]func(ctx *context.Context, args any, reply any) error{ + utils.APIerSv1SetTrendProfile: func(ctx *context.Context, args, reply any) error { + var canCast bool + if tpwo, canCast = args.(*TrendProfileWithAPIOpts); !canCast { + return fmt.Errorf("couldnt cast") + } + return nil + }, + }, + } + connID := utils.ConcatenatedKey(utils.MetaInternal, utils.MetaApier) + clientconn := make(chan birpc.ClientConnector, 1) + clientconn <- ccMock + NewConnManager(config.NewDefaultCGRConfig(), map[string]chan birpc.ClientConnector{ + connID: clientconn, + }) + testcases := []struct { + name string + extraParams string + connIDs []string + expTpwo *TrendProfileWithAPIOpts + expectedErr string + }{ + { + name: "SuccessfulRequest", + connIDs: []string{connID}, + expTpwo: &TrendProfileWithAPIOpts{ + TrendProfile: &TrendProfile{ + Tenant: "cgrates.org", + ID: "TREND1", + Schedule: "0 12 * * *", + StatID: "Stats2", + Metrics: []string{"*acc", "*tcc"}, + TTL: -1, + QueueLength: -1, + MinItems: 1, + CorrelationType: "*average", + Tolerance: 2.1, + Stored: true, + ThresholdIDs: []string{"TD1", "TD2"}, + }, + APIOpts: map[string]any{ + "key": "value", + }, + }, + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;key:value", + }, + { + name: "SuccessfulRequestWithDynamicPaths", + connIDs: []string{connID}, + expTpwo: &TrendProfileWithAPIOpts{ + TrendProfile: &TrendProfile{ + Tenant: "cgrates.org", + ID: "TREND_1001", + Schedule: "0 12 * * *", + StatID: "Stats2", + Metrics: []string{"*acc", "*tcc"}, + TTL: -1, + QueueLength: -1, + MinItems: 1, + CorrelationType: "*average", + Tolerance: 2.1, + Stored: true, + ThresholdIDs: []string{"TD1", "TD2"}, + }, + APIOpts: map[string]any{ + "key": "value", + }, + }, + extraParams: "*tenant;TREND_<~*req.Account>;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;key:value", + }, + { + name: "SuccessfulRequestEmptyFields", + connIDs: []string{connID}, + expTpwo: &TrendProfileWithAPIOpts{ + TrendProfile: &TrendProfile{ + Tenant: "cgrates.org", + ID: "TREND_1001", + Schedule: "0 12 * * *", + StatID: "", + Metrics: nil, + TTL: 0, + QueueLength: 0, + MinItems: 0, + CorrelationType: "", + Tolerance: 0, + Stored: false, + ThresholdIDs: nil, + }, + APIOpts: map[string]any{}, + }, + extraParams: "cgrates.org;TREND_1001;0 12 * * *;;;;;;;;;;", + }, + { + name: "MissingConns", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;key:value", + expectedErr: "MANDATORY_IE_MISSING: [connIDs]", + }, + { + name: "WrongNumberOfParams", + extraParams: "tenant;TREND1;;", + expectedErr: "invalid number of parameters <4> expected 13", + }, + { + name: "TTLFail", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;BadString;-1;1;*average;2.1;true;TD1&TD2;key:value", + expectedErr: `time: invalid duration "BadString"`, + }, + { + name: "QueueLengthFail", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;BadString;1;*average;2.1;true;TD1&TD2;key:value", + expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, + }, + { + name: "MinItemsFail", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;BadString;*average;2.1;true;TD1&TD2;key:value", + expectedErr: `strconv.Atoi: parsing "BadString": invalid syntax`, + }, + { + name: "ToleranceFail", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;BadString;true;TD1&TD2;key:value", + expectedErr: `strconv.ParseFloat: parsing "BadString": invalid syntax`, + }, + { + name: "StoredFail", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;BadString;TD1&TD2;key:value", + expectedErr: `strconv.ParseBool: parsing "BadString": invalid syntax`, + }, + { + name: "InvalidOptsMap", + extraParams: "cgrates.org;TREND1;0 12 * * *;Stats2;*acc&*tcc;-1;-1;1;*average;2.1;true;TD1&TD2;opt", + expectedErr: "invalid key-value pair: opt", + }, + } + + for _, tc := range testcases { + t.Run(tc.name, func(t *testing.T) { + action := &Action{ExtraParameters: tc.extraParams} + ev := &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "evID", + Time: &time.Time{}, + Event: map[string]any{ + utils.AccountField: "1001", + }, + } + t.Cleanup(func() { + tpwo = nil + }) + err := dynamicTrend(nil, action, nil, nil, ev, + SharedActionsData{}, ActionConnCfg{ + ConnIDs: tc.connIDs, + }) + if tc.expectedErr != "" { + if err == nil || err.Error() != tc.expectedErr { + t.Errorf("expected error <%v>, received <%v>", tc.expectedErr, err) + } + } else if err != nil { + t.Error(err) + } else if !reflect.DeepEqual(tpwo, tc.expTpwo) { + t.Errorf("Expected <%v>\nReceived\n<%v>", utils.ToJSON(tc.expTpwo), utils.ToJSON(tpwo)) + } + }) + } +} diff --git a/utils/consts.go b/utils/consts.go index b2b42eaed..5019e871a 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -1239,6 +1239,7 @@ const ( MetaDynamicRoute = "*dynamic_route" MetaDynamicRanking = "*dynamic_ranking" MetaDynamicRatingProfile = "*dynamic_rating_profile" + MetaDynamicTrend = "*dynamic_trend" ActionID = "ActionID" ActionType = "ActionType" ActionValue = "ActionValue"