Skip to content
Snippets Groups Projects
import.go 3.93 KiB
// Copyright 2022 schukai GmbH
// SPDX-License-Identifier: AGPL-3.0

package configuration

import (
	"bytes"
	"encoding/json"
	"fmt"
	"github.com/imdario/mergo"
	"github.com/magiconair/properties"
	toml "github.com/pelletier/go-toml/v2"
	"gitlab.schukai.com/oss/libraries/go/utilities/pathfinder.git"
	yaml "gopkg.in/yaml.v3"
	"io"
	"os"
	"path"
	"reflect"
)

func importJson[C any](config *C, reader io.Reader) error {
	decoder := json.NewDecoder(reader)
	return decoder.Decode(config)
}

func importYaml[C any](config *C, reader io.Reader) error {
	decoder := yaml.NewDecoder(reader)
	return decoder.Decode(config)
}

func importToml[C any](config *C, reader io.Reader) error {
	decoder := toml.NewDecoder(reader)
	return decoder.Decode(config)
}

func importProperties[C any](config *C, reader io.Reader) error {

	buf := &bytes.Buffer{}
	_, err := buf.ReadFrom(reader)
	if err != nil {
		return err
	}
	b := buf.Bytes()

	p, err := properties.Load(b, properties.UTF8)
	if err != nil {
		return err
	}

	return p.Decode(config)

}

func (s *Settings[C]) importStream(r reader, f ...func(n *C)) {
	var c C
	var err error

	defer func() {
		s.notifyErrorHooks()
	}()

	x := r.reader
	switch r.format {
	case Json:
		err = importJson(&c, x)
	case Yaml:
		err = importYaml(&c, x)
	case Toml:
		err = importToml(&c, x)
	case Properties:
		err = importProperties(&c, x)
	default:
		err = newFormatNotSupportedError(r.format)
	}

	if err == nil {
		s.importCounter++
	} else {
		s.errors = append(s.errors, err)
	}

	for _, fn := range f { // https://staticcheck.dev/docs/checks/#S1031
		fn(&c)
	}

	if err := mergo.Merge(&s.config, c); err != nil {
		s.errors = append(s.errors, err)
	}
}

func (s *Settings[C]) importStreams() {
	for _, r := range s.stream.readers {
		s.importStream(r)
	}
}

var typeOfPathValue PathValue

// replacePath replaces all relative paths with absolute paths
func replacePath[C any](basePath string, c C) error {

	paths := []string{}

	top := reflect.TypeOf(typeOfPathValue)
	toc := reflect.ValueOf(c)
	ptp := &paths

	pathfinder.FindPaths(toc, top, []string{}, ptp)

	for _, px := range paths {

		p, err := pathfinder.GetValue[C](c, px)
		if err != nil {
			return err
		}

		switch p.(type) {
		case PathValue:
			tp, ok := p.(PathValue)
			if !ok {
				return fmt.Errorf("type assertion failed")
			}

			if !path.IsAbs(p.(PathValue).String()) {
				newPath := PathValue(path.Join(basePath, tp.String()))

				switch reflect.TypeOf(p).Kind() {
				case reflect.Ptr:
					err = pathfinder.SetValue[C](c, px, &newPath)

				case reflect.String:

					err = pathfinder.SetValue[C](c, px, newPath.String())

				default:
					err = pathfinder.SetValue[C](c, px, newPath)
				}

				if err != nil {
					return err
				}
			}
		}

	}

	return nil

}

func (s *Settings[C]) importFiles() {

	defer func() {
		s.notifyErrorHooks()
	}()

	for _, d := range s.files.directories {
		fn := path.Join(d, s.files.name+s.files.format.Extension())

		f, err := s.files.fs.Open(fn)

		if os.IsNotExist(err) {
			_ = f.Close()
			continue
		}

		r := (io.Reader)(f)
		s.importStream(reader{s.files.format, r}, func(c *C) {
			_ = replacePath[*C](d, c)
		})
		_ = f.Close()
	}

	for _, f := range s.files.files {
		r, err := s.files.fs.Open(f.path)

		if err != nil {
			s.errors = append(s.errors, err)
			_ = r.Close()
			continue
		}

		s.importStream(reader{f.format, r}, func(c *C) {
			d := path.Dir(f.path)
			err := replacePath[*C](d, c)
			if err != nil {
				return
			}
		})

		_ = r.Close()
	}

}

func (s *Settings[C]) Import() *Settings[C] {

	s.Lock()

	defaults := s.config

	var n C
	s.config = n

	s.importCounter = 0

	s.importFiles()
	s.importStreams()

	defer func() {
		s.notifyErrorHooks()
	}()

	if err := mergo.Merge(&s.config, defaults); err != nil {
		s.errors = append(s.errors, err)
	}

	if s.importCounter == 0 {
		s.errors = append(s.errors, NoFileImportedError)
	}

	x := s.config
	s.config = defaults
	_, l := s.setConfigInternal(x)
	s.Unlock()
	s.notifyHooks(l)

	return s
}