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
}
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))
}
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.