| package userfunc |
| |
| import ( |
| "github.com/hashicorp/hcl/v2" |
| "github.com/zclconf/go-cty/cty" |
| "github.com/zclconf/go-cty/cty/function" |
| ) |
| |
| var funcBodySchema = &hcl.BodySchema{ |
| Attributes: []hcl.AttributeSchema{ |
| { |
| Name: "params", |
| Required: true, |
| }, |
| { |
| Name: "variadic_param", |
| Required: false, |
| }, |
| { |
| Name: "result", |
| Required: true, |
| }, |
| }, |
| } |
| |
| func decodeUserFunctions(body hcl.Body, blockType string, contextFunc ContextFunc) (funcs map[string]function.Function, remain hcl.Body, diags hcl.Diagnostics) { |
| schema := &hcl.BodySchema{ |
| Blocks: []hcl.BlockHeaderSchema{ |
| { |
| Type: blockType, |
| LabelNames: []string{"name"}, |
| }, |
| }, |
| } |
| |
| content, remain, diags := body.PartialContent(schema) |
| if diags.HasErrors() { |
| return nil, remain, diags |
| } |
| |
| // first call to getBaseCtx will populate context, and then the same |
| // context will be used for all subsequent calls. It's assumed that |
| // all functions in a given body should see an identical context. |
| var baseCtx *hcl.EvalContext |
| getBaseCtx := func() *hcl.EvalContext { |
| if baseCtx == nil { |
| if contextFunc != nil { |
| baseCtx = contextFunc() |
| } |
| } |
| // baseCtx might still be nil here, and that's okay |
| return baseCtx |
| } |
| |
| funcs = make(map[string]function.Function) |
| Blocks: |
| for _, block := range content.Blocks { |
| name := block.Labels[0] |
| funcContent, funcDiags := block.Body.Content(funcBodySchema) |
| diags = append(diags, funcDiags...) |
| if funcDiags.HasErrors() { |
| continue |
| } |
| |
| paramsExpr := funcContent.Attributes["params"].Expr |
| resultExpr := funcContent.Attributes["result"].Expr |
| var varParamExpr hcl.Expression |
| if funcContent.Attributes["variadic_param"] != nil { |
| varParamExpr = funcContent.Attributes["variadic_param"].Expr |
| } |
| |
| var params []string |
| var varParam string |
| |
| paramExprs, paramsDiags := hcl.ExprList(paramsExpr) |
| diags = append(diags, paramsDiags...) |
| if paramsDiags.HasErrors() { |
| continue |
| } |
| for _, paramExpr := range paramExprs { |
| param := hcl.ExprAsKeyword(paramExpr) |
| if param == "" { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid param element", |
| Detail: "Each parameter name must be an identifier.", |
| Subject: paramExpr.Range().Ptr(), |
| }) |
| continue Blocks |
| } |
| params = append(params, param) |
| } |
| |
| if varParamExpr != nil { |
| varParam = hcl.ExprAsKeyword(varParamExpr) |
| if varParam == "" { |
| diags = append(diags, &hcl.Diagnostic{ |
| Severity: hcl.DiagError, |
| Summary: "Invalid variadic_param", |
| Detail: "The variadic parameter name must be an identifier.", |
| Subject: varParamExpr.Range().Ptr(), |
| }) |
| continue |
| } |
| } |
| |
| spec := &function.Spec{} |
| for _, paramName := range params { |
| spec.Params = append(spec.Params, function.Parameter{ |
| Name: paramName, |
| Type: cty.DynamicPseudoType, |
| }) |
| } |
| if varParamExpr != nil { |
| spec.VarParam = &function.Parameter{ |
| Name: varParam, |
| Type: cty.DynamicPseudoType, |
| } |
| } |
| impl := func(args []cty.Value) (cty.Value, error) { |
| ctx := getBaseCtx() |
| ctx = ctx.NewChild() |
| ctx.Variables = make(map[string]cty.Value) |
| |
| // The cty function machinery guarantees that we have at least |
| // enough args to fill all of our params. |
| for i, paramName := range params { |
| ctx.Variables[paramName] = args[i] |
| } |
| if spec.VarParam != nil { |
| varArgs := args[len(params):] |
| ctx.Variables[varParam] = cty.TupleVal(varArgs) |
| } |
| |
| result, diags := resultExpr.Value(ctx) |
| if diags.HasErrors() { |
| // Smuggle the diagnostics out via the error channel, since |
| // a diagnostics sequence implements error. Caller can |
| // type-assert this to recover the individual diagnostics |
| // if desired. |
| return cty.DynamicVal, diags |
| } |
| return result, nil |
| } |
| spec.Type = func(args []cty.Value) (cty.Type, error) { |
| val, err := impl(args) |
| return val.Type(), err |
| } |
| spec.Impl = func(args []cty.Value, retType cty.Type) (cty.Value, error) { |
| return impl(args) |
| } |
| funcs[name] = function.New(spec) |
| } |
| |
| return funcs, remain, diags |
| } |