github.com/eliastor/durgaform@v0.0.0-20220816172711-d0ab2d17673e/internal/typeexpr/get_type.go (about) 1 package typeexpr 2 3 import ( 4 "fmt" 5 6 "github.com/hashicorp/hcl/v2" 7 "github.com/zclconf/go-cty/cty" 8 "github.com/zclconf/go-cty/cty/convert" 9 ) 10 11 const invalidTypeSummary = "Invalid type specification" 12 13 // getType is the internal implementation of Type, TypeConstraint, and 14 // TypeConstraintWithDefaults, using the passed flags to distinguish. When 15 // `constraint` is true, the "any" keyword can be used in place of a concrete 16 // type. When `withDefaults` is true, the "optional" call expression supports 17 // an additional argument describing a default value. 18 func getType(expr hcl.Expression, constraint, withDefaults bool) (cty.Type, *Defaults, hcl.Diagnostics) { 19 // First we'll try for one of our keywords 20 kw := hcl.ExprAsKeyword(expr) 21 switch kw { 22 case "bool": 23 return cty.Bool, nil, nil 24 case "string": 25 return cty.String, nil, nil 26 case "number": 27 return cty.Number, nil, nil 28 case "any": 29 if constraint { 30 return cty.DynamicPseudoType, nil, nil 31 } 32 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 33 Severity: hcl.DiagError, 34 Summary: invalidTypeSummary, 35 Detail: fmt.Sprintf("The keyword %q cannot be used in this type specification: an exact type is required.", kw), 36 Subject: expr.Range().Ptr(), 37 }} 38 case "list", "map", "set": 39 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 40 Severity: hcl.DiagError, 41 Summary: invalidTypeSummary, 42 Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", kw), 43 Subject: expr.Range().Ptr(), 44 }} 45 case "object": 46 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 47 Severity: hcl.DiagError, 48 Summary: invalidTypeSummary, 49 Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", 50 Subject: expr.Range().Ptr(), 51 }} 52 case "tuple": 53 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 54 Severity: hcl.DiagError, 55 Summary: invalidTypeSummary, 56 Detail: "The tuple type constructor requires one argument specifying the element types as a list.", 57 Subject: expr.Range().Ptr(), 58 }} 59 case "": 60 // okay! we'll fall through and try processing as a call, then. 61 default: 62 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 63 Severity: hcl.DiagError, 64 Summary: invalidTypeSummary, 65 Detail: fmt.Sprintf("The keyword %q is not a valid type specification.", kw), 66 Subject: expr.Range().Ptr(), 67 }} 68 } 69 70 // If we get down here then our expression isn't just a keyword, so we'll 71 // try to process it as a call instead. 72 call, diags := hcl.ExprCall(expr) 73 if diags.HasErrors() { 74 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 75 Severity: hcl.DiagError, 76 Summary: invalidTypeSummary, 77 Detail: "A type specification is either a primitive type keyword (bool, number, string) or a complex type constructor call, like list(string).", 78 Subject: expr.Range().Ptr(), 79 }} 80 } 81 82 switch call.Name { 83 case "bool", "string", "number": 84 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 85 Severity: hcl.DiagError, 86 Summary: invalidTypeSummary, 87 Detail: fmt.Sprintf("Primitive type keyword %q does not expect arguments.", call.Name), 88 Subject: &call.ArgsRange, 89 }} 90 case "any": 91 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 92 Severity: hcl.DiagError, 93 Summary: invalidTypeSummary, 94 Detail: fmt.Sprintf("Type constraint keyword %q does not expect arguments.", call.Name), 95 Subject: &call.ArgsRange, 96 }} 97 } 98 99 if len(call.Arguments) != 1 { 100 contextRange := call.ArgsRange 101 subjectRange := call.ArgsRange 102 if len(call.Arguments) > 1 { 103 // If we have too many arguments (as opposed to too _few_) then 104 // we'll highlight the extraneous arguments as the diagnostic 105 // subject. 106 subjectRange = hcl.RangeBetween(call.Arguments[1].Range(), call.Arguments[len(call.Arguments)-1].Range()) 107 } 108 109 switch call.Name { 110 case "list", "set", "map": 111 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 112 Severity: hcl.DiagError, 113 Summary: invalidTypeSummary, 114 Detail: fmt.Sprintf("The %s type constructor requires one argument specifying the element type.", call.Name), 115 Subject: &subjectRange, 116 Context: &contextRange, 117 }} 118 case "object": 119 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 120 Severity: hcl.DiagError, 121 Summary: invalidTypeSummary, 122 Detail: "The object type constructor requires one argument specifying the attribute types and values as a map.", 123 Subject: &subjectRange, 124 Context: &contextRange, 125 }} 126 case "tuple": 127 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 128 Severity: hcl.DiagError, 129 Summary: invalidTypeSummary, 130 Detail: "The tuple type constructor requires one argument specifying the element types as a list.", 131 Subject: &subjectRange, 132 Context: &contextRange, 133 }} 134 } 135 } 136 137 switch call.Name { 138 139 case "list": 140 ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) 141 ty := cty.List(ety) 142 return ty, collectionDefaults(ty, defaults), diags 143 case "set": 144 ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) 145 ty := cty.Set(ety) 146 return ty, collectionDefaults(ty, defaults), diags 147 case "map": 148 ety, defaults, diags := getType(call.Arguments[0], constraint, withDefaults) 149 ty := cty.Map(ety) 150 return ty, collectionDefaults(ty, defaults), diags 151 case "object": 152 attrDefs, diags := hcl.ExprMap(call.Arguments[0]) 153 if diags.HasErrors() { 154 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 155 Severity: hcl.DiagError, 156 Summary: invalidTypeSummary, 157 Detail: "Object type constructor requires a map whose keys are attribute names and whose values are the corresponding attribute types.", 158 Subject: call.Arguments[0].Range().Ptr(), 159 Context: expr.Range().Ptr(), 160 }} 161 } 162 163 atys := make(map[string]cty.Type) 164 defaultValues := make(map[string]cty.Value) 165 children := make(map[string]*Defaults) 166 var optAttrs []string 167 for _, attrDef := range attrDefs { 168 attrName := hcl.ExprAsKeyword(attrDef.Key) 169 if attrName == "" { 170 diags = append(diags, &hcl.Diagnostic{ 171 Severity: hcl.DiagError, 172 Summary: invalidTypeSummary, 173 Detail: "Object constructor map keys must be attribute names.", 174 Subject: attrDef.Key.Range().Ptr(), 175 Context: expr.Range().Ptr(), 176 }) 177 continue 178 } 179 atyExpr := attrDef.Value 180 181 // the attribute type expression might be wrapped in the special 182 // modifier optional(...) to indicate an optional attribute. If 183 // so, we'll unwrap that first and make a note about it being 184 // optional for when we construct the type below. 185 var defaultExpr hcl.Expression 186 if call, callDiags := hcl.ExprCall(atyExpr); !callDiags.HasErrors() { 187 if call.Name == "optional" { 188 if len(call.Arguments) < 1 { 189 diags = append(diags, &hcl.Diagnostic{ 190 Severity: hcl.DiagError, 191 Summary: invalidTypeSummary, 192 Detail: "Optional attribute modifier requires the attribute type as its argument.", 193 Subject: call.ArgsRange.Ptr(), 194 Context: atyExpr.Range().Ptr(), 195 }) 196 continue 197 } 198 if constraint { 199 if withDefaults { 200 switch len(call.Arguments) { 201 case 2: 202 defaultExpr = call.Arguments[1] 203 defaultVal, defaultDiags := defaultExpr.Value(nil) 204 diags = append(diags, defaultDiags...) 205 if !defaultDiags.HasErrors() { 206 optAttrs = append(optAttrs, attrName) 207 defaultValues[attrName] = defaultVal 208 } 209 case 1: 210 optAttrs = append(optAttrs, attrName) 211 default: 212 diags = append(diags, &hcl.Diagnostic{ 213 Severity: hcl.DiagError, 214 Summary: invalidTypeSummary, 215 Detail: "Optional attribute modifier expects at most two arguments: the attribute type, and a default value.", 216 Subject: call.ArgsRange.Ptr(), 217 Context: atyExpr.Range().Ptr(), 218 }) 219 } 220 } else { 221 if len(call.Arguments) == 1 { 222 optAttrs = append(optAttrs, attrName) 223 } else { 224 diags = append(diags, &hcl.Diagnostic{ 225 Severity: hcl.DiagError, 226 Summary: invalidTypeSummary, 227 Detail: "Optional attribute modifier expects only one argument: the attribute type.", 228 Subject: call.ArgsRange.Ptr(), 229 Context: atyExpr.Range().Ptr(), 230 }) 231 } 232 } 233 } else { 234 diags = append(diags, &hcl.Diagnostic{ 235 Severity: hcl.DiagError, 236 Summary: invalidTypeSummary, 237 Detail: "Optional attribute modifier is only for type constraints, not for exact types.", 238 Subject: call.NameRange.Ptr(), 239 Context: atyExpr.Range().Ptr(), 240 }) 241 } 242 atyExpr = call.Arguments[0] 243 } 244 } 245 246 aty, aDefaults, attrDiags := getType(atyExpr, constraint, withDefaults) 247 diags = append(diags, attrDiags...) 248 249 // If a default is set for an optional attribute, verify that it is 250 // convertible to the attribute type. 251 if defaultVal, ok := defaultValues[attrName]; ok { 252 _, err := convert.Convert(defaultVal, aty) 253 if err != nil { 254 diags = append(diags, &hcl.Diagnostic{ 255 Severity: hcl.DiagError, 256 Summary: "Invalid default value for optional attribute", 257 Detail: fmt.Sprintf("This default value is not compatible with the attribute's type constraint: %s.", err), 258 Subject: defaultExpr.Range().Ptr(), 259 }) 260 delete(defaultValues, attrName) 261 } 262 } 263 264 atys[attrName] = aty 265 if aDefaults != nil { 266 children[attrName] = aDefaults 267 } 268 } 269 // NOTE: ObjectWithOptionalAttrs is experimental in cty at the 270 // time of writing, so this interface might change even in future 271 // minor versions of cty. We're accepting that because Durgaform 272 // itself is considering optional attributes as experimental right now. 273 ty := cty.ObjectWithOptionalAttrs(atys, optAttrs) 274 return ty, structuredDefaults(ty, defaultValues, children), diags 275 case "tuple": 276 elemDefs, diags := hcl.ExprList(call.Arguments[0]) 277 if diags.HasErrors() { 278 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 279 Severity: hcl.DiagError, 280 Summary: invalidTypeSummary, 281 Detail: "Tuple type constructor requires a list of element types.", 282 Subject: call.Arguments[0].Range().Ptr(), 283 Context: expr.Range().Ptr(), 284 }} 285 } 286 etys := make([]cty.Type, len(elemDefs)) 287 children := make(map[string]*Defaults, len(elemDefs)) 288 for i, defExpr := range elemDefs { 289 ety, elemDefaults, elemDiags := getType(defExpr, constraint, withDefaults) 290 diags = append(diags, elemDiags...) 291 etys[i] = ety 292 if elemDefaults != nil { 293 children[fmt.Sprintf("%d", i)] = elemDefaults 294 } 295 } 296 ty := cty.Tuple(etys) 297 return ty, structuredDefaults(ty, nil, children), diags 298 case "optional": 299 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 300 Severity: hcl.DiagError, 301 Summary: invalidTypeSummary, 302 Detail: fmt.Sprintf("Keyword %q is valid only as a modifier for object type attributes.", call.Name), 303 Subject: call.NameRange.Ptr(), 304 }} 305 default: 306 // Can't access call.Arguments in this path because we've not validated 307 // that it contains exactly one expression here. 308 return cty.DynamicPseudoType, nil, hcl.Diagnostics{{ 309 Severity: hcl.DiagError, 310 Summary: invalidTypeSummary, 311 Detail: fmt.Sprintf("Keyword %q is not a valid type constructor.", call.Name), 312 Subject: expr.Range().Ptr(), 313 }} 314 } 315 } 316 317 func collectionDefaults(ty cty.Type, defaults *Defaults) *Defaults { 318 if defaults == nil { 319 return nil 320 } 321 return &Defaults{ 322 Type: ty, 323 Children: map[string]*Defaults{ 324 "": defaults, 325 }, 326 } 327 } 328 329 func structuredDefaults(ty cty.Type, defaultValues map[string]cty.Value, children map[string]*Defaults) *Defaults { 330 if len(defaultValues) == 0 && len(children) == 0 { 331 return nil 332 } 333 334 defaults := &Defaults{ 335 Type: ty, 336 } 337 if len(defaultValues) > 0 { 338 defaults.DefaultValues = defaultValues 339 } 340 if len(children) > 0 { 341 defaults.Children = children 342 } 343 344 return defaults 345 }