zclsyntax: parsing and evaluation for object constructors
This commit is contained in:
parent
cac847b163
commit
6f2bd0009c
@ -397,3 +397,98 @@ func (e *TupleConsExpr) Range() zcl.Range {
|
||||
func (e *TupleConsExpr) StartRange() zcl.Range {
|
||||
return e.OpenRange
|
||||
}
|
||||
|
||||
type ObjectConsExpr struct {
|
||||
Items []ObjectConsItem
|
||||
|
||||
SrcRange zcl.Range
|
||||
OpenRange zcl.Range
|
||||
}
|
||||
|
||||
type ObjectConsItem struct {
|
||||
KeyExpr Expression
|
||||
ValueExpr Expression
|
||||
}
|
||||
|
||||
func (e *ObjectConsExpr) walkChildNodes(w internalWalkFunc) {
|
||||
for i, item := range e.Items {
|
||||
e.Items[i].KeyExpr = w(item.KeyExpr).(Expression)
|
||||
e.Items[i].ValueExpr = w(item.ValueExpr).(Expression)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *ObjectConsExpr) Value(ctx *zcl.EvalContext) (cty.Value, zcl.Diagnostics) {
|
||||
var vals map[string]cty.Value
|
||||
var diags zcl.Diagnostics
|
||||
|
||||
// This will get set to true if we fail to produce any of our keys,
|
||||
// either because they are actually unknown or if the evaluation produces
|
||||
// errors. In all of these case we must return DynamicPseudoType because
|
||||
// we're unable to know the full set of keys our object has, and thus
|
||||
// we can't produce a complete value of the intended type.
|
||||
//
|
||||
// We still evaluate all of the item keys and values to make sure that we
|
||||
// get as complete as possible a set of diagnostics.
|
||||
known := true
|
||||
|
||||
vals = make(map[string]cty.Value, len(e.Items))
|
||||
for _, item := range e.Items {
|
||||
key, keyDiags := item.KeyExpr.Value(ctx)
|
||||
diags = append(diags, keyDiags...)
|
||||
|
||||
val, valDiags := item.ValueExpr.Value(ctx)
|
||||
diags = append(diags, valDiags...)
|
||||
|
||||
if keyDiags.HasErrors() {
|
||||
known = false
|
||||
continue
|
||||
}
|
||||
|
||||
if key.IsNull() {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Null value as key",
|
||||
Detail: "Can't use a null value as a key.",
|
||||
Subject: item.ValueExpr.Range().Ptr(),
|
||||
})
|
||||
known = false
|
||||
continue
|
||||
}
|
||||
|
||||
var err error
|
||||
key, err = convert.Convert(key, cty.String)
|
||||
if err != nil {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Incorrect key type",
|
||||
Detail: fmt.Sprintf("Can't use this value as a key: %s.", err.Error()),
|
||||
Subject: item.ValueExpr.Range().Ptr(),
|
||||
})
|
||||
known = false
|
||||
continue
|
||||
}
|
||||
|
||||
if !key.IsKnown() {
|
||||
known = false
|
||||
continue
|
||||
}
|
||||
|
||||
keyStr := key.AsString()
|
||||
|
||||
vals[keyStr] = val
|
||||
}
|
||||
|
||||
if !known {
|
||||
return cty.DynamicVal, diags
|
||||
}
|
||||
|
||||
return cty.ObjectVal(vals), diags
|
||||
}
|
||||
|
||||
func (e *ObjectConsExpr) Range() zcl.Range {
|
||||
return e.SrcRange
|
||||
}
|
||||
|
||||
func (e *ObjectConsExpr) StartRange() zcl.Range {
|
||||
return e.OpenRange
|
||||
}
|
||||
|
@ -179,6 +179,99 @@ upper(
|
||||
cty.TupleVal([]cty.Value{cty.NumberIntVal(1), cty.True}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{}`,
|
||||
nil,
|
||||
cty.EmptyObjectVal,
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{"hello": "world"}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{"hello" = "world"}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{hello = "world"}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{hello: "world"}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{"hello" = "world", "goodbye" = "cruel world"}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
"goodbye": cty.StringVal("cruel world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"hello" = "world"
|
||||
}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"hello" = "world"
|
||||
"goodbye" = "cruel world"
|
||||
}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
"goodbye": cty.StringVal("cruel world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"hello" = "world",
|
||||
"goodbye" = "cruel world"
|
||||
}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
"goodbye": cty.StringVal("cruel world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
{
|
||||
`{
|
||||
"hello" = "world",
|
||||
"goodbye" = "cruel world",
|
||||
}`,
|
||||
nil,
|
||||
cty.ObjectVal(map[string]cty.Value{
|
||||
"hello": cty.StringVal("world"),
|
||||
"goodbye": cty.StringVal("cruel world"),
|
||||
}),
|
||||
0,
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
|
@ -23,6 +23,10 @@ func (e *LiteralValueExpr) Variables() []zcl.Traversal {
|
||||
return Variables(e)
|
||||
}
|
||||
|
||||
func (e *ObjectConsExpr) Variables() []zcl.Traversal {
|
||||
return Variables(e)
|
||||
}
|
||||
|
||||
func (e *ScopeTraversalExpr) Variables() []zcl.Traversal {
|
||||
return Variables(e)
|
||||
}
|
||||
|
@ -590,6 +590,9 @@ func (p *parser) parseExpressionTerm() (Expression, zcl.Diagnostics) {
|
||||
case TokenOBrack:
|
||||
return p.parseTupleCons()
|
||||
|
||||
case TokenOBrace:
|
||||
return p.parseObjectCons()
|
||||
|
||||
default:
|
||||
var diags zcl.Diagnostics
|
||||
if !p.recovery {
|
||||
@ -752,6 +755,135 @@ func (p *parser) parseTupleCons() (Expression, zcl.Diagnostics) {
|
||||
}, diags
|
||||
}
|
||||
|
||||
func (p *parser) parseObjectCons() (Expression, zcl.Diagnostics) {
|
||||
open := p.Read()
|
||||
if open.Type != TokenOBrace {
|
||||
// Should never happen if callers are behaving
|
||||
panic("parseObjectCons called without peeker pointing to open brace")
|
||||
}
|
||||
|
||||
var close Token
|
||||
|
||||
var diags zcl.Diagnostics
|
||||
var items []ObjectConsItem
|
||||
|
||||
p.PushIncludeNewlines(true)
|
||||
defer p.PopIncludeNewlines()
|
||||
|
||||
for {
|
||||
next := p.Peek()
|
||||
if next.Type == TokenNewline {
|
||||
p.Read() // eat newline
|
||||
continue
|
||||
}
|
||||
|
||||
if next.Type == TokenCBrace {
|
||||
close = p.Read() // eat closer
|
||||
break
|
||||
}
|
||||
|
||||
// As a special case, we allow the key to be a literal identifier.
|
||||
// This means that a variable reference or function call can't appear
|
||||
// directly as key expression, and must instead be wrapped in some
|
||||
// disambiguation punctuation, like (var.a) = "b" or "${var.a}" = "b".
|
||||
var key Expression
|
||||
var keyDiags zcl.Diagnostics
|
||||
if p.Peek().Type == TokenIdent {
|
||||
nameTok := p.Read()
|
||||
key = &LiteralValueExpr{
|
||||
Val: cty.StringVal(string(nameTok.Bytes)),
|
||||
|
||||
SrcRange: nameTok.Range,
|
||||
}
|
||||
} else {
|
||||
key, keyDiags = p.ParseExpression()
|
||||
}
|
||||
|
||||
diags = append(diags, keyDiags...)
|
||||
|
||||
if p.recovery && keyDiags.HasErrors() {
|
||||
// If expression parsing failed then we are probably in a strange
|
||||
// place in the token stream, so we'll bail out and try to reset
|
||||
// to after our closing brace to allow parsing to continue.
|
||||
close = p.recover(TokenCBrace)
|
||||
break
|
||||
}
|
||||
|
||||
next = p.Peek()
|
||||
if next.Type != TokenEqual && next.Type != TokenColon {
|
||||
if !p.recovery {
|
||||
if next.Type == TokenNewline || next.Type == TokenComma {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Missing item value",
|
||||
Detail: "Expected an item value, introduced by an equals sign (\"=\").",
|
||||
Subject: &next.Range,
|
||||
Context: zcl.RangeBetween(open.Range, next.Range).Ptr(),
|
||||
})
|
||||
} else {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Missing key/value separator",
|
||||
Detail: "Expected an equals sign (\"=\") to mark the beginning of the item value.",
|
||||
Subject: &next.Range,
|
||||
Context: zcl.RangeBetween(open.Range, next.Range).Ptr(),
|
||||
})
|
||||
}
|
||||
}
|
||||
close = p.recover(TokenCBrace)
|
||||
break
|
||||
}
|
||||
|
||||
p.Read() // eat equals sign or colon
|
||||
|
||||
value, valueDiags := p.ParseExpression()
|
||||
diags = append(diags, valueDiags...)
|
||||
|
||||
if p.recovery && valueDiags.HasErrors() {
|
||||
// If expression parsing failed then we are probably in a strange
|
||||
// place in the token stream, so we'll bail out and try to reset
|
||||
// to after our closing brace to allow parsing to continue.
|
||||
close = p.recover(TokenCBrace)
|
||||
break
|
||||
}
|
||||
|
||||
items = append(items, ObjectConsItem{
|
||||
KeyExpr: key,
|
||||
ValueExpr: value,
|
||||
})
|
||||
|
||||
next = p.Peek()
|
||||
if next.Type == TokenCBrace {
|
||||
close = p.Read() // eat closer
|
||||
break
|
||||
}
|
||||
|
||||
if next.Type != TokenComma && next.Type != TokenNewline {
|
||||
if !p.recovery {
|
||||
diags = append(diags, &zcl.Diagnostic{
|
||||
Severity: zcl.DiagError,
|
||||
Summary: "Missing item separator",
|
||||
Detail: "Expected a newline or comma to mark the beginning of the next item.",
|
||||
Subject: &next.Range,
|
||||
Context: zcl.RangeBetween(open.Range, next.Range).Ptr(),
|
||||
})
|
||||
}
|
||||
close = p.recover(TokenCBrace)
|
||||
break
|
||||
}
|
||||
|
||||
p.Read() // eat comma or newline
|
||||
|
||||
}
|
||||
|
||||
return &ObjectConsExpr{
|
||||
Items: items,
|
||||
|
||||
SrcRange: zcl.RangeBetween(open.Range, close.Range),
|
||||
OpenRange: open.Range,
|
||||
}, diags
|
||||
}
|
||||
|
||||
func (p *parser) ParseTemplate(end TokenType) (Expression, zcl.Diagnostics) {
|
||||
var parts []Expression
|
||||
var diags zcl.Diagnostics
|
||||
|
Loading…
Reference in New Issue
Block a user