lang/funcs: Defaults handling of marked arguments

Defaults will now preserve marks from non-null inputs and apply marks from any default values used. I've added tests for various structural types with marks, as well as some basic unknown cases.
This commit is contained in:
Kristin Laemmert 2021-04-21 11:09:31 -04:00
parent b34588ffca
commit 5ae4c2f92b
2 changed files with 161 additions and 25 deletions

View File

@ -16,13 +16,15 @@ import (
var DefaultsFunc = function.New(&function.Spec{ var DefaultsFunc = function.New(&function.Spec{
Params: []function.Parameter{ Params: []function.Parameter{
{ {
Name: "input", Name: "input",
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
AllowNull: true, AllowNull: true,
AllowMarked: true,
}, },
{ {
Name: "defaults", Name: "defaults",
Type: cty.DynamicPseudoType, Type: cty.DynamicPseudoType,
AllowMarked: true,
}, },
}, },
Type: func(args []cty.Value) (cty.Type, error) { Type: func(args []cty.Value) (cty.Type, error) {
@ -69,8 +71,14 @@ var DefaultsFunc = function.New(&function.Spec{
func defaultsApply(input, fallback cty.Value) cty.Value { func defaultsApply(input, fallback cty.Value) cty.Value {
wantTy := input.Type() wantTy := input.Type()
if !(input.IsKnown() && fallback.IsKnown()) {
return cty.UnknownVal(wantTy) umInput, inputMarks := input.Unmark()
umFb, fallbackMarks := fallback.Unmark()
// If neither are known, we very conservatively return an unknown value
// with the union of marks on both input and default.
if !(umInput.IsKnown() && umFb.IsKnown()) {
return cty.UnknownVal(wantTy).WithMarks(inputMarks).WithMarks(fallbackMarks)
} }
// For the rest of this function we're assuming that the given defaults // For the rest of this function we're assuming that the given defaults
@ -83,15 +91,15 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
case wantTy.IsPrimitiveType(): case wantTy.IsPrimitiveType():
// For leaf primitive values the rule is relatively simple: use the // For leaf primitive values the rule is relatively simple: use the
// input if it's non-null, or fallback if input is null. // input if it's non-null, or fallback if input is null.
if !input.IsNull() { if !umInput.IsNull() {
return input return input
} }
v, err := convert.Convert(fallback, wantTy) v, err := convert.Convert(umFb, wantTy)
if err != nil { if err != nil {
// Should not happen because we checked in defaultsAssertSuitableFallback // Should not happen because we checked in defaultsAssertSuitableFallback
panic(err.Error()) panic(err.Error())
} }
return v return v.WithMarks(fallbackMarks)
case wantTy.IsObjectType(): case wantTy.IsObjectType():
// For structural types, a null input value must be passed through. We // For structural types, a null input value must be passed through. We
@ -101,18 +109,18 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
// We also pass through the input if the fallback value is null. This // We also pass through the input if the fallback value is null. This
// can happen if the given defaults do not include a value for this // can happen if the given defaults do not include a value for this
// attribute. // attribute.
if input.IsNull() || fallback.IsNull() { if umInput.IsNull() || umFb.IsNull() {
return input return input
} }
atys := wantTy.AttributeTypes() atys := wantTy.AttributeTypes()
ret := map[string]cty.Value{} ret := map[string]cty.Value{}
for attr, aty := range atys { for attr, aty := range atys {
inputSub := input.GetAttr(attr) inputSub := umInput.GetAttr(attr)
fallbackSub := cty.NullVal(aty) fallbackSub := cty.NullVal(aty)
if fallback.Type().HasAttribute(attr) { if umFb.Type().HasAttribute(attr) {
fallbackSub = fallback.GetAttr(attr) fallbackSub = umFb.GetAttr(attr)
} }
ret[attr] = defaultsApply(inputSub, fallbackSub) ret[attr] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
} }
return cty.ObjectVal(ret) return cty.ObjectVal(ret)
@ -124,16 +132,16 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
// We also pass through the input if the fallback value is null. This // We also pass through the input if the fallback value is null. This
// can happen if the given defaults do not include a value for this // can happen if the given defaults do not include a value for this
// attribute. // attribute.
if input.IsNull() || fallback.IsNull() { if umInput.IsNull() || umFb.IsNull() {
return input return input
} }
l := wantTy.Length() l := wantTy.Length()
ret := make([]cty.Value, l) ret := make([]cty.Value, l)
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
inputSub := input.Index(cty.NumberIntVal(int64(i))) inputSub := umInput.Index(cty.NumberIntVal(int64(i)))
fallbackSub := fallback.Index(cty.NumberIntVal(int64(i))) fallbackSub := umFb.Index(cty.NumberIntVal(int64(i)))
ret[i] = defaultsApply(inputSub, fallbackSub) ret[i] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
} }
return cty.TupleVal(ret) return cty.TupleVal(ret)
@ -148,10 +156,10 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
case wantTy.IsMapType(): case wantTy.IsMapType():
newVals := map[string]cty.Value{} newVals := map[string]cty.Value{}
if !input.IsNull() { if !umInput.IsNull() {
for it := input.ElementIterator(); it.Next(); { for it := umInput.ElementIterator(); it.Next(); {
k, v := it.Element() k, v := it.Element()
newVals[k.AsString()] = defaultsApply(v, fallback) newVals[k.AsString()] = defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
} }
} }
@ -162,10 +170,10 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
case wantTy.IsListType(), wantTy.IsSetType(): case wantTy.IsListType(), wantTy.IsSetType():
var newVals []cty.Value var newVals []cty.Value
if !input.IsNull() { if !umInput.IsNull() {
for it := input.ElementIterator(); it.Next(); { for it := umInput.ElementIterator(); it.Next(); {
_, v := it.Element() _, v := it.Element()
newV := defaultsApply(v, fallback) newV := defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
newVals = append(newVals, newV) newVals = append(newVals, newV)
} }
} }

View File

@ -13,6 +13,30 @@ func TestDefaults(t *testing.T) {
Want cty.Value Want cty.Value
WantErr string WantErr string
}{ }{
{ // When *either* input or default are unknown, an unknown is returned.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
},
{
// When *either* input or default are unknown, an unknown is
// returned with marks from both input and defaults.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("marked"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.UnknownVal(cty.String).Mark("marked"),
}),
},
{ {
Input: cty.ObjectVal(map[string]cty.Value{ Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String), "a": cty.NullVal(cty.String),
@ -494,6 +518,110 @@ func TestDefaults(t *testing.T) {
}), }),
WantErr: ".a: invalid default value for bool: bool required", WantErr: ".a: invalid default value for bool: bool required",
}, },
// marks: we should preserve marks from both input value and defaults as leafily as possible
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("world"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("world"),
}),
},
{ // "unused" marks don't carry over
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.NullVal(cty.String).Mark("a"),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello"),
}),
},
{ // Marks on tuples remain attached to individual elements
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey").Mark("input"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.StringVal("hello 0").Mark("fallback"),
cty.StringVal("hello 1"),
cty.StringVal("hello 2"),
}),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.TupleVal([]cty.Value{
cty.StringVal("hello 0").Mark("fallback"),
cty.StringVal("hey").Mark("input"),
cty.StringVal("hello 2"),
}),
}),
},
{ // Marks from list elements
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
cty.StringVal("hey").Mark("input"),
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello 0").Mark("fallback"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello 0").Mark("fallback"),
cty.StringVal("hey").Mark("input"),
cty.StringVal("hello 0").Mark("fallback"),
}),
}),
},
{
// Sets don't allow individually-marked elements, so the marks
// end up aggregating on the set itself anyway in this case.
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.NullVal(cty.String),
cty.NullVal(cty.String),
cty.StringVal("hey").Mark("input"),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello 0").Mark("fallback"),
}),
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.SetVal([]cty.Value{
cty.StringVal("hello 0"),
cty.StringVal("hey"),
cty.StringVal("hello 0"),
}).WithMarks(cty.NewValueMarks("fallback", "input")),
}),
},
{
Input: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.NullVal(cty.String),
}),
}),
Defaults: cty.ObjectVal(map[string]cty.Value{
"a": cty.StringVal("hello").Mark("beep"),
}).Mark("boop"),
// This is the least-intuitive case. The mark "boop" is attached to
// the default object, not it's elements, but both marks end up
// aggregated on the list element.
Want: cty.ObjectVal(map[string]cty.Value{
"a": cty.ListVal([]cty.Value{
cty.StringVal("hello").WithMarks(cty.NewValueMarks("beep", "boop")),
}),
}),
},
} }
for _, test := range tests { for _, test := range tests {