package convert import ( "bytes" "fmt" "sort" "github.com/zclconf/go-cty/cty" ) // MismatchMessage is a helper to return an English-language description of // the differences between got and want, phrased as a reason why got does // not conform to want. // // This function does not itself attempt conversion, and so it should generally // be used only after a conversion has failed, to report the conversion failure // to an English-speaking user. The result will be confusing got is actually // conforming to or convertable to want. // // The shorthand helper function Convert uses this function internally to // produce its error messages, so callers of that function do not need to // also use MismatchMessage. // // This function is similar to Type.TestConformance, but it is tailored to // describing conversion failures and so the messages it generates relate // specifically to the conversion rules implemented in this package. func MismatchMessage(got, want cty.Type) string { switch { case got.IsObjectType() && want.IsObjectType(): // If both types are object types then we may be able to say something // about their respective attributes. return mismatchMessageObjects(got, want) case got.IsTupleType() && want.IsListType() && want.ElementType() == cty.DynamicPseudoType: // If conversion from tuple to list failed then it's because we couldn't // find a common type to convert all of the tuple elements to. return "all list elements must have the same type" case got.IsTupleType() && want.IsSetType() && want.ElementType() == cty.DynamicPseudoType: // If conversion from tuple to set failed then it's because we couldn't // find a common type to convert all of the tuple elements to. return "all set elements must have the same type" case got.IsObjectType() && want.IsMapType() && want.ElementType() == cty.DynamicPseudoType: // If conversion from object to map failed then it's because we couldn't // find a common type to convert all of the object attributes to. return "all map elements must have the same type" case (got.IsTupleType() || got.IsObjectType()) && want.IsCollectionType(): return mismatchMessageCollectionsFromStructural(got, want) case got.IsCollectionType() && want.IsCollectionType(): return mismatchMessageCollectionsFromCollections(got, want) default: // If we have nothing better to say, we'll just state what was required. return want.FriendlyNameForConstraint() + " required" } } func mismatchMessageObjects(got, want cty.Type) string { // Per our conversion rules, "got" is allowed to be a superset of "want", // and so we'll produce error messages here under that assumption. gotAtys := got.AttributeTypes() wantAtys := want.AttributeTypes() // If we find missing attributes then we'll report those in preference, // but if not then we will report a maximum of one non-conforming // attribute, just to keep our messages relatively terse. // We'll also prefer to report a recursive type error from an _unsafe_ // conversion over a safe one, because these are subjectively more // "serious". var missingAttrs []string var unsafeMismatchAttr string var safeMismatchAttr string for name, wantAty := range wantAtys { gotAty, exists := gotAtys[name] if !exists { missingAttrs = append(missingAttrs, name) continue } // We'll now try to convert these attributes in isolation and // see if we have a nested conversion error to report. // We'll try an unsafe conversion first, and then fall back on // safe if unsafe is possible. // If we already have an unsafe mismatch attr error then we won't bother // hunting for another one. if unsafeMismatchAttr != "" { continue } if conv := GetConversionUnsafe(gotAty, wantAty); conv == nil { unsafeMismatchAttr = fmt.Sprintf("attribute %q: %s", name, MismatchMessage(gotAty, wantAty)) } // If we already have a safe mismatch attr error then we won't bother // hunting for another one. if safeMismatchAttr != "" { continue } if conv := GetConversion(gotAty, wantAty); conv == nil { safeMismatchAttr = fmt.Sprintf("attribute %q: %s", name, MismatchMessage(gotAty, wantAty)) } } // We should now have collected at least one problem. If we have more than // one then we'll use our preference order to decide what is most important // to report. switch { case len(missingAttrs) != 0: sort.Strings(missingAttrs) switch len(missingAttrs) { case 1: return fmt.Sprintf("attribute %q is required", missingAttrs[0]) case 2: return fmt.Sprintf("attributes %q and %q are required", missingAttrs[0], missingAttrs[1]) default: sort.Strings(missingAttrs) var buf bytes.Buffer for _, name := range missingAttrs[:len(missingAttrs)-1] { fmt.Fprintf(&buf, "%q, ", name) } fmt.Fprintf(&buf, "and %q", missingAttrs[len(missingAttrs)-1]) return fmt.Sprintf("attributes %s are required", buf.Bytes()) } case unsafeMismatchAttr != "": return unsafeMismatchAttr case safeMismatchAttr != "": return safeMismatchAttr default: // We should never get here, but if we do then we'll return // just a generic message. return "incorrect object attributes" } } func mismatchMessageCollectionsFromStructural(got, want cty.Type) string { // First some straightforward cases where the kind is just altogether wrong. switch { case want.IsListType() && !got.IsTupleType(): return want.FriendlyNameForConstraint() + " required" case want.IsSetType() && !got.IsTupleType(): return want.FriendlyNameForConstraint() + " required" case want.IsMapType() && !got.IsObjectType(): return want.FriendlyNameForConstraint() + " required" } // If the kinds are matched well enough then we'll move on to checking // individual elements. wantEty := want.ElementType() switch { case got.IsTupleType(): for i, gotEty := range got.TupleElementTypes() { if gotEty.Equals(wantEty) { continue // exact match, so no problem } if conv := getConversion(gotEty, wantEty, true); conv != nil { continue // conversion is available, so no problem } return fmt.Sprintf("element %d: %s", i, MismatchMessage(gotEty, wantEty)) } // If we get down here then something weird is going on but we'll // return a reasonable fallback message anyway. return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint()) case got.IsObjectType(): for name, gotAty := range got.AttributeTypes() { if gotAty.Equals(wantEty) { continue // exact match, so no problem } if conv := getConversion(gotAty, wantEty, true); conv != nil { continue // conversion is available, so no problem } return fmt.Sprintf("element %q: %s", name, MismatchMessage(gotAty, wantEty)) } // If we get down here then something weird is going on but we'll // return a reasonable fallback message anyway. return fmt.Sprintf("all elements must be %s", wantEty.FriendlyNameForConstraint()) default: // Should not be possible to get here since we only call this function // with got as structural types, but... return want.FriendlyNameForConstraint() + " required" } } func mismatchMessageCollectionsFromCollections(got, want cty.Type) string { // First some straightforward cases where the kind is just altogether wrong. switch { case want.IsListType() && !(got.IsListType() || got.IsSetType()): return want.FriendlyNameForConstraint() + " required" case want.IsSetType() && !(got.IsListType() || got.IsSetType()): return want.FriendlyNameForConstraint() + " required" case want.IsMapType() && !got.IsMapType(): return want.FriendlyNameForConstraint() + " required" } // If the kinds are matched well enough then we'll check the element types. gotEty := got.ElementType() wantEty := want.ElementType() noun := "element type" switch { case want.IsListType(): noun = "list element type" case want.IsSetType(): noun = "set element type" case want.IsMapType(): noun = "map element type" } return fmt.Sprintf("incorrect %s: %s", noun, MismatchMessage(gotEty, wantEty)) }