1// Package tryfunc contains some optional functions that can be exposed in
2// HCL-based languages to allow authors to test whether a particular expression
3// can succeed and take dynamic action based on that result.
4//
5// These functions are implemented in terms of the customdecode extension from
6// the sibling directory "customdecode", and so they are only useful when
7// used within an HCL EvalContext. Other systems using cty functions are
8// unlikely to support the HCL-specific "customdecode" extension.
9package tryfunc
10
11import (
12	"errors"
13	"fmt"
14	"strings"
15
16	"github.com/hashicorp/hcl/v2"
17	"github.com/hashicorp/hcl/v2/ext/customdecode"
18	"github.com/zclconf/go-cty/cty"
19	"github.com/zclconf/go-cty/cty/function"
20)
21
22// TryFunc is a variadic function that tries to evaluate all of is arguments
23// in sequence until one succeeds, in which case it returns that result, or
24// returns an error if none of them succeed.
25var TryFunc function.Function
26
27// CanFunc tries to evaluate the expression given in its first argument.
28var CanFunc function.Function
29
30func init() {
31	TryFunc = function.New(&function.Spec{
32		VarParam: &function.Parameter{
33			Name: "expressions",
34			Type: customdecode.ExpressionClosureType,
35		},
36		Type: func(args []cty.Value) (cty.Type, error) {
37			v, err := try(args)
38			if err != nil {
39				return cty.NilType, err
40			}
41			return v.Type(), nil
42		},
43		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
44			return try(args)
45		},
46	})
47	CanFunc = function.New(&function.Spec{
48		Params: []function.Parameter{
49			{
50				Name: "expression",
51				Type: customdecode.ExpressionClosureType,
52			},
53		},
54		Type: function.StaticReturnType(cty.Bool),
55		Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
56			return can(args[0])
57		},
58	})
59}
60
61func try(args []cty.Value) (cty.Value, error) {
62	if len(args) == 0 {
63		return cty.NilVal, errors.New("at least one argument is required")
64	}
65
66	// We'll collect up all of the diagnostics we encounter along the way
67	// and report them all if none of the expressions succeed, so that the
68	// user might get some hints on how to make at least one succeed.
69	var diags hcl.Diagnostics
70	for _, arg := range args {
71		closure := customdecode.ExpressionClosureFromVal(arg)
72		if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
73			// We can't safely decide if this expression will succeed yet,
74			// and so our entire result must be unknown until we have
75			// more information.
76			return cty.DynamicVal, nil
77		}
78
79		v, moreDiags := closure.Value()
80		diags = append(diags, moreDiags...)
81		if moreDiags.HasErrors() {
82			continue // try the next one, if there is one to try
83		}
84		return v, nil // ignore any accumulated diagnostics if one succeeds
85	}
86
87	// If we fall out here then none of the expressions succeeded, and so
88	// we must have at least one diagnostic and we'll return all of them
89	// so that the user can see the errors related to whichever one they
90	// were expecting to have succeeded in this case.
91	//
92	// Because our function must return a single error value rather than
93	// diagnostics, we'll construct a suitable error message string
94	// that will make sense in the context of the function call failure
95	// diagnostic HCL will eventually wrap this in.
96	var buf strings.Builder
97	buf.WriteString("no expression succeeded:\n")
98	for _, diag := range diags {
99		if diag.Subject != nil {
100			buf.WriteString(fmt.Sprintf("- %s (at %s)\n  %s\n", diag.Summary, diag.Subject, diag.Detail))
101		} else {
102			buf.WriteString(fmt.Sprintf("- %s\n  %s\n", diag.Summary, diag.Detail))
103		}
104	}
105	buf.WriteString("\nAt least one expression must produce a successful result")
106	return cty.NilVal, errors.New(buf.String())
107}
108
109func can(arg cty.Value) (cty.Value, error) {
110	closure := customdecode.ExpressionClosureFromVal(arg)
111	if dependsOnUnknowns(closure.Expression, closure.EvalContext) {
112		// Can't decide yet, then.
113		return cty.UnknownVal(cty.Bool), nil
114	}
115
116	_, diags := closure.Value()
117	if diags.HasErrors() {
118		return cty.False, nil
119	}
120	return cty.True, nil
121}
122
123// dependsOnUnknowns returns true if any of the variables that the given
124// expression might access are unknown values or contain unknown values.
125//
126// This is a conservative result that prefers to return true if there's any
127// chance that the expression might derive from an unknown value during its
128// evaluation; it is likely to produce false-positives for more complex
129// expressions involving deep data structures.
130func dependsOnUnknowns(expr hcl.Expression, ctx *hcl.EvalContext) bool {
131	for _, traversal := range expr.Variables() {
132		val, diags := traversal.TraverseAbs(ctx)
133		if diags.HasErrors() {
134			// If the traversal returned a definitive error then it must
135			// not traverse through any unknowns.
136			continue
137		}
138		if !val.IsWhollyKnown() {
139			// The value will be unknown if either it refers directly to
140			// an unknown value or if the traversal moves through an unknown
141			// collection. We're using IsWhollyKnown, so this also catches
142			// situations where the traversal refers to a compound data
143			// structure that contains any unknown values. That's important,
144			// because during evaluation the expression might evaluate more
145			// deeply into this structure and encounter the unknowns.
146			return true
147		}
148	}
149	return false
150}
151