From 18b0e4d417e48a2c802c85deb4b75c489d8dfff5 Mon Sep 17 00:00:00 2001 From: gezimbll Date: Mon, 17 Feb 2025 14:16:16 +0100 Subject: [PATCH] added new dynopts function GetDurationPointerOpts --- engine/libdynopts.go | 27 +++++ engine/libdynopts_test.go | 212 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 239 insertions(+) diff --git a/engine/libdynopts.go b/engine/libdynopts.go index 621f90d9c..b2b053ea5 100644 --- a/engine/libdynopts.go +++ b/engine/libdynopts.go @@ -73,6 +73,33 @@ func GetDurationOpts(ctx *context.Context, tnt string, dP utils.DataProvider, fS return } +// GetDurationPointerOpts checks the specified option names in order among the keys in APIOpts returning the first value it finds as *time.Duration, otherwise it +// returns the config option if at least one filter passes or the default value if none of them do +func GetDurationPointerOpts(ctx *context.Context, tnt string, dP utils.DataProvider, fS *FilterS, dynOpts []*config.DynamicDurationPointerOpt, + optNames ...string) (cfgOpt *time.Duration, err error) { + if opt, err := optIfaceFromDP(dP, optNames); err == nil { + var value time.Duration + value, err = utils.IfaceAsDuration(opt) + if err != nil { + return nil, err + } + return utils.DurationPointer(value), nil + } else if !errors.Is(err, utils.ErrNotFound) { + return nil, err + } + for _, opt := range dynOpts { // iterate through the options + if !slices.Contains([]string{utils.EmptyString, utils.MetaAny, tnt}, opt.Tenant) { + continue + } + if pass, err := fS.Pass(ctx, tnt, opt.FilterIDs, dP); err != nil { // check if the filter is passing for the DataProvider and return the option if it does + return nil, err + } else if pass { + return opt.Value(dP) + } + } + return +} + // GetStringOpts checks the specified option names in order among the keys in APIOpts returning the first value it finds as string, otherwise it // returns the config option if at least one filter passes or the default value if none of them do func GetStringOpts(ctx *context.Context, tnt string, dP utils.DataProvider, fS *FilterS, dynOpts []*config.DynamicStringOpt, diff --git a/engine/libdynopts_test.go b/engine/libdynopts_test.go index d01d994a2..de35a9964 100644 --- a/engine/libdynopts_test.go +++ b/engine/libdynopts_test.go @@ -340,6 +340,118 @@ func TestLibFiltersGetDurationOptsReturnDefaultOpt(t *testing.T) { } } +func TestLibFiltersGetDurationPointerOptsReturnOptFromAPIOpts(t *testing.T) { + cfg := config.NewDefaultCGRConfig() + dataDB := NewInternalDB(nil, nil, nil) + dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) + fS := NewFilterS(cfg, nil, dm) + ev := &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "TestEvent", + Event: map[string]any{ + utils.AccountField: 1001, + }, + APIOpts: map[string]any{ + utils.OptsResourcesUsageTTL: time.Hour, + }, + } + dynOpts := []*config.DynamicDurationPointerOpt{ + // will never get to this opt because it will return once it + // finds the one set in APIOpts + config.NewDynamicDurationPointerOpt([]string{"*string:~*req.Account:1001"}, "cgrates.org", utils.DurationPointer(time.Minute), nil), + } + + expected := time.Hour + if rcv, err := GetDurationPointerOpts(context.Background(), "cgrates.org", ev.AsDataProvider(), fS, dynOpts, + "nonExistingAPIOpt", utils.OptsResourcesUsageTTL); err != nil { + t.Error(err) + } else if *rcv != expected { + t.Errorf("expected: <%+v>,\nreceived: <%+v>", expected, rcv) + } +} +func TestLibFiltersGetDurationPointerOptsReturnConfigOpt(t *testing.T) { + cfg := config.NewDefaultCGRConfig() + dataDB := NewInternalDB(nil, nil, nil) + dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) + fS := NewFilterS(cfg, nil, dm) + ev := &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "TestEvent", + Event: map[string]any{ + utils.AccountField: 1001, + }, + APIOpts: map[string]any{}, + } + dynOpts := []*config.DynamicDurationPointerOpt{ + // tenant will not be recognized, will ignore this opt + config.NewDynamicDurationPointerOpt([]string{"*string:~*req.Account:1001"}, "cgrates.net", utils.DurationPointer(time.Millisecond), nil), + // filter will not pass, will ignore this opt + config.NewDynamicDurationPointerOpt([]string{"*string:~*req.Account:1002"}, "cgrates.net", utils.DurationPointer(time.Second), nil), + config.NewDynamicDurationPointerOpt([]string{"*string:~*req.Account:1001"}, "cgrates.org", utils.DurationPointer(time.Minute), nil), + } + + expected := time.Minute + if rcv, err := GetDurationPointerOpts(context.Background(), "cgrates.org", ev.AsDataProvider(), fS, dynOpts, + utils.OptsResourcesUsageTTL); err != nil { + t.Error(err) + } else if *rcv != expected { + t.Errorf("expected: <%+v>,\nreceived: <%+v>", expected, rcv) + } +} + +func TestLibFiltersGetDurationPointerOptsFilterCheckErr(t *testing.T) { + cfg := config.NewDefaultCGRConfig() + dataDB := NewInternalDB(nil, nil, nil) + dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) + fS := NewFilterS(cfg, nil, dm) + ev := &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "TestEvent", + Event: map[string]any{ + utils.AccountField: 1001, + }, + APIOpts: map[string]any{}, + } + dynOpts := []*config.DynamicDurationPointerOpt{ + // function will return error after trying to parse the filter + config.NewDynamicDurationPointerOpt([]string{"*string.invalid:filter"}, "cgrates.org", utils.DurationPointer(time.Second), nil), + } + + experr := `inline parse error for string: <*string.invalid:filter>` + if _, err := GetDurationPointerOpts(context.Background(), "cgrates.org", ev.AsDataProvider(), fS, dynOpts, + utils.OptsResourcesUsageTTL); err == nil || + err.Error() != experr { + t.Errorf("expected: <%+v>, \nreceived: <%+v>", experr, err) + } +} + +func TestLibFiltersGetDurationPointerOptsReturnDefaultOpt(t *testing.T) { + cfg := config.NewDefaultCGRConfig() + dataDB := NewInternalDB(nil, nil, nil) + dm := NewDataManager(dataDB, cfg.CacheCfg(), nil) + fS := NewFilterS(cfg, nil, dm) + ev := &utils.CGREvent{ + Tenant: "cgrates.org", + ID: "TestEvent", + Event: map[string]any{ + utils.AccountField: 1001, + }, + APIOpts: map[string]any{}, + } + dynOpts := []*config.DynamicDurationPointerOpt{ + // filter will not pass, will ignore this opt + config.NewDynamicDurationPointerOpt([]string{"*string:~*req.Account:1002"}, "cgrates.org", utils.DurationPointer(time.Second), nil), + config.NewDynamicDurationPointerOpt(nil, "", utils.DurationPointer(config.ResourcesUsageTTLDftOpt), nil), + } + + if rcv, err := GetDurationPointerOpts(context.Background(), "cgrates.org", ev.AsDataProvider(), fS, dynOpts, + utils.OptsResourcesUsageTTL); err != nil { + t.Error(err) + } else if *rcv != config.ResourcesUsageTTLDftOpt { + t.Errorf("expected: <%+v>,\nreceived: <%+v>", config.ResourcesUsageTTLDftOpt, rcv) + } +} + func TestLibFiltersGetDurationOptsReturnOptFromAPIOpts(t *testing.T) { cfg := config.NewDefaultCGRConfig() dataDB := NewInternalDB(nil, nil, nil) @@ -1563,6 +1675,106 @@ func TestDynamicDurationOptsDynVal(t *testing.T) { } } +func TestDynamicDurationPointerOptsDynVal(t *testing.T) { + tests := []struct { + name string + dynOpts []*config.DynamicInterfaceOpt + expVal *time.Duration + expErr error + }{ + { + name: "DynOptsVal", + dynOpts: []*config.DynamicInterfaceOpt{ + { + Tenant: "cgrates.org", + Value: "~*opts.Usage", + }, + }, + expVal: utils.DurationPointer(time.Second * 10), + }, + { + name: "DynReqVal", + dynOpts: []*config.DynamicInterfaceOpt{ + { + Tenant: "cgrates.org", + Value: "~*req.*acd", + }, + }, + expVal: utils.DurationPointer(3500000), + }, + { + name: "StaticVal", + dynOpts: []*config.DynamicInterfaceOpt{ + { + Tenant: "cgrates.org", + Value: 1000000000, + }, + }, + expVal: utils.DurationPointer(1 * time.Second), + }, + { + name: "NotFound", + dynOpts: []*config.DynamicInterfaceOpt{ + { + Tenant: "cgrates.org", + Value: "~*req.RandomField", + }, + }, + expErr: utils.ErrNotFound, + }, + { + name: "ValueNotConvertedCorrectly", + dynOpts: []*config.DynamicInterfaceOpt{ + { + Tenant: "cgrates.org", + Value: "~*req.Usage2", + }, + }, + expErr: fmt.Errorf("time: invalid duration \"twenty-five\""), + }, + } + + ev := &utils.CGREvent{ + Tenant: utils.CGRateSorg, + ID: "testIDEvent", + Event: map[string]any{ + utils.AccountField: "1001", + "*acd": 3500000, + "Usage2": "twenty-five", + }, + APIOpts: map[string]any{ + "Usage": "10s", + }, + } + fs := NewFilterS(config.CgrConfig(), nil, nil) + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + dynOpts, err := config.IfaceToDurationPointerDynamicOpts(tt.dynOpts) + if err != nil { + t.Error(err) + return + } + out, err := GetDurationPointerOpts(context.Background(), "cgrates.org", ev.AsDataProvider(), fs, dynOpts, utils.OptsRatesUsage) + if tt.expErr != nil { + if err == nil { + t.Error("expected err,received nil") + } + if err.Error() != tt.expErr.Error() { + t.Errorf("expected error %v,received %v", tt.expErr, err) + } + return + } + if err != nil { + t.Errorf("unexpected err %v", err) + } + if *tt.expVal != *out { + t.Errorf("expected %v,received %v", tt.expVal, out) + } + }) + } +} + func TestDynamicStringOptsDynVal(t *testing.T) { tests := []struct { name string