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 }