2017-09-11 23:40:37 +00:00
package hcl
2017-06-05 14:31:59 +00:00
import (
2017-06-05 14:39:14 +00:00
"fmt"
2019-04-16 16:23:32 +00:00
"math/big"
2017-06-05 14:39:14 +00:00
2017-06-05 14:31:59 +00:00
"github.com/zclconf/go-cty/cty"
2017-06-05 14:39:14 +00:00
"github.com/zclconf/go-cty/cty/convert"
2017-06-05 14:31:59 +00:00
)
// Index is a helper function that performs the same operation as the index
2018-01-27 18:53:27 +00:00
// operator in the HCL expression language. That is, the result is the
2017-06-05 14:31:59 +00:00
// 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 ( ) :
2017-06-05 14:39:14 +00:00
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 ,
} ,
}
}
2017-06-05 14:31:59 +00:00
has := collection . HasIndex ( key )
if ! has . IsKnown ( ) {
if ty . IsTupleType ( ) {
return cty . DynamicVal , nil
} else {
return cty . UnknownVal ( ty . ElementType ( ) ) , nil
}
}
if has . False ( ) {
2019-04-16 16:23:32 +00:00
// 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 ,
} ,
}
}
}
}
2017-06-05 14:31:59 +00:00
return cty . DynamicVal , Diagnostics {
{
Severity : DiagError ,
Summary : "Invalid index" ,
2017-06-05 14:39:14 +00:00
Detail : "The given key does not identify an element in this collection value." ,
2017-06-05 14:31:59 +00:00
Subject : srcRange ,
} ,
}
}
return collection . Index ( key ) , nil
case ty . IsObjectType ( ) :
2017-06-05 14:39:14 +00:00
key , keyErr := convert . Convert ( key , cty . String )
if keyErr != nil {
2017-06-05 14:31:59 +00:00
return cty . DynamicVal , Diagnostics {
{
Severity : DiagError ,
Summary : "Invalid index" ,
2017-06-05 14:39:14 +00:00
Detail : fmt . Sprintf (
"The given key does not identify an element in this collection value: %s." ,
keyErr . Error ( ) ,
) ,
Subject : srcRange ,
2017-06-05 14:31:59 +00:00
} ,
}
}
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" ,
2017-06-05 14:39:14 +00:00
Detail : "The given key does not identify an element in this collection value." ,
2017-06-05 14:31:59 +00:00
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 ,
} ,
}
}
}
2019-01-30 22:52:18 +00:00
// 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 )
2020-09-24 16:40:41 +00:00
// 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 ( ) {
2019-01-30 22:52:18 +00:00
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
}