github.com/hashicorp/hcl/v2@v2.20.0/ext/tryfunc/tryfunc.go (about)

     1  // Copyright (c) HashiCorp, Inc.
     2  // SPDX-License-Identifier: MPL-2.0
     3  
     4  // Package tryfunc contains some optional functions that can be exposed in
     5  // HCL-based languages to allow authors to test whether a particular expression
     6  // can succeed and take dynamic action based on that result.
     7  //
     8  // These functions are implemented in terms of the customdecode extension from
     9  // the sibling directory "customdecode", and so they are only useful when
    10  // used within an HCL EvalContext. Other systems using cty functions are
    11  // unlikely to support the HCL-specific "customdecode" extension.
    12  package tryfunc
    13  
    14  import (
    15  	"errors"
    16  	"fmt"
    17  	"strings"
    18  
    19  	"github.com/hashicorp/hcl/v2"
    20  	"github.com/hashicorp/hcl/v2/ext/customdecode"
    21  	"github.com/zclconf/go-cty/cty"
    22  	"github.com/zclconf/go-cty/cty/function"
    23  )
    24  
    25  // TryFunc is a variadic function that tries to evaluate all of is arguments
    26  // in sequence until one succeeds, in which case it returns that result, or
    27  // returns an error if none of them succeed.
    28  var TryFunc function.Function
    29  
    30  // CanFunc tries to evaluate the expression given in its first argument.
    31  var CanFunc function.Function
    32  
    33  func init() {
    34  	TryFunc = function.New(&function.Spec{
    35  		VarParam: &function.Parameter{
    36  			Name: "expressions",
    37  			Type: customdecode.ExpressionClosureType,
    38  		},
    39  		Type: func(args []cty.Value) (cty.Type, error) {
    40  			v, err := try(args)
    41  			if err != nil {
    42  				return cty.NilType, err
    43  			}
    44  			return v.Type(), nil
    45  		},
    46  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    47  			return try(args)
    48  		},
    49  	})
    50  	CanFunc = function.New(&function.Spec{
    51  		Params: []function.Parameter{
    52  			{
    53  				Name: "expression",
    54  				Type: customdecode.ExpressionClosureType,
    55  			},
    56  		},
    57  		Type: function.StaticReturnType(cty.Bool),
    58  		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
    59  			return can(args[0])
    60  		},
    61  	})
    62  }
    63  
    64  func try(args []cty.Value) (cty.Value, error) {
    65  	if len(args) == 0 {
    66  		return cty.NilVal, errors.New("at least one argument is required")
    67  	}
    68  
    69  	// We'll collect up all of the diagnostics we encounter along the way
    70  	// and report them all if none of the expressions succeed, so that the
    71  	// user might get some hints on how to make at least one succeed.
    72  	var diags hcl.Diagnostics
    73  	for _, arg := range args {
    74  		closure := customdecode.ExpressionClosureFromVal(arg)
    75  
    76  		v, moreDiags := closure.Value()
    77  		diags = append(diags, moreDiags...)
    78  
    79  		if moreDiags.HasErrors() {
    80  			// If there's an error we know it will always fail and can
    81  			// continue. A more refined value will not remove an error from
    82  			// the expression.
    83  			continue
    84  		}
    85  
    86  		if !v.IsWhollyKnown() {
    87  			// If there are any unknowns in the value at all, we cannot be
    88  			// certain that the final value will be consistent or have the same
    89  			// type, so wee need to be conservative and return a dynamic value.
    90  
    91  			// There are two different classes of failure that can happen when
    92  			// an expression transitions from unknown to known; an operation on
    93  			// a dynamic value becomes invalid for the type once the type is
    94  			// known, or an index expression on a collection fails once the
    95  			// collection value is known. These changes from a
    96  			// valid-partially-unknown expression to an invalid-known
    97  			// expression can produce inconsistent results by changing which
    98  			// "try" argument is returned, which may be a collection with
    99  			// different previously known values, or a different type entirely
   100  			// ("try" does not require consistent argument types)
   101  			return cty.DynamicVal, nil
   102  		}
   103  
   104  		return v, nil // ignore any accumulated diagnostics if one succeeds
   105  	}
   106  
   107  	// If we fall out here then none of the expressions succeeded, and so
   108  	// we must have at least one diagnostic and we'll return all of them
   109  	// so that the user can see the errors related to whichever one they
   110  	// were expecting to have succeeded in this case.
   111  	//
   112  	// Because our function must return a single error value rather than
   113  	// diagnostics, we'll construct a suitable error message string
   114  	// that will make sense in the context of the function call failure
   115  	// diagnostic HCL will eventually wrap this in.
   116  	var buf strings.Builder
   117  	buf.WriteString("no expression succeeded:\n")
   118  	for _, diag := range diags {
   119  		if diag.Subject != nil {
   120  			buf.WriteString(fmt.Sprintf("- %s (at %s)\n  %s\n", diag.Summary, diag.Subject, diag.Detail))
   121  		} else {
   122  			buf.WriteString(fmt.Sprintf("- %s\n  %s\n", diag.Summary, diag.Detail))
   123  		}
   124  	}
   125  	buf.WriteString("\nAt least one expression must produce a successful result")
   126  	return cty.NilVal, errors.New(buf.String())
   127  }
   128  
   129  func can(arg cty.Value) (cty.Value, error) {
   130  	closure := customdecode.ExpressionClosureFromVal(arg)
   131  	v, diags := closure.Value()
   132  	if diags.HasErrors() {
   133  		return cty.False, nil
   134  	}
   135  
   136  	if !v.IsWhollyKnown() {
   137  		// If the value is not wholly known, we still cannot be certain that
   138  		// the expression was valid. There may be yet index expressions which
   139  		// will fail once values are completely known.
   140  		return cty.UnknownVal(cty.Bool), nil
   141  	}
   142  
   143  	return cty.True, nil
   144  }