Skip to content

Commit 057fda4

Browse files
committed
The parse-tree representation of 'case' is a node of type ExpressionTokenCase, containing a list of ExpressionTokenCasePair nodes, one for each colon-delimited expression pair. Each ExpressionTokenCasePair node has two children. Child 0 is the boolean expression and child 1 is the value expression.
1 parent b0e3c08 commit 057fda4

File tree

5 files changed

+96
-12
lines changed

5 files changed

+96
-12
lines changed

compute_parser_test.go

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,13 +33,29 @@ func TestParseCompute(t *testing.T) {
3333
{[]string{"twoArgFunc(one, two) as newField"}, true},
3434
{[]string{"twoArgFunc(one, two) as newField", "tolower(three) as newFieldTwo"}, true},
3535

36+
{[]string{"case(false:0) as newField"}, true},
37+
{[]string{"case(false:0,true:1) as newField"}, true},
38+
{[]string{"case(prop eq 'one':1,prop eq 'two':2) as newField"}, true},
39+
{[]string{"case(tolower(one) eq one:'lower') as newField"}, true},
40+
{[]string{"case(contains(haystack,'needle'):1,true:1) as newField"}, true},
41+
{[]string{"case(false:1,false:2,false:3,false:4,false:5,false:6,false:7,false:8,false:9,false:10) as newField"}, true}, // max of 10 cases
42+
3643
// negative cases
3744
{[]string{"one add two as newField2"}, false},
3845
{[]string{"one add two newField2"}, false},
3946
{[]string{""}, false},
4047
{[]string{"as"}, false},
4148
{[]string{"as newField"}, false},
4249
{[]string{"zeroArgFunc() as "}, false},
50+
51+
{[]string{"case as newField"}, false},
52+
{[]string{"case() as newField"}, false},
53+
{[]string{"case(false:,true:1) as newField"}, false},
54+
{[]string{"case(false,true:1) as newField"}, false},
55+
{[]string{"case(false,true:1) as newField"}, false},
56+
{[]string{"case(1:1,true:1) as newField"}, false},
57+
{[]string{"case(1:1,true:1) as newField"}, false},
58+
{[]string{"case(false:1,false:2,false:3,false:4,false:5,false:6,false:7,false:8,false:9,false:10,false:11) as newField"}, false}, // max of 10 cases
4359
}
4460

