]>
Commit | Line | Data |
---|---|---|
15c0b25d AP |
1 | package hcl |
2 | ||
3 | import ( | |
4 | "fmt" | |
107c1cdb | 5 | "math/big" |
15c0b25d AP |
6 | |
7 | "github.com/zclconf/go-cty/cty" | |
8 | "github.com/zclconf/go-cty/cty/convert" | |
9 | ) | |
10 | ||
11 | // Index is a helper function that performs the same operation as the index | |
12 | // operator in the HCL expression language. That is, the result is the | |
13 | // same as it would be for collection[key] in a configuration expression. | |
14 | // | |
15 | // This is exported so that applications can perform indexing in a manner | |
16 | // consistent with how the language does it, including handling of null and | |
17 | // unknown values, etc. | |
18 | // | |
19 | // Diagnostics are produced if the given combination of values is not valid. | |
20 | // Therefore a pointer to a source range must be provided to use in diagnostics, | |
21 | // though nil can be provided if the calling application is going to | |
22 | // ignore the subject of the returned diagnostics anyway. | |
23 | func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) { | |
24 | if collection.IsNull() { | |
25 | return cty.DynamicVal, Diagnostics{ | |
26 | { | |
27 | Severity: DiagError, | |
28 | Summary: "Attempt to index null value", | |
29 | Detail: "This value is null, so it does not have any indices.", | |
30 | Subject: srcRange, | |
31 | }, | |
32 | } | |
33 | } | |
34 | if key.IsNull() { | |
35 | return cty.DynamicVal, Diagnostics{ | |
36 | { | |
37 | Severity: DiagError, | |
38 | Summary: "Invalid index", | |
39 | Detail: "Can't use a null value as an indexing key.", | |
40 | Subject: srcRange, | |
41 | }, | |
42 | } | |
43 | } | |
44 | ty := collection.Type() | |
45 | kty := key.Type() | |
46 | if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType { | |
47 | return cty.DynamicVal, nil | |
48 | } | |
49 | ||
50 | switch { | |
51 | ||
52 | case ty.IsListType() || ty.IsTupleType() || ty.IsMapType(): | |
53 | var wantType cty.Type | |
54 | switch { | |
55 | case ty.IsListType() || ty.IsTupleType(): | |
56 | wantType = cty.Number | |
57 | case ty.IsMapType(): | |
58 | wantType = cty.String | |
59 | default: | |
60 | // should never happen | |
61 | panic("don't know what key type we want") | |
62 | } | |
63 | ||
64 | key, keyErr := convert.Convert(key, wantType) | |
65 | if keyErr != nil { | |
66 | return cty.DynamicVal, Diagnostics{ | |
67 | { | |
68 | Severity: DiagError, | |
69 | Summary: "Invalid index", | |
70 | Detail: fmt.Sprintf( | |
71 | "The given key does not identify an element in this collection value: %s.", | |
72 | keyErr.Error(), | |
73 | ), | |
74 | Subject: srcRange, | |
75 | }, | |
76 | } | |
77 | } | |
78 | ||
79 | has := collection.HasIndex(key) | |
80 | if !has.IsKnown() { | |
81 | if ty.IsTupleType() { | |
82 | return cty.DynamicVal, nil | |
83 | } else { | |
84 | return cty.UnknownVal(ty.ElementType()), nil | |
85 | } | |
86 | } | |
87 | if has.False() { | |
107c1cdb ND |
88 | // We have a more specialized error message for the situation of |
89 | // using a fractional number to index into a sequence, because | |
90 | // that will tend to happen if the user is trying to use division | |
91 | // to calculate an index and not realizing that HCL does float | |
92 | // division rather than integer division. | |
93 | if (ty.IsListType() || ty.IsTupleType()) && key.Type().Equals(cty.Number) { | |
94 | if key.IsKnown() && !key.IsNull() { | |
95 | bf := key.AsBigFloat() | |
96 | if _, acc := bf.Int(nil); acc != big.Exact { | |
97 | return cty.DynamicVal, Diagnostics{ | |
98 | { | |
99 | Severity: DiagError, | |
100 | Summary: "Invalid index", | |
101 | Detail: fmt.Sprintf("The given key does not identify an element in this collection value: indexing a sequence requires a whole number, but the given index (%g) has a fractional part.", bf), | |
102 | Subject: srcRange, | |
103 | }, | |
104 | } | |
105 | } | |
106 | } | |
107 | } | |
108 | ||
15c0b25d AP |
109 | return cty.DynamicVal, Diagnostics{ |
110 | { | |
111 | Severity: DiagError, | |
112 | Summary: "Invalid index", | |
113 | Detail: "The given key does not identify an element in this collection value.", | |
114 | Subject: srcRange, | |
115 | }, | |
116 | } | |
117 | } | |
118 | ||
119 | return collection.Index(key), nil | |
120 | ||
121 | case ty.IsObjectType(): | |
122 | key, keyErr := convert.Convert(key, cty.String) | |
123 | if keyErr != nil { | |
124 | return cty.DynamicVal, Diagnostics{ | |
125 | { | |
126 | Severity: DiagError, | |
127 | Summary: "Invalid index", | |
128 | Detail: fmt.Sprintf( | |
129 | "The given key does not identify an element in this collection value: %s.", | |
130 | keyErr.Error(), | |
131 | ), | |
132 | Subject: srcRange, | |
133 | }, | |
134 | } | |
135 | } | |
136 | if !collection.IsKnown() { | |
137 | return cty.DynamicVal, nil | |
138 | } | |
139 | if !key.IsKnown() { | |
140 | return cty.DynamicVal, nil | |
141 | } | |
142 | ||
143 | attrName := key.AsString() | |
144 | ||
145 | if !ty.HasAttribute(attrName) { | |
146 | return cty.DynamicVal, Diagnostics{ | |
147 | { | |
148 | Severity: DiagError, | |
149 | Summary: "Invalid index", | |
150 | Detail: "The given key does not identify an element in this collection value.", | |
151 | Subject: srcRange, | |
152 | }, | |
153 | } | |
154 | } | |
155 | ||
156 | return collection.GetAttr(attrName), nil | |
157 | ||
158 | default: | |
159 | return cty.DynamicVal, Diagnostics{ | |
160 | { | |
161 | Severity: DiagError, | |
162 | Summary: "Invalid index", | |
163 | Detail: "This value does not have any indices.", | |
164 | Subject: srcRange, | |
165 | }, | |
166 | } | |
167 | } | |
168 | ||
169 | } | |
107c1cdb ND |
170 | |
171 | // GetAttr is a helper function that performs the same operation as the | |
172 | // attribute access in the HCL expression language. That is, the result is the | |
173 | // same as it would be for obj.attr in a configuration expression. | |
174 | // | |
175 | // This is exported so that applications can access attributes in a manner | |
176 | // consistent with how the language does it, including handling of null and | |
177 | // unknown values, etc. | |
178 | // | |
179 | // Diagnostics are produced if the given combination of values is not valid. | |
180 | // Therefore a pointer to a source range must be provided to use in diagnostics, | |
181 | // though nil can be provided if the calling application is going to | |
182 | // ignore the subject of the returned diagnostics anyway. | |
183 | func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) { | |
184 | if obj.IsNull() { | |
185 | return cty.DynamicVal, Diagnostics{ | |
186 | { | |
187 | Severity: DiagError, | |
188 | Summary: "Attempt to get attribute from null value", | |
189 | Detail: "This value is null, so it does not have any attributes.", | |
190 | Subject: srcRange, | |
191 | }, | |
192 | } | |
193 | } | |
194 | ||
195 | ty := obj.Type() | |
196 | switch { | |
197 | case ty.IsObjectType(): | |
198 | if !ty.HasAttribute(attrName) { | |
199 | return cty.DynamicVal, Diagnostics{ | |
200 | { | |
201 | Severity: DiagError, | |
202 | Summary: "Unsupported attribute", | |
203 | Detail: fmt.Sprintf("This object does not have an attribute named %q.", attrName), | |
204 | Subject: srcRange, | |
205 | }, | |
206 | } | |
207 | } | |
208 | ||
209 | if !obj.IsKnown() { | |
210 | return cty.UnknownVal(ty.AttributeType(attrName)), nil | |
211 | } | |
212 | ||
213 | return obj.GetAttr(attrName), nil | |
214 | case ty.IsMapType(): | |
215 | if !obj.IsKnown() { | |
216 | return cty.UnknownVal(ty.ElementType()), nil | |
217 | } | |
218 | ||
219 | idx := cty.StringVal(attrName) | |
220 | if obj.HasIndex(idx).False() { | |
221 | return cty.DynamicVal, Diagnostics{ | |
222 | { | |
223 | Severity: DiagError, | |
224 | Summary: "Missing map element", | |
225 | Detail: fmt.Sprintf("This map does not have an element with the key %q.", attrName), | |
226 | Subject: srcRange, | |
227 | }, | |
228 | } | |
229 | } | |
230 | ||
231 | return obj.Index(idx), nil | |
232 | case ty == cty.DynamicPseudoType: | |
233 | return cty.DynamicVal, nil | |
234 | default: | |
235 | return cty.DynamicVal, Diagnostics{ | |
236 | { | |
237 | Severity: DiagError, | |
238 | Summary: "Unsupported attribute", | |
239 | Detail: "This value does not have any attributes.", | |
240 | Subject: srcRange, | |
241 | }, | |
242 | } | |
243 | } | |
244 | ||
245 | } | |
246 | ||
247 | // ApplyPath is a helper function that applies a cty.Path to a value using the | |
248 | // indexing and attribute access operations from HCL. | |
249 | // | |
250 | // This is similar to calling the path's own Apply method, but ApplyPath uses | |
251 | // the more relaxed typing rules that apply to these operations in HCL, rather | |
252 | // than cty's relatively-strict rules. ApplyPath is implemented in terms of | |
253 | // Index and GetAttr, and so it has the same behavior for individual steps | |
254 | // but will stop and return any errors returned by intermediate steps. | |
255 | // | |
256 | // Diagnostics are produced if the given path cannot be applied to the given | |
257 | // value. Therefore a pointer to a source range must be provided to use in | |
258 | // diagnostics, though nil can be provided if the calling application is going | |
259 | // to ignore the subject of the returned diagnostics anyway. | |
260 | func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) { | |
261 | var diags Diagnostics | |
262 | ||
263 | for _, step := range path { | |
264 | var stepDiags Diagnostics | |
265 | switch ts := step.(type) { | |
266 | case cty.IndexStep: | |
267 | val, stepDiags = Index(val, ts.Key, srcRange) | |
268 | case cty.GetAttrStep: | |
269 | val, stepDiags = GetAttr(val, ts.Name, srcRange) | |
270 | default: | |
271 | // Should never happen because the above are all of the step types. | |
272 | diags = diags.Append(&Diagnostic{ | |
273 | Severity: DiagError, | |
274 | Summary: "Invalid path step", | |
275 | Detail: fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step), | |
276 | Subject: srcRange, | |
277 | }) | |
278 | return cty.DynamicVal, diags | |
279 | } | |
280 | ||
281 | diags = append(diags, stepDiags...) | |
282 | if stepDiags.HasErrors() { | |
283 | return cty.DynamicVal, diags | |
284 | } | |
285 | } | |
286 | ||
287 | return val, diags | |
288 | } |