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