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 }