zclsyntax: template "for" construct

This commit is contained in:
Martin Atkins 2017-06-18 08:14:55 -07:00
parent 833ff9ecd7
commit 61ebd9b65b
4 changed files with 300 additions and 2 deletions

View File

@ -85,6 +85,86 @@ func (e *TemplateExpr) StartRange() zcl.Range {
return e.Parts[0].StartRange() return e.Parts[0].StartRange()
} }
// TemplateJoinExpr is used to convert tuples of strings produced by template
// constructs (i.e. for loops) into flat strings, by converting the values
// tos strings and joining them. This AST node is not used directly; it's
// produced as part of the AST of a "for" loop in a template.
type TemplateJoinExpr struct {
Tuple Expression
}
func (e *TemplateJoinExpr) walkChildNodes(w internalWalkFunc) {
e.Tuple = w(e.Tuple).(Expression)
}
func (e *TemplateJoinExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
tuple, diags := e.Tuple.Value(ctx)
if tuple.IsNull() {
// This indicates a bug in the code that constructed the AST.
panic("TemplateJoinExpr got null tuple")
}
if tuple.Type() == cty.DynamicPseudoType {
return cty.UnknownVal(cty.String), diags
}
if !tuple.Type().IsTupleType() {
// This indicates a bug in the code that constructed the AST.
panic("TemplateJoinExpr got non-tuple tuple")
}
if !tuple.IsKnown() {
return cty.UnknownVal(cty.String), diags
}
buf := &bytes.Buffer{}
it := tuple.ElementIterator()
for it.Next() {
_, val := it.Element()
if val.IsNull() {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"An iteration result is null. Cannot include a null value in a string template.",
),
Subject: e.Range().Ptr(),
})
continue
}
if val.Type() == cty.DynamicPseudoType {
return cty.UnknownVal(cty.String), diags
}
strVal, err := convert.Convert(val, cty.String)
if err != nil {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid template interpolation value",
Detail: fmt.Sprintf(
"Cannot include one of the interpolation results into the string template: %s.",
err.Error(),
),
Subject: e.Range().Ptr(),
})
continue
}
if !val.IsKnown() {
return cty.UnknownVal(cty.String), diags
}
buf.WriteString(strVal.AsString())
}
return cty.StringVal(buf.String()), diags
}
func (e *TemplateJoinExpr) Range() zcl.Range {
return e.Tuple.Range()
}
func (e *TemplateJoinExpr) StartRange() zcl.Range {
return e.Tuple.StartRange()
}
// TemplateWrapExpr is used instead of a TemplateExpr when a template // TemplateWrapExpr is used instead of a TemplateExpr when a template
// consists _only_ of a single interpolation sequence. In that case, the // consists _only_ of a single interpolation sequence. In that case, the
// template's result is the single interpolation's result, verbatim with // template's result is the single interpolation's result, verbatim with

View File

