github.com/terramate-io/tf@v0.0.0-20230830114523-fce866b4dfcd/configs/configschema/coerce_value.go (about)

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