package hcl import ( "fmt" "math/big" "github.com/zclconf/go-cty/cty" "github.com/zclconf/go-cty/cty/convert" ) // Index is a helper function that performs the same operation as the index // operator in the HCL expression language. That is, the result is the // same as it would be for collection[key] in a configuration expression. // // This is exported so that applications can perform indexing in a manner // consistent with how the language does it, including handling of null and // unknown values, etc. // // Diagnostics are produced if the given combination of values is not valid. // Therefore a pointer to a source range must be provided to use in diagnostics, // though nil can be provided if the calling application is going to // ignore the subject of the returned diagnostics anyway. func Index(collection, key cty.Value, srcRange *Range) (cty.Value, Diagnostics) { if collection.IsNull() { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Attempt to index null value", Detail: "This value is null, so it does not have any indices.", Subject: srcRange, }, } } if key.IsNull() { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", Detail: "Can't use a null value as an indexing key.", Subject: srcRange, }, } } ty := collection.Type() kty := key.Type() if kty == cty.DynamicPseudoType || ty == cty.DynamicPseudoType { return cty.DynamicVal, nil } switch { case ty.IsListType() || ty.IsTupleType() || ty.IsMapType(): var wantType cty.Type switch { case ty.IsListType() || ty.IsTupleType(): wantType = cty.Number case ty.IsMapType(): wantType = cty.String default: // should never happen panic("don't know what key type we want") } key, keyErr := convert.Convert(key, wantType) if keyErr != nil { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", Detail: fmt.Sprintf( "The given key does not identify an element in this collection value: %s.", keyErr.Error(), ), Subject: srcRange, }, } } // Here we drop marks from HasIndex result, in order to allow basic // traversal of a marked list, tuple, or map in the same way we can // traverse a marked object has, _ := collection.HasIndex(key).Unmark() if !has.IsKnown() { if ty.IsTupleType() { return cty.DynamicVal, nil } else { return cty.UnknownVal(ty.ElementType()), nil } } if has.False() { // We have a more specialized error message for the situation of // using a fractional number to index into a sequence, because // that will tend to happen if the user is trying to use division // to calculate an index and not realizing that HCL does float // division rather than integer division. if (ty.IsListType() || ty.IsTupleType()) && key.Type().Equals(cty.Number) { if key.IsKnown() && !key.IsNull() { key, _ := key.Unmark() bf := key.AsBigFloat() if _, acc := bf.Int(nil); acc != big.Exact { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", 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), Subject: srcRange, }, } } } } return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", Detail: "The given key does not identify an element in this collection value.", Subject: srcRange, }, } } return collection.Index(key), nil case ty.IsObjectType(): key, keyErr := convert.Convert(key, cty.String) if keyErr != nil { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", Detail: fmt.Sprintf( "The given key does not identify an element in this collection value: %s.", keyErr.Error(), ), Subject: srcRange, }, } } if !collection.IsKnown() { return cty.DynamicVal, nil } if !key.IsKnown() { return cty.DynamicVal, nil } key, _ = key.Unmark() attrName := key.AsString() if !ty.HasAttribute(attrName) { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", Detail: "The given key does not identify an element in this collection value.", Subject: srcRange, }, } } return collection.GetAttr(attrName), nil default: return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Invalid index", Detail: "This value does not have any indices.", Subject: srcRange, }, } } } // GetAttr is a helper function that performs the same operation as the // attribute access in the HCL expression language. That is, the result is the // same as it would be for obj.attr in a configuration expression. // // This is exported so that applications can access attributes in a manner // consistent with how the language does it, including handling of null and // unknown values, etc. // // Diagnostics are produced if the given combination of values is not valid. // Therefore a pointer to a source range must be provided to use in diagnostics, // though nil can be provided if the calling application is going to // ignore the subject of the returned diagnostics anyway. func GetAttr(obj cty.Value, attrName string, srcRange *Range) (cty.Value, Diagnostics) { if obj.IsNull() { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Attempt to get attribute from null value", Detail: "This value is null, so it does not have any attributes.", Subject: srcRange, }, } } ty := obj.Type() switch { case ty.IsObjectType(): if !ty.HasAttribute(attrName) { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Unsupported attribute", Detail: fmt.Sprintf("This object does not have an attribute named %q.", attrName), Subject: srcRange, }, } } if !obj.IsKnown() { return cty.UnknownVal(ty.AttributeType(attrName)), nil } return obj.GetAttr(attrName), nil case ty.IsMapType(): if !obj.IsKnown() { return cty.UnknownVal(ty.ElementType()), nil } idx := cty.StringVal(attrName) // Here we drop marks from HasIndex result, in order to allow basic // traversal of a marked map in the same way we can traverse a marked // object hasIndex, _ := obj.HasIndex(idx).Unmark() if hasIndex.False() { return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Missing map element", Detail: fmt.Sprintf("This map does not have an element with the key %q.", attrName), Subject: srcRange, }, } } return obj.Index(idx), nil case ty == cty.DynamicPseudoType: return cty.DynamicVal, nil default: return cty.DynamicVal, Diagnostics{ { Severity: DiagError, Summary: "Unsupported attribute", Detail: "This value does not have any attributes.", Subject: srcRange, }, } } } // ApplyPath is a helper function that applies a cty.Path to a value using the // indexing and attribute access operations from HCL. // // This is similar to calling the path's own Apply method, but ApplyPath uses // the more relaxed typing rules that apply to these operations in HCL, rather // than cty's relatively-strict rules. ApplyPath is implemented in terms of // Index and GetAttr, and so it has the same behavior for individual steps // but will stop and return any errors returned by intermediate steps. // // Diagnostics are produced if the given path cannot be applied to the given // value. Therefore a pointer to a source range must be provided to use in // diagnostics, though nil can be provided if the calling application is going // to ignore the subject of the returned diagnostics anyway. func ApplyPath(val cty.Value, path cty.Path, srcRange *Range) (cty.Value, Diagnostics) { var diags Diagnostics for _, step := range path { var stepDiags Diagnostics switch ts := step.(type) { case cty.IndexStep: val, stepDiags = Index(val, ts.Key, srcRange) case cty.GetAttrStep: val, stepDiags = GetAttr(val, ts.Name, srcRange) default: // Should never happen because the above are all of the step types. diags = diags.Append(&Diagnostic{ Severity: DiagError, Summary: "Invalid path step", Detail: fmt.Sprintf("Go type %T is not a valid path step. This is a bug in this program.", step), Subject: srcRange, }) return cty.DynamicVal, diags } diags = append(diags, stepDiags...) if stepDiags.HasErrors() { return cty.DynamicVal, diags } } return val, diags }