package funcs import ( "fmt" "testing" "github.com/zclconf/go-cty/cty" ) func TestDefaults(t *testing.T) { tests := []struct { Input, Defaults cty.Value 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), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hey"), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hey"), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), }), Defaults: cty.ObjectVal(map[string]cty.Value{}), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), }), }, { Input: cty.ObjectVal(map[string]cty.Value{}), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), }), WantErr: `.a: target type does not expect an attribute named "a"`, }, { 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"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.StringVal("hello"), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.NullVal(cty.String), cty.StringVal("hey"), cty.NullVal(cty.String), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.StringVal("hello"), cty.StringVal("hey"), cty.StringVal("hello"), }), }), }, { // Using defaults with single set elements is a pretty // odd thing to do, but this behavior is just here because // it generalizes from how we handle collections. It's // tested only to ensure it doesn't change accidentally // in future. Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.SetVal([]cty.Value{ cty.NullVal(cty.String), cty.StringVal("hey"), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.SetVal([]cty.Value{ cty.StringVal("hey"), cty.StringVal("hello"), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.MapVal(map[string]cty.Value{ "x": cty.NullVal(cty.String), "y": cty.StringVal("hey"), "z": cty.NullVal(cty.String), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.MapVal(map[string]cty.Value{ "x": cty.StringVal("hello"), "y": cty.StringVal("hey"), "z": cty.StringVal("hello"), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.NullVal(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), }), }, { Input: cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.NullVal(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), Want: cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("boop"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.NullVal(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("boop"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.SetVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.NullVal(cty.String), }), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.SetVal([]cty.Value{ // After applying defaults, the one with a null value // coalesced with the one with a non-null value, // and so there's only one left. cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.MapVal(map[string]cty.Value{ "boop": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), "beep": cty.ObjectVal(map[string]cty.Value{ "b": cty.NullVal(cty.String), }), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.MapVal(map[string]cty.Value{ "boop": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), "beep": cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hello"), }), }), }), }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.NullVal(cty.String), }), cty.ObjectVal(map[string]cty.Value{ "b": cty.StringVal("hey"), }), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), WantErr: `.a: the default value for a collection of an object type must itself be an object type, not string`, }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.NullVal(cty.String), cty.StringVal("hey"), cty.NullVal(cty.String), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ // The default value for a list must be a single value // of the list's element type which provides defaults // for each element separately, so the default for a // list of string should be just a single string, not // a list of string. "a": cty.ListVal([]cty.Value{ cty.StringVal("hello"), }), }), WantErr: `.a: invalid default value for string: string required`, }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.TupleVal([]cty.Value{ cty.NullVal(cty.String), cty.StringVal("hey"), cty.NullVal(cty.String), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), }), WantErr: `.a: the default value for a tuple type must itself be a tuple type, not string`, }, { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.TupleVal([]cty.Value{ cty.NullVal(cty.String), cty.StringVal("hey"), cty.NullVal(cty.String), }), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.TupleVal([]cty.Value{ cty.StringVal("hello 0"), cty.StringVal("hello 1"), cty.StringVal("hello 2"), }), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.TupleVal([]cty.Value{ cty.StringVal("hello 0"), cty.StringVal("hey"), cty.StringVal("hello 2"), }), }), }, { // There's no reason to use this function for plain primitive // types, because the "default" argument in a variable definition // already has the equivalent behavior. This function is only // to deal with the situation of a complex-typed variable where // only parts of the data structure are optional. Input: cty.NullVal(cty.String), Defaults: cty.StringVal("hello"), WantErr: `only object types and collections of object types can have defaults applied`, }, // When applying default values to structural types, null objects or // tuples in the input should be passed through. { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.Object(map[string]cty.Type{ "x": cty.String, "y": cty.String, })), "b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.ObjectVal(map[string]cty.Value{ "x": cty.StringVal("hello"), "y": cty.StringVal("there"), }), "b": cty.TupleVal([]cty.Value{ cty.StringVal("how are"), cty.StringVal("you?"), }), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.Object(map[string]cty.Type{ "x": cty.String, "y": cty.String, })), "b": cty.NullVal(cty.Tuple([]cty.Type{cty.String, cty.String})), }), }, // When applying default values to structural types, we permit null // values in the defaults, and just pass through the input value. { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "p": cty.StringVal("xyz"), "q": cty.StringVal("xyz"), }), }), "b": cty.SetVal([]cty.Value{ cty.TupleVal([]cty.Value{ cty.NumberIntVal(0), cty.NumberIntVal(2), }), cty.TupleVal([]cty.Value{ cty.NumberIntVal(1), cty.NumberIntVal(3), }), }), "c": cty.NullVal(cty.String), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "c": cty.StringVal("tada"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListVal([]cty.Value{ cty.ObjectVal(map[string]cty.Value{ "p": cty.StringVal("xyz"), "q": cty.StringVal("xyz"), }), }), "b": cty.SetVal([]cty.Value{ cty.TupleVal([]cty.Value{ cty.NumberIntVal(0), cty.NumberIntVal(2), }), cty.TupleVal([]cty.Value{ cty.NumberIntVal(1), cty.NumberIntVal(3), }), }), "c": cty.StringVal("tada"), }), }, // When applying default values to collection types, null collections in the // input should result in empty collections in the output. { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.List(cty.String)), "b": cty.NullVal(cty.Map(cty.String)), "c": cty.NullVal(cty.Set(cty.String)), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("hello"), "b": cty.StringVal("hi"), "c": cty.StringVal("greetings"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.ListValEmpty(cty.String), "b": cty.MapValEmpty(cty.String), "c": cty.SetValEmpty(cty.String), }), }, // When specifying fallbacks, we allow mismatched primitive attribute // types so long as a safe conversion is possible. This means that we // can accept number or boolean values for string attributes. { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.String), "b": cty.NullVal(cty.String), "c": cty.NullVal(cty.String), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.NumberIntVal(5), "b": cty.True, "c": cty.StringVal("greetings"), }), Want: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("5"), "b": cty.StringVal("true"), "c": cty.StringVal("greetings"), }), }, // Fallbacks with mismatched primitive attribute types which do not // have safe conversions must not pass the suitable fallback check, // even if unsafe conversion would be possible. { Input: cty.ObjectVal(map[string]cty.Value{ "a": cty.NullVal(cty.Bool), }), Defaults: cty.ObjectVal(map[string]cty.Value{ "a": cty.StringVal("5"), }), 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 { t.Run(fmt.Sprintf("defaults(%#v, %#v)", test.Input, test.Defaults), func(t *testing.T) { got, gotErr := Defaults(test.Input, test.Defaults) if test.WantErr != "" { if gotErr == nil { t.Fatalf("unexpected success\nwant error: %s", test.WantErr) } if got, want := gotErr.Error(), test.WantErr; got != want { t.Fatalf("wrong error\ngot: %s\nwant: %s", got, want) } return } else if gotErr != nil { t.Fatalf("unexpected error\ngot: %s", gotErr.Error()) } if !test.Want.RawEquals(got) { t.Errorf("wrong result\ngot: %#v\nwant: %#v", got, test.Want) } }) } }