hashicorp / hcl

HCL is the HashiCorp configuration language.
Mozilla Public License 2.0
5.24k stars 590 forks source link

Unmarshal Custom Types #349

Open icholy opened 4 years ago

icholy commented 4 years ago

Is there a way to decode custom types? I'm looking for something equivalent to encoding/json.Unmarshaler. I've tried implementing encoding.TextUnmarshaler which didn't work.

Here's an example use-case.

type Duration struct {
   time.Duration
}

func (d *Duration) UnmarshalText(data []byte) error {
    d0, err := time.ParseDuration(string(data))
    if err != nil {
        return err
    }
    d.Duration = d0
    return nil
}

SO Question: https://stackoverflow.com/questions/60515131/hcl-unmarshal-custom-types

icholy commented 4 years ago

Alternatively, gohcl could allow registering mapping/conversions for specific types.

func RegisterDecoder(t reflect.Type, convert func(hcl.Expression) (interface{}, error)) {}

func RegisterEncoder(t reflect.Type, convert func(interface{}) (hcl.Expression, error)) {}
tv42 commented 4 years ago

Adding links for the benefit of others spelunking this issue:

https://github.com/hashicorp/hcl/issues/89 (and many linked there)
https://github.com/hashicorp/hcl/pull/203

One nasty side effect of this is that forcing users to parse/validate e.g. a timestamp in a string after the parse means any error reported from that will no longer be able to point the exact source file location.

ViViDboarder commented 2 years ago

Is there any canonical alternative way to do this? I've tried reading through the documentation as well as poking through the source for Packer and Vault, but those all seem far more complex implementations compared to what I'm trying to do. Generally, I want to use the standard decoder but provide some way to decode a custom type

jaysoncena commented 11 months ago

I got this from nomad's code

// from https://github.com/hashicorp/nomad/blob/0ccf942b26f8c47582f18f324114d02d0bb03a43/jobspec2/hcl_conversions.go
package main

import (
    "fmt"
    "reflect"
    "time"

    "github.com/hashicorp/hcl/v2"
    "github.com/hashicorp/hcl/v2/gohcl"
    "github.com/zclconf/go-cty/cty"
    "github.com/zclconf/go-cty/cty/gocty"
)

var hclDecoder *gohcl.Decoder

func init() {
    hclDecoder = newHCLDecoder()
}

func newHCLDecoder() *gohcl.Decoder {
    decoder := &gohcl.Decoder{}

    // time conversion
    d := time.Duration(0)
    decoder.RegisterExpressionDecoder(reflect.TypeOf(d), decodeDuration)

    return decoder
}

func decodeDuration(expr hcl.Expression, ctx *hcl.EvalContext, val interface{}) hcl.Diagnostics {
    srcVal, diags := expr.Value(ctx)

    if srcVal.Type() == cty.String {
        dur, err := time.ParseDuration(srcVal.AsString())
        if err != nil {
            diags = append(diags, &hcl.Diagnostic{
                Severity: hcl.DiagError,
                Summary:  "Unsuitable value type",
                Detail:   fmt.Sprintf("Unsuitable duration value: %s", err.Error()),
                Subject:  expr.StartRange().Ptr(),
                Context:  expr.Range().Ptr(),
            })
            return diags
        }

        srcVal = cty.NumberIntVal(int64(dur))
    }

    if srcVal.Type() != cty.Number {
        diags = append(diags, &hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Unsuitable value type",
            Detail:   fmt.Sprintf("Unsuitable value: expected a string but found %s", srcVal.Type()),
            Subject:  expr.StartRange().Ptr(),
            Context:  expr.Range().Ptr(),
        })
        return diags

    }

    err := gocty.FromCtyValue(srcVal, val)
    if err != nil {
        diags = append(diags, &hcl.Diagnostic{
            Severity: hcl.DiagError,
            Summary:  "Unsuitable value type",
            Detail:   fmt.Sprintf("Unsuitable value: %s", err.Error()),
            Subject:  expr.StartRange().Ptr(),
            Context:  expr.Range().Ptr(),
        })
    }

    return diags
}

The functions used above like RegisterExpressionDecoder are available in v2.9.2-0.20220525143345-ab3cae0737bc. I didn't check the other tags but it is not available on the versions 2.9.1, 2.10.0 and 2.18.1

then on how to use it

# version from https://github.com/hashicorp/nomad/blob/0ccf942b26f8c47582f18f324114d02d0bb03a43/go.mod#L76
go get github.com/hashicorp/hcl/v2/hclparse@v2.9.2-0.20220525143345-ab3cae0737bc
go mod tidy
# go mod vendor
package main

import (
    "log"
    "time"

    "github.com/hashicorp/hcl/v2/hclparse"
)

type Config struct {
    Duration time.Duration `hcl:"duration"`
}

var input []byte = []byte(`duration = "5s"`)

func main() {

    parser := hclparse.NewParser()
    f, parseDiag := parser.ParseHCL(input, "dummy.hcl")

    if parseDiag.HasErrors() {
        log.Fatal(parseDiag.Error())
    }

    conf := Config{}
    decodeDiag := hclDecoder.DecodeBody(f.Body, nil, &conf)
    if decodeDiag.HasErrors() {
        log.Fatal(decodeDiag.Error())
    }

    log.Printf("conf: %#v", conf)
}
$ go run ./
2023/10/06 15:28:23 conf: main.Config{Duration:5000000000}
johanjanssens commented 3 weeks ago

Note, if you searching for this code, it's only available in a diff branch containing specific tweaks for Nomad. Use:

go get github.com/hashicorp/hcl/v2@nomad-v2.20.1+tweaks