]>
Commit | Line | Data |
---|---|---|
107c1cdb ND |
1 | package configschema |
2 | ||
3 | import ( | |
4 | "fmt" | |
5 | ||
6 | "github.com/zclconf/go-cty/cty" | |
7 | "github.com/zclconf/go-cty/cty/convert" | |
8 | ) | |
9 | ||
10 | // CoerceValue attempts to force the given value to conform to the type | |
11 | // implied by the receiever, while also applying the same validation and | |
12 | // transformation rules that would be applied by the decoder specification | |
13 | // returned by method DecoderSpec. | |
14 | // | |
15 | // This is useful in situations where a configuration must be derived from | |
16 | // an already-decoded value. It is always better to decode directly from | |
17 | // configuration where possible since then source location information is | |
18 | // still available to produce diagnostics, but in special situations this | |
19 | // function allows a compatible result to be obtained even if the | |
20 | // configuration objects are not available. | |
21 | // | |
22 | // If the given value cannot be converted to conform to the receiving schema | |
23 | // then an error is returned describing one of possibly many problems. This | |
24 | // error may be a cty.PathError indicating a position within the nested | |
25 | // data structure where the problem applies. | |
26 | func (b *Block) CoerceValue(in cty.Value) (cty.Value, error) { | |
27 | var path cty.Path | |
28 | return b.coerceValue(in, path) | |
29 | } | |
30 | ||
31 | func (b *Block) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { | |
32 | switch { | |
33 | case in.IsNull(): | |
34 | return cty.NullVal(b.ImpliedType()), nil | |
35 | case !in.IsKnown(): | |
36 | return cty.UnknownVal(b.ImpliedType()), nil | |
37 | } | |
38 | ||
39 | ty := in.Type() | |
40 | if !ty.IsObjectType() { | |
41 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("an object is required") | |
42 | } | |
43 | ||
44 | for name := range ty.AttributeTypes() { | |
45 | if _, defined := b.Attributes[name]; defined { | |
46 | continue | |
47 | } | |
48 | if _, defined := b.BlockTypes[name]; defined { | |
49 | continue | |
50 | } | |
51 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("unexpected attribute %q", name) | |
52 | } | |
53 | ||
54 | attrs := make(map[string]cty.Value) | |
55 | ||
56 | for name, attrS := range b.Attributes { | |
57 | var val cty.Value | |
58 | switch { | |
59 | case ty.HasAttribute(name): | |
60 | val = in.GetAttr(name) | |
61 | case attrS.Computed || attrS.Optional: | |
62 | val = cty.NullVal(attrS.Type) | |
63 | default: | |
64 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", name) | |
65 | } | |
66 | ||
67 | val, err := attrS.coerceValue(val, append(path, cty.GetAttrStep{Name: name})) | |
68 | if err != nil { | |
69 | return cty.UnknownVal(b.ImpliedType()), err | |
70 | } | |
71 | ||
72 | attrs[name] = val | |
73 | } | |
74 | for typeName, blockS := range b.BlockTypes { | |
75 | switch blockS.Nesting { | |
76 | ||
77 | case NestingSingle, NestingGroup: | |
78 | switch { | |
79 | case ty.HasAttribute(typeName): | |
80 | var err error | |
81 | val := in.GetAttr(typeName) | |
82 | attrs[typeName], err = blockS.coerceValue(val, append(path, cty.GetAttrStep{Name: typeName})) | |
83 | if err != nil { | |
84 | return cty.UnknownVal(b.ImpliedType()), err | |
85 | } | |
86 | case blockS.MinItems != 1 && blockS.MaxItems != 1: | |
87 | if blockS.Nesting == NestingGroup { | |
88 | attrs[typeName] = blockS.EmptyValue() | |
89 | } else { | |
90 | attrs[typeName] = cty.NullVal(blockS.ImpliedType()) | |
91 | } | |
92 | default: | |
93 | // We use the word "attribute" here because we're talking about | |
94 | // the cty sense of that word rather than the HCL sense. | |
95 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName) | |
96 | } | |
97 | ||
98 | case NestingList: | |
99 | switch { | |
100 | case ty.HasAttribute(typeName): | |
101 | coll := in.GetAttr(typeName) | |
102 | ||
103 | switch { | |
104 | case coll.IsNull(): | |
105 | attrs[typeName] = cty.NullVal(cty.List(blockS.ImpliedType())) | |
106 | continue | |
107 | case !coll.IsKnown(): | |
108 | attrs[typeName] = cty.UnknownVal(cty.List(blockS.ImpliedType())) | |
109 | continue | |
110 | } | |
111 | ||
112 | if !coll.CanIterateElements() { | |
113 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a list") | |
114 | } | |
115 | l := coll.LengthInt() | |
863486a6 AG |
116 | |
117 | // Assume that if there are unknowns this could have come from | |
118 | // a dynamic block, and we can't validate MinItems yet. | |
119 | if l < blockS.MinItems && coll.IsWhollyKnown() { | |
107c1cdb ND |
120 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems) |
121 | } | |
122 | if l > blockS.MaxItems && blockS.MaxItems > 0 { | |
123 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("too many items for attribute %q; cannot have more than %d", typeName, blockS.MaxItems) | |
124 | } | |
125 | if l == 0 { | |
126 | attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) | |
127 | continue | |
128 | } | |
129 | elems := make([]cty.Value, 0, l) | |
130 | { | |
131 | path = append(path, cty.GetAttrStep{Name: typeName}) | |
132 | for it := coll.ElementIterator(); it.Next(); { | |
133 | var err error | |
134 | idx, val := it.Element() | |
135 | val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx})) | |
136 | if err != nil { | |
137 | return cty.UnknownVal(b.ImpliedType()), err | |
138 | } | |
139 | elems = append(elems, val) | |
140 | } | |
141 | } | |
142 | attrs[typeName] = cty.ListVal(elems) | |
143 | case blockS.MinItems == 0: | |
144 | attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) | |
145 | default: | |
146 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName) | |
147 | } | |
148 | ||
149 | case NestingSet: | |
150 | switch { | |
151 | case ty.HasAttribute(typeName): | |
152 | coll := in.GetAttr(typeName) | |
153 | ||
154 | switch { | |
155 | case coll.IsNull(): | |
156 | attrs[typeName] = cty.NullVal(cty.Set(blockS.ImpliedType())) | |
157 | continue | |
158 | case !coll.IsKnown(): | |
159 | attrs[typeName] = cty.UnknownVal(cty.Set(blockS.ImpliedType())) | |
160 | continue | |
161 | } | |
162 | ||
163 | if !coll.CanIterateElements() { | |
164 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set") | |
165 | } | |
166 | l := coll.LengthInt() | |
863486a6 AG |
167 | |
168 | // Assume that if there are unknowns this could have come from | |
169 | // a dynamic block, and we can't validate MinItems yet. | |
170 | if l < blockS.MinItems && coll.IsWhollyKnown() { | |
107c1cdb ND |
171 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems) |
172 | } | |
173 | if l > blockS.MaxItems && blockS.MaxItems > 0 { | |
174 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("too many items for attribute %q; cannot have more than %d", typeName, blockS.MaxItems) | |
175 | } | |
176 | if l == 0 { | |
177 | attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) | |
178 | continue | |
179 | } | |
180 | elems := make([]cty.Value, 0, l) | |
181 | { | |
182 | path = append(path, cty.GetAttrStep{Name: typeName}) | |
183 | for it := coll.ElementIterator(); it.Next(); { | |
184 | var err error | |
185 | idx, val := it.Element() | |
186 | val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx})) | |
187 | if err != nil { | |
188 | return cty.UnknownVal(b.ImpliedType()), err | |
189 | } | |
190 | elems = append(elems, val) | |
191 | } | |
192 | } | |
193 | attrs[typeName] = cty.SetVal(elems) | |
194 | case blockS.MinItems == 0: | |
195 | attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) | |
196 | default: | |
197 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName) | |
198 | } | |
199 | ||
200 | case NestingMap: | |
201 | switch { | |
202 | case ty.HasAttribute(typeName): | |
203 | coll := in.GetAttr(typeName) | |
204 | ||
205 | switch { | |
206 | case coll.IsNull(): | |
207 | attrs[typeName] = cty.NullVal(cty.Map(blockS.ImpliedType())) | |
208 | continue | |
209 | case !coll.IsKnown(): | |
210 | attrs[typeName] = cty.UnknownVal(cty.Map(blockS.ImpliedType())) | |
211 | continue | |
212 | } | |
213 | ||
214 | if !coll.CanIterateElements() { | |
215 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map") | |
216 | } | |
217 | l := coll.LengthInt() | |
218 | if l == 0 { | |
219 | attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) | |
220 | continue | |
221 | } | |
222 | elems := make(map[string]cty.Value) | |
223 | { | |
224 | path = append(path, cty.GetAttrStep{Name: typeName}) | |
225 | for it := coll.ElementIterator(); it.Next(); { | |
226 | var err error | |
227 | key, val := it.Element() | |
228 | if key.Type() != cty.String || key.IsNull() || !key.IsKnown() { | |
229 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map") | |
230 | } | |
231 | val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key})) | |
232 | if err != nil { | |
233 | return cty.UnknownVal(b.ImpliedType()), err | |
234 | } | |
235 | elems[key.AsString()] = val | |
236 | } | |
237 | } | |
238 | ||
239 | // If the attribute values here contain any DynamicPseudoTypes, | |
240 | // the concrete type must be an object. | |
241 | useObject := false | |
242 | switch { | |
243 | case coll.Type().IsObjectType(): | |
244 | useObject = true | |
245 | default: | |
246 | // It's possible that we were given a map, and need to coerce it to an object | |
247 | ety := coll.Type().ElementType() | |
248 | for _, v := range elems { | |
249 | if !v.Type().Equals(ety) { | |
250 | useObject = true | |
251 | break | |
252 | } | |
253 | } | |
254 | } | |
255 | ||
256 | if useObject { | |
257 | attrs[typeName] = cty.ObjectVal(elems) | |
258 | } else { | |
259 | attrs[typeName] = cty.MapVal(elems) | |
260 | } | |
261 | default: | |
262 | attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) | |
263 | } | |
264 | ||
265 | default: | |
266 | // should never happen because above is exhaustive | |
267 | panic(fmt.Errorf("unsupported nesting mode %#v", blockS.Nesting)) | |
268 | } | |
269 | } | |
270 | ||
271 | return cty.ObjectVal(attrs), nil | |
272 | } | |
273 | ||
274 | func (a *Attribute) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { | |
275 | val, err := convert.Convert(in, a.Type) | |
276 | if err != nil { | |
277 | return cty.UnknownVal(a.Type), path.NewError(err) | |
278 | } | |
279 | return val, nil | |
280 | } |