zclconf / go-cty

A type system for dynamic values in Go applications
MIT License
348 stars 71 forks source link

How to convert cty.Value of unknown type into `interface{}` #158

Closed manterfield closed 1 year ago

manterfield commented 1 year ago

I'm probably just missing something obvious here, and if so apologies for the noise.

I'm attempting to take user input from HCL that is composed of data with types that are unknown until runtime. The example HCL could look something like this:

myblock {
  user_provided_attrs = {
    foo = "foostring"
    bar = true
    buzz = vars.some_complex_var
  }
}

My first attempt was to decode this into cty Values and then transform using gocty.FromCtyValue(val, &decodedVal) where decoded val is map[string]interface{}. Of course, that doesn't work as FromCtyValue uses reflection on the type and maps to specific concrete types. interface{} doesn't match, so I get error: "incorrect type" which is the default error case.

I can see I could put something together using recursion myself, and if all else fails that's what I'll do. The only thing that has stopped me is the assumption that I must be missing a built in way to handle this case. Especially since the package docs have quite a bit of material on converting between unknown types etc.

ljluestc commented 1 year ago
package main

import (
    "fmt"
    "github.com/zclconf/go-cty/cty"
    "github.com/zclconf/go-cty/cty/json"
)

func convertToInterface(val cty.Value) interface{} {
    switch val.Type() {
    case cty.String:
        return val.AsString()
    case cty.Bool:
        return val.True()
    case cty.Number:
        return val.AsBigFloat().String()
    case cty.Object:
        obj := make(map[string]interface{})
        val.ForEach(func(key cty.Value, value cty.Value) bool {
            obj[key.AsString()] = convertToInterface(value)
            return true
        })
        return obj
    case cty.List:
        list := make([]interface{}, 0)
        val.ForEachElement(func(index int, value cty.Value) bool {
            list = append(list, convertToInterface(value))
            return true
        })
        return list
    default:
        return nil
    }
}

func main() {
    hcl := `{
        "foo": "foostring",
        "bar": true,
        "buzz": [1, 2, 3]
    }`

    val, err := json.Unmarshal([]byte(hcl), cty.DynamicPseudoType)
    if err != nil {
        fmt.Println(err)
        return
    }

    result := convertToInterface(val)

    fmt.Printf("%+v\n", result)
}
manterfield commented 1 year ago

@ljluestc Lovely example, thank you. This is the recursive version I had in mind, it's fairly neat so probably not a big deal that it's not built in.