blob: e7f054981903e2efd3e72c4b42781208948c42fe [file] [log] [blame]
// Copyright (c) HashiCorp, Inc.
// SPDX-License-Identifier: BUSL-1.1
package mocking
import (
"fmt"
"sort"
"github.com/zclconf/go-cty/cty"
"github.com/zclconf/go-cty/cty/convert"
"github.com/hashicorp/terraform/internal/configs/configschema"
)
// FillAttribute makes the input value match the specified attribute by adding
// attributes and/or performing conversions to make the input value correct.
//
// It is similar to FillType, except it accepts attributes instead of types.
func FillAttribute(in cty.Value, attribute *configschema.Attribute) (cty.Value, error) {
return fillAttribute(in, attribute, cty.Path{})
}
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:
var names []string
for name := range attribute.NestedType.Attributes {
names = append(names, name)
}
if len(names) == 0 {
return cty.EmptyObjectVal, nil
}
// Make the order we iterate through the attributes deterministic. We
// are generating random strings in here so it's worth making the
// operation repeatable.
sort.Strings(names)
children := make(map[string]cty.Value)
for _, name := range names {
if in.Type().HasAttribute(name) {
child, err := fillAttribute(in.GetAttr(name), attribute.NestedType.Attributes[name], path.GetAttr(name))
if err != nil {
return cty.NilVal, err
}
children[name] = child
continue
}
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
case configschema.NestingMap:
return cty.MapValEmpty(attribute.ImpliedType().ElementType()), nil
default:
panic(fmt.Errorf("unknown nesting mode: %d", attribute.NestedType.Nesting))
}
}
return fillType(in, attribute.Type, path)
}
// 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.
//
// This function can error if one of the embedded types within value doesn't
// match the type expected by target.
//
// If the supplied value isn't an object (or a map that can be treated as an
// object) then a normal conversion is attempted from value into target.
//
// Superfluous attributes within the supplied value (ie. attributes not
// mentioned by the target type) are dropped without error.
func FillType(in cty.Value, target cty.Type) (cty.Value, error) {
return fillType(in, target, cty.Path{})
}
func fillType(in cty.Value, target cty.Type, path cty.Path) (cty.Value, error) {
// If we're targeting an object directly, then the in value must be an
// object or a map. We'll check for those two cases specifically.
if target.IsObjectType() {
var attributes []string
for attribute := range target.AttributeTypes() {
attributes = append(attributes, attribute)
}
// Make the order we iterate through the attributes deterministic. We
// are generating random strings in here so it's worth making the
// operation repeatable.
sort.Strings(attributes)
if in.Type().IsObjectType() {
if len(attributes) == 0 {
return cty.EmptyObjectVal, nil
}
children := make(map[string]cty.Value)
for _, attribute := range attributes {
if in.Type().HasAttribute(attribute) {
child, err := fillType(in.GetAttr(attribute), target.AttributeType(attribute), path.IndexString(attribute))
if err != nil {
return cty.NilVal, err
}
children[attribute] = child
continue
}
children[attribute] = GenerateValueForType(target.AttributeType(attribute))
}
return cty.ObjectVal(children), nil
}
if in.Type().IsMapType() {
if len(attributes) == 0 {
return cty.EmptyObjectVal, nil
}
children := make(map[string]cty.Value)
for _, attribute := range attributes {
attributeType := target.AttributeType(attribute)
index := cty.StringVal(attribute)
if in.HasIndex(index).True() {
child, err := fillType(in.Index(index), attributeType, path.Index(index))
if err != nil {
return cty.NilVal, err
}
children[attribute] = child
continue
}
children[attribute] = GenerateValueForType(attributeType)
}
return cty.ObjectVal(children), nil
}
// If the target is an object type, and the input wasn't an object or
// a map, then we have incompatible types.
return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName())
}
// We also do a special check for any types that might contain an object as
// we'll need to recursively call fill over the nested objects.
if target.IsCollectionType() && target.ElementType().IsObjectType() {
switch {
case target.IsListType():
var values []cty.Value
switch {
case in.Type().IsSetType(), in.Type().IsListType(), in.Type().IsTupleType():
for iterator := in.ElementIterator(); iterator.Next(); {
index, value := iterator.Element()
child, err := fillType(value, target.ElementType(), path.Index(index))
if err != nil {
return cty.NilVal, err
}
values = append(values, child)
}
default:
return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName())
}
if len(values) == 0 {
return cty.ListValEmpty(target.ElementType()), nil
}
return cty.ListVal(values), nil
case target.IsSetType():
var values []cty.Value
switch {
case in.Type().IsSetType(), in.Type().IsListType(), in.Type().IsTupleType():
for iterator := in.ElementIterator(); iterator.Next(); {
index, value := iterator.Element()
child, err := fillType(value, target.ElementType(), path.Index(index))
if err != nil {
return cty.NilVal, err
}
values = append(values, child)
}
default:
return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName())
}
if len(values) == 0 {
return cty.SetValEmpty(target.ElementType()), nil
}
return cty.SetVal(values), nil
case target.IsMapType():
values := make(map[string]cty.Value)
switch {
case in.Type().IsMapType():
var keys []string
for key := range in.AsValueMap() {
keys = append(keys, key)
}
// Make the order we iterate through the map deterministic. We
// are generating random strings in here so it's worth making
// the operation repeatable.
sort.Strings(keys)
for _, key := range keys {
child, err := fillType(in.Index(cty.StringVal(key)), target.ElementType(), path.IndexString(key))
if err != nil {
return cty.NilVal, err
}
values[key] = child
}
case in.Type().IsObjectType():
var attributes []string
for attribute := range in.Type().AttributeTypes() {
attributes = append(attributes, attribute)
}
// Make the order we iterate through the map deterministic. We
// are generating random strings in here so it's worth making
// the operation repeatable.
sort.Strings(attributes)
for _, name := range attributes {
child, err := fillType(in.GetAttr(name), target.ElementType(), path.IndexString(name))
if err != nil {
return cty.NilVal, err
}
values[name] = child
}
default:
return cty.NilVal, path.NewErrorf("incompatible types; expected %s, found %s", target.FriendlyName(), in.Type().FriendlyName())
}
if len(values) == 0 {
return cty.MapValEmpty(target.ElementType()), nil
}
return cty.MapVal(values), nil
default:
panic(fmt.Errorf("unrecognized collection type: %s", target.FriendlyName()))
}
}
if target.IsTupleType() && in.Type().IsTupleType() {
if target.Length() != in.Type().Length() {
return cty.NilVal, path.NewErrorf("incompatible types; expected %s with length %d, found %s with length %d", target.FriendlyName(), target.Length(), in.Type().FriendlyName(), in.Type().Length())
}
var values []cty.Value
for ix, value := range in.AsValueSlice() {
child, err := fillType(value, target.TupleElementType(ix), path.IndexInt(ix))
if err != nil {
return cty.NilVal, err
}
values = append(values, child)
}
if len(values) == 0 {
return cty.EmptyTupleVal, nil
}
return cty.TupleVal(values), nil
}
// Otherwise, we don't have any nested object types we need to fill and this
// isn't an actual object either. So we can just do a simple conversion into
// the target type.
value, err := convert.Convert(in, target)
if err != nil {
return value, path.NewError(err)
}
return value, nil
}