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.
pull/28476/head
Kristin Laemmert 5 years ago
parent b34588ffca
commit 5ae4c2f92b

@ -16,13 +16,15 @@ import (
var DefaultsFunc = function.New(&function.Spec{
Params: []function.Parameter{
{
Name: "input",
Type: cty.DynamicPseudoType,
AllowNull: true,
Name: "input",
Type: cty.DynamicPseudoType,
AllowNull: true,
AllowMarked: true,
},
{
Name: "defaults",
Type: cty.DynamicPseudoType,
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)
}
}

@ -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 {

Loading…
Cancel
Save