hashicorp / hcl2

Former temporary home for experimental new version of HCL
https://github.com/hashicorp/hcl
Mozilla Public License 2.0
374 stars 64 forks source link

Example usage of custom defined function #5

Closed fatih closed 6 years ago

fatih commented 6 years ago

Hi,

I see that this project will replace HCL and HIL going forward. Are there any example on how to use it with custom defined function (like how Terraform injects into the HIL's eval context). The README and doc is highly technical written and seems like until I read the source code it's not easily discoverable.

Thanks

fatih commented 6 years ago

Here is an example of doing it one way, if anyone looks how to implement it:

package main

import (
    "encoding/json"
    "fmt"
    "log"
    "time"

    "github.com/hashicorp/hcl2/gohcl"
    "github.com/hashicorp/hcl2/hcl"
    "github.com/hashicorp/hcl2/hcldec"
    "github.com/hashicorp/hcl2/hclparse"
    "github.com/zclconf/go-cty/cty"
    "github.com/zclconf/go-cty/cty/function"
    "github.com/zclconf/go-cty/cty/function/stdlib"
    ctyjson "github.com/zclconf/go-cty/cty/json"
)

func main() {
    var src = `
variable "city" {
  default = "Ankara"
}

variable "year" {
  default = "2000"
}

resource "fatih" "arslan" {
  result = "Address: ${upper(var.city)}, Age: ${age(var.year)}"
}
`
    parser := hclparse.NewParser()
    file, diags := parser.ParseHCL([]byte(src), "demo.hcl")
    if len(diags) != 0 {
        for _, diag := range diags {
            fmt.Printf("- %s\n", diag)
        }
        return
    }

    body := file.Body

    type Variable struct {
        Name    string         `hcl:"name,label"`
        Default hcl.Attributes `hcl:"default,remain"`
    }

    type Resource struct {
        Type   string   `hcl:"type,label"`
        Name   string   `hcl:"name,label"`
        Config hcl.Body `hcl:",remain"`
    }
    type Root struct {
        Variables []*Variable `hcl:"variable,block"`
        Resources []*Resource `hcl:"resource,block"`
    }

    var root Root
    diags = gohcl.DecodeBody(body, nil, &root)
    if len(diags) != 0 {
        for _, diag := range diags {
            fmt.Printf("decoding - %s\n", diag)
        }
        return
    }

    variables := map[string]cty.Value{}
    for _, v := range root.Variables {
        if len(v.Default) == 0 {
            continue
        }

        val, diags := v.Default["default"].Expr.Value(nil)
        if len(diags) != 0 {
            for _, diag := range diags {
                fmt.Printf("decoding - %s\n", diag)
            }
            return
        }

        variables[v.Name] = val
    }

    evalContext := &hcl.EvalContext{
        Variables: map[string]cty.Value{
            "var": cty.ObjectVal(variables),
        },
        Functions: map[string]function.Function{
            "upper": stdlib.UpperFunc,
            "age":   age(),
        },
    }

    // just for resource address and bar
    spec := &hcldec.ObjectSpec{
        "result": &hcldec.AttrSpec{
            Name:     "result",
            Required: true,
            Type:     cty.String,
        },
    }

    cfg, diags := hcldec.Decode(root.Resources[0].Config, spec, evalContext)
    if len(diags) != 0 {
        for _, diag := range diags {
            fmt.Printf("- %s\n", diag)
        }
        return
    }

    wantCfg := cty.ObjectVal(map[string]cty.Value{
        "result": cty.StringVal("Address: ANKARA, Age: 18"),
    })

    if !cfg.RawEquals(wantCfg) {
        log.Fatalf("wrong config\ngot:  %#v\nwant: %#v", cfg, wantCfg)
    }

    out, err := json.MarshalIndent(ctyjson.SimpleJSONValue{cfg}, "", "  ")
    if err != nil {
        log.Fatalln(err)
    }

    fmt.Println(string(out))
}

// age() is a custom function that returns back the age from the birth year
func age() function.Function {
    return function.New(&function.Spec{
        Params: []function.Parameter{
            {
                Name: "year",
                Type: cty.Number,
            },
        },
        Type: function.StaticReturnType(cty.Number),
        Impl: func(args []cty.Value, retType cty.Type) (cty.Value, error) {
            in := args[0].AsBigFloat()
            year, _ := in.Int64()
            age := int64(time.Now().Year()) - year
            return cty.NumberIntVal(age), nil
        },
    })
}

Prints:

{
  "result": "Address: ANKARA, Age: 18"
}