Zbob
Zbob

Reputation: 31

Get a value from a known key in a dynamic nested YAML

I'm pretty new to Golang. I have a YAML file with dynamic keys but only one is known, and it's not necessarily the first one (after config). There is no other key at the level of config.

The yaml file :

config:
    foo:bar: baz
    bar:foo: baz
    abs:getit: myvalue

I want to retrieve myvalue from the nested key config:abs:getit. This nested key name will never change, it will always be config:abs:getit. All other keys can be whatever, we don't care, with different types of content (arrays, int, strings, array of array).

What is the best way to recover the value ?

I worked with yaml package, but I have to fix every field in a struct to unmarshall it, but I don't know how many nested keys there can be so I cannot write a struct which works all the time.

I worked with a map, but I can figure out which map I have to use, because if I can have a field with 6 nested keys or 3 nested keys with array in it before the value I'm searching and it will fails.

I am pretty lost with those kind of things in a dynamic context.

Ideally, I want to do a cat myFile.yaml | yq '.config."abs:getit"', but in Golang...

Any ideas and best practices to do that ?

Upvotes: 3

Views: 933

Answers (3)

slaff.bg
slaff.bg

Reputation: 139

I think you need this code. Just put the correct path to your "myFile.yaml" file. In main() function, you will see two different examples of how to use the code according to your needs.

getConfVal finds a node of a YAML tree with an arbitrary sequence in N-dimensional depth. If the node does not exist, the value will be nil.

myFile.yaml

config:
    foo:bar: baz
    bar:foo: baz
    abs:getit: myvalue
    foo:
      bar: "conf-foo-bar"
    bar:
      foo: "conf-bar-foo"
    abs:
      getit: "conf-abs-getit"
one:
  two:
    three:
      four:
        five: 5
        five2: [4, 7]

package main

import (
    "fmt"
    "os"

    yaml "gopkg.in/yaml.v3"
)

func main() {
    if err := readTConf("./path/to/myFile.yaml", &cfg); err != nil {
        fmt.Printf("Read YAML Conf: %v\n", err)
        return
    }

    // e.g.
    getConfVal(cfg, []string{"foo:bar"})
    // getConfVal(cfg, []string{"foo", "bar"})
    // getConfVal(cfg, []string{"four", "five"})
    // getConfVal(cfg, []string{"four", "five2"})

    fmt.Printf("\nThis is the result you are looking for. (%v)\n", needleRes)
}

var needleRes interface{}
var cfg map[string]interface{}

func getConfVal(o map[string]interface{}, ns []string) (map[string]interface{}, bool) {
    nsCnt := len(ns)
    for kn, vn := range ns {
        for ko, vo := range o {
            if fmt.Sprintf("%T", vo) == "map[string]interface {}" {
                res, ok := getConfVal(vo.(map[string]interface{}), ns)
                if ok {
                    return res, true
                    break
                }
            }

            if fmt.Sprintf("%T", vo) == "string" {
                if ko == vn {
                    if kn+1 == nsCnt {
                        needleRes = vo
                        return map[string]interface{}{}, true
                    }
                }
            }
        }
    }

    return map[string]interface{}{}, false
}

func readTConf(f string, c *map[string]interface{}) error {
    yamlFile, err := os.ReadFile(f)
    if err != nil {
        return err
    }

    if err := yaml.Unmarshal([]byte(yamlFile), &c); err != nil {
        return err
    }

    return nil
}

Upvotes: 1

Zbob
Zbob

Reputation: 31

Thank you for your precise answer. I'm sorry but there is an error in the question, and I apologize for the mistake.

It's not a single flow scalar Yaml but a map, since there is space before the value :

config:
    foo:bar: baz
    bar:foo: baz
    abs:getit: myvalue

The code above logically returns a conversion error like this :

panic: yaml: unmarshal errors:
  line 2: cannot unmarshal !!map into string

My whole code is here. The file I read is a Pulumi config Yaml, which will be different for all projects, except for one common key ("abs:getit:"), only the value is different.

The original question file has been modified. Really sorry for that...

Upvotes: 0

mkopriva
mkopriva

Reputation: 38243

You can do:

func main() {
    var obj struct {
        Config struct {
            AbsGetit string `yaml:"abs:getit"`
        } `yaml:"config"`
    }
    err := yaml.Unmarshal(data, &obj)
    if err != nil {
        panic(err)
    }
    fmt.Printf("%q\n", obj.Config.AbsGetit)
}

https://go.dev/play/p/KJ_lzZxaZBy

Upvotes: 1

Related Questions