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
|
@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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 {
|
||||||
|
|
Loading…
Reference in New Issue