-1

XY problem: I'm trying to read in a YAML file such as the one below, and output a set of tuples that combine certain keys and values from the YAML file. E.g. given this YAML data:

---
fruit:
    apple:
        colour:
            - green
    'banana':
        colour:
            - yellow
    pear:
        colour:
            - green
            - yellow

I want to combine each key under "fruit" with each value under "colour" into tuples. My tuples would look like this:

apple:green
banana:yellow
pear:green
pear:yellow

To do this, I'm using a map[string]interface{} to Unmarshal my YAML data into Go - I can't use a struct because the names of the keys below "fruit" could be anything, so I need to use a dynamic type. This is my code so far:

package main

import (
    "fmt"
    "log"

    "gopkg.in/yaml.v3"
)

var data string = `
---
fruit:
    apple:
        colour:
            - green
    'banana':
        colour:
            - yellow
    pear:
        colour:
            - green
            - yellow
`

func main() {
    m := make(map[string]interface{})
    err := yaml.Unmarshal([]byte(data), &m)
    if err != nil {
        log.Fatal("Failed to parse YAML file")
    }

    for _, v := range m {
        fruits := v.(map[string]interface{})
        for fruit, v2 := range fruits {
            colours := v2.(map[string]interface{})["colour"]
            for colour := range colours {
                fmt.Println("%v:%v\n", fruit, colour)
            }
        }
    }
}

Playground link: https://go.dev/play/p/v8iuzmMLtjX

The problem is for colour := range colours - I get the error:

cannot range over colours (type interface {})

I found this answer to a similar question which says that I cannot directly convert a []interface{} to []string, and must instead iterate over the values. That's what I've tried to do here. The v2 variable works out to a map[string]interface {} type, which for example could be map[colour:[green yellow]]. Then I've tried converting that into another map[string]interface{} to get the value of "colour", which works out to a []interface{} type [green yellow] and is stored in the colours variable.

But I can't iterate over colours for some reason. I don't understand what's different about my solution and icza's solution in the linked answer (I've linked their Playground link). The data type of colours in my solution is []interface{}, and in icza's solution the data type of t is also []interface{} - but in that case it is possible to iterate through the slice and access the values within.

Another solution I tried was from this answer to a different question, which was to try directly converting the []interface{} to a []string:

c := colours.([]string)

That also didn't work:

panic: interface conversion: interface {} is []interface {}, not []string

What do I need to do to make this solution work?

Lou
  • 2,200
  • 2
  • 33
  • 66
  • You can be much more specific than `map[string]interface{}`: https://go.dev/play/p/Le00DlzOz8T. This should make it trivial to build your values. – Peter Jan 06 '22 at 17:35
  • @CeriseLimón That helps me iterate through colours - but then I can't assert to a string; `for colour := range c { cs := colour.(string) }` --> `invalid type assertion: colour.(string) (non-interface type int on left)`. Still trying to work out why it thinks I'm converting to an int. – Lou Jan 06 '22 at 17:42
  • @Peter I've had issues trying to Unmarshal to a struct when the keys are dynamic/arbitrarily named and haven't found any wisdom from other questions on the topic. Do you know how I would use the structs you suggested to Unmarshal my YAML data? – Lou Jan 06 '22 at 17:43
  • Ah, bingo! That's solved it for me, thank you @CeriseLimón :)) – Lou Jan 06 '22 at 17:52

2 Answers2

1

Since only the keys of the map are unknown at compile time but the structure is known you can be much more specific than map[string]interface{}:

type Document struct {
    Fruits map[string]Fruit `yaml:"fruit"`
}

type Fruit struct {
    Colours []string `yaml:"colour"`
}

This makes it trivial to build your values:

package main

import (
    "fmt"

    "gopkg.in/yaml.v3"
)


var data string = `
---
fruit:
    apple:
        colour:
            - green
    'banana':
        colour:
            - yellow
    pear:
        colour:
            - green
            - yellow
`

func main() {
    var m Document
    yaml.Unmarshal([]byte(data), &m)

    for name, fruit := range m.Fruits {
        for _, colour := range fruit.Colours {
            fmt.Printf("%s:%s\n", name, colour)
        }
    }
}

Try it on the playground: https://go.dev/play/p/phnvRriQmMh

Peter
  • 29,454
  • 5
  • 48
  • 60
  • Excellent, thanks! So with Cerise's help I've fixed my original solution and with your help I've got a different but also working solution. Next working day I'll try applying this to my real use-case and see which works better. Either way thanks for the help! – Lou Jan 06 '22 at 17:55
1

Thanks to Cerise Limón for providing the corrections that helped me to fix my solution!

package main

import (
    "fmt"
    "log"

    "gopkg.in/yaml.v3"
)

var data string = `
---
fruit:
    apple:
        colour:
            - green
    'banana':
        colour:
            - yellow
    pear:
        colour:
            - green
            - yellow
`

func main() {
    m := make(map[string]interface{})
    err := yaml.Unmarshal([]byte(data), &m)
    if err != nil {
        log.Fatal("Failed to parse YAML file")
    }

    for _, v := range m {
        fruits := v.(map[string]interface{})
        for fruit, v2 := range fruits {
            colours := v2.(map[string]interface{})["colour"]
            c := colours.([]interface{})
            for _, colour := range c {
                cs := colour.(string)
                fmt.Printf("%v:%v\n", fruit, cs)
            }
        }
    }
}

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

Lou
  • 2,200
  • 2
  • 33
  • 66