Skip to content

Commit 291c231

Browse files
committed
Fix splitting of $compute parameter based on comma separator. Implementation previously incorrectly split on all commas including those located within a single $compute item such as to separate function arguments.
1 parent aeecfb2 commit 291c231

File tree

2 files changed

+145
-4
lines changed

2 files changed

+145
-4
lines changed

compute_parser.go

Lines changed: 80 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ package godata
22

33
import (
44
"context"
5+
"errors"
6+
"fmt"
57
"regexp"
68
"strings"
79
)
@@ -18,25 +20,40 @@ type ComputeItem struct {
1820
Field string // The name of the computed dynamic property.
1921
}
2022

23+
// GlobalAllTokenParser is a Tokenizer which matches all tokens and ignores none. It differs from the
24+
// GlobalExpressionTokenizer which ignores whitespace tokens.
25+
var GlobalAllTokenParser *Tokenizer
26+
27+
func init() {
28+
t := NewExpressionParser().tokenizer
29+
t.TokenMatchers = append(t.IgnoreMatchers, t.TokenMatchers...)
30+
t.IgnoreMatchers = nil
31+
GlobalAllTokenParser = t
32+
}
33+
2134
func ParseComputeString(ctx context.Context, compute string) (*GoDataComputeQuery, error) {
22-
items := strings.Split(compute, ",")
35+
items, err := SplitComputeItems(compute)
36+
if err != nil {
37+
return nil, err
38+
}
2339

2440
result := make([]*ComputeItem, 0)
41+
fields := map[string]struct{}{}
2542

2643
for _, v := range items {
2744
v = strings.TrimSpace(v)
2845
parts := strings.Split(v, computeAsSeparator)
2946
if len(parts) != 2 {
3047
return nil, &GoDataError{
3148
ResponseCode: 400,
32-
Message: "Invalid $compute query option",
49+
Message: "Invalid $compute query format",
3350
}
3451
}
3552
field := strings.TrimSpace(parts[1])
3653
if !computeFieldRegex.MatchString(field) {
3754
return nil, &GoDataError{
3855
ResponseCode: 400,
39-
Message: "Invalid $compute query option",
56+
Message: "Invalid $compute field name",
4057
}
4158
}
4259

@@ -45,7 +62,7 @@ func ParseComputeString(ctx context.Context, compute string) (*GoDataComputeQuer
4562
case *GoDataError:
4663
return nil, &GoDataError{
4764
ResponseCode: e.ResponseCode,
48-
Message: "Invalid $compute query option",
65+
Message: fmt.Sprintf("Invalid $compute query option, %s", e.Message),
4966
Cause: e,
5067
}
5168
default:
@@ -62,6 +79,16 @@ func ParseComputeString(ctx context.Context, compute string) (*GoDataComputeQuer
6279
Message: "Invalid $compute query option",
6380
}
6481
}
82+
83+
if _, ok := fields[field]; ok {
84+
return nil, &GoDataError{
85+
ResponseCode: 400,
86+
Message: "Invalid $compute, duplicate field name",
87+
}
88+
}
89+
90+
fields[field] = struct{}{}
91+
6592
result = append(result, &ComputeItem{
6693
Tree: tree.Tree,
6794
Field: field,
@@ -71,3 +98,52 @@ func ParseComputeString(ctx context.Context, compute string) (*GoDataComputeQuer
7198

7299
return &GoDataComputeQuery{result, compute}, nil
73100
}
101+
102+
// SplitComputeItems splits the input string based on the comma delimiter. It does so with awareness as to
103+
// which commas delimit $compute items and which ones are an inline part of the item, such as a separator
104+
// for function arguments.
105+
//
106+
// For example the input "someFunc(one,two) as three, 1 add 2 as four" results in the
107+
// output ["someFunc(one,two) as three", "1 add 2 as four"]
108+
func SplitComputeItems(in string) ([]string, error) {
109+
110+
var ret []string
111+
112+
tokens, err := GlobalAllTokenParser.Tokenize(context.Background(), in)
113+
if err != nil {
114+
return nil, err
115+
}
116+
117+
item := strings.Builder{}
118+
parenGauge := 0
119+
120+
for _, v := range tokens {
121+
switch v.Type {
122+
case ExpressionTokenOpenParen:
123+
parenGauge++
124+
case ExpressionTokenCloseParen:
125+
if parenGauge == 0 {
126+
return nil, errors.New("unmatched parentheses")
127+
}
128+
parenGauge--
129+
case ExpressionTokenComma:
130+
if parenGauge == 0 {
131+
ret = append(ret, item.String())
132+
item.Reset()
133+
continue
134+
}
135+
}
136+
137+
item.WriteString(v.Value)
138+
}
139+
140+
if parenGauge != 0 {
141+
return nil, errors.New("unmatched parentheses")
142+
}
143+
144+
if item.Len() > 0 {
145+
ret = append(ret, item.String())
146+
}
147+
148+
return ret, nil
149+
}

compute_parser_test.go

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package godata
2+
3+
import (
4+
"context"
5+
"strings"
6+
"testing"
7+
)
8+
9+
func TestParseCompute(t *testing.T) {
10+
DefineCustomFunctions([]CustomFunctionInput{
11+
{Name: "zeroArgFunc", NumParams: []int{0}},
12+
{Name: "oneArgFunc", NumParams: []int{1}},
13+
{Name: "twoArgFunc", NumParams: []int{2}},
14+
})
15+
16+
type testcase struct {
17+
computeStrings []string
18+
shouldPass bool
19+
}
20+
21+
testCases := []testcase{
22+
{[]string{"oldField as newField"}, true},
23+
{[]string{"1 as newField"}, true},
24+
{[]string{"one add 2 as newField"}, true},
25+
{[]string{"one add two as extra/newField"}, true},
26+
{[]string{"zeroArgFunc() as newField"}, true},
27+
{[]string{"oneArgFunc(one) as newField"}, true},
28+
{[]string{"twoArgFunc(one, two) as newField"}, true},
29+
{[]string{"twoArgFunc(one, two) as newField", "tolower(three) as newFieldTwo"}, true},
30+
31+
// negative cases
32+
{[]string{"one add two as newField2"}, false},
33+
{[]string{"one add two newField2"}, false},
34+
{[]string{""}, false},
35+
{[]string{"as"}, false},
36+
{[]string{"as newField"}, false},
37+
{[]string{"zeroArgFunc() as "}, false},
38+
}
39+
40+
for i, v := range testCases {
41+
42+
result, err := ParseComputeString(context.Background(), strings.Join(v.computeStrings, ","))
43+
if v.shouldPass {
44+
if err != nil {
45+
t.Errorf("testcase %d: %v", i, err)
46+
t.FailNow()
47+
}
48+
if result == nil {
49+
t.Errorf("testcase %d: nil result", i)
50+
t.FailNow()
51+
}
52+
53+
if len(result.ComputeItems) != len(v.computeStrings) {
54+
t.Errorf("testcase %d: wrong number of result items", i)
55+
t.FailNow()
56+
}
57+
} else {
58+
if err == nil {
59+
t.Errorf("testcase %d: expected error", i)
60+
t.FailNow()
61+
}
62+
}
63+
64+
}
65+
}

0 commit comments

Comments
 (0)