Skip to content
Closed
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
126 changes: 117 additions & 9 deletions internal/moduletest/mocking/fill.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,12 @@ func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value,
func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Path) (cty.Value, error) {
if attribute.NestedType != nil {

// Then the in value must be an object.
if !in.Type().IsObjectType() {
return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName())
}

switch attribute.NestedType.Nesting {
case configschema.NestingSingle, configschema.NestingGroup:
if !in.Type().IsObjectType() {
return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName())
}

var names []string
for name := range attribute.NestedType.Attributes {
names = append(names, name)
Expand All @@ -57,12 +56,76 @@ func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Pat
children[name] = GenerateValueForAttribute(attribute.NestedType.Attributes[name])
}
return cty.ObjectVal(children), nil
case configschema.NestingSet:
return cty.SetValEmpty(attribute.ImpliedType().ElementType()), nil
case configschema.NestingList:
return cty.ListValEmpty(attribute.ImpliedType().ElementType()), nil
elemType := attribute.ImpliedType().ElementType()
switch {
case in.Type().IsListType(), in.Type().IsTupleType(), in.Type().IsSetType():
var values []cty.Value
for iterator := in.ElementIterator(); iterator.Next(); {
_, value := iterator.Element()
child, err := fillNestedObject(value, attribute.NestedType.Attributes, path)
if err != nil {
return cty.NilVal, err
}
values = append(values, child)
}
if len(values) == 0 {
return cty.ListValEmpty(elemType), nil
}
return cty.ListVal(values), nil
case in.Type().IsObjectType():
// Object input for a list attribute is treated as a
// template — there are no existing elements to apply
// it to, so return an empty list.
return cty.ListValEmpty(elemType), nil
default:
return cty.NilVal, path.NewErrorf("incompatible types; expected list type, found %s", in.Type().FriendlyName())
}
case configschema.NestingSet:
elemType := attribute.ImpliedType().ElementType()
switch {
case in.Type().IsListType(), in.Type().IsTupleType(), in.Type().IsSetType():
var values []cty.Value
for iterator := in.ElementIterator(); iterator.Next(); {
_, value := iterator.Element()
child, err := fillNestedObject(value, attribute.NestedType.Attributes, path)
if err != nil {
return cty.NilVal, err
}
values = append(values, child)
}
if len(values) == 0 {
return cty.SetValEmpty(elemType), nil
}
return cty.SetVal(values), nil
case in.Type().IsObjectType():
return cty.SetValEmpty(elemType), nil
default:
return cty.NilVal, path.NewErrorf("incompatible types; expected set type, found %s", in.Type().FriendlyName())
}
case configschema.NestingMap:
return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil
elemType := attribute.ImpliedType().ElementType()
switch {
case in.Type().IsMapType():
values := make(map[string]cty.Value)
for iterator := in.ElementIterator(); iterator.Next(); {
key, value := iterator.Element()
child, err := fillNestedObject(value, attribute.NestedType.Attributes, path)
if err != nil {
return cty.NilVal, err
}
values[key.AsString()] = child
}
if len(values) == 0 {
return cty.MapValEmpty(elemType), nil
}
return cty.MapVal(values), nil
case in.Type().IsObjectType():
// Object input is treated as a template — return empty map.
return cty.MapValEmpty(elemType), nil
default:
return cty.NilVal, path.NewErrorf("incompatible types; expected map type, found %s", in.Type().FriendlyName())
}
default:
panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting))
}
Expand All @@ -71,6 +134,51 @@ func fillAttribute(in cty.Value, attribute *configschema.Attribute, path cty.Pat
return fillType(in, attribute.Type, path)
}

// fillNestedObject takes a single element value (expected to be an object or
// map) and fills it against the nested attribute schema, generating values for
// any attributes not present in the input.
func fillNestedObject(in cty.Value, attrs map[string]*configschema.Attribute, path cty.Path) (cty.Value, error) {
if !in.Type().IsObjectType() && !in.Type().IsMapType() {
return cty.NilVal, path.NewErrorf("incompatible types; expected object type, found %s", in.Type().FriendlyName())
}

var names []string
for name := range attrs {
names = append(names, name)
}
if len(names) == 0 {
return cty.EmptyObjectVal, nil
}
sort.Strings(names)

children := make(map[string]cty.Value)
for _, name := range names {
hasAttr := false
if in.Type().IsObjectType() {
hasAttr = in.Type().HasAttribute(name)
} else if in.Type().IsMapType() {
hasAttr = in.HasIndex(cty.StringVal(name)).True()
}

if hasAttr {
var elem cty.Value
if in.Type().IsObjectType() {
elem = in.GetAttr(name)
} else {
elem = in.Index(cty.StringVal(name))
}
child, err := fillAttribute(elem, attrs[name], path.GetAttr(name))
if err != nil {
return cty.NilVal, err
}
children[name] = child
continue
}
children[name] = GenerateValueForAttribute(attrs[name])
}
return cty.ObjectVal(children), nil
}

// FillType makes the input value match the target type by adding attributes
// directly to it or to any nested objects. Essentially, this is a "safe"
// conversion between two objects.
Expand Down
Loading