156 lines
3.5 KiB
Go
156 lines
3.5 KiB
Go
// Package bkl implements a layered configuration language parser.
|
|
package bkl
|
|
|
|
import (
|
|
"bytes"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
)
|
|
|
|
var (
|
|
Err = fmt.Errorf("bkl error")
|
|
ErrUnknownFormat = fmt.Errorf("unknown format (%w)", Err)
|
|
ErrDecode = fmt.Errorf("decoding error (%w)", Err)
|
|
)
|
|
|
|
// Parser carries state for parse operations with multiple layered inputs.
|
|
type Parser struct {
|
|
docs []any
|
|
}
|
|
|
|
// New creates and returns a new [Parser] with an empty starting document.
|
|
//
|
|
// New always succeeds and returns a Parser instance.
|
|
func New() *Parser {
|
|
return &Parser{}
|
|
}
|
|
|
|
// NewFromFile creates a new [Parser] then calls [MergeFileLayers()] with
|
|
// the supplied path.
|
|
func NewFromFile(path string) (*Parser, error) {
|
|
p := New()
|
|
|
|
err := p.MergeFileLayers(path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return p, nil
|
|
}
|
|
|
|
// MergePatch applies the supplied patch to the [Parser]'s current internal
|
|
// document state (at the specified document index) using bkl's merge
|
|
// semantics.
|
|
func (p *Parser) MergePatch(index int, patch any) error {
|
|
// XXX
|
|
p.docs[index] = patch
|
|
|
|
return nil
|
|
}
|
|
|
|
// MergeIndexBytes parses the supplied doc bytes as the format specified by ext
|
|
// (file extension), then calls [MergePatch()].
|
|
func (p *Parser) MergeIndexBytes(index int, doc []byte, ext string) error {
|
|
f, found := formatByExtension[ext]
|
|
if !found {
|
|
return fmt.Errorf("%s: %w", ext, ErrUnknownFormat)
|
|
}
|
|
|
|
patch, err := f.decode(doc)
|
|
if err != nil {
|
|
return fmt.Errorf("%w / %w", err, ErrDecode)
|
|
}
|
|
|
|
err = p.MergePatch(index, patch)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MergeMultiBytes calls [MergeIndexBytes()] once for each item in the outer
|
|
// slice.
|
|
func (p *Parser) MergeMultiBytes(bs [][]byte, ext string) error {
|
|
for i, b := range bs {
|
|
err := p.MergeIndexBytes(i, b, ext)
|
|
if err != nil {
|
|
return fmt.Errorf("document index %d (of [0,%d]): %w", i, len(bs)-1, err)
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MergeBytes splits its input into multiple documents (using the ---
|
|
// delimiter) then calls [MergeMultiBytes()].
|
|
func (p *Parser) MergeBytes(b []byte, ext string) error {
|
|
docs := bytes.SplitAfter(b, []byte("\n---\n"))
|
|
|
|
for i, doc := range docs {
|
|
// Leave the initial \n attached
|
|
docs[i] = bytes.TrimSuffix(doc, []byte("---\n"))
|
|
}
|
|
|
|
return p.MergeMultiBytes(docs, ext)
|
|
}
|
|
|
|
// MergeReader reads all input then calls [MergeBytes()].
|
|
func (p *Parser) MergeReader(in io.Reader, ext string) error {
|
|
b, err := io.ReadAll(in)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return p.MergeBytes(b, ext)
|
|
}
|
|
|
|
// MergeFile opens the supplied path and determines the file format from the
|
|
// file extension, then calls [MergeReader()].
|
|
func (p *Parser) MergeFile(path string) error {
|
|
fh, err := os.Open(path)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
|
|
defer fh.Close()
|
|
|
|
parts := strings.Split(filepath.Base(path), ".")
|
|
ext := parts[len(parts)-1]
|
|
|
|
err = p.MergeReader(fh, ext)
|
|
if err != nil {
|
|
return fmt.Errorf("%s: %w", path, err)
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// MergeFileLayers determines relevant layers from the supplied path and merges
|
|
// them in order.
|
|
func (p *Parser) MergeFileLayers(path string) error {
|
|
dir := filepath.Dir(path)
|
|
base := filepath.Base(path)
|
|
|
|
parts := strings.Split(base, ".")
|
|
ext := parts[len(parts)-1]
|
|
|
|
for i := 1; i < len(parts); i++ {
|
|
layerParts := []string{}
|
|
layerParts = append(layerParts, parts[:i]...)
|
|
layerParts = append(layerParts, ext)
|
|
|
|
layerPath := filepath.Join(dir, strings.Join(layerParts, "."))
|
|
|
|
err := p.MergeFile(layerPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|