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

@ -19,10 +19,12 @@ var DefaultsFunc = function.New(&function.Spec{
Name: "input",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowMarked: true,
},
{
Name: "defaults",
Type: cty.DynamicPseudoType,
AllowMarked: true,
},
},
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 {
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
@ -83,15 +91,15 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
case wantTy.IsPrimitiveType():
// For leaf primitive values the rule is relatively simple: use the
// input if it's non-null, or fallback if input is null.
if !input.IsNull() {
if !umInput.IsNull() {
return input
}
v, err := convert.Convert(fallback, wantTy)
v, err := convert.Convert(umFb, wantTy)
if err != nil {
// Should not happen because we checked in defaultsAssertSuitableFallback
panic(err.Error())
}
return v
return v.WithMarks(fallbackMarks)
case wantTy.IsObjectType():
// 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
// can happen if the given defaults do not include a value for this
// attribute.
if input.IsNull() || fallback.IsNull() {
if umInput.IsNull() || umFb.IsNull() {
return input
}
atys := wantTy.AttributeTypes()
ret := map[string]cty.Value{}
for attr, aty := range atys {
inputSub := input.GetAttr(attr)
inputSub := umInput.GetAttr(attr)
fallbackSub := cty.NullVal(aty)
if fallback.Type().HasAttribute(attr) {
fallbackSub = fallback.GetAttr(attr)
if umFb.Type().HasAttribute(attr) {
fallbackSub = umFb.GetAttr(attr)
}
ret[attr] = defaultsApply(inputSub, fallbackSub)
ret[attr] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
}
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
// can happen if the given defaults do not include a value for this
// attribute.
if input.IsNull() || fallback.IsNull() {
if umInput.IsNull() || umFb.IsNull() {
return input
}
l := wantTy.Length()
ret := make([]cty.Value, l)
for i := 0; i < l; i++ {
inputSub := input.Index(cty.NumberIntVal(int64(i)))
fallbackSub := fallback.Index(cty.NumberIntVal(int64(i)))
ret[i] = defaultsApply(inputSub, fallbackSub)
inputSub := umInput.Index(cty.NumberIntVal(int64(i)))
fallbackSub := umFb.Index(cty.NumberIntVal(int64(i)))
ret[i] = defaultsApply(inputSub.WithMarks(inputMarks), fallbackSub.WithMarks(fallbackMarks))
}
return cty.TupleVal(ret)
@ -148,10 +156,10 @@ func defaultsApply(input, fallback cty.Value) cty.Value {
case wantTy.IsMapType():
newVals := map[string]cty.Value{}
if !input.IsNull() {
for it := input.ElementIterator(); it.Next(); {
if !umInput.IsNull() {
for it := umInput.ElementIterator(); it.Next(); {
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():
var newVals []cty.Value
if !input.IsNull() {
for it := input.ElementIterator(); it.Next(); {
if !umInput.IsNull() {
for it := umInput.ElementIterator(); it.Next(); {
_, v := it.Element()
newV := defaultsApply(v, fallback)
newV := defaultsApply(v.WithMarks(inputMarks), fallback.WithMarks(fallbackMarks))
newVals = append(newVals, newV)
}
}

View File

@ -13,6 +13,30 @@ func TestDefaults(t *testing.T) {
Want cty.Value
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{
"a": cty.NullVal(cty.String),
@ -494,6 +518,110 @@ func TestDefaults(t *testing.T) {
}),
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 {