hcl/ops.go
Martin Atkins 6c4344623b Unfold the "hcl" directory up into the root
The main HCL package is more visible this way, and so it's easier than
having to pick it out from dozens of other package directories.
2019-09-09 16:08:19 -07:00

289 lines
8.2 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,
},
}
}
has := collection.HasIndex(key)
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)
if obj.HasIndex(idx).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
}