Load a shipped category on the first call reaching it, from an index generated from data/

This commit is contained in:
2026-10-03 17:12:44 +02:00
committed by Lillem4n
parent 75597664e0
commit 5667cd5dc3
17 changed files with 422 additions and 142 deletions
+1
View File
@@ -43,6 +43,7 @@ In [docs/decisions.md](docs/decisions.md):
- A padded JSON argument is rejected, not trimmed - A padded JSON argument is rejected, not trimmed
- `FakeTemplate` and `NewTemplate` both stay - `FakeTemplate` and `NewTemplate` both stay
- The shipped data is embedded, not discovered - The shipped data is embedded, not discovered
- The shipped set alone loads a category on the first call reaching it; beside a `--data-path` it loads whole
- A bare reference draws each time; a reference path is held - A bare reference draws each time; a reference path is held
- Reference sigils follow the filesystem - Reference sigils follow the filesystem
- A change to what exists is a major; a minor only adds - A change to what exists is a major; a minor only adds
+9 -5
View File
@@ -813,9 +813,11 @@ token {w} is repeated, and uppercase operand "w" holds "w" to one draw per expan
The target is about a microsecond per value (goal 12). The target is about a microsecond per value (goal 12).
Each file is parsed, validated and weight-indexed once, in `New`, bar a table's name and Each file is parsed, validated and weight-indexed once, bar a table's name and parent
parent columns, which are mapped on the first `Fake` that selects by name or descends columns, which are mapped on the first `Fake` that selects by name or descends through
through it. Proving the draw fences adds a pass over the loaded tree per it. With the shipped set alone, a category loads on the first call reaching it, with
every category it reads, and `List` reads an index; beside a `--data-path`, every
category loads in `New`. Proving the draw fences adds a pass over the loaded tree per
fence, and walks what a render reads only where data holds a reference, so a set that fence, and walks what a render reads only where data holds a reference, so a set that
holds none pays for the passes alone. A `Fake` call then costs about what its output holds none pays for the passes alone. A `Fake` call then costs about what its output
costs: an unweighted pick is O(1) whatever the list's length, a weighted one costs: an unweighted pick is O(1) whatever the list's length, a weighted one
@@ -931,7 +933,9 @@ Where the project is heading; the sections before Audience document what ships t
``` ```
doc.go the package doc, and the vocabulary the package is written in doc.go the package doc, and the vocabulary the package is written in
fejkdata.go Generator, New, options, the embedded data set, List fejkdata.go Generator, New, options, List
shipped.go the embedded data set, and loading a shipped category on the first call reaching it
shippedindex.go the index of shipped categories, generated from data/
node.go the node model and JSON -> node compilation, with its fence on a nested drawGroup node.go the node model and JSON -> node compilation, with its fence on a nested drawGroup
table.go tables: the rows TSV, its options and links, row selection and draws table.go tables: the rows TSV, its options and links, row selection and draws
name.go names: {ref as n} bindings, the scopes they live in, and the reads they answer name.go names: {ref as n} bindings, the scopes they live in, and the reads they answer
@@ -1016,7 +1020,7 @@ what the score asks of a pull request. Goal 5 is checked against simulated templ
writers from the Audience. writers from the Audience.
A change to the shipped data re-pins [`testdata/shipped_shape.txt`](testdata/shipped_shape.txt) A change to the shipped data re-pins [`testdata/shipped_shape.txt`](testdata/shipped_shape.txt)
in its own commit: and regenerates [`shippedindex.go`](shippedindex.go) in its own commit:
```sh ```sh
REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test REPIN=1 docker compose run --rm --user "$(id -u):$(id -g)" test
+58 -18
View File
@@ -55,7 +55,7 @@ func loadData(sources []dataSource) (map[string]node, error) {
if len(root) == 0 { if len(root) == 0 {
return nil, fmt.Errorf("no .json data found") return nil, fmt.Errorf("no .json data found")
} }
if err := treeBinding(root).bind(); err != nil { if err := categoryBinding(categorySites(&folder{children: root}), root).bind(); err != nil {
return nil, err return nil, err
} }
return root, nil return root, nil
@@ -69,12 +69,7 @@ func loadDir(src dataSource, dir string) (*folder, error) {
return nil, fmt.Errorf("%s: %w", src.labelled(dir), err) return nil, fmt.Errorf("%s: %w", src.labelled(dir), err)
} }
g := &folder{children: map[string]node{}} g := &folder{children: map[string]node{}}
files := &categoryFiles{src: src, dir: dir, tsv: map[string]bool{}} files := newCategoryFiles(src, dir, entries)
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".tsv") && !e.IsDir() {
files.tsv[e.Name()] = false
}
}
for _, e := range entries { for _, e := range entries {
if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data if strings.HasPrefix(e.Name(), ".") { // hidden: a checkout or an editor's file, never data
continue continue
@@ -106,6 +101,16 @@ type categoryFiles struct {
tsv map[string]bool tsv map[string]bool
} }
func newCategoryFiles(src dataSource, dir string, entries []fs.DirEntry) *categoryFiles {
files := &categoryFiles{src: src, dir: dir, tsv: map[string]bool{}}
for _, e := range entries {
if strings.HasSuffix(e.Name(), ".tsv") && !e.IsDir() {
files.tsv[e.Name()] = false
}
}
return files
}
func (c *categoryFiles) readRows(name string) (string, error) { func (c *categoryFiles) readRows(name string) (string, error) {
if _, present := c.tsv[name]; !present { if _, present := c.tsv[name]; !present {
return "", fmt.Errorf("rows names %s, which is not beside it in %s", name, c.src.labelled(c.dir)) return "", fmt.Errorf("rows names %s, which is not beside it in %s", name, c.src.labelled(c.dir))
@@ -175,12 +180,45 @@ func mergeChildren(dst, src map[string]node) {
} }
} }
// nodeScope is the set of nodes one validation pass covers: a whole loaded tree, // nodeScope is the set of nodes one validation pass covers: the categories one load
// or a single inline node. // binds, or a single inline node.
type nodeScope func(fn func(path string, n node) error) error type nodeScope func(fn func(path string, n node) error) error
func treeScope(root map[string]node) nodeScope { // categorySite is a loaded category and where it sits: the folder holding it, that
return func(fn func(path string, n node) error) error { return walkNodes(root, fn) } // folder's path, and its own dot path.
type categorySite struct {
dir []string
path string
in *folder
n node
}
// categorySites lists every category under root, folders and names in sorted order.
func categorySites(root *folder) []categorySite {
var out []categorySite
var walk func(dir []string, g *folder)
walk = func(dir []string, g *folder) {
for _, name := range sortedNames(g.children) {
if sub, isFolder := g.children[name].(*folder); isFolder {
walk(append(dir[:len(dir):len(dir)], name), sub)
continue
}
out = append(out, categorySite{dir: dir, path: join(strings.Join(dir, "."), name), in: g, n: g.children[name]})
}
}
walk(nil, root)
return out
}
func sitesScope(sites []categorySite) nodeScope {
return func(fn func(path string, n node) error) error {
for _, s := range sites {
if err := eachNode(s.n, s.path, fn); err != nil {
return err
}
}
return nil
}
} }
func inlineScope(n node, label string) nodeScope { func inlineScope(n node, label string) nodeScope {
@@ -197,17 +235,19 @@ type binding struct {
typedByGo bool typedByGo bool
} }
func treeBinding(root map[string]node) binding { // categoryBinding binds the categories of one load, their references resolving
// against root.
func categoryBinding(sites []categorySite, root map[string]node) binding {
return binding{ return binding{
scope: treeScope(root), scope: sitesScope(sites),
link: func() error { link: func() error {
setTablePaths(root) setTablePaths(sites)
if err := linkTables(root); err != nil { if err := linkTables(sites); err != nil {
return err return err
} }
return linkRefs(root) return linkRefs(sites, root)
}, },
scopeFence: func() error { return checkNoCycles(root) }, scopeFence: func() error { return checkNoCycles(sites) },
} }
} }
@@ -229,7 +269,7 @@ func (b binding) bind() error {
// checkNodeFences runs the per-node fences every binding needs over a scope, each // checkNodeFences runs the per-node fences every binding needs over a scope, each
// over the whole scope before the next, so which of several broken nodes is reported // over the whole scope before the next, so which of several broken nodes is reported
// does not depend on the walk. Its walks recurse unguarded, so the scope's cycles are // does not depend on the walk. Its walks recurse unguarded, so the scope's cycles are
// refused first: a tree by checkNoCycles, and an inline node closes none, since nothing // refused first: categories by checkNoCycles, and an inline node closes none, since nothing
// references it. // references it.
func checkNodeFences(s nodeScope) error { func checkNodeFences(s nodeScope) error {
mem := renderCounts{} mem := renderCounts{}
+13 -2
View File
@@ -114,8 +114,19 @@ and `regexp.Match`, not two spellings of one result.
A directory a machine happens to have would make `--seed 42` machine-dependent. Data A directory a machine happens to have would make `--seed 42` machine-dependent. Data
still lives in `data/` as JSON; `--data-path` layers over it. still lives in `data/` as JSON; `--data-path` layers over it.
`todo.md` items 76 and 77 revise it: shipped data loads on demand from an index built at `todo.md` item 77 revises it: a library imports the shipped data as Go modules.
build time, and a library imports it as Go modules.
## The shipped set alone loads a category on the first call reaching it; beside a `--data-path` it loads whole
2026-10-03, approved by the maintainer as the restructure of `todo.md` item 76. Goal 12: a
`New` pays nothing for the categories a run never reaches, which matters once the shipped
set grows to hundreds of MB. A category loads with every category it references and its
whole table family, and is bound as a whole load binds, so it renders as it would from
`New`. `List` reads `shippedindex.go`, generated from `data/` with `REPIN=1`, and
`TestShippedIndexIsCurrent` fails while it is stale. CI loads the whole shipped set, so a
shipped category never fails at first reach. Goal 6: beside a `--data-path` or a
`WithDataFS`, every category loads in `New`, so each mistake in a source the user wrote is
`New`'s error. Valid while the shipped set is proven whole in CI.
## A bare reference draws each time; a reference path is held ## A bare reference draws each time; a reference path is held
+4 -4
View File
@@ -31,7 +31,7 @@ type fenceRoot struct {
func fenceRoots(t *testing.T, f *Generator) []fenceRoot { func fenceRoots(t *testing.T, f *Generator) []fenceRoot {
t.Helper() t.Helper()
var roots []fenceRoot var roots []fenceRoot
_ = walkNodes(f.root.children, func(path string, n node) error { _ = sitesScope(categorySites(&f.root))(func(path string, n node) error {
if tm, ok := n.(*template); ok { if tm, ok := n.(*template); ok {
roots = append(roots, fenceRoot{path, tm}) roots = append(roots, fenceRoot{path, tm})
} }
@@ -58,7 +58,7 @@ func TestPairsFindWhatAWholeReplayFinds(t *testing.T) {
corpus := fenceCorpus(t) corpus := fenceCorpus(t)
roots := fenceRoots(t, corpus) roots := fenceRoots(t, corpus)
shipped := newShippedWhole(t) shipped := newShippedWhole(t)
_ = walkNodes(shipped.root.children, func(path string, n node) error { _ = sitesScope(categorySites(&shipped.root))(func(path string, n node) error {
if tm, ok := n.(*template); ok { if tm, ok := n.(*template); ok {
roots = append(roots, fenceRoot{path, tm}) roots = append(roots, fenceRoot{path, tm})
} }
@@ -84,7 +84,7 @@ func TestPairsFindWhatAWholeReplayOfSelectedRowsFinds(t *testing.T) {
} }
var reads []pathRead var reads []pathRead
fold := newReadFold() fold := newReadFold()
_ = walkNodes(f.root.children, func(_ string, n node) error { _ = sitesScope(categorySites(&f.root))(func(_ string, n node) error {
tb, isTable := n.(*table) tb, isTable := n.(*table)
if !isTable || tb.keyIndex < 0 || tb.familyRoot().path != "region" { if !isTable || tb.keyIndex < 0 || tb.familyRoot().path != "region" {
return nil return nil
@@ -190,7 +190,7 @@ func TestReadSurveyReportsAPinClashAsAnError(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("loadDir = %v", err) t.Fatalf("loadDir = %v", err)
} }
if err := treeBinding(g.children).link(); err != nil { if err := categoryBinding(categorySites(g), g.children).link(); err != nil {
t.Fatalf("link = %v", err) t.Fatalf("link = %v", err)
} }
r, isTemplate := g.children["r"].(*template) r, isTemplate := g.children["r"].(*template)
+31 -23
View File
@@ -2,7 +2,6 @@ package fejkdata
import ( import (
crand "crypto/rand" crand "crypto/rand"
"embed"
"encoding/binary" "encoding/binary"
"errors" "errors"
"fmt" "fmt"
@@ -14,9 +13,6 @@ import (
"sync" "sync"
) )
//go:embed data
var shippedFS embed.FS
// MaxRepeat caps a repeat, and caps the renders nested repeats multiply to along // MaxRepeat caps a repeat, and caps the renders nested repeats multiply to along
// any one path; the CLI's --repeat shares it. // any one path; the CLI's --repeat shares it.
const MaxRepeat = 1 << 20 const MaxRepeat = 1 << 20
@@ -30,9 +26,8 @@ var ErrNoData = errors.New("no data: WithoutShippedData needs at least one WithD
// It is safe for concurrent use; a seeded sequence is reproducible only when drawn // It is safe for concurrent use; a seeded sequence is reproducible only when drawn
// from one goroutine. // from one goroutine.
type Generator struct { type Generator struct {
// mu guards rand, records and structs. The tree under root is fixed after New bar each // mu guards rand, records, structs and root, which loads a shipped category on the
// table's row lookup, built under its sync.Once, so List, NewTemplate and // first call reaching it.
// NewRecordTemplate read it without mu.
mu sync.Mutex mu sync.Mutex
rand *generatorState rand *generatorState
root folder // the categories as the node a path walks from, owned here so a walk allocates none root folder // the categories as the node a path walks from, owned here so a walk allocates none
@@ -97,15 +92,7 @@ func New(opts ...Option) (*Generator, error) {
for _, opt := range opts { for _, opt := range opts {
opt(&c) opt(&c)
} }
var sources []dataSource root, err := c.load()
if c.shipped {
sources = append(sources, dataSource{fsys: shippedFS, baseDir: "data"})
}
sources = append(sources, c.sources...)
if len(sources) == 0 {
return nil, fmt.Errorf("fejkdata: %w", ErrNoData)
}
cats, err := loadData(sources)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
@@ -113,19 +100,35 @@ func New(opts ...Option) (*Generator, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
return &Generator{rand: rng, root: folder{children: cats}}, nil return &Generator{rand: rng, root: root}, nil
}
// load is the tree New starts from. The shipped data set alone loads each category on the
// first call reaching it; beside any other source, every category loads here, so each
// mistake in that source is New's error.
func (c config) load() (folder, error) {
if c.shipped && len(c.sources) == 0 {
return shelve(), nil
}
var sources []dataSource
if c.shipped {
sources = append(sources, shippedSource)
}
sources = append(sources, c.sources...)
if len(sources) == 0 {
return folder{}, ErrNoData
}
cats, err := loadData(sources)
return folder{children: cats}, err
} }
// List returns the sorted dotted paths Fake renders: each category and every field, // List returns the sorted dotted paths Fake renders: each category and every field,
// column and linked table below it, a direct descent at a time. A choice consumes no // column and linked table below it, a direct descent at a time. A choice consumes no
// segment, so a path continues through one only where every variant carries it. // segment, so a path continues through one only where every variant carries it.
func (f *Generator) List() []string { func (f *Generator) List() []string {
var out []string f.mu.Lock()
for _, name := range sortedNames(f.root.children) { defer f.mu.Unlock()
for _, p := range paths(f.root.children[name]) { out := paths(&f.root)
out = append(out, join(name, p))
}
}
sort.Strings(out) sort.Strings(out)
return out return out
} }
@@ -141,6 +144,11 @@ func paths(n node) []string {
out = append(out, join(name, p)) out = append(out, join(name, p))
} }
} }
for name, c := range n.unloaded {
for _, p := range c.paths {
out = append(out, join(name, p))
}
}
return out return out
case *template: case *template:
out := []string{""} out := []string{""}
+2 -12
View File
@@ -5,16 +5,6 @@ import (
"sort" "sort"
) )
// walkNodes calls fn once per contained node, passing the dot path that reaches it.
func walkNodes(root map[string]node, fn func(path string, n node) error) error {
for _, name := range sortedNames(root) {
if err := eachNode(root[name], name, fn); err != nil {
return err
}
}
return nil
}
// eachNode visits n and every node contained within it once, passing the dot path // eachNode visits n and every node contained within it once, passing the dot path
// that reaches each. It never crosses a reference edge — a {/path} reference is // that reaches each. It never crosses a reference edge — a {/path} reference is
// skipped — so a single inline node is walked on its own. // skipped — so a single inline node is walked on its own.
@@ -196,7 +186,7 @@ func repeatCheck(path string, n node, mem renderCounts) error {
// graph (renderEdges). Every node is a root: a field its parent's format never // graph (renderEdges). Every node is a root: a field its parent's format never
// renders is still reachable by dot path, so a cycle in one would otherwise reach // renders is still reachable by dot path, so a cycle in one would otherwise reach
// render and be fatal there. // render and be fatal there.
func checkNoCycles(root map[string]node) error { func checkNoCycles(sites []categorySite) error {
const ( const (
grey = 1 grey = 1
black = 2 black = 2
@@ -219,5 +209,5 @@ func checkNoCycles(root map[string]node) error {
color[n] = black color[n] = black
return nil return nil
} }
return walkNodes(root, func(path string, n node) error { return visit(n, path) }) return sitesScope(sites)(func(path string, n node) error { return visit(n, path) })
} }
+10 -3
View File
@@ -32,7 +32,9 @@ func (f *Generator) NewTemplate(input string) (*Template, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
if err := inlineBinding(n, "template", f.root.children).bind(); err != nil { f.mu.Lock()
defer f.mu.Unlock()
if err := inlineBinding(n, "template", &f.root).bind(); err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
return &Template{g: f, n: n}, nil return &Template{g: f, n: n}, nil
@@ -146,11 +148,16 @@ func inputValue(input string) (any, error) {
return raw, nil return raw, nil
} }
func inlineBinding(n node, label string, root map[string]node) binding { func inlineBinding(n node, label string, root *folder) binding {
scope := inlineScope(n, label) scope := inlineScope(n, label)
return binding{ return binding{
scope: scope, scope: scope,
link: func() error { return linkNodeRefs(scope, root) }, link: func() error {
if err := loadShipped(root, shippedReads(root, nil, scope)); err != nil {
return err
}
return linkNodeRefs(scope, root.children)
},
scopeFence: func() error { scopeFence: func() error {
if t, isTemplate := n.(*template); isTemplate && t.drawGroup != "" { if t, isTemplate := n.(*template); isTemplate && t.drawGroup != "" {
return fmt.Errorf("%s: drawGroup %q names nothing, since nothing can reference an inline template; drop it", label, t.drawGroup) return fmt.Errorf("%s: drawGroup %q names nothing, since nothing can reference an inline template; drop it", label, t.drawGroup)
+4 -1
View File
@@ -16,7 +16,10 @@ type node interface{ isNode() }
// folder is a namespace of named children, built from a directory of JSON files // folder is a namespace of named children, built from a directory of JSON files
// and subdirectories. It has no value of its own: descend into a named child by // and subdirectories. It has no value of its own: descend into a named child by
// dot path; rendering one is an error (see Fake). // dot path; rendering one is an error (see Fake).
type folder struct{ children map[string]node } type folder struct {
children map[string]node
unloaded map[string]shippedCategory // the shipped categories here a call has not reached yet
}
func (*folder) isNode() {} func (*folder) isNode() {}
+1 -1
View File
@@ -118,7 +118,7 @@ func TestReadFoldWalksACellByEveryRoute(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("loadDir = %v", err) t.Fatalf("loadDir = %v", err)
} }
if err := treeBinding(g.children).link(); err != nil { if err := categoryBinding(categorySites(g), g.children).link(); err != nil {
t.Fatalf("link = %v", err) t.Fatalf("link = %v", err)
} }
r, isTemplate := g.children["r"].(*template) r, isTemplate := g.children["r"].(*template)
+3
View File
@@ -126,6 +126,9 @@ func (f *Generator) FakeRecord(path string) (*Record, error) {
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %w", err) return nil, fmt.Errorf("fejkdata: %w", err)
} }
if err := f.loadShippedAt(segments); err != nil {
return nil, fmt.Errorf("fejkdata: %w", err)
}
_, n, tail, err := resolveCategory(f.root.children, segments) _, n, tail, err := resolveCategory(f.root.children, segments)
if err != nil { if err != nil {
return nil, fmt.Errorf("fejkdata: %s: %w", path, err) return nil, fmt.Errorf("fejkdata: %s: %w", path, err)
+16 -37
View File
@@ -71,17 +71,16 @@ func refSegments(name string, folder []string) ([]string, error) {
// so a reference sees the override-resolved tree. A reference's head binds into // so a reference sees the override-resolved tree. A reference's head binds into
// refHeads under its root path and its tail reads like a sibling path, so two // refHeads under its root path and its tail reads like a sibling path, so two
// spellings of one target are one draw. // spellings of one target are one draw.
func linkRefs(root map[string]node) error { func linkRefs(sites []categorySite, root map[string]node) error {
if err := eachTemplate(root, func(folder []string, path string, t *template) error { if err := eachTemplate(sites, func(s categorySite, path string, t *template) error {
category := strings.Join(strings.Split(path, ".")[:len(folder)+1], ".") return linkTemplate(s.dir, path, s.path, t, root)
return linkTemplate(folder, path, category, t, root)
}); err != nil { }); err != nil {
return err return err
} }
if err := eachTemplate(root, func(_ []string, path string, t *template) error { return linkNames(path, t) }); err != nil { if err := eachTemplate(sites, func(_ categorySite, path string, t *template) error { return linkNames(path, t) }); err != nil {
return err return err
} }
return eachTemplate(root, func(_ []string, path string, t *template) error { return checkNameReads(path, t) }) return eachTemplate(sites, func(_ categorySite, path string, t *template) error { return checkNameReads(path, t) })
} }
// linkTemplate resolves t's references in category, "" for an inline template, and // linkTemplate resolves t's references in category, "" for an inline template, and
@@ -196,40 +195,20 @@ func loneRef(toks []formatToken) (string, bool) {
return toks[0].names[0], true return toks[0].names[0], true
} }
// eachTemplate calls fn once per template, with the folder its category sits in // eachTemplate calls fn once per template of the categories, with the category it sits
// and the dot path reaching it, folders and names in sorted order. // in and the dot path reaching it.
func eachTemplate(root map[string]node, fn func(folder []string, path string, t *template) error) error { func eachTemplate(sites []categorySite, fn func(s categorySite, path string, t *template) error) error {
var inCategory func(folder []string, path string, n node) error for _, s := range sites {
inCategory = func(folder []string, path string, n node) error { if err := eachNode(s.n, s.path, func(path string, n node) error {
if t, ok := n.(*template); ok { if t, isTemplate := n.(*template); isTemplate {
if err := fn(folder, path, t); err != nil { return fn(s, path, t)
return err
} }
return nil
}); err != nil {
return err
} }
for _, c := range contained(n) {
if err := inCategory(folder, join(path, c.name), c.node); err != nil {
return err
}
}
return nil
} }
var inFolder func(dir []string, children map[string]node) error return nil
inFolder = func(dir []string, children map[string]node) error {
for _, name := range sortedNames(children) {
path := join(strings.Join(dir, "."), name)
if g, ok := children[name].(*folder); ok {
if err := inFolder(append(dir[:len(dir):len(dir)], name), g.children); err != nil {
return err
}
continue
}
if err := inCategory(dir, path, children[name]); err != nil {
return err
}
}
return nil
}
return inFolder(nil, root)
} }
// resolveCategory walks a dotted path through the folders to the category it // resolveCategory walks a dotted path through the folders to the category it
+3
View File
@@ -24,6 +24,9 @@ func (f *Generator) Fake(path string) (string, error) {
if err != nil { if err != nil {
return "", fmt.Errorf("fejkdata: %w", err) return "", fmt.Errorf("fejkdata: %w", err)
} }
if err := f.loadShippedAt(segments); err != nil {
return "", fmt.Errorf("fejkdata: %w", err)
}
var draws renderDraws var draws renderDraws
sc := renderScope{draws: &draws} sc := renderScope{draws: &draws}
n, err := descend(f.rand, &f.root, segments, sc) n, err := descend(f.rand, &f.root, segments, sc)
+168
View File
@@ -0,0 +1,168 @@
package fejkdata
import (
"embed"
"fmt"
"io/fs"
"path"
"sort"
"strings"
)
//go:embed data
var shippedFS embed.FS
var shippedSource = dataSource{fsys: shippedFS, baseDir: "data"}
// shippedCategory is a shipped category as shippedindex.go lists it: the table beside it
// that it links to, "" for none, and the paths List advertises below it.
type shippedCategory struct {
parent string
paths []string
}
// shelve builds the folders of the shipped index, with every category in them unloaded.
func shelve() folder {
root := folder{children: map[string]node{}}
for p, c := range shippedIndex {
segs := strings.Split(p, ".")
g := &root
for _, seg := range segs[:len(segs)-1] {
sub, isFolder := g.children[seg].(*folder)
if !isFolder {
sub = &folder{children: map[string]node{}}
g.children[seg] = sub
}
g = sub
}
if g.unloaded == nil {
g.unloaded = map[string]shippedCategory{}
}
g.unloaded[segs[len(segs)-1]] = c
}
return root
}
// pendingLoad is an unloaded shipped category: the folder holding it, that folder's path,
// and its name there.
type pendingLoad struct {
dir []string
in *folder
name string
}
// unloadedAt is the unloaded shipped category a path from root names, or descends into.
func unloadedAt(root *folder, segs []string) (pendingLoad, bool) {
g := root
for i, seg := range segs {
switch n := g.children[seg].(type) {
case *folder:
g = n
continue
case nil:
if _, unloaded := g.unloaded[seg]; unloaded {
return pendingLoad{dir: segs[:i:i], in: g, name: seg}, true
}
}
return pendingLoad{}, false
}
return pendingLoad{}, false
}
// loadShippedAt loads the shipped category a caller's path names or descends into, unless
// it is loaded already.
func (f *Generator) loadShippedAt(segs []string) error {
if p, unloaded := unloadedAt(&f.root, segs); unloaded {
return loadShipped(&f.root, []pendingLoad{p})
}
return nil
}
// shippedReads is every unloaded shipped category the templates of scope reference, each
// reference read from the folder dir.
func shippedReads(root *folder, dir []string, scope nodeScope) []pendingLoad {
var out []pendingLoad
_ = scope(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 // the link reports it
}
if p, unloaded := unloadedAt(root, segs); unloaded {
out = append(out, p)
}
}
return nil
})
return out
}
// loadShipped loads the shipped categories wanted, with every category they read and the
// whole table family of each, then binds what it loaded, as New binds a whole load. A failed
// bind leaves every category it loaded unloaded again.
func loadShipped(root *folder, wanted []pendingLoad) (err error) {
var loaded []pendingLoad
var entries []shippedCategory
defer func() {
if err == nil {
return
}
for i, p := range loaded {
delete(p.in.children, p.name)
p.in.unloaded[p.name] = entries[i]
}
}()
var sites []categorySite
for queue := wanted; len(queue) > 0; queue = queue[1:] {
p := queue[0]
c, unloaded := p.in.unloaded[p.name]
if !unloaded {
continue
}
site, err := p.load()
if err != nil {
return err
}
loaded, entries = append(loaded, p), append(entries, c)
sites = append(sites, site)
queue = append(queue, shippedReads(root, p.dir, sitesScope([]categorySite{site}))...)
queue = append(queue, p.family(c)...)
}
sort.Slice(sites, func(i, j int) bool { return sites[i].path < sites[j].path })
return categoryBinding(sites, root.children).bind()
}
// load parses and compiles the category, and moves it from its folder's unloaded
// categories to its children.
func (p pendingLoad) load() (categorySite, error) {
dir := path.Join(append([]string{shippedSource.baseDir}, p.dir...)...)
entries, err := fs.ReadDir(shippedSource.fsys, dir)
if err != nil {
return categorySite{}, fmt.Errorf("%s: %w", dir, err)
}
file := p.name + ".json"
if err := loadFile(shippedSource, p.in, path.Join(dir, file), file, newCategoryFiles(shippedSource, dir, entries)); err != nil {
return categorySite{}, err
}
delete(p.in.unloaded, p.name)
return categorySite{dir: p.dir, path: join(strings.Join(p.dir, "."), p.name), in: p.in, n: p.in.children[p.name]}, nil
}
// family is the tables beside p that a table family links it to: its parent, and every
// table whose parent it is.
func (p pendingLoad) family(c shippedCategory) []pendingLoad {
var out []pendingLoad
if c.parent != "" {
out = append(out, pendingLoad{dir: p.dir, in: p.in, name: c.parent})
}
for name, sibling := range p.in.unloaded {
if sibling.parent == p.name {
out = append(out, pendingLoad{dir: p.dir, in: p.in, name: name})
}
}
return out
}
+81
View File
@@ -0,0 +1,81 @@
// Code generated by TestShippedIndexIsCurrent with REPIN=1. DO NOT EDIT.
package fejkdata
var shippedIndex = map[string]shippedCategory{
"en_US.address": {paths: []string{"", "locality", "postal-code", "region", "street", "street-number"}},
"en_US.color": {paths: []string{""}},
"en_US.company": {paths: []string{"", "base", "suffix"}},
"en_US.date": {paths: []string{""}},
"en_US.email": {paths: []string{"", "domain", "local", "local.n"}},
"en_US.first-name": {parent: "sex", paths: []string{"", "count", "name", "sex"}},
"en_US.ip": {paths: []string{""}},
"en_US.itin": {paths: []string{"", "area", "group", "serial"}},
"en_US.last-name": {paths: []string{"", "count", "name"}},
"en_US.person": {paths: []string{"", "first", "last", "prefix", "sex"}},
"en_US.phone": {paths: []string{"", "area", "exch", "line"}},
"en_US.price": {paths: []string{"", "amt", "cents"}},
"en_US.sentence": {paths: []string{"", "adj", "noun", "prep", "verb"}},
"en_US.sex": {paths: []string{"", "code", "first-name", "first-name.count", "first-name.name", "first-name.sex", "name", "title", "title.name", "title.sex", "title.share"}},
"en_US.ssn": {paths: []string{"", "area", "group", "serial"}},
"en_US.time": {paths: []string{""}},
"en_US.title": {parent: "sex", paths: []string{"", "name", "sex", "share"}},
"en_US.url": {paths: []string{"", "host", "path"}},
"en_US.username": {paths: []string{"", "n"}},
"en_US.version": {paths: []string{"", "n", "pre", "suffix"}},
"en_US.word": {paths: []string{""}},
"geo.SE.address": {paths: []string{"", "locality", "postal-code", "street", "street-number"}},
"geo.SE.locality": {parent: "municipality", paths: []string{"", "lat", "lon", "municipality", "name", "population", "postal-code", "postal-code.code", "postal-code.locality", "street", "street.locality", "street.name", "street.segments"}},
"geo.SE.municipality": {parent: "region", paths: []string{"", "code", "locality", "locality.lat", "locality.lon", "locality.municipality", "locality.name", "locality.population", "locality.postal-code", "locality.postal-code.code", "locality.postal-code.locality", "locality.street", "locality.street.locality", "locality.street.name", "locality.street.segments", "name", "population", "region"}},
"geo.SE.postal-code": {parent: "locality", paths: []string{"", "code", "locality"}},
"geo.SE.region": {paths: []string{"", "code", "municipality", "municipality.code", "municipality.locality", "municipality.locality.lat", "municipality.locality.lon", "municipality.locality.municipality", "municipality.locality.name", "municipality.locality.population", "municipality.locality.postal-code", "municipality.locality.postal-code.code", "municipality.locality.postal-code.locality", "municipality.locality.street", "municipality.locality.street.locality", "municipality.locality.street.name", "municipality.locality.street.segments", "municipality.name", "municipality.population", "municipality.region", "name", "population", "timezone"}},
"geo.SE.street": {parent: "locality", paths: []string{"", "locality", "name", "segments"}},
"geo.US.address": {paths: []string{"", "locality", "postal-code", "region", "street", "street-number"}},
"geo.US.locality": {parent: "municipality", paths: []string{"", "code", "lat", "lon", "municipality", "name", "population", "postal-code", "postal-code.addresses", "postal-code.code", "postal-code.locality", "street", "street.addresses", "street.locality", "street.name"}},
"geo.US.municipality": {parent: "region", paths: []string{"", "code", "locality", "locality.code", "locality.lat", "locality.lon", "locality.municipality", "locality.name", "locality.population", "locality.postal-code", "locality.postal-code.addresses", "locality.postal-code.code", "locality.postal-code.locality", "locality.street", "locality.street.addresses", "locality.street.locality", "locality.street.name", "name", "population", "region"}},
"geo.US.postal-code": {parent: "locality", paths: []string{"", "addresses", "code", "locality"}},
"geo.US.region": {paths: []string{"", "abbr", "code", "municipality", "municipality.code", "municipality.locality", "municipality.locality.code", "municipality.locality.lat", "municipality.locality.lon", "municipality.locality.municipality", "municipality.locality.name", "municipality.locality.population", "municipality.locality.postal-code", "municipality.locality.postal-code.addresses", "municipality.locality.postal-code.code", "municipality.locality.postal-code.locality", "municipality.locality.street", "municipality.locality.street.addresses", "municipality.locality.street.locality", "municipality.locality.street.name", "municipality.name", "municipality.population", "municipality.region", "name", "population", "timezone"}},
"geo.US.street": {parent: "locality", paths: []string{"", "addresses", "locality", "name"}},
"misc.car": {paths: []string{"", "make", "model"}},
"misc.coordinate": {paths: []string{"", "lat", "lon"}},
"misc.creditcard": {paths: []string{"", "d"}},
"misc.currency": {paths: []string{"", "code", "decimals", "name", "numeric", "symbol"}},
"misc.datetime": {paths: []string{""}},
"misc.emoji": {paths: []string{""}},
"misc.httpmethod": {paths: []string{"", "idempotent", "method", "safe"}},
"misc.httpstatus": {paths: []string{"", "code", "reason"}},
"misc.language": {paths: []string{"", "code", "code3", "name"}},
"misc.loglevel": {paths: []string{"", "code", "keyword", "severity"}},
"misc.mac": {paths: []string{""}},
"misc.mimetype": {paths: []string{"", "ext", "type"}},
"misc.objectid": {paths: []string{""}},
"misc.port": {paths: []string{"", "number", "service"}},
"misc.protocol": {paths: []string{"", "keyword", "name", "number"}},
"misc.territory": {paths: []string{"", "alpha2", "alpha3", "calling-code", "capital", "country", "currency", "flag", "languages", "name", "numeric", "timezone", "timezone.offset", "timezone.territory", "timezone.weight", "timezone.zone", "tld"}},
"misc.timezone": {parent: "territory", paths: []string{"", "offset", "territory", "weight", "zone"}},
"misc.tld": {paths: []string{"", "tld", "type", "unicode"}},
"misc.useragent": {paths: []string{"", "browser", "device", "os", "ua"}},
"misc.uuid": {paths: []string{"", "variant"}},
"sv_SE.address": {paths: []string{"", "locality", "postal-code", "street", "street-number"}},
"sv_SE.birth-number": {parent: "sex", paths: []string{"", "number", "sex"}},
"sv_SE.color": {paths: []string{""}},
"sv_SE.company": {paths: []string{"", "base", "suffix"}},
"sv_SE.date": {paths: []string{""}},
"sv_SE.email": {paths: []string{"", "domain", "local", "local.n"}},
"sv_SE.first-name": {parent: "sex", paths: []string{"", "count", "name", "sex"}},
"sv_SE.ip": {paths: []string{""}},
"sv_SE.last-name": {paths: []string{"", "count", "name"}},
"sv_SE.person": {paths: []string{"", "first", "last", "prefix", "sex"}},
"sv_SE.personnummer": {paths: []string{""}},
"sv_SE.phone": {paths: []string{"", "a", "b", "c", "prefix"}},
"sv_SE.price": {paths: []string{"", "amt", "ore"}},
"sv_SE.samordningsnummer": {paths: []string{""}},
"sv_SE.sentence": {paths: []string{"", "adj", "noun", "prep", "verb"}},
"sv_SE.sex": {paths: []string{"", "birth-number", "birth-number.number", "birth-number.sex", "code", "first-name", "first-name.count", "first-name.name", "first-name.sex", "name"}},
"sv_SE.time": {paths: []string{""}},
"sv_SE.title": {paths: []string{"", "name", "share"}},
"sv_SE.url": {paths: []string{"", "host", "path"}},
"sv_SE.username": {paths: []string{"", "n"}},
"sv_SE.version": {paths: []string{"", "n", "pre", "suffix"}},
"sv_SE.word": {paths: []string{""}},
}
+5 -5
View File
@@ -49,7 +49,7 @@ func (f *Generator) structShapeOf(t reflect.Type) (*structShape, error) {
if label == "" { if label == "" {
label = "struct" label = "struct"
} }
sc := &structCompile{categories: f.root.children, visiting: map[reflect.Type]bool{}, structsLeft: maxStructs} sc := &structCompile{root: &f.root, visiting: map[reflect.Type]bool{}, structsLeft: maxStructs}
shape, err := sc.compileShape(t, label) shape, err := sc.compileShape(t, label)
if err == nil && shape.empty() { if err == nil && shape.empty() {
err = fmt.Errorf("%s has no fake tags, so nothing to fill", t) err = fmt.Errorf("%s has no fake tags, so nothing to fill", t)
@@ -82,7 +82,7 @@ func (s *structShape) empty() bool { return s.record == nil && len(s.nested) ==
// loaded tree, the types compiling or embedded above, so a pointer back to one is left alone // loaded tree, the types compiling or embedded above, so a pointer back to one is left alone
// rather than filled without end, and how many more structs it may walk. // rather than filled without end, and how many more structs it may walk.
type structCompile struct { type structCompile struct {
categories map[string]node root *folder
visiting map[reflect.Type]bool visiting map[reflect.Type]bool
structsLeft int structsLeft int
} }
@@ -109,7 +109,7 @@ func (sc *structCompile) compileShape(t reflect.Type, label string) (*structShap
return nil, err return nil, err
} }
if len(c.tags) > 0 { if len(c.tags) > 0 {
if err := c.shape.compileRecord(sc.categories, t, label, c.tags); err != nil { if err := c.shape.compileRecord(sc.root, t, label, c.tags); err != nil {
return nil, err return nil, err
} }
} }
@@ -252,13 +252,13 @@ func checkTaggedType(sf reflect.StructField) error {
// compileRecord compiles the tagged fields of t as one record, and proves each column holds // compileRecord compiles the tagged fields of t as one record, and proves each column holds
// only what its field's Go type can. // only what its field's Go type can.
func (s *structShape) compileRecord(categories map[string]node, t reflect.Type, label string, tags map[string]any) error { func (s *structShape) compileRecord(root *folder, t reflect.Type, label string, tags map[string]any) error {
tags["format"] = "" tags["format"] = ""
n, err := compile(tags) n, err := compile(tags)
if err != nil { if err != nil {
return fmt.Errorf("%s: %w", label, err) return fmt.Errorf("%s: %w", label, err)
} }
b := inlineBinding(n, label, categories) b := inlineBinding(n, label, root)
b.typedByGo = true b.typedByGo = true
if err := b.bind(); err != nil { if err := b.bind(); err != nil {
return err return err
+13 -31
View File
@@ -365,47 +365,29 @@ func refuseTableBinding(toks []formatToken) error {
// setTablePaths gives every table the path a selector on it is written at, before a // setTablePaths gives every table the path a selector on it is written at, before a
// link or a draw can name one. // link or a draw can name one.
func setTablePaths(root map[string]node) { func setTablePaths(sites []categorySite) {
var walk func(dir string, children map[string]node) for _, s := range sites {
walk = func(dir string, children map[string]node) { if t, isTable := s.n.(*table); isTable {
for name, child := range children { t.path = s.path
switch n := child.(type) {
case *folder:
walk(join(dir, name), n.children)
case *table:
n.path = join(dir, name)
}
} }
} }
walk("", root)
} }
// linkTables binds every table's parent to the table beside it, and proves the // linkTables binds every table's parent to the table beside it, and proves the
// links: a parent has a key, every link cell is one, every parent row is linked // links: a parent has a key, every link cell is one, every parent row is linked
// to, no chain of parents closes, and no child is named like a parent's column. // to, no chain of parents closes, and no child is named like a parent's column.
// docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error // docs/decisions.md#a-parent-row-with-no-child-row-is-a-load-error
func linkTables(root map[string]node) error { func linkTables(sites []categorySite) error {
var walk func(dir string, children map[string]node) error for _, s := range sites {
walk = func(dir string, children map[string]node) error { t, isTable := s.n.(*table)
for _, name := range sortedNames(children) { if !isTable || t.parentIndex < 0 {
path := join(dir, name) continue
switch n := children[name].(type) { }
case *folder: if err := t.linkParent(s.path, s.in.children); err != nil {
if err := walk(path, n.children); err != nil { return fmt.Errorf("%s: %w", s.path, err)
return err
}
case *table:
if n.parentIndex < 0 {
continue
}
if err := n.linkParent(path, children); err != nil {
return fmt.Errorf("%s: %w", path, err)
}
}
} }
return nil
} }
return walk("", root) return nil
} }
func (t *table) linkParent(path string, siblings map[string]node) error { func (t *table) linkParent(path string, siblings map[string]node) error {