244 lines
7.0 KiB
Go
244 lines
7.0 KiB
Go
package fejkdata
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
|
|
"github.com/larvit/fejkdata/internal/datafiles"
|
|
"github.com/larvit/fejkdata/internal/grammar"
|
|
)
|
|
|
|
// ErrLoad marks data that fails to load: in New, or on the first call reaching a category
|
|
// of an indexed source.
|
|
var ErrLoad = errors.New("data fails to load")
|
|
|
|
// loadError is a load's error, unchanged in its text, matching ErrLoad.
|
|
type loadError struct{ error }
|
|
|
|
func (e loadError) Unwrap() error { return e.error }
|
|
func (loadError) Is(target error) bool { return target == ErrLoad }
|
|
|
|
// indexed is a category an index names and no call has reached yet, and the source holding it.
|
|
type indexed struct {
|
|
entry datafiles.IndexEntry
|
|
src datafiles.Source
|
|
}
|
|
|
|
// placeIndex sets every category index names unloaded in root, in place of what root holds
|
|
// at its path.
|
|
func placeIndex(root *folder, src datafiles.Source, index map[string]datafiles.IndexEntry) {
|
|
for _, p := range sortedNames(index) {
|
|
segs := strings.Split(p, ".")
|
|
madeFolder(root, segs[:len(segs)-1]).putUnloaded(segs[len(segs)-1], &indexed{entry: index[p], src: src})
|
|
}
|
|
}
|
|
|
|
// 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}); 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
|
|
}
|
|
|
|
// affectedBy is every unloaded category under root whose index entry leads to one of
|
|
// paths: through its reads, its parent or a child table, and through other unloaded
|
|
// categories.
|
|
func affectedBy(root *folder, paths []string) []categoryAt {
|
|
if len(paths) == 0 {
|
|
return nil
|
|
}
|
|
unloaded := unloadedUnder(root)
|
|
readers := map[string][]string{}
|
|
for _, p := range sortedNames(unloaded) {
|
|
c := unloaded[p]
|
|
e := c.in.unloaded[c.name].entry
|
|
for _, r := range e.Reads {
|
|
readers[r] = append(readers[r], p)
|
|
}
|
|
if e.Parent != "" {
|
|
parent := categoryPath(c.dir, e.Parent)
|
|
readers[parent] = append(readers[parent], p)
|
|
readers[p] = append(readers[p], parent)
|
|
}
|
|
}
|
|
var out []categoryAt
|
|
seen := map[string]bool{}
|
|
queue := slices.Clone(paths)
|
|
sort.Strings(queue)
|
|
for ; len(queue) > 0; queue = queue[1:] {
|
|
for _, p := range readers[queue[0]] {
|
|
if c, isUnloaded := unloaded[p]; isUnloaded && !seen[p] {
|
|
seen[p] = true
|
|
out = append(out, c)
|
|
queue = append(queue, p)
|
|
}
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func unloadedUnder(root *folder) map[string]categoryAt {
|
|
out := map[string]categoryAt{}
|
|
var walk func(g *folder, dir []string)
|
|
walk = func(g *folder, dir []string) {
|
|
for name := range g.unloaded {
|
|
c := categoryAt{dir: dir, in: g, name: name}
|
|
out[categoryPath(dir, name)] = c
|
|
}
|
|
for name, n := range g.children {
|
|
if sub, isFolder := n.(*folder); isFolder {
|
|
walk(sub, append(dir[:len(dir):len(dir)], name))
|
|
}
|
|
}
|
|
}
|
|
walk(root, nil)
|
|
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.
|
|
func loadReached(root *folder, sites []categorySite, wanted []categoryAt) 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 {
|
|
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. A table whose parent
|
|
// column disagrees with its entry stops it, so a table loads with its parent and no table
|
|
// loaded before links to one putBack 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 {
|
|
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 names parent %q, and its table's parent column is %q", x.src.ManifestPath(), s.path, x.entry.Parent, 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()
|
|
}
|
|
|
|
func (b batch) putBack() {
|
|
for _, l := range b {
|
|
l.site.in.putUnloaded(l.site.name, l.was)
|
|
}
|
|
}
|
|
|
|
func parentOf(n node) string {
|
|
if t, isTable := n.(*table); isTable {
|
|
return t.rows.Options().Parent
|
|
}
|
|
return ""
|
|
}
|