// Package loader reads YAML into a config.Config.
//
// See mercemay.top/src/portr/ for context.
package loader
import (
"fmt"
"io"
"os"
"gopkg.in/yaml.v3"
"github.com/mercemay/portr/internal/config"
)
// LoadFile reads path and decodes it on top of config.Defaults().
func LoadFile(path string) (config.Config, error) {
f, err := os.Open(path)
if err != nil {
return config.Config{}, fmt.Errorf("loader: open %s: %w", path, err)
}
defer f.Close()
return Load(f)
}
// Load decodes from any reader.
func Load(r io.Reader) (config.Config, error) {
b, err := io.ReadAll(r)
if err != nil {
return config.Config{}, fmt.Errorf("loader: read: %w", err)
}
c := config.Defaults()
dec := yaml.NewDecoder(bytesReader(b))
dec.KnownFields(true)
if err := dec.Decode(&c); err != nil && err != io.EOF {
return config.Config{}, fmt.Errorf("loader: decode: %w", err)
}
if err := c.Validate(); err != nil {
return config.Config{}, err
}
return c, nil
}
// bytesReader is a tiny helper so tests can feed strings without
// pulling in bytes.NewReader here (keeps imports minimal).
func bytesReader(b []byte) io.Reader {
return &sliceReader{b: b}
}
type sliceReader struct {
b []byte
i int
}
func (s *sliceReader) Read(p []byte) (int, error) {
if s.i >= len(s.b) {
return 0, io.EOF
}
n := copy(p, s.b[s.i:])
s.i += n
return n, nil
}