github.com/opentofu/opentofu@v1.7.1/internal/configs/configschema/coerce_value.go (about)

     1  // Copyright (c) The OpenTofu Authors
     2  // SPDX-License-Identifier: MPL-2.0
     3  // Copyright (c) 2023 HashiCorp, Inc.
     4  // SPDX-License-Identifier: MPL-2.0
     5  
     6  package configschema
     7  
     8  import (
     9  	"fmt"
    10  	"sort"
    11  
    12  	"github.com/zclconf/go-cty/cty"
    13  	"github.com/zclconf/go-cty/cty/convert"
    14  )
    15  
    16  // CoerceValue attempts to force the given value to conform to the type
    17  // implied by the receiever.
    18  //
    19  // This is useful in situations where a configuration must be derived from
    20  // an already-decoded value. It is always better to decode directly from
    21  // configuration where possible since then source location information is
    22  // still available to produce diagnostics, but in special situations this
    23  // function allows a compatible result to be obtained even if the
    24  // configuration objects are not available.
    25  //
    26  // If the given value cannot be converted to conform to the receiving schema
    27  // then an error is returned describing one of possibly many problems. This
    28  // error may be a cty.PathError indicating a position within the nested
    29  // data structure where the problem applies.
    30  func (b *Block) CoerceValue(in cty.Value) (cty.Value, error) {
    31  	var path cty.Path
    32  	return b.coerceValue(in, path)
    33  }
    34  
    35  func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) {
    36  	convType := b.specType()
    37  	impliedType := convType.WithoutOptionalAttributesDeep()
    38  
    39  	switch {
    40  	case in.IsNull():
    41  		return cty.NullVal(impliedType), nil
    42  	case !in.IsKnown():
    43  		return cty.UnknownVal(impliedType), nil
    44  	}
    45  
    46  	ty := in.Type()
    47  	if !ty.IsObjectType() {
    48  		return cty.UnknownVal(impliedType), path.NewErrorf("an object is required")
    49  	}
    50  
    51  	for name := range ty.AttributeTypes() {
    52  		if _, defined := b.Attributes[name]; defined {
    53  			continue
    54  		}
    55  		if _, defined := b.BlockTypes[name]; defined {
    56  			continue
    57  		}
    58  		return cty.UnknownVal(impliedType), path.NewErrorf("unexpected attribute %q", name)
    59  	}
    60  
    61  	attrs := make(map[string]cty.Value)
    62  
    63  	// Stable sort keys for consistent error reports
    64  	attrKeys := make([]string, 0, len(b.Attributes))
    65  	for key := range b.Attributes {
    66  		attrKeys = append(attrKeys, key)
    67  	}
    68  	sort.Strings(attrKeys)
    69  
    70  	for _, name := range attrKeys {
    71  		attrS := b.Attributes[name]
    72  		attrType := impliedType.AttributeType(name)
    73  		attrConvType := convType.AttributeType(name)
    74  
    75  		var val cty.Value
    76  		switch {
    77  		case ty.HasAttribute(name):
    78  			val = in.GetAttr(name)
    79  		case attrS.Computed || attrS.Optional:
    80  			val = cty.NullVal(attrType)
    81  		default:
    82  			return cty.UnknownVal(impliedType), path.NewErrorf("attribute %q is required", name)
    83  		}
    84  
    85  		val, err := convert.Convert(val, attrConvType)
    86  		if err != nil {
    87  			return cty.UnknownVal(impliedType), append(path, cty.GetAttrStep{Name: name}).NewError(err)
    88  		}
    89  		attrs[name] = val
    90  	}
    91  
    92  	// Stable sort keys for consistent error reports
    93  	typeKeys := make([]string, 0, len(b.BlockTypes))
    94  	for key := range b.BlockTypes {
    95  		typeKeys = append(typeKeys, key)
    96  	}
    97  	sort.Strings(typeKeys)
    98  
    99  	for _, typeName := range typeKeys {
   100  		blockS := b.BlockTypes[typeName]
   101  		switch blockS.Nesting {
   102  
   103  		case NestingSingle, NestingGroup:
   104  			switch {
   105  			case ty.HasAttribute(typeName):
   106  				var err error
   107  				val := in.GetAttr(typeName)
   108  				attrs[typeName], err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName}))
   109  				if err != nil {
   110  					return cty.UnknownVal(impliedType), err
   111  				}
   112  			default:
   113  				attrs[typeName] = blockS.EmptyValue()
   114  			}
   115  
   116  		case NestingList:
   117  			switch {
   118  			case ty.HasAttribute(typeName):
   119  				coll := in.GetAttr(typeName)
   120  
   121  				switch {
   122  				case coll.IsNull():
   123  					attrs[typeName] = cty.NullVal(cty.List(blockS.ImpliedType()))
   124  					continue
   125  				case !coll.IsKnown():
   126  					attrs[typeName] = cty.UnknownVal(cty.List(blockS.ImpliedType()))
   127  					continue
   128  				}
   129  
   130  				if !coll.CanIterateElements() {
   131  					return cty.UnknownVal(impliedType), path.NewErrorf("must be a list")
   132  				}
   133  				l := coll.LengthInt()
   134  
   135  				if l == 0 {
   136  					attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType())
   137  					continue
   138  				}
   139  				elems := make([]cty.Value, 0, l)
   140  				{
   141  					path = append(path, cty.GetAttrStep{Name: typeName})
   142  					for it := coll.ElementIterator(); it.Next(); {
   143  						var err error
   144  						idx, val := it.Element()
   145  						val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
   146  						if err != nil {
   147  							return cty.UnknownVal(impliedType), err
   148  						}
   149  						elems = append(elems, val)
   150  					}
   151  				}
   152  				attrs[typeName] = cty.ListVal(elems)
   153  			default:
   154  				attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType())
   155  			}
   156  
   157  		case NestingSet:
   158  			switch {
   159  			case ty.HasAttribute(typeName):
   160  				coll := in.GetAttr(typeName)
   161  
   162  				switch {
   163  				case coll.IsNull():
   164  					attrs[typeName] = cty.NullVal(cty.Set(blockS.ImpliedType()))
   165  					continue
   166  				case !coll.IsKnown():
   167  					attrs[typeName] = cty.UnknownVal(cty.Set(blockS.ImpliedType()))
   168  					continue
   169  				}
   170  
   171  				if !coll.CanIterateElements() {
   172  					return cty.UnknownVal(impliedType), path.NewErrorf("must be a set")
   173  				}
   174  				l := coll.LengthInt()
   175  
   176  				if l == 0 {
   177  					attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType())
   178  					continue
   179  				}
   180  				elems := make([]cty.Value, 0, l)
   181  				{
   182  					path = append(path, cty.GetAttrStep{Name: typeName})
   183  					for it := coll.ElementIterator(); it.Next(); {
   184  						var err error
   185  						idx, val := it.Element()
   186  						val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx}))
   187  						if err != nil {
   188  							return cty.UnknownVal(impliedType), err
   189  						}
   190  						elems = append(elems, val)
   191  					}
   192  				}
   193  				attrs[typeName] = cty.SetVal(elems)
   194  			default:
   195  				attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType())
   196  			}
   197  
   198  		case NestingMap:
   199  			switch {
   200  			case ty.HasAttribute(typeName):
   201  				coll := in.GetAttr(typeName)
   202  
   203  				switch {
   204  				case coll.IsNull():
   205  					attrs[typeName] = cty.NullVal(cty.Map(blockS.ImpliedType()))
   206  					continue
   207  				case !coll.IsKnown():
   208  					attrs[typeName] = cty.UnknownVal(cty.Map(blockS.ImpliedType()))
   209  					continue
   210  				}
   211  
   212  				if !coll.CanIterateElements() {
   213  					return cty.UnknownVal(impliedType), path.NewErrorf("must be a map")
   214  				}
   215  				l := coll.LengthInt()
   216  				if l == 0 {
   217  					attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType())
   218  					continue
   219  				}
   220  				elems := make(map[string]cty.Value)
   221  				{
   222  					path = append(path, cty.GetAttrStep{Name: typeName})
   223  					for it := coll.ElementIterator(); it.Next(); {
   224  						var err error
   225  						key, val := it.Element()
   226  						if key.Type() != cty.String || key.IsNull() || !key.IsKnown() {
   227  							return cty.UnknownVal(impliedType), path.NewErrorf("must be a map")
   228  						}
   229  						val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key}))
   230  						if err != nil {
   231  							return cty.UnknownVal(impliedType), err
   232  						}
   233  						elems[key.AsString()] = val
   234  					}
   235  				}
   236  
   237  				// If the attribute values here contain any DynamicPseudoTypes,
   238  				// the concrete type must be an object.
   239  				useObject := false
   240  				switch {
   241  				case coll.Type().IsObjectType():
   242  					useObject = true
   243  				default:
   244  					// It's possible that we were given a map, and need to coerce it to an object
   245  					ety := coll.Type().ElementType()
   246  					for _, v := range elems {
   247  						if !v.Type().Equals(ety) {
   248  							useObject = true
   249  							break
   250  						}
   251  					}
   252  				}
   253  
   254  				if useObject {
   255  					attrs[typeName] = cty.ObjectVal(elems)
   256  				} else {
   257  					attrs[typeName] = cty.MapVal(elems)
   258  				}
   259  			default:
   260  				attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType())
   261  			}
   262  
   263  		default:
   264  			// should never happen because above is exhaustive
   265  			panic(fmt.Errorf("unsupported nesting mode %#v", blockS.Nesting))
   266  		}
   267  	}
   268  
   269  	return cty.ObjectVal(attrs), nil
   270  }