mirror of
https://codeberg.org/readeck/file-compose.git
synced 2026-06-18 09:24:32 +00:00
206 lines
4.0 KiB
Go
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
|
|
}
|
|
}
|
|
}
|