zclsyntax: template "for" construct
This commit is contained in:
parent
833ff9ecd7
commit
61ebd9b65b
@ -85,6 +85,86 @@ func (e *TemplateExpr) StartRange() zcl.Range {
|
||||
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
|
||||
// consists _only_ of a single interpolation sequence. In that case, the
|
||||
// template's result is the single interpolation's result, verbatim with
|
||||
|
@ -171,6 +171,52 @@ trim`,
|
||||
cty.UnknownVal(cty.String),
|
||||
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 {
|
||||
|
@ -55,6 +55,10 @@ func (e *TemplateExpr) Variables() []zcl.Traversal {
|
||||
return Variables(e)
|
||||
}
|
||||
|
||||
func (e *TemplateJoinExpr) Variables() []zcl.Traversal {
|
||||
return Variables(e)
|
||||
}
|
||||
|
||||
func (e *TemplateWrapExpr) Variables() []zcl.Traversal {
|
||||
return Variables(e)
|
||||
}
|
||||
|
@ -240,6 +240,103 @@ Token:
|
||||
}, 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 {
|
||||
return p.Tokens[p.pos]
|
||||
}
|
||||
@ -354,8 +451,8 @@ Token:
|
||||
if !p.recovery {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Invalid template control keyword",
|
||||
Detail: "A template control keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.",
|
||||
Summary: "Invalid template directive",
|
||||
Detail: "A template directive keyword (\"if\", \"for\", etc) is expected at the beginning of a !{ sequence.",
|
||||
Subject: &kw.Range,
|
||||
Context: zcl.RangeBetween(next.Range, kw.Range).Ptr(),
|
||||
})
|
||||
@ -388,6 +485,77 @@ Token:
|
||||
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:
|
||||
if !p.recovery {
|
||||
suggestions := []string{"if", "for", "else", "endif", "endfor"}
|
||||
|
Loading…
Reference in New Issue
Block a user