Files
2023-10-21 16:43:22 +02:00

206 lines
4.0 KiB
Go

// SPDX-FileCopyrightText: © 2023 Olivier Meunier <olivier@neokraft.net>
//
// SPDX-License-Identifier: AGPL-3.0-only
package resolver
import (
"encoding/json"
"errors"
"fmt"
"io"
"io/fs"
"path"
"strings"
"github.com/itchyny/gojq"
"gopkg.in/yaml.v3"
)
func decodeFile(filename string, r io.Reader, v any) error {
ext := path.Ext(filename)
switch ext {
case ".json":
return json.NewDecoder(r).Decode(v)
case ".yaml", ".yml":
return yaml.NewDecoder(r).Decode(v)
}
return fmt.Errorf("%s: unknown format", ext)
}
type Resolver struct {
fs fs.FS
resolved map[string]map[string]any
cRef *gojq.Code
}
func New(f fs.FS) (*Resolver, error) {
r := &Resolver{
fs: f,
resolved: make(map[string]map[string]any),
}
qRef, err := gojq.Parse(`(.. | select(has("$merge")?)) | resolveMerge($filename)`)
if err != nil {
return nil, err
}
r.cRef, err = gojq.Compile(qRef,
gojq.WithFunction("resolveMerge", 1, 1, r.resolveMerge),
gojq.WithVariables([]string{"$filename"}),
)
if err != nil {
return nil, err
}
return r, nil
}
func (r *Resolver) Load(name string) (map[string]any, error) {
err := r.loadFile(name)
if err != nil {
return nil, err
}
return r.resolved[name], nil
}
func (r *Resolver) loadFile(name string) error {
file, err := r.fs.Open(name)
if err != nil {
return err
}
spec := map[string]any{}
if err = decodeFile(name, file, &spec); err != nil {
return err
}
r.resolved[name] = spec
iter := r.cRef.Run(r.resolved[name], name)
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
return err
}
}
return nil
}
func (r *Resolver) resolveMerge(value any, args []any) any {
currentFile := args[0].(string)
x, ok := value.(map[string]any)
if !ok {
return errors.New("not a map")
}
merges := []string{}
switch t := x["$merge"].(type) {
case string:
merges = append(merges, t)
case []any:
for _, x := range t {
if m, ok := x.(string); ok {
merges = append(merges, m)
}
}
}
for _, m := range merges {
xtra, err := r.lookup(currentFile, m)
if err != nil {
return err
}
// for k, v := range mergeMaps(x, xtra) {
// x[k] = v
// }
// mergo.Merge(&x, xtra)
deepMerge(x, xtra)
}
delete(x, "$merge")
return x
}
func (r *Resolver) lookup(currentFile, p string) (map[string]any, error) {
parts := strings.SplitN(p, "#", 2)
var filename, query string
if len(parts) == 1 {
query = parts[0]
} else {
filename = parts[0]
query = parts[1]
}
if filename == "" {
filename = currentFile
}
q, err := gojq.Parse(query)
if err != nil {
return nil, err
}
if _, ok := r.resolved[filename]; !ok {
if err = r.loadFile(filename); err != nil {
return nil, err
}
}
iter := q.Run(r.resolved[filename])
for {
v, ok := iter.Next()
if !ok {
break
}
if err, ok := v.(error); ok {
return nil, err
}
if v == nil {
return nil, fmt.Errorf(`value "%s" not found`, query)
}
x, ok := v.(map[string]any)
if !ok {
return nil, fmt.Errorf(`value "%s" is not a map`, query)
} else {
return x, nil
}
}
return nil, fmt.Errorf(`value "%s" not found`, query)
}
func deepMerge(dst, src map[string]interface{}) {
for srcKey, srcValue := range src {
if srcValueAsMap, ok := srcValue.(map[string]interface{}); ok { // maps
if dstValue, ok := dst[srcKey]; ok {
if dstValueAsMap, ok := dstValue.(map[string]interface{}); ok {
deepMerge(dstValueAsMap, srcValueAsMap)
continue
}
} else {
dst[srcKey] = make(map[string]interface{})
}
deepMerge(dst[srcKey].(map[string]interface{}), srcValueAsMap)
} else if srcValueAsSlice, ok := srcValue.([]interface{}); ok { // slices
if dstValue, ok := dst[srcKey]; ok {
if dstValueAsSlice, ok := dstValue.([]interface{}); ok {
// If both src and dst are slices, we'll copy the elements from that src slice over to the dst slice
dst[srcKey] = append(dstValueAsSlice, srcValueAsSlice...)
continue
}
}
dst[srcKey] = srcValueAsSlice
} else { // primitives
dst[srcKey] = srcValue
}
}
}