]>
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() | |
116 | if l < blockS.MinItems { | |
117 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems) | |
118 | } | |
119 | if l > blockS.MaxItems && blockS.MaxItems > 0 { | |
120 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("too many items for attribute %q; cannot have more than %d", typeName, blockS.MaxItems) | |
121 | } | |
122 | if l == 0 { | |
123 | attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) | |
124 | continue | |
125 | } | |
126 | elems := make([]cty.Value, 0, l) | |
127 | { | |
128 | path = append(path, cty.GetAttrStep{Name: typeName}) | |
129 | for it := coll.ElementIterator(); it.Next(); { | |
130 | var err error | |
131 | idx, val := it.Element() | |
132 | val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx})) | |
133 | if err != nil { | |
134 | return cty.UnknownVal(b.ImpliedType()), err | |
135 | } | |
136 | elems = append(elems, val) | |
137 | } | |
138 | } | |
139 | attrs[typeName] = cty.ListVal(elems) | |
140 | case blockS.MinItems == 0: | |
141 | attrs[typeName] = cty.ListValEmpty(blockS.ImpliedType()) | |
142 | default: | |
143 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName) | |
144 | } | |
145 | ||
146 | case NestingSet: | |
147 | switch { | |
148 | case ty.HasAttribute(typeName): | |
149 | coll := in.GetAttr(typeName) | |
150 | ||
151 | switch { | |
152 | case coll.IsNull(): | |
153 | attrs[typeName] = cty.NullVal(cty.Set(blockS.ImpliedType())) | |
154 | continue | |
155 | case !coll.IsKnown(): | |
156 | attrs[typeName] = cty.UnknownVal(cty.Set(blockS.ImpliedType())) | |
157 | continue | |
158 | } | |
159 | ||
160 | if !coll.CanIterateElements() { | |
161 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a set") | |
162 | } | |
163 | l := coll.LengthInt() | |
164 | if l < blockS.MinItems { | |
165 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("insufficient items for attribute %q; must have at least %d", typeName, blockS.MinItems) | |
166 | } | |
167 | if l > blockS.MaxItems && blockS.MaxItems > 0 { | |
168 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("too many items for attribute %q; cannot have more than %d", typeName, blockS.MaxItems) | |
169 | } | |
170 | if l == 0 { | |
171 | attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) | |
172 | continue | |
173 | } | |
174 | elems := make([]cty.Value, 0, l) | |
175 | { | |
176 | path = append(path, cty.GetAttrStep{Name: typeName}) | |
177 | for it := coll.ElementIterator(); it.Next(); { | |
178 | var err error | |
179 | idx, val := it.Element() | |
180 | val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: idx})) | |
181 | if err != nil { | |
182 | return cty.UnknownVal(b.ImpliedType()), err | |
183 | } | |
184 | elems = append(elems, val) | |
185 | } | |
186 | } | |
187 | attrs[typeName] = cty.SetVal(elems) | |
188 | case blockS.MinItems == 0: | |
189 | attrs[typeName] = cty.SetValEmpty(blockS.ImpliedType()) | |
190 | default: | |
191 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("attribute %q is required", typeName) | |
192 | } | |
193 | ||
194 | case NestingMap: | |
195 | switch { | |
196 | case ty.HasAttribute(typeName): | |
197 | coll := in.GetAttr(typeName) | |
198 | ||
199 | switch { | |
200 | case coll.IsNull(): | |
201 | attrs[typeName] = cty.NullVal(cty.Map(blockS.ImpliedType())) | |
202 | continue | |
203 | case !coll.IsKnown(): | |
204 | attrs[typeName] = cty.UnknownVal(cty.Map(blockS.ImpliedType())) | |
205 | continue | |
206 | } | |
207 | ||
208 | if !coll.CanIterateElements() { | |
209 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map") | |
210 | } | |
211 | l := coll.LengthInt() | |
212 | if l == 0 { | |
213 | attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) | |
214 | continue | |
215 | } | |
216 | elems := make(map[string]cty.Value) | |
217 | { | |
218 | path = append(path, cty.GetAttrStep{Name: typeName}) | |
219 | for it := coll.ElementIterator(); it.Next(); { | |
220 | var err error | |
221 | key, val := it.Element() | |
222 | if key.Type() != cty.String || key.IsNull() || !key.IsKnown() { | |
223 | return cty.UnknownVal(b.ImpliedType()), path.NewErrorf("must be a map") | |
224 | } | |
225 | val, err = blockS.coerceValue(val, append(path, cty.IndexStep{Key: key})) | |
226 | if err != nil { | |
227 | return cty.UnknownVal(b.ImpliedType()), err | |
228 | } | |
229 | elems[key.AsString()] = val | |
230 | } | |
231 | } | |
232 | ||
233 | // If the attribute values here contain any DynamicPseudoTypes, | |
234 | // the concrete type must be an object. | |
235 | useObject := false | |
236 | switch { | |
237 | case coll.Type().IsObjectType(): | |
238 | useObject = true | |
239 | default: | |
240 | // It's possible that we were given a map, and need to coerce it to an object | |
241 | ety := coll.Type().ElementType() | |
242 | for _, v := range elems { | |
243 | if !v.Type().Equals(ety) { | |
244 | useObject = true | |
245 | break | |
246 | } | |
247 | } | |
248 | } | |
249 | ||
250 | if useObject { | |
251 | attrs[typeName] = cty.ObjectVal(elems) | |
252 | } else { | |
253 | attrs[typeName] = cty.MapVal(elems) | |
254 | } | |
255 | default: | |
256 | attrs[typeName] = cty.MapValEmpty(blockS.ImpliedType()) | |
257 | } | |
258 | ||
259 | default: | |
260 | // should never happen because above is exhaustive | |
261 | panic(fmt.Errorf("unsupported nesting mode %#v", blockS.Nesting)) | |
262 | } | |
263 | } | |
264 | ||
265 | return cty.ObjectVal(attrs), nil | |
266 | } | |
267 | ||
268 | func (a *Attribute) coerceValue(in cty.Value, path cty.Path) (cty.Value, error) { | |
269 | val, err := convert.Convert(in, a.Type) | |
270 | if err != nil { | |
271 | return cty.UnknownVal(a.Type), path.NewError(err) | |
272 | } | |
273 | return val, nil | |
274 | } |