Files
fejkdata/index.go
T

213 lines
6.5 KiB
Go

package fejkdata
import (
"errors"
"fmt"
"io/fs"
"slices"
"strings"
"github.com/larvit/fejkdata/internal/datafiles"
"github.com/larvit/fejkdata/internal/grammar"
)
// indexed is a category an index names and no call has reached yet, the source holding it,
// and the modules that source reads by default.
type indexed struct {
entry datafiles.IndexEntry
src datafiles.Source
defaultModules []string
}
// placeIndex sets every category m's index names as unloaded in root, in place of what
// root holds at its path.
func placeIndex(root *folder, src datafiles.Source, m datafiles.Manifest) {
for _, p := range sortedNames(m.Index) {
segs := strings.Split(p, ".")
madeFolder(root, segs[:len(segs)-1]).putUnloaded(segs[len(segs)-1], &indexed{entry: m.Index[p], src: src, defaultModules: m.Reads})
}
}
// categoryAt is where a category, loaded or not, sits: the folder holding it, that
// folder's path, and its name.
type categoryAt struct {
dir []string
in *folder
name string
}
func (c categoryAt) unloaded() bool { return c.in.unloaded[c.name] != nil }
// categoryOn is the category, loaded or not, that a path from root names or passes.
func categoryOn(root *folder, segs []string) (categoryAt, bool) {
g, i := folderAt(root, segs)
if i == len(segs) {
return categoryAt{}, false
}
c := categoryAt{dir: segs[:i:i], in: g, name: segs[i]}
return c, c.unloaded() || g.children[c.name] != nil
}
// loadCallerPath strips the / a caller's path may start with, splits the path, and loads the
// unloaded category it names or passes. The caller walks it next, and a walk loads nothing.
func (f *Generator) loadCallerPath(path string) (string, []string, error) {
path, err := grammar.CallerPath(path)
if err != nil {
return "", nil, fmt.Errorf("fejkdata: %w", err)
}
segs, err := grammar.SplitPath(path)
if err != nil {
return "", nil, fmt.Errorf("fejkdata: %w", err)
}
if c, ok := categoryOn(&f.root, segs); ok && c.unloaded() {
if err := loadReached(&f.root, nil, []categoryAt{c}, nil); err != nil {
return "", nil, fmt.Errorf("fejkdata: %w", loadError{err})
}
}
return path, segs, nil
}
// referenced is every category, loaded or not, that the templates of nodes reference, each
// reference read from the folder dir.
func referenced(root *folder, dir []string, nodes nodeSet) []categoryAt {
var out []categoryAt
_ = nodes(func(_ string, n node) error {
t, isTemplate := n.(*template)
if !isTemplate {
return nil
}
for _, name := range refTokens(t.tokens) {
segs, err := refSegments(name, dir)
if err != nil {
continue // resolveRefs reports it
}
if c, ok := categoryOn(root, segs); ok {
out = append(out, c)
}
}
return nil
})
return out
}
// needs is every category, loaded or not, that s references, the table beside s that its
// parent column names, and every unloaded table beside s whose parent s is.
func needs(root *folder, s categorySite) []categoryAt {
out := referenced(root, s.dir, siteNodes([]categorySite{s}))
if parent := parentOf(s.n); parent != "" {
out = append(out, categoryAt{dir: s.dir, in: s.in, name: parent})
}
for _, name := range sortedNames(s.in.unloaded) {
if s.in.unloaded[name].entry.Parent == s.name {
out = append(out, categoryAt{dir: s.dir, in: s.in, name: name})
}
}
return out
}
// loadReached loads the unloaded categories among wanted and every unloaded category they
// or sites need, then runs sites and all it loaded through a whole load's pipeline. On an
// error it puts back what it loaded, so the next call fails the same way. defaultModules
// holds, by path, the modules the source of each of sites reads by default.
func loadReached(root *folder, sites []categorySite, wanted []categoryAt, defaultModules map[string][]string) error {
queue := wanted
for _, s := range sites {
queue = append(queue, needs(root, s)...)
}
var b batch
err := b.load(root, queue)
if err == nil {
err = b.run(root, sites)
}
if err != nil {
err = withDefaultModules(err, func(category string) []string {
for _, l := range b {
if l.site.path == category {
return l.was.defaultModules
}
}
return defaultModules[category]
})
b.putBack()
}
return err
}
// batch is the categories one loadReached loaded, each beside the entry it was loaded from.
type batch []loaded
type loaded struct {
site categorySite
was *indexed
}
// load loads each unloaded category in queue and what it needs. It stops at a table whose
// parent column disagrees with its entry. Otherwise a table could load without its parent,
// or an earlier table could link to one that putBack then removes.
func (b *batch) load(root *folder, queue []categoryAt) error {
for ; len(queue) > 0; queue = queue[1:] {
c := queue[0]
x := c.in.unloaded[c.name]
if x == nil {
continue
}
if err := x.src.Load(c.dir, c.name, compileInto(func([]string) *folder { return c.in })); err != nil {
if errors.Is(err, fs.ErrNotExist) {
err = fmt.Errorf("%s indexes %s: %w", x.src.ManifestPath(), categoryPath(c.dir, c.name), err)
}
return err
}
s := siteIn(c.dir, c.in, c.name)
*b = append(*b, loaded{site: s, was: x})
if parent := parentOf(s.n); parent != x.entry.Parent {
return fmt.Errorf("%s: the index entry for %s %s, and its table %s", x.src.ManifestPath(), s.path, namesParent(x.entry.Parent), namesParent(parent))
}
queue = append(queue, needs(root, s)...)
}
return nil
}
func (b batch) run(root *folder, sites []categorySite) error {
sites = slices.Clip(sites)
for _, l := range b {
sites = append(sites, l.site)
}
if len(sites) == 0 {
return nil
}
return categoryPipeline(sites, root.children).run()
}
// withDefaultModules adds to err, where a reference of a category names nothing, the modules
// the category's source reads by default, or that it names none.
func withDefaultModules(err error, of func(category string) []string) error {
var u unresolvedRead
if !errors.As(err, &u) {
return err
}
if modules := of(u.category); len(modules) > 0 {
return fmt.Errorf("%w; the module holding %s reads by default %s", err, u.category, strings.Join(modules, ", "))
}
return fmt.Errorf("%w, and the module holding %s names no module it reads by default", err, u.category)
}
func (b batch) putBack() {
for _, l := range b {
l.site.in.putUnloaded(l.site.name, l.was)
}
}
func namesParent(parent string) string {
if parent == "" {
return "names no parent"
}
return fmt.Sprintf("names parent %q", parent)
}
func parentOf(n node) string {
if t, isTable := n.(*table); isTable {
return t.rows.Options().Parent
}
return ""
}