diff --git a/utils/consts.go b/utils/consts.go index 2f4306eb0..860dae476 100644 --- a/utils/consts.go +++ b/utils/consts.go @@ -147,6 +147,9 @@ const ( MetaSingle = "*single" MetaZero = "*zero" MetaASAP = "*asap" + MetaNil = "*nil" + MetaSpace = "*space" + MetaChar = "*char" CommentChar = '#' CSVSep = ',' FallbackSep = ';' @@ -658,6 +661,7 @@ const ( E164Converter = "*e164" MetaJoin = "*join" MetaSplit = "*split" + MetaStrip = "*strip" MetaReload = "*reload" MetaLoad = "*load" MetaFloat64 = "*float64" @@ -1051,6 +1055,7 @@ const ( MetaString = "*string" MetaPrefix = "*prefix" MetaSuffix = "*suffix" + MetaBoth = "*both" MetaEmpty = "*empty" MetaExists = "*exists" MetaCronExp = "*cronexp" diff --git a/utils/dataconverter.go b/utils/dataconverter.go index abbac443b..2c07e77d0 100644 --- a/utils/dataconverter.go +++ b/utils/dataconverter.go @@ -139,6 +139,8 @@ func NewDataConverter(params string) (conv DataConverter, err error) { return splitConverter(InfieldSep), nil } return splitConverter(params[len(MetaSplit)+1:]), nil + case strings.HasPrefix(params, MetaStrip): + return NewStripConverter(params) default: return nil, fmt.Errorf("unsupported converter definition: <%s>", params) } @@ -686,3 +688,138 @@ func (jsnC JSONConverter) Convert(in any) (any, error) { } return string(b), nil } + +// StripConverter strips the prefix, the suffix or both from a string. +type StripConverter struct { + side string // side represents which part of the string to strip: prefix, suffix, or both. + substr string // substr represents the substring to be removed from the string. + amount int // amount represents the number of characters to be removed from the string. +} + +// NewStripConverter initializes and returns a new StripConverter with configurations +// based on the provided parameters in the input string. Each parameter in the input +// string should be separated by ':'. +// +// The input string must follow one of the following formats: +// 1. "*strip::" +// 2. "*strip::[:]" +// 3. "*strip::*char:[:]" +// +// Explanation of placeholders: +// - : Specifies which part of the string to strip. Must be one of "*prefix", "*suffix", or "*both". +// - : Identifies the substring to remove. It can be a specific string, "*nil" for null characters, +// "*space" for spaces, or any other character. +// - (optional): Determines the number of characters to remove. If omitted, all instances of +// are removed. +// +// Examples: +// - "*strip:*prefix:5": Removes the first 5 characters from the string's prefix. +// - "*strip:*suffix:*nil": Eliminates all trailing null characters in the string. +// - "*strip:*both:*space:2": Clears 2 spaces from both the prefix and suffix of the string. +// - "*strip:*suffix:*char:abc": Removes the substring "abc" from the suffix of the string. +// - "*strip:*prefix:*char:abc:2": Strips the substring "abc" from the prefix of the string, repeated 2 times. +func NewStripConverter(params string) (DataConverter, error) { + paramSlice := strings.Split(params, InInFieldSep) + paramCount := len(paramSlice) + if paramCount < 3 || paramCount > 5 { + return nil, errors.New("strip converter: invalid number of parameters (should have 3, 4 or 5)") + } + sc := StripConverter{ + side: paramSlice[1], + substr: paramSlice[2], + amount: -1, + } + var err error + switch sc.substr { + case EmptyString: + return nil, errors.New("strip converter: substr parameter cannot be empty") + case MetaNil, MetaSpace: + if paramCount == 5 { + return nil, errors.New("strip converter: cannot have 5 params in *nil/*space case") + } + if sc.substr == MetaNil { + sc.substr = "\u0000" + } else { + sc.substr = " " + } + if paramCount == 4 { + sc.amount, err = strconv.Atoi(paramSlice[3]) + if err != nil { + return nil, fmt.Errorf("strip converter: invalid amount parameter (%w)", err) + } + sc.substr = strings.Repeat(sc.substr, sc.amount) + } + case MetaChar: + if paramCount < 4 || paramSlice[3] == EmptyString { + return nil, errors.New("strip converter: usage of *char implies the need of 4 or 5 non-empty params") + } + sc.substr = paramSlice[3] + if paramCount == 5 { + sc.amount, err = strconv.Atoi(paramSlice[4]) + if err != nil { + return nil, fmt.Errorf("strip converter: invalid amount parameter (%w)", err) + } + sc.substr = strings.Repeat(sc.substr, sc.amount) + } + default: + if paramCount > 3 { + return nil, errors.New("strip converter: just the amount specified, cannot have more than 3 params") + } + sc.amount, err = strconv.Atoi(paramSlice[2]) + if err != nil { + return nil, fmt.Errorf("strip converter: invalid amount parameter (%w)", err) + } + sc.substr = "" + } + return sc, nil +} + +// Convert trims the input string based on the StripConverter's configuration. +// It returns a CAST_FAILED error if the input is not a string. +func (sc StripConverter) Convert(in any) (any, error) { + str, ok := in.(string) + if !ok { + return nil, fmt.Errorf("strip converter: %w", ErrCastFailed) + } + if sc.amount <= 0 && sc.amount != -1 { + return str, nil + } + switch sc.side { + case MetaPrefix: + if sc.substr == EmptyString { + if sc.amount < len(str) { + return str[sc.amount:], nil + } + return EmptyString, nil + } + if sc.amount != -1 { + return strings.TrimPrefix(str, sc.substr), nil + } + return strings.TrimLeft(str, sc.substr), nil + case MetaSuffix: + if sc.substr == EmptyString { + if sc.amount < len(str) { + return str[:len(str)-sc.amount], nil + } + return EmptyString, nil + } + if sc.amount != -1 { + return strings.TrimSuffix(str, sc.substr), nil + } + return strings.TrimRight(str, sc.substr), nil + case MetaBoth: + if sc.substr == EmptyString { + if sc.amount*2 < len(str) { + return str[sc.amount : len(str)-sc.amount], nil + } + return EmptyString, nil + } + if sc.amount != -1 { + str = strings.TrimPrefix(str, sc.substr) + return strings.TrimSuffix(str, sc.substr), nil + } + return strings.Trim(str, sc.substr), nil + default: + return EmptyString, errors.New("strip converter: invalid side parameter") + } +} diff --git a/utils/dataconverter_test.go b/utils/dataconverter_test.go index 95a9ad053..09a58ef61 100644 --- a/utils/dataconverter_test.go +++ b/utils/dataconverter_test.go @@ -1480,3 +1480,318 @@ func TestDataConverterConvertJSONOK(t *testing.T) { t.Errorf("expected: <%+v>, \nreceived: <%+v>", exp, rcv) } } + +func TestStripConverter(t *testing.T) { + tests := []struct { + name string + params string + input string + expected string + constructorErr bool + convertErr bool + }{ + + { + name: "Strip 5 leading characters", + params: "*strip:*prefix:5", + input: "12345TEST12345", + expected: "TEST12345", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 5 trailing characters", + params: "*strip:*suffix:5", + input: "12345TEST12345", + expected: "12345TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 5 characters from both sides", + params: "*strip:*both:5", + input: "12345TEST12345", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all trailing nils", + params: "*strip:*suffix:*nil", + input: "TEST\u0000\u0000\u0000\u0000", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all leading nils", + params: "*strip:*prefix:*nil", + input: "\u0000\u0000\u0000\u0000TEST", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all nils from both sides", + params: "*strip:*both:*nil", + input: "\u0000\u0000TEST\u0000\u0000", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 2 trailing nils", + params: "*strip:*suffix:*nil:2", + input: "TEST\u0000\u0000\u0000\u0000", + expected: "TEST\u0000\u0000", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 2 leading nils", + params: "*strip:*prefix:*nil:2", + input: "\u0000\u0000\u0000\u0000TEST", + expected: "\u0000\u0000TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 1 nil from both sides", + params: "*strip:*both:*nil:1", + input: "\u0000\u0000TEST\u0000\u0000", + expected: "\u0000TEST\u0000", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all trailing spaces", + params: "*strip:*suffix:*space", + input: "TEST ", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all leading spaces", + params: "*strip:*prefix:*space", + input: " TEST", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all spaces from both sides", + params: "*strip:*both:*space", + input: " TEST ", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 2 trailing spaces", + params: "*strip:*suffix:*space:2", + input: "TEST ", + expected: "TEST ", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 2 leading spaces", + params: "*strip:*prefix:*space:2", + input: " TEST", + expected: " TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 1 space from both sides", + params: "*strip:*both:*space:1", + input: " TEST ", + expected: " TEST ", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all trailing 'abcd' char groups", + params: "*strip:*suffix:*char:abcd", + input: "TESTabcdabcdabcdabcd", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all leading 'abcd' char groups", + params: "*strip:*prefix:*char:abcd", + input: "abcdabcdabcdabcdTEST", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip all 'abcd' char groups from both sides", + params: "*strip:*both:*char:abcd", + input: "abcdabcdTESTabcdabcd", + expected: "TEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 2 trailing 'abcd' char groups", + params: "*strip:*suffix:*char:abcd:2", + input: "TESTabcdabcdabcdabcd", + expected: "TESTabcdabcd", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 2 leading 'abcd' char groups", + params: "*strip:*prefix:*char:abcd:2", + input: "abcdabcdabcdabcdTEST", + expected: "abcdabcdTEST", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip 1 'abcd' char group from both sides", + params: "*strip:*both:*char:abcd:1", + input: "abcdabcdTESTabcdabcd", + expected: "abcdTESTabcd", + constructorErr: false, + convertErr: false, + }, + { + name: "Empty third parameter", + params: "*strip:*prefix:", + input: "TEST", + expected: "strip converter: substr parameter cannot be empty", + constructorErr: true, + convertErr: false, + }, + { + name: "Invalid side parameter", + params: "*strip:*invalid:*nil", + input: "TEST", + expected: "strip converter: invalid side parameter", + constructorErr: false, + convertErr: true, + }, + { + name: "Invalid nr. of params *char", + params: "*strip:*prefix:*char:*nil:abc:3", + input: "TEST", + expected: "strip converter: invalid number of parameters (should have 3, 4 or 5)", + constructorErr: true, + convertErr: false, + }, + { + name: "Invalid amount parameter", + params: "*strip:*prefix:*char:0:three", + input: "000TEST", + expected: "strip converter: invalid amount parameter (strconv.Atoi: parsing \"three\": invalid syntax)", + constructorErr: true, + convertErr: false, + }, + { + name: "Strip a prefix longer than the value", + params: "*strip:*prefix:5", + input: "TEST", + expected: "", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip a suffix longer than the value", + params: "*strip:*suffix:5", + input: "TEST", + expected: "", + constructorErr: false, + convertErr: false, + }, + { + name: "Strip from both ends an amount of characters longer than the value", + params: "*strip:*both:3", + input: "TEST", + expected: "", + constructorErr: false, + convertErr: false, + }, + { + name: "*char missing substring case 1", + params: "*strip:*prefix:*char", + input: "12345TEST", + expected: "strip converter: usage of *char implies the need of 4 or 5 non-empty params", + constructorErr: true, + convertErr: false, + }, + { + name: "*char missing substring case 2", + params: "*strip:*prefix:*char::2", + input: "12345TEST", + expected: "strip converter: usage of *char implies the need of 4 or 5 non-empty params", + constructorErr: true, + convertErr: false, + }, + { + name: "*char missing substring case 3", + params: "*strip:*prefix:*char:", + input: "12345TEST", + expected: "strip converter: usage of *char implies the need of 4 or 5 non-empty params", + constructorErr: true, + convertErr: false, + }, + { + name: "*nil/*space too many parameters", + params: "*strip:*prefix:*nil:5:12345", + input: "12345TEST", + expected: "strip converter: cannot have 5 params in *nil/*space case", + constructorErr: true, + convertErr: false, + }, + { + name: "third param numeric with too many params case 1", + params: "*strip:*prefix:1:1", + input: "12345TEST", + expected: "strip converter: just the amount specified, cannot have more than 3 params", + constructorErr: true, + convertErr: false, + }, + { + name: "third param numeric with too many params case 2", + params: "*strip:*prefix:1:12345:1", + input: "12345TEST", + expected: "strip converter: just the amount specified, cannot have more than 3 params", + constructorErr: true, + convertErr: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + sc, err := NewDataConverter(tt.params) + if (err != nil) != tt.constructorErr { + t.Errorf("NewStripConverter() error = %v, constructorErr %v", err, tt.constructorErr) + return + } + if tt.constructorErr { + if err.Error() != tt.expected { + t.Errorf("expected error message: %v, received: %v", tt.expected, err.Error()) + } + return + } + rcv, err := sc.Convert(tt.input) + if (err != nil) != tt.convertErr { + t.Errorf("Convert() error = %v, convertErr %v", err, tt.convertErr) + return + } + if tt.convertErr { + if err.Error() != tt.expected { + t.Errorf("expected error message: %s, received: %s", tt.expected, err.Error()) + } + return + } + if rcv != tt.expected { + t.Errorf("expected: %q, received: %q", tt.expected, rcv) + } + }) + } +}