d0cb134fec
We need to unmark the result of HasIndex in order to compare it with true or false without a panic.
297 lines
8.6 KiB
Go
297 lines
8.6 KiB
Go
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() {
|
|
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
|
|
}
|
|
|
|
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
|
|
}
|