hashicorp / hcl

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

How does `hclwrite` remove `objectelem` inside a `collectionValue` of an attribute #695

Open magodo opened 2 weeks ago

magodo commented 2 weeks ago

I have a piece of code using hclwrite to modify some Terraform code, in that it will conditionally remove attributes/blocks. The question raises when I encounter, e.g. the following HCL (regarded as a TF attribute):

foo = {
  bar = {
  }
  baz = "xxx"
}

(The above code is not prevalent in SDKv2 based providers, but will prevail for FW based ones as attributes are preferred to blocks)

I don't know how could I, e.g., remove the bar inside the foo attribute's expression (i.e. the collectionValue).

I've tried with something similar to below, but doesn't output what I expected:

package main

import (
    "fmt"
    "log"

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

func main() {
    f, diags := hclwrite.ParseConfig([]byte(`
foo = {
  bar = {
  }
  baz = "xxx"
}
`), "main.hcl", hcl.InitialPos)
    if diags.HasErrors() {
        log.Fatal(diags.Error())
    }
    foo := f.Body().Attributes()["foo"]
    expr := foo.Expr()

    tks := expr.BuildTokens(hclwrite.TokensForIdentifier("tmp"))
    ftmp, diags := hclwrite.ParseConfig(tks.Bytes(), "tmp", hcl.InitialPos)
    if diags.HasErrors() {
        log.Fatal(diags.Error())
    }

    body := ftmp.Body()
    body.Blocks()[0].Body().RemoveAttribute("bar")

    f.Body().SetAttributeRaw("foo", body.Blocks()[0].Body().BuildTokens(nil))
    fmt.Println(string(f.Bytes()))
}
magodo commented 2 weeks ago

Updated: I finally manage to do this via the following, though apparently not ideal:

package main

import (
    "fmt"
    "log"
    "slices"

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

func main() {
    f, diags := hclwrite.ParseConfig([]byte(`
foo = {
  bar = {
  }
  baz = "xxx"
}
`), "main.hcl", hcl.InitialPos)
    if diags.HasErrors() {
        log.Fatal(diags.Error())
    }
    foo := f.Body().Attributes()["foo"]

    tks, diags := removeExpressionAttributes(foo.Expr(), "bar")
    if diags.HasErrors() {
        log.Fatal(diags.Error())
    }
    f.Body().SetAttributeRaw("foo", tks)
    fmt.Println(string(f.Bytes()))
}

func removeExpressionAttributes(expr *hclwrite.Expression, attributes ...string) (hclwrite.Tokens, hcl.Diagnostics) {
    tks := expr.BuildTokens(hclwrite.TokensForIdentifier("tmp"))
    ftmp, diags := hclwrite.ParseConfig(tks.Bytes(), "tmp", hcl.InitialPos)
    if diags.HasErrors() {
        return nil, diags
    }

    body := ftmp.Body().Blocks()[0].Body()

    bodyAttributes := body.Attributes()
    var objectAttrTokens []hclwrite.ObjectAttrTokens
    for attrName, attr := range bodyAttributes {
        if slices.Contains(attributes, attrName) {
            continue
        }
        objectAttrTokens = append(objectAttrTokens, hclwrite.ObjectAttrTokens{
            Name:  hclwrite.TokensForIdentifier(attrName),
            Value: attr.Expr().BuildTokens(nil),
        })
    }
    return hclwrite.TokensForObject(objectAttrTokens), nil
}

Any idea about how to do this idiomatically would be appreciated!