From cf784c19f095fd1c0c8c58609de259c2daed0bd8 Mon Sep 17 00:00:00 2001 From: jnmoyne Date: Thu, 28 Jul 2022 12:53:14 -0700 Subject: [PATCH] - Adds new subject mapping functions: {{SplitFromLeft(wildcard index, position)}} {{SplitFromRight(wildcard index, position)}} {{SliceFromLeft(wildcard index, slice size)}} {{SliceFromRight(wildcard index, slice size)}} {{Split(wildcard index, deliminator)}} Examples: shouldMatch("*", "{{splitfromleft(1,3)}}", "12345", "123.45") shouldMatch("*", "{{SplitFromRight(1,3)}}", "12345", "12.345") shouldMatch("*", "{{SliceFromLeft(1,3)}}", "1234567890", "123.456.789.0") shouldMatch("*", "{{SliceFromRight(1,3)}}", "1234567890", "1.234.567.890") shouldMatch("*", "{{split(1,-)}}", "-abc-def--ghi-", "abc.def.ghi") shouldMatch("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}", "foo.-abc-def--ghij-", "abc.def.ghij.fo.o") - Subject mapping functions can now be all lower case or Pascal case (or a combination): e.g. splitfromleft, SplitFromLeft, splitFromleft, etc... --- server/accounts.go | 281 +++++++++++++++++++++++++++++++++------- server/accounts_test.go | 47 ++++--- server/sublist.go | 8 +- server/sublist_test.go | 2 + 4 files changed, 273 insertions(+), 65 deletions(-) diff --git a/server/accounts.go b/server/accounts.go index 2249f7cc..fb5bdb21 100644 --- a/server/accounts.go +++ b/server/accounts.go @@ -166,8 +166,13 @@ const ( ) var commaSeparatorRegEx = regexp.MustCompile(`,\s*`) -var partitionMappingFunctionRegEx = regexp.MustCompile(`{{\s*partition\s*\((.*)\)\s*}}`) -var wildcardMappingFunctionRegEx = regexp.MustCompile(`{{\s*wildcard\s*\((.*)\)\s*}}`) +var partitionMappingFunctionRegEx = regexp.MustCompile(`{{\s*[pP]artition\s*\((.*)\)\s*}}`) +var wildcardMappingFunctionRegEx = regexp.MustCompile(`{{\s*[wW]ildcard\s*\((.*)\)\s*}}`) +var splitFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[lL]eft\s*\((.*)\)\s*}}`) +var splitFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit[fF]rom[rR]ight\s*\((.*)\)\s*}}`) +var sliceFromLeftMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[lL]eft\s*\((.*)\)\s*}}`) +var sliceFromRightMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]lice[fF]rom[rR]ight\s*\((.*)\)\s*}}`) +var splitMappingFunctionRegEx = regexp.MustCompile(`{{\s*[sS]plit\s*\((.*)\)\s*}}`) // Enum for the subject mapping transform function types const ( @@ -179,6 +184,7 @@ const ( SplitFromRight SliceFromLeft SliceFromRight + Split ) // String helper. @@ -857,7 +863,7 @@ func (a *Account) selectMappedSubject(dest string) (string, bool) { } if d != nil { - if len(d.tr.dtokmftis) == 0 { + if len(d.tr.dtokmftokindexesargs) == 0 { ndest = d.tr.dest } else if nsubj, err := d.tr.transform(tts); err == nil { ndest = nsubj @@ -4175,12 +4181,13 @@ func (dr *CacheDirAccResolver) Reload() error { // These can also be used for proper mapping on wildcard exports/imports. // These will be grouped and caching and locking are assumed to be in the upper layers. type transform struct { - src, dest string - dtoks []string // destination tokens - stoks []string // source tokens - dtokmfts []int16 // destination token mapping function types - dtokmftis [][]int // destination token mapping function array of source token index arguments - dtokmfias []int32 // destination token mapping function int32 arguments + src, dest string + dtoks []string // destination tokens + stoks []string // source tokens + dtokmftypes []int16 // destination token mapping function types + dtokmftokindexesargs [][]int // destination token mapping function array of source token index arguments + dtokmfintargs []int32 // destination token mapping function int32 arguments + dtokmfstringargs []string // destination token mapping function string arguments } func getMappingFunctionArgs(functionRegEx *regexp.Regexp, token string) []string { @@ -4192,18 +4199,18 @@ func getMappingFunctionArgs(functionRegEx *regexp.Regexp, token string) []string } // Helper to ingest and index the transform destination token (e.g. $x or {{}}) in the token -// returns a transformation type, and two function arguments: an array of source subject token indexes, and a single number (e.g. number of partitions) +// returns a transformation type, and three function arguments: an array of source subject token indexes, and a single number (e.g. number of partitions, or a slice size), and a string (e.g.a split delimiter) -func indexPlaceHolders(token string) (int16, []int, int32, error) { +func indexPlaceHolders(token string) (int16, []int, int32, string, error) { length := len(token) if length > 1 { // old $1, $2, etc... mapping format still supported to maintain backwards compatibility if token[0] == '$' { // simple non-partition mapping tp, err := strconv.Atoi(token[1:]) if err != nil { - return NoTransform, []int{-1}, -1, nil // other things rely on tokens starting with $ so not an error just leave it as is + return NoTransform, []int{-1}, -1, "", nil // other things rely on tokens starting with $ so not an error just leave it as is } - return Wildcard, []int{tp}, -1, nil + return Wildcard, []int{tp}, -1, "", nil } // New 'mustache' style mapping @@ -4212,16 +4219,16 @@ func indexPlaceHolders(token string) (int16, []int, int32, error) { args := getMappingFunctionArgs(wildcardMappingFunctionRegEx, token) if args != nil { if len(args) == 1 && args[0] == _EMPTY_ { - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} } if len(args) == 1 { - tp, err := strconv.Atoi(strings.Trim(args[0], " ")) + tokenIndex, err := strconv.Atoi(strings.Trim(args[0], " ")) if err != nil { - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} } - return Wildcard, []int{tp}, -1, nil + return Wildcard, []int{tokenIndex}, -1, "", nil } else { - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments} + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionTooManyArguments} } } @@ -4229,29 +4236,135 @@ func indexPlaceHolders(token string) (int16, []int, int32, error) { args = getMappingFunctionArgs(partitionMappingFunctionRegEx, token) if args != nil { if len(args) < 2 { - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} } if len(args) >= 2 { - tphnp, err := strconv.Atoi(strings.Trim(args[0], " ")) + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[0], " ")) if err != nil { - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} } var numPositions = len(args[1:]) - tps := make([]int, numPositions) + tokenIndexes := make([]int, numPositions) for ti, t := range args[1:] { i, err := strconv.Atoi(strings.Trim(t, " ")) if err != nil { - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} } - tps[ti] = i + tokenIndexes[ti] = i } - return Partition, tps, int32(tphnp), nil + + return Partition, tokenIndexes, int32(mappingFunctionIntArg), "", nil } } - return BadTransform, []int{}, -1, &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction} + + // SplitFromLeft(token, position) + args = getMappingFunctionArgs(splitFromLeftMappingFunctionRegEx, token) + if args != nil { + if len(args) < 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + + return SplitFromLeft, []int{i}, int32(mappingFunctionIntArg), "", nil + } + + // SplitFromRight(token, position) + args = getMappingFunctionArgs(splitFromRightMappingFunctionRegEx, token) + if args != nil { + if len(args) < 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + + return SplitFromRight, []int{i}, int32(mappingFunctionIntArg), "", nil + } + + // SliceFromLeft(token, position) + args = getMappingFunctionArgs(sliceFromLeftMappingFunctionRegEx, token) + if args != nil { + if len(args) < 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + + return SliceFromLeft, []int{i}, int32(mappingFunctionIntArg), "", nil + } + + // SliceFromRight(token, position) + args = getMappingFunctionArgs(sliceFromRightMappingFunctionRegEx, token) + if args != nil { + if len(args) < 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + mappingFunctionIntArg, err := strconv.Atoi(strings.Trim(args[1], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + + return SliceFromRight, []int{i}, int32(mappingFunctionIntArg), "", nil + } + + // split(token, deliminator) + args = getMappingFunctionArgs(splitMappingFunctionRegEx, token) + if args != nil { + if len(args) < 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionNotEnoughArguments} + } + if len(args) > 2 { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + i, err := strconv.Atoi(strings.Trim(args[0], " ")) + if err != nil { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrorMappingDestinationFunctionInvalidArgument} + } + if strings.Contains(args[1], " ") || strings.Contains(args[1], ".") { + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token: token, err: ErrorMappingDestinationFunctionInvalidArgument} + } + + return Split, []int{i}, -1, args[1], nil + } + + return BadTransform, []int{}, -1, "", &mappingDestinationErr{token, ErrUnknownMappingDestinationFunction} } } - return NoTransform, []int{-1}, -1, nil + return NoTransform, []int{-1}, -1, "", nil } // SubjectTransformer transforms subjects using mappings @@ -4279,9 +4392,10 @@ func newTransform(src, dest string) (*transform, error) { return nil, ErrBadSubject } + var dtokMappingFunctionTypes []int16 var dtokMappingFunctionTokenIndexes [][]int var dtokMappingFunctionIntArgs []int32 - var dtokMappingFunctionTypes []int16 + var dtokMappingFunctionStringArgs []string // If the src has partial wildcards then the dest needs to have the token place markers. if npwcs > 0 || hasFwc { @@ -4295,30 +4409,33 @@ func newTransform(src, dest string) (*transform, error) { nphs := 0 for _, token := range dtokens { - tt, tp, nb, err := indexPlaceHolders(token) + tranformType, transformArgWildcardIndexes, transfomArgInt, transformArgString, err := indexPlaceHolders(token) if err != nil { return nil, err } - if tt == NoTransform { + if tranformType == NoTransform { { dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, NoTransform) dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, []int{-1}) dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, -1) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, "") } } else { nphs++ // Now build up our runtime mapping from dest to source tokens. var stis []int - for _, position := range tp { - if position > npwcs { - return nil, &mappingDestinationErr{fmt.Sprintf("%s: [%d]", token, position), ErrorMappingDestinationFunctionWildcardIndexOutOfRange} + for _, wildcardIndex := range transformArgWildcardIndexes { + if wildcardIndex > npwcs { + return nil, &mappingDestinationErr{fmt.Sprintf("%s: [%d]", token, wildcardIndex), ErrorMappingDestinationFunctionWildcardIndexOutOfRange} } - stis = append(stis, sti[position]) + stis = append(stis, sti[wildcardIndex]) } - dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tt) + dtokMappingFunctionTypes = append(dtokMappingFunctionTypes, tranformType) dtokMappingFunctionTokenIndexes = append(dtokMappingFunctionTokenIndexes, stis) - dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, nb) + dtokMappingFunctionIntArgs = append(dtokMappingFunctionIntArgs, transfomArgInt) + dtokMappingFunctionStringArgs = append(dtokMappingFunctionStringArgs, transformArgString) + } } if nphs < npwcs { @@ -4327,7 +4444,7 @@ func newTransform(src, dest string) (*transform, error) { } } - return &transform{src: src, dest: dest, dtoks: dtokens, stoks: stokens, dtokmfts: dtokMappingFunctionTypes, dtokmftis: dtokMappingFunctionTokenIndexes, dtokmfias: dtokMappingFunctionIntArgs}, nil + return &transform{src: src, dest: dest, dtoks: dtokens, stoks: stokens, dtokmftypes: dtokMappingFunctionTypes, dtokmftokindexesargs: dtokMappingFunctionTokenIndexes, dtokmfintargs: dtokMappingFunctionIntArgs, dtokmfstringargs: dtokMappingFunctionStringArgs}, nil } // Match will take a literal published subject that is associated with a client and will match and transform @@ -4389,22 +4506,22 @@ func (tr *transform) getHashPartition(key []byte, numBuckets int) string { // Do a transform on the subject to the dest subject. func (tr *transform) transform(tokens []string) (string, error) { - if len(tr.dtokmfts) == 0 { + if len(tr.dtokmftypes) == 0 { return tr.dest, nil } var b strings.Builder - var token string + var transformedToken string // We need to walk destination tokens and create the mapped subject pulling tokens or mapping functions // This is slow and that is ok, transforms should have caching layer in front for mapping transforms // and export/import semantics with streams and services. - li := len(tr.dtokmfts) - 1 - for i, mfType := range tr.dtokmfts { + li := len(tr.dtokmftypes) - 1 + for i, mfType := range tr.dtokmftypes { if mfType == NoTransform { - token = tr.dtoks[i] + transformedToken = tr.dtoks[i] // Break if fwc - if len(token) == 1 && token[0] == fwc { + if len(transformedToken) == 1 && transformedToken[0] == fwc { break } } else { @@ -4414,15 +4531,83 @@ func (tr *transform) transform(tokens []string) (string, error) { _buffer [64]byte keyForHashing = _buffer[:0] ) - for _, sourceToken := range tr.dtokmftis[i] { + for _, sourceToken := range tr.dtokmftokindexesargs[i] { keyForHashing = append(keyForHashing, []byte(tokens[sourceToken])...) } - token = tr.getHashPartition(keyForHashing, int(tr.dtokmfias[i])) + transformedToken = tr.getHashPartition(keyForHashing, int(tr.dtokmfintargs[i])) case Wildcard: // simple substitution - token = tokens[tr.dtokmftis[i][0]] + transformedToken = tokens[tr.dtokmftokindexesargs[i][0]] + case SplitFromLeft: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + position := int(tr.dtokmfintargs[i]) + if position > 0 && position < sourceTokenLen { + transformedToken = sourceToken[:position] + "." + sourceToken[position:] + } else { // too small to split at the requested position: don't split + transformedToken = sourceToken + } + case SplitFromRight: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + position := int(tr.dtokmfintargs[i]) + if position > 0 && position < sourceTokenLen { + transformedToken = sourceToken[:sourceTokenLen-position] + "." + sourceToken[sourceTokenLen-position:] + } else { // too small to split at the requested position: don't split + transformedToken = sourceToken + } + case SliceFromLeft: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + sliceSize := int(tr.dtokmfintargs[i]) + if sliceSize > 0 && sliceSize < sourceTokenLen { + transformedToken = "" + for i := 0; i+sliceSize <= sourceTokenLen; i += sliceSize { + if i != 0 { + transformedToken = transformedToken + "." + } + transformedToken = transformedToken + sourceToken[i:i+sliceSize] + if i+sliceSize != sourceTokenLen && i+sliceSize+sliceSize > sourceTokenLen { + transformedToken = transformedToken + "." + sourceToken[i+sliceSize:] + break + } + } + } else { // too small to slice at the requested size: don't slice + transformedToken = sourceToken + } + case SliceFromRight: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + sourceTokenLen := len(sourceToken) + sliceSize := int(tr.dtokmfintargs[i]) + if sliceSize > 0 && sliceSize < sourceTokenLen { + transformedToken = "" + remainder := sourceTokenLen % sliceSize + if remainder > 0 { + transformedToken = transformedToken + sourceToken[:remainder] + "." + } + for i := remainder; i+sliceSize <= sourceTokenLen; i += sliceSize { + transformedToken = transformedToken + sourceToken[i:i+sliceSize] + if i+sliceSize < sourceTokenLen { + transformedToken = transformedToken + "." + } + } + } else { // too small to slice at the requested size: don't slice + transformedToken = sourceToken + } + case Split: + sourceToken := tokens[tr.dtokmftokindexesargs[i][0]] + splits := strings.Split(sourceToken, tr.dtokmfstringargs[i]) + transformedToken = "" + for j, split := range splits { + if split != "" { + transformedToken = transformedToken + split + } + if j < len(splits)-1 && splits[j+1] != "" && j != 0 { + transformedToken = transformedToken + "." + } + } } } - b.WriteString(token) + b.WriteString(transformedToken) if i < li { b.WriteByte(btsep) } @@ -4442,7 +4627,7 @@ func (tr *transform) transform(tokens []string) (string, error) { // Reverse a transform. func (tr *transform) reverse() *transform { - if len(tr.dtokmftis) == 0 { + if len(tr.dtokmftokindexesargs) == 0 { rtr, _ := newTransform(tr.dest, tr.src) return rtr } diff --git a/server/accounts_test.go b/server/accounts_test.go index c9eec193..f9bc8623 100644 --- a/server/accounts_test.go +++ b/server/accounts_test.go @@ -50,7 +50,8 @@ func simpleAccountServer(t *testing.T) (*Server, *Account, *Account) { func TestPlaceHolderIndex(t *testing.T) { testString := "$1" - transformType, indexes, nbPartitions, err := indexPlaceHolders(testString) + transformType, indexes, nbPartitions, _, err := indexPlaceHolders(testString) + var position int32 if err != nil || transformType != Wildcard || len(indexes) != 1 || indexes[0] != 1 || nbPartitions != -1 { t.Fatalf("Error parsing %s", testString) @@ -58,39 +59,45 @@ func TestPlaceHolderIndex(t *testing.T) { testString = "{{partition(10,1,2,3)}}" - transformType, indexes, nbPartitions, err = indexPlaceHolders(testString) + transformType, indexes, nbPartitions, _, err = indexPlaceHolders(testString) if err != nil || transformType != Partition || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { t.Fatalf("Error parsing %s", testString) } - testString = "{{ partition(10,1,2,3) }}" + testString = "{{ Partition (10,1,2,3) }}" - transformType, indexes, nbPartitions, err = indexPlaceHolders(testString) - - if err != nil || transformType != Partition || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { - t.Fatalf("Error parsing %s", testString) - } - - testString = "{{partition (10,1,2,3)}}" - - transformType, indexes, nbPartitions, err = indexPlaceHolders(testString) + transformType, indexes, nbPartitions, _, err = indexPlaceHolders(testString) if err != nil || transformType != Partition || !reflect.DeepEqual(indexes, []int{1, 2, 3}) || nbPartitions != 10 { t.Fatalf("Error parsing %s", testString) } testString = "{{wildcard(2)}}" - transformType, indexes, nbPartitions, err = indexPlaceHolders(testString) + transformType, indexes, nbPartitions, _, err = indexPlaceHolders(testString) if err != nil || transformType != Wildcard || len(indexes) != 1 || indexes[0] != 2 || nbPartitions != -1 { t.Fatalf("Error parsing %s", testString) } - testString = "{{ wildcard (2) }}" - transformType, indexes, nbPartitions, err = indexPlaceHolders(testString) + testString = "{{SplitFromLeft(2,1)}}" + transformType, indexes, position, _, err = indexPlaceHolders(testString) - if err != nil || transformType != Wildcard || len(indexes) != 1 || indexes[0] != 2 || nbPartitions != -1 { + if err != nil || transformType != SplitFromLeft || len(indexes) != 1 || indexes[0] != 2 || position != 1 { + t.Fatalf("Error parsing %s", testString) + } + + testString = "{{SplitFromRight(3,2)}}" + transformType, indexes, position, _, err = indexPlaceHolders(testString) + + if err != nil || transformType != SplitFromRight || len(indexes) != 1 || indexes[0] != 3 || position != 2 { + t.Fatalf("Error parsing %s", testString) + } + + testString = "{{SliceFromLeft(2,2)}}" + transformType, indexes, sliceSize, _, err := indexPlaceHolders(testString) + + if err != nil || transformType != SliceFromLeft || len(indexes) != 1 || indexes[0] != 2 || sliceSize != 2 { t.Fatalf("Error parsing %s", testString) } } @@ -3290,6 +3297,7 @@ func TestSubjectTransforms(t *testing.T) { shouldErr("foo.*", "foo.{{wildcard()}}") // Not enough arguments passed to the mapping function shouldErr("foo.*", "foo.{{wildcard(1,2)}}") // Too many arguments passed to the mapping function shouldErr("foo.*", "foo.{{ wildcard5) }}") // Bad mapping function + shouldErr("foo.*", "foo.{{splitLeft(2,2}}") // arg out of range shouldBeOK := func(src, dest string) *transform { t.Helper() @@ -3303,6 +3311,7 @@ func TestSubjectTransforms(t *testing.T) { shouldBeOK("foo", "bar") shouldBeOK("foo.*.bar.*.baz", "req.$2.$1") shouldBeOK("baz.>", "mybaz.>") + shouldBeOK("*", "{{splitfromleft(1,1)}}") shouldMatch := func(src, dest, sample, expected string) { t.Helper() @@ -3321,6 +3330,12 @@ func TestSubjectTransforms(t *testing.T) { shouldMatch("baz.>", "my.pre.>", "baz.1.2.3", "my.pre.1.2.3") shouldMatch("baz.>", "foo.bar.>", "baz.1.2.3", "foo.bar.1.2.3") shouldMatch("*", "foo.bar.$1", "foo", "foo.bar.foo") + shouldMatch("*", "{{splitfromleft(1,3)}}", "12345", "123.45") + shouldMatch("*", "{{SplitFromRight(1,3)}}", "12345", "12.345") + shouldMatch("*", "{{SliceFromLeft(1,3)}}", "1234567890", "123.456.789.0") + shouldMatch("*", "{{SliceFromRight(1,3)}}", "1234567890", "1.234.567.890") + shouldMatch("*", "{{split(1,-)}}", "-abc-def--ghi-", "abc.def.ghi") + shouldMatch("*.*", "{{split(2,-)}}.{{splitfromleft(1,2)}}", "foo.-abc-def--ghij-", "abc.def.ghij.fo.o") // combo + checks split for multiple instance of deliminator and deliminator being at the start or end } func TestAccountSystemPermsWithGlobalAccess(t *testing.T) { diff --git a/server/sublist.go b/server/sublist.go index 99230dde..bae47306 100644 --- a/server/sublist.go +++ b/server/sublist.go @@ -1140,7 +1140,13 @@ func ValidateMappingDestination(subject string) error { } if length > 4 && t[0] == '{' && t[1] == '{' && t[length-2] == '}' && t[length-1] == '}' { - if !partitionMappingFunctionRegEx.MatchString(t) && !wildcardMappingFunctionRegEx.MatchString(t) { + if !partitionMappingFunctionRegEx.MatchString(t) && + !wildcardMappingFunctionRegEx.MatchString(t) && + !splitFromLeftMappingFunctionRegEx.MatchString(t) && + !splitFromRightMappingFunctionRegEx.MatchString(t) && + !sliceFromLeftMappingFunctionRegEx.MatchString(t) && + !sliceFromRightMappingFunctionRegEx.MatchString(t) && + !splitMappingFunctionRegEx.MatchString(t) { return &mappingDestinationErr{t, ErrUnknownMappingDestinationFunction} } else { continue diff --git a/server/sublist_test.go b/server/sublist_test.go index 05a14dd2..f1fb1fde 100644 --- a/server/sublist_test.go +++ b/server/sublist_test.go @@ -669,6 +669,8 @@ func TestValidateDestinationSubject(t *testing.T) { checkError(ValidateMappingDestination("foo.{{ wildcard(1) }}"), nil, t) checkError(ValidateMappingDestination("foo.{{wildcard( 1 )}}"), nil, t) checkError(ValidateMappingDestination("foo.{{partition(2,1)}}"), nil, t) + checkError(ValidateMappingDestination("foo.{{SplitFromLeft(2,1)}}"), nil, t) + checkError(ValidateMappingDestination("foo.{{SplitFromRight(2,1)}}"), nil, t) checkError(ValidateMappingDestination("foo.{{unknown(1)}}"), ErrInvalidMappingDestination, t) checkError(ValidateMappingDestination("foo..}"), ErrInvalidMappingDestination, t) checkError(ValidateMappingDestination("foo. bar}"), ErrInvalidMappingDestinationSubject, t)