hcl icon indicating copy to clipboard operation
hcl copied to clipboard

No support for optional blocks

Open pontaoski opened this issue 3 years ago • 4 comments

I'm writing a program where I have a large amount of config where the defaults are fine and don't need to be explicitly specified by the end user; I'd like to be able to do e.g. hcl:"Policies,block,optional" and have hclsimple be fine with that block not being present.

pontaoski avatar Dec 13 '20 03:12 pontaoski

@pontaoski You can have optional blocks if you use a struct pointer for the property type.

Policies *PoliciesConfig `hcl:"policies,block"`

mickael-menu avatar Dec 25 '20 19:12 mickael-menu

Making everything pointers in order to have an optional type in config is less than ergonomic, IMO

pontaoski avatar Dec 26 '20 03:12 pontaoski

I agree with @pontaoski that having to define an optional block with pointers is less than ergonomic and, in some cases, impractical when using third party libraries that generate structs such as entgo.io

frybin avatar Feb 18 '21 04:02 frybin

gohcl is generally designed to be used with structs that are written specifically for HCL, rather than structs written for other purposes and then annotated with hcl tags.

A relatively common pattern with gohcl as it exists today is to declare a local struct type inside your decode function and then transform it into whatever your broader application wants before returning:

func decodeConfig(body hcl.Body) (*Config, hcl.Diagnostics) {
    type HCLPoliciesConfig struct {
        // ...
    }
    type HCLConfig struct {
        Policies *HCLPoliciesConfig `hcl:"policies,block"`
    }

    var config HCLConfig
    diags := gohcl.DecodeConfig(body, nil, &config)
    if diags.HasErrors() {
        return nil, diags
    }

    ret := &Config{}
    // Populate ret from config...
    if config.Policies != nil {
        // assign policies settings into "ret" if set
    } else {
        // somehow mark in "ret" that policies wasn't set
    }

    return ret, diags
}

The local struct types here are therefore acting just as a concise way of describing the expected configuration structure using Go syntax, not as a way to avoid writing normal code to process the user's input.

HCL requires a pointer in this case because otherwise it would be ambiguous whether the block was omitted or whether it was present and contained the zero value for each of the nested fields. The gohcl API is generally optimized for giving an application detailed information about what the user provided so it can respond precisely, and so indeed some of the design decisions could be classified as "unergonomic" in the sense that they are forcing you to deal with each of the possible inputs separately and be intentional about how to handle them.

apparentlymart avatar Apr 15 '21 22:04 apparentlymart