1package convert 2 3import ( 4 "bytes" 5 "fmt" 6 "sort" 7 8 "github.com/zclconf/go-cty/cty" 9) 10 11// MismatchMessage is a helper to return an English-language description of 12// the differences between got and want, phrased as a reason why got does 13// not conform to want. 14// 15// This function does not itself attempt conversion, and so it should generally 16// be used only after a conversion has failed, to report the conversion failure 17// to an English-speaking user. The result will be confusing got is actually 18// conforming to or convertable to want. 19// 20// The shorthand helper function Convert uses this function internally to 21// produce its error messages, so callers of that function do not need to 22// also use MismatchMessage. 23// 24// This function is similar to Type.TestConformance, but it is tailored to 25// describing conversion failures and so the messages it generates relate 26// specifically to the conversion rules implemented in this package. 27func MismatchMessage(got, want cty.Type) string { 28 switch { 29 30 case got.IsObjectType() && want.IsObjectType(): 31 // If both types are object types then we may be able to say something 32 // about their respective attributes. 33 return mismatchMessageObjects(got, want) 34 35 case got.IsTupleType() && want.IsListType() && want.ElementType() == cty.DynamicPseudoType: 36 // If conversion from tuple to list failed then it's because we couldn't 37 // find a common type to convert all of the tuple elements to. 38 return "all list elements must have the same type" 39 40 case got.IsTupleType() && want.IsSetType() && want.ElementType() == cty.DynamicPseudoType: 41 // If conversion from tuple to set failed then it's because we couldn't 42 // find a common type to convert all of the tuple elements to. 43 return "all set elements must have the same type" 44 45 case got.IsObjectType() && want.IsMapType() && want.ElementType() == cty.DynamicPseudoType: 46 // If conversion from object to map failed then it's because we couldn't 47 // find a common type to convert all of the object attributes to. 48 return "all map elements must have the same type" 49 50 case (got.IsTupleType() || got.IsObjectType()) && want.IsCollectionType(): 51 return mismatchMessageCollectionsFromStructural(got, want) 52 53 case got.IsCollectionType() && want.IsCollectionType(): 54 return mismatchMessageCollectionsFromCollections(got, want) 55 56 default: 57 // If we have nothing better to say, we'll just state what was required. 58 return want.FriendlyNameForConstraint() + " required" 59 } 60} 61 62func mismatchMessageObjects(got, want cty.Type) string { 63 // Per our conversion rules, "got" is allowed to be a superset of "want", 64 // and so we'll produce error messages here under that assumption. 65 gotAtys := got.AttributeTypes() 66 wantAtys := want.AttributeTypes() 67 68 // If we find missing attributes then we'll report those in preference, 69 // but if not then we will report a maximum of one non-conforming 70 // attribute, just to keep our messages relatively terse. 71 // We'll also prefer to report a recursive type error from an _unsafe_ 72 // conversion over a safe one, because these are subjectively more 73 // "serious". 74 var missingAttrs []string 75 var unsafeMismatchAttr string 76 var safeMismatchAttr string 77 78 for name, wantAty := range wantAtys { 79 gotAty, exists := gotAtys[name] 80 if !exists { 81 if !want.AttributeOptional(name) { 82 missingAttrs = append(missingAttrs, name) 83 } 84 continue 85 } 86 87 if gotAty.Equals(wantAty) { 88 continue // exact match, so no problem 89 } 90 91 // We'll now try to convert these attributes in isolation and 92 // see if we have a nested conversion error to report. 93 // We'll try an unsafe conversion first, and then fall back on 94 // safe if unsafe is possible. 95 96 // If we already have an unsafe mismatch attr error then we won't bother 97 // hunting for another one. 98 if unsafeMismatchAttr != "" { 99 continue 100 } 101 if conv := GetConversionUnsafe(gotAty, wantAty); conv == nil { 102 unsafeMismatchAttr = fmt.Sprintf("attribute %q: %s", name, MismatchMessage(gotAty, wantAty)) 103 } 104 105 // If we already have a safe mismatch attr error then we won't bother 106 // hunting for another one. 107 if safeMismatchAttr != "" { 108 continue 109 } 110 if conv := GetConversion(gotAty, wantAty); conv == nil { 111 safeMismatchAttr = fmt.Sprintf("attribute %q: %s", name, MismatchMessage(gotAty, wantAty)) 112 } 113 } 114 115 // We should now have collected at least one problem. If we have more than 116 // one then we'll use our preference order to decide what is most important 117 // to report. 118 switch { 119 120 case len(missingAttrs) != 0: 121 sort.Strings(missingAttrs) 122 switch len(missingAttrs) { 123 case 1: 124 return fmt.Sprintf("attribute %q is required", missingAttrs[0]) 125 case 2: 126 return fmt.Sprintf("attributes %q and %q are required", missingAttrs[0], missingAttrs[1]) 127 default: 128 sort.Strings(missingAttrs) 129 var buf bytes.Buffer 130 for _, name := range missingAttrs[:len(missingAttrs)-1] { 131 fmt.Fprintf(&buf, "%q, ", name) 132 } 133 fmt.Fprintf(&buf, "and %q", missingAttrs[len(missingAttrs)-1]) 134 return fmt.Sprintf("attributes %s are required", buf.Bytes()) 135 } 136 137 case unsafeMismatchAttr != "": 138 return unsafeMismatchAttr 139 140 case safeMismatchAttr != "": 141 return safeMismatchAttr 142 143 default: 144 // We should never get here, but if we do then we'll return 145 // just a generic message. 146 return "incorrect object attributes" 147 } 148} 149 150func mismatchMessageCollectionsFromStructural(got, want cty.Type) string { 151 // First some straightforward cases where the kind is just altogether wrong. 152 switch { 153 case want.IsListType() && !got.IsTupleType(): 154 return want.FriendlyNameForConstraint() + " required" 155 case want.IsSetType() && !got.IsTupleType(): 156 return want.FriendlyNameForConstraint() + " required" 157 case want.IsMapType() && !got.IsObjectType(): 158 return want.FriendlyNameForConstraint() + " required" 159 } 160 161 // If the kinds are matched well enough then we'll move on to checking 162 // individual elements. 163 wantEty := want.ElementType() 164 switch { 165 case got.IsTupleType(): 166 for i, gotEty := range got.TupleElementTypes() { 167 if gotEty.Equals(wantEty) { 168 continue // exact match, so no problem 169 } 170 if conv := getConversion(gotEty, wantEty, true); conv != nil { 171 continue // conversion is available, so no problem 172 } 173 return fmt.Sprintf("element %d: %s", i, MismatchMessage(gotEty, wantEty)) 174 } 175 176 // If we get down here then something weird is going on but we'll 177 // return a reasonable fallback message anyway. 178 return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint()) 179 180 case got.IsObjectType(): 181 for name, gotAty := range got.AttributeTypes() { 182 if gotAty.Equals(wantEty) { 183 continue // exact match, so no problem 184 } 185 if conv := getConversion(gotAty, wantEty, true); conv != nil { 186 continue // conversion is available, so no problem 187 } 188 return fmt.Sprintf("element %q: %s", name, MismatchMessage(gotAty, wantEty)) 189 } 190 191 // If we get down here then something weird is going on but we'll 192 // return a reasonable fallback message anyway. 193 return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint()) 194 195 default: 196 // Should not be possible to get here since we only call this function 197 // with got as structural types, but... 198 return want.FriendlyNameForConstraint() + " required" 199 } 200} 201 202func mismatchMessageCollectionsFromCollections(got, want cty.Type) string { 203 // First some straightforward cases where the kind is just altogether wrong. 204 switch { 205 case want.IsListType() && !(got.IsListType() || got.IsSetType()): 206 return want.FriendlyNameForConstraint() + " required" 207 case want.IsSetType() && !(got.IsListType() || got.IsSetType()): 208 return want.FriendlyNameForConstraint() + " required" 209 case want.IsMapType() && !got.IsMapType(): 210 return want.FriendlyNameForConstraint() + " required" 211 } 212 213 // If the kinds are matched well enough then we'll check the element types. 214 gotEty := got.ElementType() 215 wantEty := want.ElementType() 216 noun := "element type" 217 switch { 218 case want.IsListType(): 219 noun = "list element type" 220 case want.IsSetType(): 221 noun = "set element type" 222 case want.IsMapType(): 223 noun = "map element type" 224 } 225 return fmt.Sprintf("incorrect %s: %s", noun, MismatchMessage(gotEty, wantEty)) 226} 227