Skip to content

Commit 0dbc0d9

Browse files
committed
put pair parsing into internal pkg, add some more unit tests, update fmt specifier for errs
1 parent 6b0a26d commit 0dbc0d9

File tree

7 files changed

+361
-91
lines changed

7 files changed

+361
-91
lines changed
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package parseutils // import "github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/parseutils"
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package parseutils
5+
6+
import (
7+
"testing"
8+
9+
"go.uber.org/goleak"
10+
)
11+
12+
func TestMain(m *testing.M) {
13+
goleak.VerifyTestMain(m)
14+
}
Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package parseutils
5+
6+
import "fmt"
7+
8+
// SplitString will split the input on the delimiter and return the resulting slice while respecting quotes. Outer quotes are stripped.
9+
// Use in place of `strings.Split` when quotes need to be respected.
10+
// Requires `delimiter` not be an empty string
11+
func SplitString(input, delimiter string) ([]string, error) {
12+
var result []string
13+
current := ""
14+
delimiterLength := len(delimiter)
15+
quoteChar := "" // "" means we are not in quotes
16+
17+
for i := 0; i < len(input); i++ {
18+
if quoteChar == "" && i+delimiterLength <= len(input) && input[i:i+delimiterLength] == delimiter { // delimiter
19+
if current == "" { // leading || trailing delimiter; ignore
20+
i += delimiterLength - 1
21+
continue
22+
}
23+
result = append(result, current)
24+
current = ""
25+
i += delimiterLength - 1
26+
continue
27+
}
28+
29+
if quoteChar == "" && (input[i] == '"' || input[i] == '\'') { // start of quote
30+
quoteChar = string(input[i])
31+
continue
32+
}
33+
if string(input[i]) == quoteChar { // end of quote
34+
quoteChar = ""
35+
continue
36+
}
37+
38+
current += string(input[i])
39+
}
40+
41+
if quoteChar != "" { // check for closed quotes
42+
return nil, fmt.Errorf("never reached the end of a quoted value")
43+
}
44+
if current != "" { // avoid adding empty value bc of a trailing delimiter
45+
return append(result, current), nil
46+
}
47+
48+
return result, nil
49+
}
Lines changed: 186 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,186 @@
1+
// Copyright The OpenTelemetry Authors
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
package parseutils
5+
6+
import (
7+
"fmt"
8+
"testing"
9+
10+
"github.com/stretchr/testify/assert"
11+
)
12+
13+
func Test_SplitString(t *testing.T) {
14+
testCases := []struct {
15+
name string
16+
input string
17+
delimiter string
18+
expected []string
19+
expectedErr error
20+
}{
21+
{
22+
name: "simple",
23+
input: "a b c",
24+
delimiter: " ",
25+
expected: []string{
26+
"a",
27+
"b",
28+
"c",
29+
},
30+
},
31+
{
32+
name: "single quotes",
33+
input: "a 'b c d'",
34+
delimiter: " ",
35+
expected: []string{
36+
"a",
37+
"b c d",
38+
},
39+
},
40+
{
41+
name: "double quotes",
42+
input: `a " b c " d`,
43+
delimiter: " ",
44+
expected: []string{
45+
"a",
46+
" b c ",
47+
"d",
48+
},
49+
},
50+
{
51+
name: "multi-char delimiter",
52+
input: "abc!@! def !@! g",
53+
delimiter: "!@!",
54+
expected: []string{
55+
"abc",
56+
" def ",
57+
" g",
58+
},
59+
},
60+
{
61+
name: "leading and trailing delimiters",
62+
input: " name=ottl func=key_value hello=world ",
63+
delimiter: " ",
64+
expected: []string{
65+
"name=ottl",
66+
"func=key_value",
67+
"hello=world",
68+
},
69+
},
70+
{
71+
name: "embedded double quotes in single quoted value",
72+
input: `ab c='this is a "co ol" value'`,
73+
delimiter: " ",
74+
expected: []string{
75+
"ab",
76+
`c=this is a "co ol" value`,
77+
},
78+
},
79+
{
80+
name: "embedded double quotes end single quoted value",
81+
input: `ab c='this is a "co ol"'`,
82+
delimiter: " ",
83+
expected: []string{
84+
"ab",
85+
`c=this is a "co ol"`,
86+
},
87+
},
88+
{
89+
name: "quoted values include whitespace",
90+
input: `name=" ottl " func=" key_ value"`,
91+
delimiter: " ",
92+
expected: []string{
93+
"name= ottl ",
94+
"func= key_ value",
95+
},
96+
},
97+
{
98+
name: "delimiter longer than input",
99+
input: "abc",
100+
delimiter: "aaaa",
101+
expected: []string{
102+
"abc",
103+
},
104+
},
105+
{
106+
name: "delimiter not found",
107+
input: "a b c",
108+
delimiter: "!",
109+
expected: []string{
110+
"a b c",
111+
},
112+
},
113+
{
114+
name: "newlines in input",
115+
input: `a
116+
b
117+
c`,
118+
delimiter: " ",
119+
expected: []string{
120+
"a\nb\nc",
121+
},
122+
},
123+
{
124+
name: "newline delimiter",
125+
input: `a b c
126+
d e f
127+
g
128+
h`,
129+
delimiter: "\n",
130+
expected: []string{
131+
"a b c",
132+
"d e f",
133+
"g ",
134+
"h",
135+
},
136+
},
137+
{
138+
name: "empty input",
139+
input: "",
140+
delimiter: " ",
141+
expected: nil,
142+
},
143+
{
144+
name: "equal input and delimiter",
145+
input: "abc",
146+
delimiter: "abc",
147+
expected: nil,
148+
},
149+
{
150+
name: "unclosed quotes",
151+
input: "a 'b c",
152+
delimiter: " ",
153+
expectedErr: fmt.Errorf("never reached the end of a quoted value"),
154+
},
155+
{
156+
name: "mismatched quotes",
157+
input: `a 'b c' "d '`,
158+
delimiter: " ",
159+
expectedErr: fmt.Errorf("never reached the end of a quoted value"),
160+
},
161+
{
162+
name: "tab delimiters",
163+
input: "a b c",
164+
delimiter: "\t",
165+
expected: []string{
166+
"a",
167+
"b",
168+
"c",
169+
},
170+
},
171+
}
172+
173+
for _, tc := range testCases {
174+
t.Run(tc.name, func(t *testing.T) {
175+
result, err := SplitString(tc.input, tc.delimiter)
176+
177+
if tc.expectedErr == nil {
178+
assert.NoError(t, err)
179+
assert.Equal(t, tc.expected, result)
180+
} else {
181+
assert.EqualError(t, err, tc.expectedErr.Error())
182+
assert.Nil(t, result)
183+
}
184+
})
185+
}
186+
}

