Search code examples
dictionarygoyaml

Merge two map hierarchies from YAML files preserving all the keys


I'm working on solution that involve nested keys from yaml files. The software will read the files passed in args and load them in order updating/adding keys.

I have 2 yaml file and I want to merge them without lose any key. I want to stack all the config file to generate a single map without removing any key.

so I have yaml 1

env: test1
template:
  app:
    database: 
      name: oracle

yaml2

env: test2
template:
  app:
    database: 
      version : 12

The result that I want is ( order would be yaml1 - yaml2)

env: test2
template:
  app:
    database: 
      name: oracle
      version: 12

I tried to use maps to copy, but as the keys has the same name I end up with

env: test2
template:
    app:
        database:
            version: 12

I'm using

gopkg.in/yaml.v3 to read the yamls with gives me map[string]interface{}
and maps to use the Copy

package main

import (
    "fmt"
    "log"
    "maps"
    "os"
    "path/filepath"

    "gopkg.in/yaml.v3"
)

type configuration struct {
    c  m
    fl []string
}

type m = map[string]interface{}

func (c *configuration) Update(nc m) {
    if c.c == nil {
        c.c = nc
    } else {
        maps.Copy(c.c, nc)
    }
}

func (c configuration) Print() {
    d, err := yaml.Marshal(&c.c)
    if err != nil {
        log.Fatalf("error: %v", err)
    }
    fmt.Printf("---:\n%s\n\n", string(d))
}

func (c configuration) ParseDir(path string) {

}

func (c *configuration) LoadFromFile(filename string) {

    // YAML string stored in a variable
    yf, yfErr := os.ReadFile(filename)

    if yfErr != nil {
        log.Fatal("Error reading the file ", yfErr)
    }
    // Map to store the parsed YAML data
    var data m

    // Unmarshal the YAML string into the data map
    err := yaml.Unmarshal(yf, &data)
    if err != nil {
        log.Fatal(err)
    }
    c.Update(data)
}

func listFiles(path string) []string {
    var returnLf []string
    err := filepath.Walk(path,
        func(path string, info os.FileInfo, err error) error {
            if err != nil {
                return err
            }
            if info.Mode().IsRegular() {
                returnLf = append(returnLf, path)
            }
            return nil
        })
    if err != nil {
        log.Println(err)
    }
    return returnLf

}

Solution

  • Assuming you want to merge YAML mappings keyed by "template" in the both YAML documents, a rather trivial implementation would be something like this:

    package main
    
    import (
        "fmt"
    
        "gopkg.in/yaml.v3"
    )
    
    const data1 = `---
    env: test1
    template:
      app:
        database: 
          name: oracle
          foo: whatever
    `
    
    const data2 = `---
    env: test2
    template:
      app:
        some_stuff: [1, 2, 3, 4]
        database: 
          version : 12
          foo: 42
    `
    
    type T struct {
        Env  string         `yaml:"env"`
        Tmpl map[string]any `yaml:"template"`
    }
    
    func mergeMapsRecursively(dst, src map[string]any) map[string]any {
        res := make(map[string]any)
    
        for dstKey, dstVal := range dst {
            srcVal, exists := src[dstKey]
            if !exists {
                res[dstKey] = dstVal
                continue
            }
    
            dstValMap, dstValIsMap := dstVal.(map[string]any)
            srcValMap, srcValIsMap := srcVal.(map[string]any)
            if dstValIsMap && srcValIsMap {
                res[dstKey] = mergeMapsRecursively(dstValMap, srcValMap)
            } else {
                res[dstKey] = srcVal
            }
        }
    
        for srcKey, srcVal := range src {
            if _, exists := dst[srcKey]; !exists {
                res[srcKey] = srcVal
            }
        }
    
        return res
    }
    
    func main() {
        var a, b T
    
        if err := yaml.Unmarshal([]byte(data1), &a); err != nil {
            panic(err)
        }
    
        if err := yaml.Unmarshal([]byte(data2), &b); err != nil {
            panic(err)
        }
    
        fmt.Printf("%#v\n%#v\n%#v\n", a.Tmpl, b.Tmpl, mergeMapsRecursively(a.Tmpl, b.Tmpl))
    }
    

    Playground link.

    The mergeMapsRecursively functions recursively merges fields present in both maps, if they are all maps, or replaces the value in dst with the value in src, otherwise — just like maps.Copy does.