github.com/hashicorp/hcl/v2@v2.20.0/ext/userfunc/decode.go (about) 1 // Copyright (c) HashiCorp, Inc. 2 // SPDX-License-Identifier: MPL-2.0 3 4 package userfunc 5 6 import ( 7 "github.com/hashicorp/hcl/v2" 8 "github.com/zclconf/go-cty/cty" 9 "github.com/zclconf/go-cty/cty/function" 10 ) 11 12 var funcBodySchema = &hcl.BodySchema{ 13 Attributes: []hcl.AttributeSchema{ 14 { 15 Name: "params", 16 Required: true, 17 }, 18 { 19 Name: "variadic_param", 20 Required: false, 21 }, 22 { 23 Name: "result", 24 Required: true, 25 }, 26 }, 27 } 28 29 func decodeUserFunctions(body hcl.Body, blockType string, contextFunc ContextFunc) (funcs map[string]function.Function, remain hcl.Body, diags hcl.Diagnostics) { 30 schema := &hcl.BodySchema{ 31 Blocks: []hcl.BlockHeaderSchema{ 32 { 33 Type: blockType, 34 LabelNames: []string{"name"}, 35 }, 36 }, 37 } 38 39 content, remain, diags := body.PartialContent(schema) 40 if diags.HasErrors() { 41 return nil, remain, diags 42 } 43 44 // first call to getBaseCtx will populate context, and then the same 45 // context will be used for all subsequent calls. It's assumed that 46 // all functions in a given body should see an identical context. 47 var baseCtx *hcl.EvalContext 48 getBaseCtx := func() *hcl.EvalContext { 49 if baseCtx == nil { 50 if contextFunc != nil { 51 baseCtx = contextFunc() 52 } 53 } 54 // baseCtx might still be nil here, and that's okay 55 return baseCtx 56 } 57 58 funcs = make(map[string]function.Function) 59 Blocks: 60 for _, block := range content.Blocks { 61 name := block.Labels[0] 62 funcContent, funcDiags := block.Body.Content(funcBodySchema) 63 diags = append(diags, funcDiags...) 64 if funcDiags.HasErrors() { 65 continue 66 } 67 68 paramsExpr := funcContent.Attributes["params"].Expr 69 resultExpr := funcContent.Attributes["result"].Expr 70 var varParamExpr hcl.Expression 71 if funcContent.Attributes["variadic_param"] != nil { 72 varParamExpr = funcContent.Attributes["variadic_param"].Expr 73 } 74 75 var params []string 76 var varParam string 77 78 paramExprs, paramsDiags := hcl.ExprList(paramsExpr) 79 diags = append(diags, paramsDiags...) 80 if paramsDiags.HasErrors() { 81 continue 82 } 83 for _, paramExpr := range paramExprs { 84 param := hcl.ExprAsKeyword(paramExpr) 85 if param == "" { 86 diags = append(diags, &hcl.Diagnostic{ 87 Severity: hcl.DiagError, 88 Summary: "Invalid param element", 89 Detail: "Each parameter name must be an identifier.", 90 Subject: paramExpr.Range().Ptr(), 91 }) 92 continue Blocks 93 } 94 params = append(params, param) 95 } 96 97 if varParamExpr != nil { 98 varParam = hcl.ExprAsKeyword(varParamExpr) 99 if varParam == "" { 100 diags = append(diags, &hcl.Diagnostic{ 101 Severity: hcl.DiagError, 102 Summary: "Invalid variadic_param", 103 Detail: "The variadic parameter name must be an identifier.", 104 Subject: varParamExpr.Range().Ptr(), 105 }) 106 continue 107 } 108 } 109 110 spec := &function.Spec{} 111 for _, paramName := range params { 112 spec.Params = append(spec.Params, function.Parameter{ 113 Name: paramName, 114 Type: cty.DynamicPseudoType, 115 }) 116 } 117 if varParamExpr != nil { 118 spec.VarParam = &function.Parameter{ 119 Name: varParam, 120 Type: cty.DynamicPseudoType, 121 } 122 } 123 impl := func(args []cty.Value) (cty.Value, error) { 124 ctx := getBaseCtx() 125 ctx = ctx.NewChild() 126 ctx.Variables = make(map[string]cty.Value) 127 128 // The cty function machinery guarantees that we have at least 129 // enough args to fill all of our params. 130 for i, paramName := range params { 131 ctx.Variables[paramName] = args[i] 132 } 133 if spec.VarParam != nil { 134 varArgs := args[len(params):] 135 ctx.Variables[varParam] = cty.TupleVal(varArgs) 136 } 137 138 result, diags := resultExpr.Value(ctx) 139 if diags.HasErrors() { 140 // Smuggle the diagnostics out via the error channel, since 141 // a diagnostics sequence implements error. Caller can 142 // type-assert this to recover the individual diagnostics 143 // if desired. 144 return cty.DynamicVal, diags 145 } 146 return result, nil 147 } 148 spec.Type = func(args []cty.Value) (cty.Type, error) { 149 val, err := impl(args) 150 return val.Type(), err 151 } 152 spec.Impl = func(args []cty.Value, retType cty.Type) (cty.Value, error) { 153 return impl(args) 154 } 155 funcs[name] = function.New(spec) 156 } 157 158 return funcs, remain, diags 159 }