github.com/iaas-resource-provision/iaas-rpc@v1.0.7-0.20211021023331-ed21f798c408/internal/lang/funcs/defaults.go (about) 1 package funcs 2 3 import ( 4 "fmt" 5 6 "github.com/iaas-resource-provision/iaas-rpc/internal/tfdiags" 7 "github.com/zclconf/go-cty/cty" 8 "github.com/zclconf/go-cty/cty/convert" 9 "github.com/zclconf/go-cty/cty/function" 10 ) 11 12 // DefaultsFunc is a helper function for substituting default values in 13 // place of null values in a given data structure. 14 // 15 // See the documentation for function Defaults for more information. 16 var DefaultsFunc = function.New(&function.Spec{ 17 Params: []function.Parameter{ 18 { 19 Name: "input", 20 Type: cty.DynamicPseudoType, 21 AllowNull: true, 22 }, 23 { 24 Name: "defaults", 25 Type: cty.DynamicPseudoType, 26 }, 27 }, 28 Type: func(args []cty.Value) (cty.Type, error) { 29 // The result type is guaranteed to be the same as the input type, 30 // since all we're doing is replacing null values with non-null 31 // values of the same type. 32 retType := args[0].Type() 33 defaultsType := args[1].Type() 34 35 // This function is aimed at filling in object types or collections 36 // of object types where some of the attributes might be null, so 37 // it doesn't make sense to use a primitive type directly with it. 38 // (The "coalesce" function may be appropriate for such cases.) 39 if retType.IsPrimitiveType() { 40 // This error message is a bit of a fib because we can actually 41 // apply defaults to tuples too, but we expect that to be so 42 // unusual as to not be worth mentioning here, because mentioning 43 // it would require using some less-well-known Terraform language 44 // terminology in the message (tuple types, structural types). 45 return cty.DynamicPseudoType, function.NewArgErrorf(1, "only object types and collections of object types can have defaults applied") 46 } 47 48 defaultsPath := make(cty.Path, 0, 4) // some capacity so that most structures won't reallocate 49 if err := defaultsAssertSuitableFallback(retType, defaultsType, defaultsPath); err != nil { 50 errMsg := tfdiags.FormatError(err) // add attribute path prefix 51 return cty.DynamicPseudoType, function.NewArgErrorf(1, "%s", errMsg) 52 } 53 54 return retType, nil 55 }, 56 Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) { 57 if args[0].Type().HasDynamicTypes() { 58 // If the types our input object aren't known yet for some reason 59 // then we'll defer all of our work here, because our 60 // interpretation of the defaults depends on the types in 61 // the input. 62 return cty.UnknownVal(retType), nil 63 } 64 65 v := defaultsApply(args[0], args[1]) 66 return v, nil 67 }, 68 }) 69 70 func defaultsApply(input, fallback cty.Value) cty.Value { 71 wantTy := input.Type() 72 if !(input.IsKnown() && fallback.IsKnown()) { 73 return cty.UnknownVal(wantTy) 74 } 75 76 // For the rest of this function we're assuming that the given defaults 77 // will always be valid, because we expect to have caught any problems 78 // during the type checking phase. Any inconsistencies that reach here are 79 // therefore considered to be implementation bugs, and so will panic. 80 81 // Our strategy depends on the kind of type we're working with. 82 switch { 83 case wantTy.IsPrimitiveType(): 84 // For leaf primitive values the rule is relatively simple: use the 85 // input if it's non-null, or fallback if input is null. 86 if !input.IsNull() { 87 return input 88 } 89 v, err := convert.Convert(fallback, wantTy) 90 if err != nil { 91 // Should not happen because we checked in defaultsAssertSuitableFallback 92 panic(err.Error()) 93 } 94 return v 95 96 case wantTy.IsObjectType(): 97 // For structural types, a null input value must be passed through. We 98 // do not apply default values for missing optional structural values, 99 // only their contents. 100 // 101 // We also pass through the input if the fallback value is null. This 102 // can happen if the given defaults do not include a value for this 103 // attribute. 104 if input.IsNull() || fallback.IsNull() { 105 return input 106 } 107 atys := wantTy.AttributeTypes() 108 ret := map[string]cty.Value{} 109 for attr, aty := range atys { 110 inputSub := input.GetAttr(attr) 111 fallbackSub := cty.NullVal(aty) 112 if fallback.Type().HasAttribute(attr) { 113 fallbackSub = fallback.GetAttr(attr) 114 } 115 ret[attr] = defaultsApply(inputSub, fallbackSub) 116 } 117 return cty.ObjectVal(ret) 118 119 case wantTy.IsTupleType(): 120 // For structural types, a null input value must be passed through. We 121 // do not apply default values for missing optional structural values, 122 // only their contents. 123 // 124 // We also pass through the input if the fallback value is null. This 125 // can happen if the given defaults do not include a value for this 126 // attribute. 127 if input.IsNull() || fallback.IsNull() { 128 return input 129 } 130 131 l := wantTy.Length() 132 ret := make([]cty.Value, l) 133 for i := 0; i < l; i++ { 134 inputSub := input.Index(cty.NumberIntVal(int64(i))) 135 fallbackSub := fallback.Index(cty.NumberIntVal(int64(i))) 136 ret[i] = defaultsApply(inputSub, fallbackSub) 137 } 138 return cty.TupleVal(ret) 139 140 case wantTy.IsCollectionType(): 141 // For collection types we apply a single fallback value to each 142 // element of the input collection, because in the situations this 143 // function is intended for we assume that the number of elements 144 // is the caller's decision, and so we'll just apply the same defaults 145 // to all of the elements. 146 ety := wantTy.ElementType() 147 switch { 148 case wantTy.IsMapType(): 149 newVals := map[string]cty.Value{} 150 151 if !input.IsNull() { 152 for it := input.ElementIterator(); it.Next(); { 153 k, v := it.Element() 154 newVals[k.AsString()] = defaultsApply(v, fallback) 155 } 156 } 157 158 if len(newVals) == 0 { 159 return cty.MapValEmpty(ety) 160 } 161 return cty.MapVal(newVals) 162 case wantTy.IsListType(), wantTy.IsSetType(): 163 var newVals []cty.Value 164 165 if !input.IsNull() { 166 for it := input.ElementIterator(); it.Next(); { 167 _, v := it.Element() 168 newV := defaultsApply(v, fallback) 169 newVals = append(newVals, newV) 170 } 171 } 172 173 if len(newVals) == 0 { 174 if wantTy.IsSetType() { 175 return cty.SetValEmpty(ety) 176 } 177 return cty.ListValEmpty(ety) 178 } 179 if wantTy.IsSetType() { 180 return cty.SetVal(newVals) 181 } 182 return cty.ListVal(newVals) 183 default: 184 // There are no other collection types, so this should not happen 185 panic(fmt.Sprintf("invalid collection type %#v", wantTy)) 186 } 187 default: 188 // We should've caught anything else in defaultsAssertSuitableFallback, 189 // so this should not happen. 190 panic(fmt.Sprintf("invalid target type %#v", wantTy)) 191 } 192 } 193 194 func defaultsAssertSuitableFallback(wantTy, fallbackTy cty.Type, fallbackPath cty.Path) error { 195 // If the type we want is a collection type then we need to keep peeling 196 // away collection type wrappers until we find the non-collection-type 197 // that's underneath, which is what the fallback will actually be applied 198 // to. 199 inCollection := false 200 for wantTy.IsCollectionType() { 201 wantTy = wantTy.ElementType() 202 inCollection = true 203 } 204 205 switch { 206 case wantTy.IsPrimitiveType(): 207 // The fallback is valid if it's equal to or convertible to what we want. 208 if fallbackTy.Equals(wantTy) { 209 return nil 210 } 211 conversion := convert.GetConversion(fallbackTy, wantTy) 212 if conversion == nil { 213 msg := convert.MismatchMessage(fallbackTy, wantTy) 214 return fallbackPath.NewErrorf("invalid default value for %s: %s", wantTy.FriendlyName(), msg) 215 } 216 return nil 217 case wantTy.IsObjectType(): 218 if !fallbackTy.IsObjectType() { 219 if inCollection { 220 return fallbackPath.NewErrorf("the default value for a collection of an object type must itself be an object type, not %s", fallbackTy.FriendlyName()) 221 } 222 return fallbackPath.NewErrorf("the default value for an object type must itself be an object type, not %s", fallbackTy.FriendlyName()) 223 } 224 for attr, wantAty := range wantTy.AttributeTypes() { 225 if !fallbackTy.HasAttribute(attr) { 226 continue // it's always okay to not have a default value 227 } 228 fallbackSubpath := fallbackPath.GetAttr(attr) 229 fallbackSubTy := fallbackTy.AttributeType(attr) 230 err := defaultsAssertSuitableFallback(wantAty, fallbackSubTy, fallbackSubpath) 231 if err != nil { 232 return err 233 } 234 } 235 for attr := range fallbackTy.AttributeTypes() { 236 if !wantTy.HasAttribute(attr) { 237 fallbackSubpath := fallbackPath.GetAttr(attr) 238 return fallbackSubpath.NewErrorf("target type does not expect an attribute named %q", attr) 239 } 240 } 241 return nil 242 case wantTy.IsTupleType(): 243 if !fallbackTy.IsTupleType() { 244 if inCollection { 245 return fallbackPath.NewErrorf("the default value for a collection of a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName()) 246 } 247 return fallbackPath.NewErrorf("the default value for a tuple type must itself be a tuple type, not %s", fallbackTy.FriendlyName()) 248 } 249 wantEtys := wantTy.TupleElementTypes() 250 fallbackEtys := fallbackTy.TupleElementTypes() 251 if got, want := len(wantEtys), len(fallbackEtys); got != want { 252 return fallbackPath.NewErrorf("the default value for a tuple type of length %d must also have length %d, not %d", want, want, got) 253 } 254 for i := 0; i < len(wantEtys); i++ { 255 fallbackSubpath := fallbackPath.IndexInt(i) 256 wantSubTy := wantEtys[i] 257 fallbackSubTy := fallbackEtys[i] 258 err := defaultsAssertSuitableFallback(wantSubTy, fallbackSubTy, fallbackSubpath) 259 if err != nil { 260 return err 261 } 262 } 263 return nil 264 default: 265 // No other types are supported right now. 266 return fallbackPath.NewErrorf("cannot apply defaults to %s", wantTy.FriendlyName()) 267 } 268 } 269 270 // Defaults is a helper function for substituting default values in 271 // place of null values in a given data structure. 272 // 273 // This is primarily intended for use with a module input variable that 274 // has an object type constraint (or a collection thereof) that has optional 275 // attributes, so that the receiver of a value that omits those attributes 276 // can insert non-null default values in place of the null values caused by 277 // omitting the attributes. 278 func Defaults(input, defaults cty.Value) (cty.Value, error) { 279 return DefaultsFunc.Call([]cty.Value{input, defaults}) 280 }