133 lines
4.0 KiB
Go
133 lines
4.0 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/larvit/fejkdata/internal/grammar"
|
|
)
|
|
|
|
// resolvedRef is what a reference resolves to. head is "/" and the category's path from the root,
|
|
// the key the category is held under; tail is the path read into the category.
|
|
type resolvedRef struct {
|
|
head string
|
|
tail []string
|
|
}
|
|
|
|
// categoryOf is the category path a resolvedRef's head names.
|
|
func categoryOf(head string) string { return strings.TrimPrefix(head, "/") }
|
|
|
|
// refSegments resolves a reference written in folder to a path from the root.
|
|
func refSegments(name string, folder []string) ([]string, error) {
|
|
sigil, rest, err := grammar.RefShape(name)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
var base []string
|
|
switch sigil {
|
|
case ".":
|
|
base = folder
|
|
case "..":
|
|
if len(folder) == 0 {
|
|
return nil, fmt.Errorf("no folder above the root")
|
|
}
|
|
base = folder[:len(folder)-1]
|
|
}
|
|
segs, _ := grammar.SplitPath(rest) // grammar.RefShape proved it splits
|
|
return append(append([]string{}, base...), segs...), nil
|
|
}
|
|
|
|
// resolveRefs resolves every reference t reads.
|
|
func (t *template) resolveRefs(folder []string, label string, root map[string]node) (templateRefs, error) {
|
|
var refs templateRefs
|
|
names := refTokens(t.tokens)
|
|
if len(names) == 0 {
|
|
return refs, nil
|
|
}
|
|
refs.byName = make(map[string]resolvedRef, len(names))
|
|
refs.categories = make(map[string]node, len(names))
|
|
for _, name := range names {
|
|
segments, err := refSegments(name, folder)
|
|
if err != nil {
|
|
return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
|
|
}
|
|
categorySegs, target, tail, err := resolveCategory(root, segments)
|
|
if err != nil {
|
|
return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
|
|
}
|
|
head := "/" + strings.Join(categorySegs, ".")
|
|
if err := provePath(target, tail, head); err != nil {
|
|
return refs, fmt.Errorf("%s: reference {%s}: %w", label, name, err)
|
|
}
|
|
refs.categories[head] = target
|
|
refs.byName[name] = resolvedRef{head, tail}
|
|
}
|
|
return refs, nil
|
|
}
|
|
|
|
// resolveCategory walks a dotted path through the folders to the category it
|
|
// names, returning that category's segments, the node, and the tail left to read into it.
|
|
func resolveCategory(root map[string]node, segments []string) (categorySegs []string, target node, tail []string, err error) {
|
|
g, i := folderAt(&folder{children: root}, segments)
|
|
switch {
|
|
case i == len(segments):
|
|
return nil, nil, nil, fmt.Errorf("names a folder, not a value")
|
|
case grammar.IsSelector(segments[i]):
|
|
return nil, nil, nil, fmt.Errorf("%s is a folder, not a table, so it has no row to select", strings.Join(segments[:i], "."))
|
|
}
|
|
n, ok := g.children[segments[i]]
|
|
if !ok {
|
|
return nil, nil, nil, noEntry{segments[i]}
|
|
}
|
|
return segments[:i+1], n, segments[i+1:], nil
|
|
}
|
|
|
|
// folderAt is the deepest folder a path from root walks into, and the index of the first
|
|
// segment naming no folder in it, len(segs) where every segment does.
|
|
func folderAt(root *folder, segs []string) (*folder, int) {
|
|
g := root
|
|
for i, seg := range segs {
|
|
sub, isFolder := g.children[seg].(*folder)
|
|
if !isFolder {
|
|
return g, i
|
|
}
|
|
g = sub
|
|
}
|
|
return g, len(segs)
|
|
}
|
|
|
|
// refTokens returns the reference names a format reads, as tokens or as operands, or binds.
|
|
func refTokens(toks []grammar.Token) []string {
|
|
var refs []string
|
|
seen := map[string]bool{}
|
|
for _, tok := range toks {
|
|
names := tokenReads(tok)
|
|
if tok.Kind == grammar.NameBind {
|
|
names = []string{tok.BoundRef}
|
|
}
|
|
for _, name := range names {
|
|
if grammar.IsRef(name) && !seen[name] {
|
|
seen[name] = true
|
|
refs = append(refs, name)
|
|
}
|
|
}
|
|
}
|
|
return refs
|
|
}
|
|
|
|
// noEntry is a path segment naming nothing in the folder it reads.
|
|
type noEntry struct{ seg string }
|
|
|
|
func (e noEntry) Error() string { return fmt.Sprintf("no entry %q", e.seg) }
|
|
|
|
// noDataNote adds to err, where a segment names nothing and f holds no data at all, that no
|
|
// data is loaded.
|
|
func (f *Generator) noDataNote(err error) error {
|
|
var ne noEntry
|
|
if errors.As(err, &ne) && len(f.root.children) == 0 && len(f.root.unloaded) == 0 {
|
|
return fmt.Errorf("%w; no data is loaded", err)
|
|
}
|
|
return err
|
|
}
|