pkg/ottl/ottlfuncs/README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -847,7 +847,7 @@ Examples:
847847

848848
The `ParseKeyValue` Converter returns a `pcommon.Map` that is a result of parsing the target string for key value pairs.
849849

850-
`target` is a Getter that returns a string. `delimiter` is an optional string that is used to split the key and value in a pair, the default is `=`. `pair_delimiter` is an optional string that is used to split key value pairs, the default is white space.
850+
`target` is a Getter that returns a string. `delimiter` is an optional string that is used to split the key and value in a pair, the default is `=`. `pair_delimiter` is an optional string that is used to split key value pairs, the default is a single space (` `).
851851

852852
For example, the following target `"k1=v1 k2=v2 k3=v3"` will use default delimiters and be parsed into the following map:
853853
```

pkg/ottl/ottlfuncs/func_parse_key_value.go

Lines changed: 7 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ import (
88
"fmt"
99
"strings"
1010

11+
"github.com/open-telemetry/opentelemetry-collector-contrib/internal/coreinternal/parseutils"
1112
"github.com/open-telemetry/opentelemetry-collector-contrib/pkg/ottl"
1213
"go.opentelemetry.io/collector/pdata/pcommon"
1314
)
@@ -34,17 +35,17 @@ func createParseKeyValueFunction[K any](_ ottl.FunctionContext, oArgs ottl.Argum
3435

3536
func parseKeyValue[K any](target ottl.StringGetter[K], d ottl.Optional[string], p ottl.Optional[string]) (ottl.ExprFunc[K], error) {
3637
delimiter := "="
37-
if !d.IsEmpty() {
38+
if !d.IsEmpty() && d.Get() != "" {
3839
delimiter = d.Get()
3940
}
4041

4142
pairDelimiter := " "
42-
if !p.IsEmpty() {
43+
if !p.IsEmpty() && p.Get() != "" {
4344
pairDelimiter = p.Get()
4445
}
4546

4647
if pairDelimiter == delimiter {
47-
return nil, fmt.Errorf("pair delimiter \"%s\" cannot be equal to delimiter \"%s\"", pairDelimiter, delimiter)
48+
return nil, fmt.Errorf("pair delimiter %q cannot be equal to delimiter %q", pairDelimiter, delimiter)
4849
}
4950

5051
return func(ctx context.Context, tCtx K) (any, error) {
@@ -57,16 +58,16 @@ func parseKeyValue[K any](target ottl.StringGetter[K], d ottl.Optional[string],
5758
return nil, fmt.Errorf("cannot parse from empty target")
5859
}
5960

60-
pairs, err := splitPairs(source, pairDelimiter)
61+
pairs, err := parseutils.SplitString(source, pairDelimiter)
6162
if err != nil {
62-
return nil, fmt.Errorf("splitting pairs failed: %w", err)
63+
return nil, fmt.Errorf("splitting source %q into pairs failed: %w", source, err)
6364
}
6465

6566
parsed := make(map[string]any)
6667
for _, p := range pairs {
6768
pair := strings.SplitN(p, delimiter, 2)
6869
if len(pair) != 2 {
69-
return nil, fmt.Errorf("cannot split '%s' into 2 items, got %d", p, len(pair))
70+
return nil, fmt.Errorf("cannot split %q into 2 items, got %d item(s)", p, len(pair))
7071
}
7172
key := strings.TrimSpace(pair[0])
7273
value := strings.TrimSpace(pair[1])
@@ -78,44 +79,3 @@ func parseKeyValue[K any](target ottl.StringGetter[K], d ottl.Optional[string],
7879
return result, err
7980
}, nil
8081
}
81-
82-
// splitPairs will split the input on the pairDelimiter and return the resulting slice.
83-
// `strings.Split` is not used because it does not respect quotes and will split if the delimiter appears in a quoted value
84-
func splitPairs(input, pairDelimiter string) ([]string, error) {
85-
var result []string
86-
currentPair := ""
87-
delimiterLength := len(pairDelimiter)
88-
quoteChar := "" // "" means we are not in quotes
89-
90-
for i := 0; i < len(input); i++ {
91-
if quoteChar == "" && i+delimiterLength <= len(input) && input[i:i+delimiterLength] == pairDelimiter { // delimiter
92-
if currentPair == "" { // leading || trailing delimiter; ignore
93-
continue
94-
}
95-
result = append(result, currentPair)
96-
currentPair = ""
97-
i += delimiterLength - 1
98-
continue
99-
}
100-
101-
if quoteChar == "" && (input[i] == '"' || input[i] == '\'') { // start of quote
102-
quoteChar = string(input[i])
103-
continue
104-
}
105-
if string(input[i]) == quoteChar { // end of quote
106-
quoteChar = ""
107-
continue
108-
}
109-
110-
currentPair += string(input[i])
111-
}
112-
113-
if quoteChar != "" { // check for closed quotes
114-
return nil, fmt.Errorf("never reached end of a quoted value")
115-
}
116-
if currentPair != "" { // avoid adding empty value bc of a trailing delimiter
117-
return append(result, currentPair), nil
118-
}
119-
120-
return result, nil
121-
}

0 commit comments

Comments
 (0)