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:
parent
b34588ffca
commit
5ae4c2f92b
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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…
Reference in New Issue