aboutsummaryrefslogtreecommitdiffhomepage
path: root/vendor/github.com/zclconf/go-cty/cty/convert/mismatch_msg.go
blob: 581304ecd5b0de5b7fa28e51d3aeec49d963541d (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
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))
}