Convert string to nested structure

Any ideas on if its possible to convert a string into structure names. Consider the function below

func ResolveValue(cr *productv1beta1.Server, CrKey string) (string, error)  {
    if cr.Spec.Resources.Server.Requests.Cpu != "" {
   // if CrKey.toStructure != "" { 
		return cr.Spec.Resources.Server.Requests.Cpu, nil
           // return CrKey.toStructure, nil
    } else if defaultValues[CrKey] != "" {
        return defaultValues[CrKey], nil
    } 
    return "", errors.New("No image for given image type and version")
}

Any ideas how I can take the CrKey in as a string and then convert it into the structure i.e. they supply the structure a string such as cr.Spec.Resources.Server.Requests.Cpu and then I can use the actual structure value of the string.

What would be a sample content of CrKey? And what is CrKey.toStructure supposed to return?

So an example of CrKey string would be cr.Spec.Resources.Server.Requests.Cpu and inside of the function we would like to actual read the value of the cr.Spec.Resources.Server.Requests.Cpu structure

Do you mean

CrKey := "cr.Spec.Resources.Server.Requests.Cpu"

And do you want to interpret this string as the steps no navigate into an existing nested struct?

Yes we wanted to use that string to navigate the nested Structure i.e. normally we would do something like

fmt.Printf("CPU Requests - %s", cr.Spec.Resources.Server.Requests.Cpu)  

which would then print the value

Go is not JavaScript where the member of an object can be accessed as

foo.bar

or

foo["bar"]

The only way I can think of to do what you want is to split the navigation string into parts and then build a huge and complicated if/else construct that inspects each part and and selects the requested element.

Maybe a second idea: Serialize the struct to JSON and use JSON Pointer for dynamic selection.

Just to confirm, to avoid crossed wires - where I previously mentioned

fmt.Printf("CPU Requests - %s", cr.Spec.Resources.Server.Requests.Cpu)  

This works today.

One thought was to split the string and then try and then use reflection on each bit of the string to produce the correct type of object.

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.getter(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.

1 Like