Convert string to nested structure

Hi, @IBMRob,

I don’t follow exactly what you’re looking for; do you want to pass in a value and then a “path” to select from the value like "Spec.Resources.Server.Requests.Cpu" and get the resulting field? If so, I believe you cannot do that with any built-in language features and you’d probably need to do that with the reflect package. I can think of some alternatives that might work better (or worse) for you, though:

A simple (but potentially wasteful, depending on your use case) solution might be to “jsonify” the data:

func ResolveValue(cr *productv1beta1.Server, CrKey string) (string, error) {
    bs, err := json.Marshal(cr)
    if err != nil {
        return "", err
    }
    var m map[string]interface{}
    if err := json.Unmarshal(bs, &m); err != nil {
        return "", err
    }
    path := strings.Split(CrKey, ".")
    for _, hop := range path[:len(path)-1] {
        var ok bool
        if m, ok = m[hop].(map[string]interface{}); !ok {
            return "", fmt.Errorf("invalid key %q")
        }
    }
    return fmt.Sprint(m[path[len(path)-1]]), nil
}

If you have many CrKeys to retrieve from one piece of data, It might not actually be a bad idea to change the signature of this function to accept multiple CrKeys and return a slice of results. If you only need to select a few CrKey fields and/or have many actual *productv1beta1.Server values, this would be inefficient.

If that’s the case, I’d ask if you have control over this productv1beta1.Server type. If you do, I’d recommend some code generation to make that type implement an interface like this:

type ValueResolver interface {
    ResolveValue(key string) (interface{}, error)
}

Then in .../productv1beta1/server.go or wherever:

package productv1beta1

// ...

type Server struct {
    // ...
    Resources SomeType
    // ...
}

var serverFields = map[string]func(s *Server) interface{}{
    // ...
    "Resources": func(s *Server) interface{} { return s.Resources },
    // ...
}

func (s *Server) ResolveValue(key string) (interface{}, error) {
    f, ok := serverFields[key]
    if !ok {
        return nil, fmt.Errorf("invalid field: %q", key)
    }
    return f(s), nil
}

Then change your ResolveValue function to do something like this:

func ResolveValue(vr ValueResolver, key string) (string, error) {
    var ok bool
    path := strings.Split(key, ".")
    for _, hop := range path[:len(path)-1] {
        if vr, ok = vr.ResolveValue(hop).(ValueResolver); !ok {
            return "", fmt.Errorf("cannot resolve %q from %T", hop, vr)
        }
    }
    v, err := vr.ResolveValue(path[len(path)-1])
    if err != nil {
        return "", err
    }
    return fmt.Sprint(v), nil
}

I suspect that will be more efficient than either the reflect- or the encoding/json-based solutions but at the cost of more complexity in your code.

You might also have luck with the text/template package by treating your CrKey as a template (e.g. "{{.Spec.Resources.Server.Requests.Cpu}}" and then your *productv1beta1.Server as a model to execute against. Not sure how that’d perform against reflection, json, or the ValueResolver solution above.

If you’re only dealing with struct fields in *productv1beta1.Server, you could also leverage the unsafe package with the reflect package to make it really fast.

2 Likes