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()
|
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
|
||||||
|
@ -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 {
|
||||||
|
@ -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)
|
||||||
}
|
}
|
||||||
|
@ -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"}
|
||||||
|
Loading…
Reference in New Issue
Block a user