From c3c963729f9c2ac19fc763c83d0567787250de20 Mon Sep 17 00:00:00 2001 From: Jasper Van der Jeugt Date: Wed, 28 Jun 2023 20:55:13 +0200 Subject: [PATCH] feat: support arrays in ARM functions --- pkg/input/arm/expressions.go | 25 +++++++ pkg/input/arm/parser.go | 141 +++++++++++++++++++++-------------- pkg/input/arm/parser_test.go | 15 ++++ pkg/input/arm/tokenizer.go | 6 ++ 4 files changed, 133 insertions(+), 54 deletions(-) diff --git a/pkg/input/arm/expressions.go b/pkg/input/arm/expressions.go index 379f8ff3..fd23c400 100644 --- a/pkg/input/arm/expressions.go +++ b/pkg/input/arm/expressions.go @@ -68,3 +68,28 @@ func (p propertyExpr) eval(evalCtx EvaluationContext) (interface{}, error) { return objMap[p.property], nil } + +func makePropertyExpr(e expression, properties []string) expression { + if len(properties) == 0 { + return e + } + return makePropertyExpr( + propertyExpr{obj: e, property: properties[0]}, + properties[1:], + ) +} + +type arrayExpr []expression + +func (e arrayExpr) eval(evalCtx EvaluationContext) (interface{}, error) { + vals := []interface{}{} + for _, expr := range e { + val, err := expr.eval(evalCtx) + if err != nil { + return nil, err + } + vals = append(vals, val) + + } + return vals, nil +} diff --git a/pkg/input/arm/parser.go b/pkg/input/arm/parser.go index d7dedbd1..bba4b304 100644 --- a/pkg/input/arm/parser.go +++ b/pkg/input/arm/parser.go @@ -45,6 +45,17 @@ func (p *parser) parse() (expression, error) { return stringLiteralExpr(strToken), nil } + // Parse arrays + if _, ok := tkn.(openBracket); ok { + items, err := parseList(p, comma{}, closeBracket{}, func() (expression, error) { + return p.parse() + }) + if err != nil { + return nil, err + } + return arrayExpr(items), nil + } + // If we reach here, we are building a function expression, because there are // no "direct" identifier dereferences in ARM template expressions. The // identifier is the function name. @@ -66,65 +77,32 @@ func (p *parser) parse() (expression, error) { } var args []expression - for { - // In the first iteration, we've just peeked successfully. In all subsequent - // iterations, we'd have broken out of the loop if we had exhausted all - // tokens. - tkn, _ := p.peek() - if _, ok := tkn.(closeParen); ok { - p.pop() // pop the close paren - expr := functionExpr{name: string(idToken), args: args} - tkn, ok := p.peek() - if !ok { - return expr, nil - } - if _, ok := tkn.(dot); ok { - return p.buildPropertyAccessExpression(expr) - } - return expr, nil - } - - // There is a comma between args, so not before the first arg - if len(args) > 0 { - // We can't reach here if we have exhausted all tokens above - tkn, _ := p.peek() - if _, ok := tkn.(comma); !ok { - return nil, newParserError(fmt.Errorf("expected token %#v to be a comma", tkn)) - } - p.pop() // pop the comma - } - - nextArg, err := p.parse() - if err != nil { - return nil, err - } - args = append(args, nextArg) + args, err := parseList(p, comma{}, closeParen{}, func() (expression, error) { + return p.parse() + }) + if err != nil { + return nil, err } + expr := functionExpr{name: string(idToken), args: args} + return p.buildPropertyAccessExpression(expr) } func (p *parser) buildPropertyAccessExpression(expr expression) (expression, error) { - // we only enter this function from parse() if we peeked at a dot, so we know - // it gets past here at least once, and so always builds a real property - // access expression. - tkn, ok := p.peek() - if !ok { - return expr, nil - } - if _, ok := tkn.(dot); !ok { - return expr, nil - } - - p.pop() // pop the dot - tkn, ok = p.pop() - if !ok { - return nil, newParserError(errors.New("expression cannot terminate with a dot")) - } - nextPropChainElement, ok := tkn.(identifier) - if !ok { - return nil, newParserError(fmt.Errorf("expected token %#v to be an identifier", tkn)) + identifiers, err := parsePairs(p, dot{}, func() (string, error) { + if tkn, ok := p.pop(); ok { + if id, ok := tkn.(identifier); ok { + return string(id), nil + } else { + return "", newParserError(fmt.Errorf("expected token %#v to be an identifier", tkn)) + } + } else { + return "", newParserError(errors.New("expression cannot terminate with a dot")) + } + }) + if err != nil { + return nil, err } - expr = propertyExpr{obj: expr, property: string(nextPropChainElement)} - return p.buildPropertyAccessExpression(expr) + return makePropertyExpr(expr, identifiers), nil } func (p *parser) peek() (token, bool) { @@ -146,3 +124,58 @@ func (p *parser) pop() (token, bool) { func newParserError(underlying error) error { return Error{underlying: underlying, kind: ParserError} } + +// parsePairs parses ([leading][item])* +func parsePairs[T any]( + p *parser, + leading token, + parseItem func() (T, error), +) ([]T, error) { + items := []T{} + for { + tkn, ok := p.peek() + if !ok || tkn != leading { + return items, nil + } + p.pop() + item, err := parseItem() + if err != nil { + return nil, err + } + items = append(items, item) + } +} + +// parseList parses [item]?([seperator][item])*[trailing] +// This is a possibly empty list, separated by separator (usually ',') and +// ended by trailing (think ')' or ']'). +func parseList[T any]( + p *parser, + separator token, + trailing token, + parseItem func() (T, error), +) ([]T, error) { + tkn, ok := p.peek() + if !ok { + return nil, newParserError(errors.New("expected list to be closed")) + } + if tkn == trailing { + p.pop() + return nil, nil // Empty list + } + item0, err := parseItem() + if err != nil { + return nil, err + } + items := []T{item0} + moreItems, err := parsePairs(p, comma{}, parseItem) + if err != nil { + return nil, err + } + items = append(items, moreItems...) + tkn, ok = p.pop() + if !ok || tkn != trailing { + return nil, newParserError(fmt.Errorf("expected list to be closed with %#v", trailing)) + } + return items, nil +} diff --git a/pkg/input/arm/parser_test.go b/pkg/input/arm/parser_test.go index 4334202a..755e88c6 100644 --- a/pkg/input/arm/parser_test.go +++ b/pkg/input/arm/parser_test.go @@ -85,6 +85,21 @@ func TestParse(t *testing.T) { property: "baz", }, }, + { + name: "supports arrays", + input: "[[], ['foo'], [resourceGroup().location, 'bar']]", + expected: arrayExpr([]expression{ + arrayExpr(nil), + arrayExpr([]expression{stringLiteralExpr("foo")}), + arrayExpr([]expression{ + propertyExpr{ + obj: functionExpr{name: "resourceGroup"}, + property: "location", + }, + stringLiteralExpr("bar"), + }), + }), + }, } { t.Run(tc.name, func(t *testing.T) { tokens, err := tokenize(tc.input) diff --git a/pkg/input/arm/tokenizer.go b/pkg/input/arm/tokenizer.go index 117f0219..7bf0bd1c 100644 --- a/pkg/input/arm/tokenizer.go +++ b/pkg/input/arm/tokenizer.go @@ -71,6 +71,10 @@ func (t *tokenizer) next() (token, error) { return openParen{}, nil case ')': return closeParen{}, nil + case '[': + return openBracket{}, nil + case ']': + return closeBracket{}, nil case ',': return comma{}, nil case '.': @@ -130,6 +134,8 @@ type token interface { type openParen struct{} type closeParen struct{} +type openBracket struct{} +type closeBracket struct{} type comma struct{} type dot struct{} type identifier string