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 }