Skip to content

Commit e424590

Browse files
authored
Merge pull request #42 from CiscoM31/compute-bug-fix
Bug fixes for $compute parsing
2 parents 9bbe242 + 870c693 commit e424590

File tree

4 files changed

+161
-5
lines changed

4 files changed

+161
-5
lines changed

.github/workflows/golangci-lint.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ jobs:
1414
uses: golangci/golangci-lint-action@v2
1515
with:
1616
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
17-
version: v1.49
17+
version: v1.53.3
1818

1919
# Optional: working directory, useful for monorepos
2020
# working-directory: somedir

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
)
@@ -10,18 +12,33 @@ import (
1012
// See https://docs.oasis-open.org/odata/odata/v4.01/os/part2-url-conventions/odata-v4.01-os-part2-url-conventions.html#sec_SystemQueryOptioncompute
1113
const computeAsSeparator = " as "
1214

13-
// Dynamic property names are restricted to case-insensitive a-z
14-
var computeFieldRegex = regexp.MustCompile("^[a-zA-Z]+$")
15+
// Dynamic property names are restricted to case-insensitive a-z and the path separator /.
16+
var computeFieldRegex = regexp.MustCompile("^[a-zA-Z/]+$")
1517

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

url_parser_test.go

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -539,6 +539,15 @@ func TestUnescapeStringTokens(t *testing.T) {
539539
},
540540
},
541541
},
542+
{
543+
url: "/Product?$compute=Price mul Quantity as Extra/TotalPrice",
544+
errRegex: nil,
545+
expectedCompute: []ComputeItem{
546+
{
547+
Field: "Extra/TotalPrice",
548+
},
549+
},
550+
},
542551
{
543552
url: "/Product?$compute=Price mul Quantity as TotalPrice,A add B as C",
544553
expectedCompute: []ComputeItem{

0 commit comments

Comments
 (0)