@ -171,6 +171,52 @@ trim`,
cty.UnknownVal(cty.String), cty.UnknownVal(cty.String),
2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected 2, // "of" is not a valid control keyword, and "endif" is therefore also unexpected
}, },
{
`%{ for v in ["a", "b", "c"] }${v}%{ endfor }`,
nil,
cty.StringVal("abc"),
0,
},
{
`%{ for v in ["a", "b", "c"] } ${v} %{ endfor }`,
nil,
cty.StringVal(" a b c "),
0,
},
{
`%{ for v in ["a", "b", "c"] ~} ${v} %{~ endfor }`,
nil,
cty.StringVal("abc"),
0,
},
{
`%{ for v in [] }${v}%{ endfor }`,
nil,
cty.StringVal(""),
0,
},
{
`%{ for i, v in ["a", "b", "c"] }${i}${v}%{ endfor }`,
nil,
cty.StringVal("0a1b2c"),
0,
},
{
`%{ for k, v in {"A" = "a", "B" = "b", "C" = "c"} }${k}${v}%{ endfor }`,
nil,
cty.StringVal("AaBbCc"),
0,
},
{
`%{ for v in ["a", "b", "c"] }${v}${nl}%{ endfor }`,
&zcl.EvalContext{
Variables: map[string]cty.Value{
"nl": cty.StringVal("\n"),
},
},
cty.StringVal("a\nb\nc\n"),
0,
},
} }
for _, test := range tests { for _, test := range tests {

View File

@ -55,6 +55,10 @@ func (e *TemplateExpr) Variables() []zcl.Traversal {
return Variables(e) return Variables(e)
} }
func (e *TemplateJoinExpr) Variables() []zcl.Traversal {
return Variables(e)
}
func (e *TemplateWrapExpr) Variables() []zcl.Traversal { func (e *TemplateWrapExpr) Variables() []zcl.Traversal {
return Variables(e) return Variables(e)
} }

View File

@ -240,6 +240,103 @@ Token:
}, diags }, diags
} }
func (p *templateParser) parseFor() (Expression, zcl.Diagnostics) {
open := p.Read()
openFor, isFor := open.(*templateForToken)
if !isFor {
// should never happen if caller is behaving
panic("parseFor called with peeker not pointing at for token")
}
var contentExprs []Expression
var diags zcl.Diagnostics
var endforRange zcl.Range
Token:
for {
next := p.Peek()
if end, isEnd := next.(*templateEndToken); isEnd {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unexpected end of template",
Detail: fmt.Sprintf(
"The for directive at %s is missing its corresponding endfor directive.",
openFor.SrcRange,
),
Subject: &end.SrcRange,
})
return errPlaceholderExpr(end.SrcRange), diags
}
if end, isCtrlEnd := next.(*templateEndCtrlToken); isCtrlEnd {
p.Read() // eat end directive
switch end.Type {
case templateElse:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Unexpected else directive",
Detail: "An else clause is not expected for a for directive.",
Subject: &end.SrcRange,
})
case templateEndFor:
endforRange = end.SrcRange
break Token
default:
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: fmt.Sprintf("Unexpected %s directive", end.Name()),
Detail: fmt.Sprintf(
"Expecting an endfor directive corresponding to the for directive at %s.",
openFor.SrcRange,
),
Subject: &end.SrcRange,
})
}
return errPlaceholderExpr(end.SrcRange), diags
}
expr, exprDiags := p.parseExpr()
diags = append(diags, exprDiags...)
contentExprs = append(contentExprs, expr)
}
if len(contentExprs) == 0 {
contentExprs = append(contentExprs, &LiteralValueExpr{
Val: cty.StringVal(""),
SrcRange: zcl.Range{
Filename: openFor.SrcRange.Filename,
Start: openFor.SrcRange.End,
End: openFor.SrcRange.End,
},
})
}
contentExpr := &TemplateExpr{
Parts: contentExprs,
SrcRange: zcl.RangeBetween(contentExprs[0].Range(), contentExprs[len(contentExprs)-1].Range()),
}
forExpr := &ForExpr{
KeyVar: openFor.KeyVar,
ValVar: openFor.ValVar,
CollExpr: openFor.CollExpr,
ValExpr: contentExpr,
SrcRange: zcl.RangeBetween(openFor.SrcRange, endforRange),
OpenRange: openFor.SrcRange,
CloseRange: endforRange,
}
return &TemplateJoinExpr{
Tuple: forExpr,
}, diags
}
func (p *templateParser) Peek() templateToken { func (p *templateParser) Peek() templateToken {
return p.Tokens[p.pos] return p.Tokens[p.pos]
} }
@ -354,8 +451,8 @@ Token:
if !p.recovery { if !p.recovery {
diags = append(diags, &zcl.Diagnostic{ diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError, Severity: zcl.DiagError,
Summary: "Invalid template control keyword", Summary: "Invalid template directive",
Detail: "A template control keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.", Detail: "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.",
Subject: &kw.Range, Subject: &kw.Range,
Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(), Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(),
}) })
@ -388,6 +485,77 @@ Token:
SrcRange: zcl.RangeBetween(next.Range, p.NextRange()), SrcRange: zcl.RangeBetween(next.Range, p.NextRange()),
}) })
case forKeyword.TokenMatches(kw):
var keyName, valName string
if p.Peek().Type != TokenIdent {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid 'for' directive",
Detail: "For directive requires variable name after 'for'.",
Subject: p.Peek().Range.Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
p.PopIncludeNewlines()
continue Token
}
valName = string(p.Read().Bytes)
if p.Peek().Type == TokenComma {
// What we just read was actually the key, then.
keyName = valName
p.Read() // eat comma
if p.Peek().Type != TokenIdent {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid 'for' directive",
Detail: "For directive requires value variable name after comma.",
Subject: p.Peek().Range.Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
p.PopIncludeNewlines()
continue Token
}
valName = string(p.Read().Bytes)
}
if !inKeyword.TokenMatches(p.Peek()) {
if !p.recovery {
diags = append(diags, &zcl.Diagnostic{
Severity: zcl.DiagError,
Summary: "Invalid 'for' directive",
Detail: "For directive requires 'in' keyword after names.",
Subject: p.Peek().Range.Ptr(),
})
}
p.recover(TokenTemplateSeqEnd)
p.PopIncludeNewlines()
continue Token
}
p.Read() // eat 'in' keyword
collExpr, collDiags := p.ParseExpression()
diags = append(diags, collDiags...)
parts = append(parts, &templateForToken{
KeyVar: keyName,
ValVar: valName,
CollExpr: collExpr,
SrcRange: zcl.RangeBetween(next.Range, p.NextRange()),
})
case endforKeyword.TokenMatches(kw):
parts = append(parts, &templateEndCtrlToken{
Type: templateEndFor,
SrcRange: zcl.RangeBetween(next.Range, p.NextRange()),
})
default: default:
if !p.recovery { if !p.recovery {
suggestions := []string{"if", "for", "else", "endif", "endfor"} suggestions := []string{"if", "for", "else", "endif", "endfor"}