Skip to content

Commit

Permalink
refactor: remove brackets rather than lex them
Browse files Browse the repository at this point in the history
  • Loading branch information
Craig Furman committed Jun 21, 2023
1 parent d78a045 commit 340410d
Show file tree
Hide file tree
Showing 6 changed files with 66 additions and 73 deletions.
32 changes: 27 additions & 5 deletions pkg/input/arm/eval.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,43 @@ package arm

import (
"fmt"
"strings"
)

type EvaluationContext struct {
// The entrypoint to this whole interpreter package.
// TODO lots of doc comments everywhere...
func EvaluateARMTemplateString(input string) (interface{}, error) {
if !isARMTemplateExpression(input) {
return input, nil
}

evalCtx := newEvaluationContext()
return evalCtx.eval(extractAndEscapeARMTemplate(input))
}

func isARMTemplateExpression(input string) bool {
return strings.HasPrefix(input, "[") && strings.HasSuffix(input, "]")
}

func extractAndEscapeARMTemplate(input string) string {
// Remove brackets
input = input[1:(len(input) - 1)]
return input
}

type evaluationContext struct {
funcs map[string]armFnImpl
}

func NewEvaluationContext() *EvaluationContext {
return &EvaluationContext{
func newEvaluationContext() *evaluationContext {
return &evaluationContext{
funcs: map[string]armFnImpl{
"concat": concatImpl,
},
}
}

func (e EvaluationContext) Eval(input string) (interface{}, error) {
func (e evaluationContext) eval(input string) (interface{}, error) {
tokens, err := tokenize(input)
if err != nil {
return nil, err
Expand All @@ -42,7 +64,7 @@ func (e EvaluationContext) Eval(input string) (interface{}, error) {
return e.evalExpr(expr)
}

func (e EvaluationContext) evalExpr(expr expression) (interface{}, error) {
func (e evaluationContext) evalExpr(expr expression) (interface{}, error) {
return expr.eval(e)
}

Expand Down
5 changes: 2 additions & 3 deletions pkg/input/arm/eval_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import (
"github.com/stretchr/testify/require"
)

func TestEval(t *testing.T) {
func TestEvalARMTemplateStrings(t *testing.T) {
for _, tc := range []struct {
name string
input string
Expand All @@ -33,8 +33,7 @@ func TestEval(t *testing.T) {
},
} {
t.Run(tc.name, func(t *testing.T) {
evalCtx := NewEvaluationContext()
val, err := evalCtx.Eval(tc.input)
val, err := EvaluateARMTemplateString(tc.input)
require.NoError(t, err)
require.Equal(t, tc.expected, val)
})
Expand Down
18 changes: 4 additions & 14 deletions pkg/input/arm/parser.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,18 +19,8 @@ import (
"fmt"
)

// TODO treat enclosing brackets as "special", indicating that there is an
// expression, in the tokenizer
// TODO refactor to something that doesn't repeat that pop/peek a lot
func parse(tokens []token) (expression, error) {
if len(tokens) < 2 {
return nil, nil
}
if tokens[0] != (openBracket{}) && tokens[len(tokens)-1] != (closeBracket{}) {
return nil, nil
}
tokens = tokens[1 : len(tokens)-1]

return buildTree(&tokens)
}

Expand Down Expand Up @@ -90,12 +80,12 @@ func buildTree(remaining *[]token) (expression, error) {
}

type expression interface {
eval(evalCtx EvaluationContext) (interface{}, error)
eval(evalCtx evaluationContext) (interface{}, error)
}

type stringLiteralExpr string

func (s stringLiteralExpr) eval(evalCtx EvaluationContext) (interface{}, error) {
func (s stringLiteralExpr) eval(evalCtx evaluationContext) (interface{}, error) {
return string(s), nil
}

Expand All @@ -104,7 +94,7 @@ type functionExpr struct {
args []expression
}

func (f functionExpr) eval(evalCtx EvaluationContext) (interface{}, error) {
func (f functionExpr) eval(evalCtx evaluationContext) (interface{}, error) {
impl, ok := evalCtx.funcs[f.name]
if !ok {
return nil, fmt.Errorf("no implementation found for function %s", f.name)
Expand All @@ -126,6 +116,6 @@ type propertyExpr struct {
property string
}

func (p propertyExpr) eval(evalCtx EvaluationContext) (interface{}, error) {
func (p propertyExpr) eval(evalCtx evaluationContext) (interface{}, error) {
return nil, nil
}
18 changes: 5 additions & 13 deletions pkg/input/arm/parser_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -27,43 +27,35 @@ func TestParse(t *testing.T) {
input string
expected expression
}{
{
name: "returns nil when input is empty",
input: "",
},
{
name: "returns nil when input is not an expression",
input: "some words",
},
{
name: "returns simple expression for a single scalar",
input: "['hi']",
input: "'hi'",
expected: stringLiteralExpr("hi"),
},
{
name: "returns expression for single function call (no args)",
input: "[resourceGroup()]",
input: "resourceGroup()",
expected: functionExpr{name: "resourceGroup"},
},
{
name: "returns expression for single function call (1 arg)",
input: "[bork('foo')]",
input: "bork('foo')",
expected: functionExpr{
name: "bork",
args: []expression{stringLiteralExpr("foo")},
},
},
{
name: "returns expression for single function call (2 args)",
input: "[concat('foo', 'bar')]",
input: "concat('foo', 'bar')",
expected: functionExpr{
name: "concat",
args: []expression{stringLiteralExpr("foo"), stringLiteralExpr("bar")},
},
},
{
name: "returns expression for nested function calls",
input: "[concat(resourceGroup(), '-thing')]",
input: "concat(resourceGroup(), '-thing')",
expected: functionExpr{
name: "concat",
args: []expression{
Expand Down
10 changes: 1 addition & 9 deletions pkg/input/arm/tokenizer.go
Original file line number Diff line number Diff line change
Expand Up @@ -42,12 +42,6 @@ func (t *tokenizer) next() token {
}

switch []rune(t.remaining)[0] {
case '[':
t.remaining = t.remaining[1:]
return openBracket{}
case ']':
t.remaining = t.remaining[1:]
return closeBracket{}
case '(':
t.remaining = t.remaining[1:]
return openParen{}
Expand All @@ -73,7 +67,7 @@ func (t *tokenizer) next() token {
// if we reach here, the token is an identifier
endOfIdentifierIdx := strCharIndex(t.remaining, func(char rune) bool {
switch char {
case '[', ']', '(', ')', ',', '.':
case '(', ')', ',', '.':
return true
default:
return unicode.IsSpace(char)
Expand All @@ -92,8 +86,6 @@ func (t *tokenizer) chopLeadingWhitespace() {
type token interface {
}

type openBracket struct{}
type closeBracket struct{}
type openParen struct{}
type closeParen struct{}
type comma struct{}
Expand Down
56 changes: 27 additions & 29 deletions pkg/input/arm/tokenizer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,67 +20,65 @@ import (
"github.com/stretchr/testify/require"
)

func TestTokenizesStrings(t *testing.T) {
func TestTokenizesARMExpressions(t *testing.T) {
for _, tc := range []struct {
name string
input string
expected []token
}{
{
name: "tokenizes simple string",
input: "I am a string",
name: "tokenizes expression containing commas, brackets, and parentheses",
input: "someFn(arg1, ARG_2)",
expected: []token{
identifier{"I"},
identifier{"am"},
identifier{"a"},
identifier{"string"},
identifier{name: "someFn"},
openParen{},
identifier{name: "arg1"},
comma{},
identifier{name: "ARG_2"},
closeParen{},
},
},
{
name: "tokenizes string with variable whitespace",
input: "I am\t\ta string",
name: "tokenizes expression containing variable whitespace",
input: "someFn (\t\targ1, ARG_2)",
expected: []token{
identifier{"I"},
identifier{"am"},
identifier{"a"},
identifier{"string"},
identifier{name: "someFn"},
openParen{},
identifier{name: "arg1"},
comma{},
identifier{name: "ARG_2"},
closeParen{},
},
},
{
name: "tokenizes string with commas, brackets, and parentheses",
input: "[someFn(arg1, ARG_2)]",
name: "tokenizes expression containing string literals",
input: "someFn('a-string', 'another string')",
expected: []token{
openBracket{},
identifier{name: "someFn"},
openParen{},
identifier{name: "arg1"},
stringLiteral{"a-string"},
comma{},
identifier{name: "ARG_2"},
stringLiteral{"another string"},
closeParen{},
closeBracket{},
},
},
{
name: "tokenizes strings containing literals",
input: "I 'am' a 'string'",
name: "tokenizes expression containing string literals with special characters",
input: "'[some]' '(special, characters)'",
expected: []token{
identifier{"I"},
stringLiteral{"am"},
identifier{"a"},
stringLiteral{"string"},
stringLiteral{"[some]"},
stringLiteral{"(special, characters)"},
},
},
{
name: "tokenizes strings containing property dereferences",
input: "[resourceGroup().location]",
name: "tokenizes expression containing property dereferences",
input: "resourceGroup().location",
expected: []token{
openBracket{},
identifier{"resourceGroup"},
openParen{},
closeParen{},
dot{},
identifier{"location"},
closeBracket{},
},
},
} {
Expand Down

0 comments on commit 340410d

Please sign in to comment.