4561
for i, v := range testCases {

expression_parser.go

Lines changed: 15 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,23 @@ const (
4242
ExpressionTokenFunc // Function, e.g. contains, substring...
4343
ExpressionTokenLambdaNav // "/" token when used in lambda expression, e.g. tags/any()
4444
ExpressionTokenLambda // [10] any(), all() lambda functions
45+
ExpressionTokenCase // A case() statement. See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_case
46+
ExpressionTokenCasePair // A case statement expression pair [ <boolean expression> : <value expression> ]
4547
ExpressionTokenNull //
4648
ExpressionTokenIt // The '$it' token
47-
ExpressionTokenRoot // The '$root' token
49+
ExpressionTokenRoot // [15] The '$root' token
4850
ExpressionTokenFloat // A floating point value.
49-
ExpressionTokenInteger // [15] An integer value
51+
ExpressionTokenInteger // An integer value
5052
ExpressionTokenString // SQUOTE *( SQUOTE-in-string / pchar-no-SQUOTE ) SQUOTE
5153
ExpressionTokenDate // A date value
52-
ExpressionTokenTime // A time value
54+
ExpressionTokenTime // [20] A time value
5355
ExpressionTokenDateTime // A date-time value
54-
ExpressionTokenBoolean // [20]
55-
ExpressionTokenLiteral //
56+
ExpressionTokenBoolean // A literal boolean value
57+
ExpressionTokenLiteral // A literal non-boolean value
5658
ExpressionTokenDuration // duration = [ "duration" ] SQUOTE durationValue SQUOTE
57-
ExpressionTokenGuid // A 128-bit GUID
59+
ExpressionTokenGuid // [25] A 128-bit GUID
5860
ExpressionTokenAssignement // The '=' assignement for function arguments.
59-
ExpressionTokenGeographyPolygon // [25]
61+
ExpressionTokenGeographyPolygon //
6062
ExpressionTokenGeometryPolygon //
6163
expressionTokenLast
6264
)
@@ -74,6 +76,8 @@ func (e ExpressionTokenType) String() string {
7476
"ExpressionTokenFunc",
7577
"ExpressionTokenLambdaNav",
7678
"ExpressionTokenLambda",
79+
"ExpressionTokenCase",
80+
"ExpressionTokenCasePair",
7781
"ExpressionTokenNull",
7882
"ExpressionTokenIt",
7983
"ExpressionTokenRoot",
@@ -205,6 +209,7 @@ func NewExpressionTokenizer() *Tokenizer {
205209
// anyExpr = "any" OPEN BWS [ lambdaVariableExpr BWS COLON BWS lambdaPredicateExpr ] BWS CLOSE
206210
// allExpr = "all" OPEN BWS lambdaVariableExpr BWS COLON BWS lambdaPredicateExpr BWS CLOSE
207211
t.Add("(?i)^(?P<token>(any|all))[\\s(]", ExpressionTokenLambda)
212+
t.Add("(?i)^(?P<token>(case))[\\s(]", ExpressionTokenCase)
208213
t.Add("^null", ExpressionTokenNull)
209214
t.Add("^\\$it", ExpressionTokenIt)
210215
t.Add("^\\$root", ExpressionTokenRoot)
@@ -322,6 +327,9 @@ func NewExpressionParser() *ExpressionParser {
322327
parser.DefineFunction("any", []int{0, 2}, true)
323328
// 'all' requires two arguments of a form similar to 'any'.
324329
parser.DefineFunction("all", []int{2}, true)
330+
// Define 'case' as a function accepting 1-10 arguments. Each argument is a pair of expressions separated by a colon.
331+
// See https://docs.oasis-open.org/odata/odata/v4.01/odata-v4.01-part2-url-conventions.html#sec_case
332+
parser.DefineFunction("case", []int{1,2,3,4,5,6,7,8,9,10}, true)
325333

326334
return parser
327335
}

expression_parser_fixture_test.go

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -76,6 +76,46 @@ var testCases = []struct {
7676
{Value: "'Redmond'", Depth: 1, Type: ExpressionTokenString},
7777
},
7878
},
79+
{
80+
expression: "case(false:0,true:1)",
81+
tree: []expectedParseNode{
82+
{Value: "case", Depth: 0, Type: ExpressionTokenCase},
83+
{Value: "", Depth: 1, Type: ExpressionTokenCasePair},
84+
{Value: "false", Depth: 2, Type: ExpressionTokenBoolean},
85+
{Value: "0", Depth: 2, Type: ExpressionTokenInteger},
86+
{Value: "", Depth: 1, Type: ExpressionTokenCasePair},
87+
{Value: "true", Depth: 2, Type: ExpressionTokenBoolean},
88+
{Value: "1", Depth: 2, Type: ExpressionTokenInteger},
89+
},
90+
},
91+
{
92+
expression: "case(prop eq 'one':1,true:0)",
93+
tree: []expectedParseNode{
94+
{Value: "case", Depth: 0, Type: ExpressionTokenCase},
95+
{Value: "", Depth: 1, Type: ExpressionTokenCasePair},
96+
{Value: "eq", Depth: 2, Type: ExpressionTokenLogical},
97+
{Value: "prop", Depth: 3, Type: ExpressionTokenLiteral},
98+
{Value: "'one'", Depth: 3, Type: ExpressionTokenString},
99+
{Value: "1", Depth: 2, Type: ExpressionTokenInteger},
100+
{Value: "", Depth: 1, Type: ExpressionTokenCasePair},
101+
{Value: "true", Depth: 2, Type: ExpressionTokenBoolean},
102+
{Value: "0", Depth: 2, Type: ExpressionTokenInteger},
103+
},
104+
},
105+
{
106+
expression: "case(contains(prop,'val'):0,true:1)",
107+
tree: []expectedParseNode{
108+
{Value: "case", Depth: 0, Type: ExpressionTokenCase},
109+
{Value: "", Depth: 1, Type: ExpressionTokenCasePair},
110+
{Value: "contains", Depth: 2, Type: ExpressionTokenFunc},
111+
{Value: "prop", Depth: 3, Type: ExpressionTokenLiteral},
112+
{Value: "'val'", Depth: 3, Type: ExpressionTokenString},
113+
{Value: "0", Depth: 2, Type: ExpressionTokenInteger},
114+
{Value: "", Depth: 1, Type: ExpressionTokenCasePair},
115+
{Value: "true", Depth: 2, Type: ExpressionTokenBoolean},
116+
{Value: "1", Depth: 2, Type: ExpressionTokenInteger},
117+
},
118+
},
79119
{
80120
/*
81121
{

expression_parser_test.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,7 @@ func TestInvalidBooleanExpressionSyntax(t *testing.T) {
246246
"now()",
247247
"tolower(Name)",
248248
"concat(First,Last)",
249+
"case(false:0,true:1)",
249250
}
250251
p := NewExpressionParser()
251252
p.ExpectBoolExpr = true

parser.go

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -679,12 +679,31 @@ func (p *Parser) PostfixToTree(ctx context.Context, queue *tokenQueue) (*ParseNo
679679
return nil, fmt.Errorf("expected list expression token, got '%v'", n.Token.Type)
680680
}
681681

682-
// Get function parameters.
683-
// Some functions, e.g. substring, can take a variable number of arguments.
684-
for _, c := range n.Children {
685-
c.Parent = node
682+
if node.Token.Type == ExpressionTokenCase {
683+
// Create argument pairs for case() statement by translating flat list into pairs
684+
if len(n.Children)%2 != 0 {
685+
return nil, fmt.Errorf("expected even number of comma-separated arguments to case statement")
686+
}
687+
for i:=0; i<len(n.Children); i+=2 {
688+
if !p.isBooleanExpression(n.Children[i].Token) {
689+
return nil, fmt.Errorf("expected boolean expression in case statement")
690+
}
691+
c := &ParseNode{
692+
Token: &Token{Type: ExpressionTokenCasePair},
693+
Parent: node,
694+
Children: []*ParseNode{n.Children[i],n.Children[i+1]},
695+
}
696+
node.Children = append(node.Children, c)
697+
}
698+
} else {
699+
// Collapse function arguments as direct children of function node
700+
for _, c := range n.Children {
701+
c.Parent = node
702+
}
703+
node.Children = n.Children
686704
}
687-
node.Children = n.Children
705+
706+
// Some functions, e.g. substring, can take a variable number of arguments. Enforce legal number of arguments
688707
foundMatch := false
689708
f := p.Functions[node.Token.Value]
690709
for _, expectedArgCount := range f.Params {

0 commit comments

Comments
 (0)