Answer architecture round 1: one owner keeps a folder's loaded and unloaded names apart, each loaded category is checked against its index entry, addSource returns what it replaced, the manifest cache goes, ErrLoad exits 1, and the docs say what New and Fake now refuse

This commit is contained in:
2026-10-09 19:26:54 +02:00
committed by Lillem4n
parent 642fe3a506
commit c783d4af23
13 changed files with 319 additions and 200 deletions
+54 -44
View File
@@ -3,7 +3,6 @@ package datafiles
import (
"bytes"
"embed"
"encoding/json"
"errors"
"fmt"
@@ -12,7 +11,6 @@ import (
"os"
"path"
"strings"
"sync"
"github.com/larvit/fejkdata/internal/grammar"
"github.com/larvit/fejkdata/internal/jsonvalue"
@@ -79,47 +77,28 @@ type IndexEntry struct {
Reads []string `json:"reads,omitempty"`
}
// embeddedManifests holds each manifest read from an embed.FS, whose files never change.
var embeddedManifests sync.Map
type embeddedManifest struct {
fsys embed.FS
base string
}
// Manifest reads the manifest at the source's root, the zero Manifest where there is none.
func (s Source) Manifest() (Manifest, error) {
e, embedded := s.fsys.(embed.FS)
key := embeddedManifest{fsys: e, base: s.base}
if m, read := embeddedManifests.Load(key); embedded && read {
return m.(Manifest), nil
if err := s.check(); err != nil {
return Manifest{}, err
}
m, err := s.readManifest()
if embedded && err == nil {
embeddedManifests.Store(key, m)
}
return m, err
}
func (s Source) readManifest() (Manifest, error) {
if s.onDisk && s.label == "" {
return Manifest{}, nil // Walk refuses the empty data path
}
full := path.Join(s.base, ManifestFile)
b, err := fs.ReadFile(s.fsys, full)
b, err := fs.ReadFile(s.fsys, path.Join(s.base, ManifestFile))
if errors.Is(err, fs.ErrNotExist) {
return Manifest{}, nil
}
if err != nil {
return Manifest{}, fmt.Errorf("%s: %w", s.labelled(full), err)
return Manifest{}, fmt.Errorf("%s: %w", s.ManifestPath(), err)
}
m, err := decodeManifest(b)
if err != nil {
return Manifest{}, fmt.Errorf("%s: %w", s.labelled(full), err)
return Manifest{}, fmt.Errorf("%s: %w", s.ManifestPath(), err)
}
return m, nil
}
// ManifestPath is the manifest's path as errors name it.
func (s Source) ManifestPath() string { return s.labelled(path.Join(s.base, ManifestFile)) }
func decodeManifest(b []byte) (Manifest, error) {
dec := json.NewDecoder(bytes.NewReader(b))
dec.DisallowUnknownFields()
@@ -130,36 +109,67 @@ func decodeManifest(b []byte) (Manifest, error) {
if _, err := dec.Token(); err != io.EOF {
return Manifest{}, fmt.Errorf("more than one JSON value")
}
for p := range m.Index {
for _, seg := range strings.Split(p, ".") {
if err := grammar.CheckIdentifier(seg); err != nil {
return Manifest{}, fmt.Errorf("index names %q: %w", p, err)
for p, e := range m.Index {
for _, q := range append([]string{p}, e.Reads...) {
if err := checkCategoryPath(q); err != nil {
return Manifest{}, fmt.Errorf("index entry %q: %w", p, err)
}
}
if e.Parent == "" {
continue
}
if err := grammar.CheckIdentifier(e.Parent); err != nil {
return Manifest{}, fmt.Errorf("index entry %q: parent %w", p, err)
}
sibling := e.Parent
if dot := strings.LastIndexByte(p, '.'); dot >= 0 {
sibling = p[:dot+1] + e.Parent
}
if _, ok := m.Index[sibling]; !ok {
return Manifest{}, fmt.Errorf("index entry %q: parent %q names no entry beside it", p, e.Parent)
}
}
return m, nil
}
func checkCategoryPath(p string) error {
for _, seg := range strings.Split(p, ".") {
if err := grammar.CheckIdentifier(seg); err != nil {
return fmt.Errorf("%q: %w", p, err)
}
}
return nil
}
// Walk hands compile every category of the tree, folders and files in name order. A
// folder holding no category anywhere below it is skipped; a hidden file or folder is no
// category, though a category may name a hidden rows file beside it.
func (s Source) Walk(compile func(Category) error) error {
if s.onDisk {
if s.label == "" {
return fmt.Errorf("a data path is empty")
}
info, err := os.Stat(s.label)
if err != nil {
return fmt.Errorf("data path %s: %w", s.label, err)
}
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", s.label)
}
if err := s.check(); err != nil {
return err
}
_, err := s.walkDir(nil, compile)
return err
}
// check refuses a data path that is empty or names no directory.
func (s Source) check() error {
if !s.onDisk {
return nil
}
if s.label == "" {
return fmt.Errorf("a data path is empty")
}
info, err := os.Stat(s.label)
if err != nil {
return fmt.Errorf("data path %s: %w", s.label, err)
}
if !info.IsDir() {
return fmt.Errorf("%s is not a directory", s.label)
}
return nil
}
// walkDir walks the folder that dir names, and reports whether it handed over any category.
func (s Source) walkDir(dir []string, compile func(Category) error) (bool, error) {
full := s.dirPath(dir)