Make great documentation goal 1.2, and plan the README's reorganisation and the nested-template gaps #33
@@ -47,7 +47,7 @@ In [docs/decisions.md](docs/decisions.md):
|
|||||||
- `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 data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0
|
- The shipped data is Go packages a library imports by choice, and the CLI carries every one, from 0.1.0
|
||||||
- A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads and every indexed category reaching what it replaces
|
- A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads and every indexed category depending on what it provides or a later source replaced
|
||||||
- Every `{…}` draws afresh, and only a name keeps a pick
|
- Every `{…}` draws afresh, and only a name keeps a pick
|
||||||
- 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
|
||||||
|
|||||||
+4
-3
@@ -126,6 +126,7 @@ replacement, and each removed path, column or flag.
|
|||||||
`sv_SE.sex[f].first-name[Kim]`, and a category with no columns names the record
|
`sv_SE.sex[f].first-name[Kim]`, and a category with no columns names the record
|
||||||
that gives it one.
|
that gives it one.
|
||||||
- A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each
|
- A data source whose root holds a `.fejkdata.json` manifest with an `index` loads each
|
||||||
category on the first call reaching it, as the shipped set does. A source without one
|
category on the first call reaching it, as the shipped set does, and a category failing
|
||||||
loads in `New`, with the shipped categories it reads and every shipped category
|
there returns an error wrapping `ErrLoad`, on which the CLI exits 1. A source without
|
||||||
reaching one it replaces; the rest of the shipped set still loads on first reach.
|
one loads in `New`, with the shipped categories it reads and every shipped category
|
||||||
|
depending on one it replaces; the rest of the shipped set still loads on first reach.
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ template holding one from a quoted heredoc or a file. The rest of stdin's rules
|
|||||||
`--name value` and `--name=value` both work, a short flag's value attaches or
|
`--name value` and `--name=value` both work, a short flag's value attaches or
|
||||||
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
|
follows (`-n3`, `-n 3`) and short flags bundle (`-hn 3`) — see
|
||||||
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
[Decisions](docs/decisions.md#flags-follow-getopt_long); flags go anywhere, `--` ends them. Exit codes: `0` success, `1` runtime error (missing
|
||||||
dir, a lone reference to nothing under `--format`), `2` misuse — a bad flag, an argument
|
dir, data that fails to load, a lone reference to nothing under `--format`), `2` misuse — a bad flag, an argument
|
||||||
other than a flag, an unreadable stdin, nothing piped in, or a
|
other than a flag, an unreadable stdin, nothing piped in, or a
|
||||||
template that does not compile. From a checkout:
|
template that does not compile. From a checkout:
|
||||||
`go run ./cmd/fejkdata …`.
|
`go run ./cmd/fejkdata …`.
|
||||||
@@ -173,10 +173,15 @@ entry is never a category or a folder, so a data directory can also be a checkou
|
|||||||
a category may name a hidden rows file beside it.
|
a category may name a hidden rows file beside it.
|
||||||
|
|
||||||
A source whose root holds `.fejkdata.json` with an `index` loads each category on the
|
A source whose root holds `.fejkdata.json` with an `index` loads each category on the
|
||||||
first call reaching it, and its author proves each loads. The shipped set's
|
first call reaching it, and its author proves each loads. Any other source loads in
|
||||||
|
`New`, and so does every indexed category that depends on what such a source provides
|
||||||
|
or on a category a later source replaced. The index names every category of its source,
|
||||||
|
so a category file it leaves out never loads. The shipped set's
|
||||||
[`data/.fejkdata.json`](data/.fejkdata.json) shows the shape: each category's dot path,
|
[`data/.fejkdata.json`](data/.fejkdata.json) shows the shape: each category's dot path,
|
||||||
its `parent` table, the `paths` `List` advertises below it, and the categories it
|
its `parent` table, every path `List` advertises below it, `""` for the category itself,
|
||||||
`reads`. `//go:embed` of a directory leaves the manifest out; name it in the pattern too.
|
and the other categories its templates `reads`. A category whose entry says otherwise
|
||||||
|
fails at first reach, naming the entry it calls for. `//go:embed` of a directory leaves
|
||||||
|
the manifest out; name it in the pattern too.
|
||||||
|
|
||||||
Each locale carries `address`, `color`, `company`, `date`, `email`, `first-name`,
|
Each locale carries `address`, `color`, `company`, `date`, `email`, `first-name`,
|
||||||
`ip`, `last-name`, `person`, `phone`, `price`, `sentence`, `sex`, `time`, `url`,
|
`ip`, `last-name`, `person`, `phone`, `price`, `sentence`, `sex`, `time`, `url`,
|
||||||
@@ -336,13 +341,14 @@ ok, err := fejkdata.IsTemplate(arg) // an inline template by its shape, el
|
|||||||
| `WithoutShippedData()` | load only what you give |
|
| `WithoutShippedData()` | load only what you give |
|
||||||
|
|
||||||
`New` refuses data that breaks the grammar, could mean two things or cannot render a valid
|
`New` refuses data that breaks the grammar, could mean two things or cannot render a valid
|
||||||
value, and `NewTemplate` refuses such a template. On a loaded generator:
|
value, and `NewTemplate` refuses such a template. A category of an indexed source is
|
||||||
|
checked on the first call reaching it instead, and a failure there wraps `ErrLoad`, the
|
||||||
|
same error every call. On a loaded generator:
|
||||||
|
|
||||||
- `Fake` fails only for a path that names nothing, could name two things, reads one
|
- `Fake` fails only for a path that names nothing, could name two things, or reads one
|
||||||
draw of a level carrying a `repeat`, or reaches a category of an indexed source that
|
draw of a level carrying a `repeat`, with the same error every call, or with `ErrLoad`.
|
||||||
fails to load, with the same error every call.
|
|
||||||
- `FakeStruct` fails only for a non-struct argument or a type its tags do not describe,
|
- `FakeStruct` fails only for a non-struct argument or a type its tags do not describe,
|
||||||
with the same error every call.
|
with the same error every call, or with `ErrLoad`.
|
||||||
- `Template.Fake` cannot fail.
|
- `Template.Fake` cannot fail.
|
||||||
|
|
||||||
A path, wherever a call takes one, may start with one `/`, as a reference does.
|
A path, wherever a call takes one, may start with one `/`, as a reference does.
|
||||||
@@ -842,9 +848,7 @@ The target is about a microsecond per value (goal 13).
|
|||||||
|
|
||||||
A source with an [index](#data), as the shipped set has, loads nothing in `New`: a
|
A source with an [index](#data), as the shipped set has, loads nothing in `New`: a
|
||||||
category loads on the first `Fake`, `FakeRecord`, `FakeStruct` or template that reads
|
category loads on the first `Fake`, `FakeRecord`, `FakeStruct` or template that reads
|
||||||
it, with every category it reads and its whole table family. A source without one loads
|
it, with every category it reads and its whole table family. Each file is parsed, validated and weight-indexed once, bar a
|
||||||
in `New`, with the indexed categories it reads and every indexed category reaching one
|
|
||||||
it replaces, so each mistake it causes is `New`'s error. Each file is parsed, validated and weight-indexed once, bar a
|
|
||||||
table's name and parent columns, which are mapped on the first `Fake` that selects by
|
table's name and parent columns, which are mapped on the first `Fake` that selects by
|
||||||
name or descends through it. A `Fake` call then costs about what its output
|
name or descends through it. 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
|
||||||
@@ -866,7 +870,7 @@ and regenerate seeded fixtures and expected values when you raise that version.
|
|||||||
|---------|-------|-------|
|
|---------|-------|-------|
|
||||||
| Shipped data | remove or rename a path; change a category's format; remove a value, or change a weight or a repeat; add a reference from one shipped category into another; change a table's key, name, weight or parent column, or remove a row | a path outside a record's columns, a locale, a value in a list, a row |
|
| Shipped data | remove or rename a path; change a category's format; remove a value, or change a weight or a repeat; add a reference from one shipped category into another; change a table's key, name, weight or parent column, or remove a row | a path outside a record's columns, a locale, a value in a list, a row |
|
||||||
| Records (a table is one) | remove, rename, retype or add a column; let a column be null | a record, as a new category |
|
| Records (a table is one) | remove, rename, retype or add a column; let a column be null | a record, as a new category |
|
||||||
| Data format | a fence: a spelling `New` rejects that it accepted; a template option, since it reserves a field name | a builtin |
|
| Data format | a fence: a spelling `New` rejects that it accepted, in a category or a manifest; a template option, since it reserves a field name | a builtin |
|
||||||
| CLI | remove or rename a flag, or change its default; change what an exit code means; change the framing a `--format` writes (header, quoting, statement shape), the `--list` layout, or what an error names | a flag, a format |
|
| CLI | remove or rename a flag, or change its default; change what an exit code means; change the framing a `--format` writes (header, quoting, statement shape), the `--list` layout, or what an error names | a flag, a format |
|
||||||
| Library | change or remove an exported name; raise the lowest supported Go | an exported name, a `With…` option |
|
| Library | change or remove an exported name; raise the lowest supported Go | an exported name, a `With…` option |
|
||||||
|
|
||||||
|
|||||||
+10
-2
@@ -340,7 +340,7 @@ func (in invocation) write(f *fejkdata.Generator, src input, w io.Writer) error
|
|||||||
}
|
}
|
||||||
t, err := f.NewTemplate(src.template)
|
t, err := f.NewTemplate(src.template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return inputError{err}
|
return templateError(err)
|
||||||
}
|
}
|
||||||
out := bufio.NewWriter(w)
|
out := bufio.NewWriter(w)
|
||||||
for i := 0; i < in.repeat; i++ {
|
for i := 0; i < in.repeat; i++ {
|
||||||
@@ -359,7 +359,7 @@ func (in invocation) recordStream(f *fejkdata.Generator, src input) (func() (*fe
|
|||||||
if src.record == "" {
|
if src.record == "" {
|
||||||
t, err := f.NewRecordTemplate(src.template)
|
t, err := f.NewRecordTemplate(src.template)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", inputError{spacedReference(src.template, err)}
|
return nil, "", templateError(spacedReference(src.template, err))
|
||||||
}
|
}
|
||||||
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
record = func() (*fejkdata.Record, error) { return t.Fake(), nil }
|
||||||
}
|
}
|
||||||
@@ -621,6 +621,14 @@ func readStdin(stdin io.Reader) (input, error) {
|
|||||||
return parseInput(string(raw)), nil
|
return parseInput(string(raw)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// templateError is misuse unless the data the template reads failed to load.
|
||||||
|
func templateError(err error) error {
|
||||||
|
if errors.Is(err, fejkdata.ErrLoad) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return inputError{err}
|
||||||
|
}
|
||||||
|
|
||||||
func fail(stderr io.Writer, err error) int {
|
func fail(stderr io.Writer, err error) int {
|
||||||
var te inputError
|
var te inputError
|
||||||
if errors.As(err, &te) {
|
if errors.As(err, &te) {
|
||||||
|
|||||||
@@ -7,64 +7,65 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// loadSources merges sources in order, the last winning a clash. A source whose manifest
|
// loadSources merges sources in order, the last winning a clash. A source whose manifest
|
||||||
// carries an index leaves its categories unloaded; every other source's categories load
|
// carries an index leaves its categories unloaded; every other source loads here. So do
|
||||||
// here, with what they reach and every unloaded category leading to one of them or to one a
|
// the unloaded categories that depend on one it loaded or on one a later source replaced,
|
||||||
// later source replaced.
|
// so that a reader such a change breaks fails New, not a first reach.
|
||||||
func loadSources(sources []datafiles.Source) (folder, error) {
|
func loadSources(sources []datafiles.Source) (folder, error) {
|
||||||
root := folder{children: map[string]node{}}
|
root := folder{children: map[string]node{}}
|
||||||
targets := map[string]bool{}
|
var changed []string
|
||||||
for _, src := range sources {
|
for _, src := range sources {
|
||||||
before := categoriesUnder(&root)
|
replaced, err := addSource(&root, src)
|
||||||
provides, err := place(&root, src)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return folder{}, err
|
return folder{}, err
|
||||||
}
|
}
|
||||||
if len(before) == 0 {
|
changed = append(changed, replaced...)
|
||||||
continue
|
|
||||||
}
|
|
||||||
after := categoriesUnder(&root)
|
|
||||||
for p := range before {
|
|
||||||
if provides(p) || !after[p] {
|
|
||||||
targets[p] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
sites := categorySites(&root)
|
sites := categorySites(&root)
|
||||||
for _, s := range sites {
|
for _, s := range sites {
|
||||||
targets[s.path] = true
|
changed = append(changed, s.path)
|
||||||
}
|
}
|
||||||
return root, loadReached(&root, sites, reaching(&root, targets))
|
return root, loadReached(&root, sites, dependents(&root, changed))
|
||||||
}
|
}
|
||||||
|
|
||||||
// place adds the categories of src to root, and returns whether src provides a path.
|
// addSource adds the categories of src to root, and returns the path of each category
|
||||||
func place(root *folder, src datafiles.Source) (func(string) bool, error) {
|
// already there that src replaced or put out of reach.
|
||||||
|
func addSource(root *folder, src datafiles.Source) ([]string, error) {
|
||||||
m, err := src.Manifest()
|
m, err := src.Manifest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
before := standing(root)
|
||||||
if m.Index != nil {
|
if m.Index != nil {
|
||||||
placeIndex(root, src, m.Index)
|
placeIndex(root, src, m.Index)
|
||||||
return func(p string) bool { _, ok := m.Index[p]; return ok }, nil
|
} else {
|
||||||
|
g := &folder{children: map[string]node{}}
|
||||||
|
if err := src.Walk(compileInto(func(dir []string) *folder { return madeFolder(g, dir) })); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mergeFolder(root, g)
|
||||||
}
|
}
|
||||||
g := &folder{children: map[string]node{}}
|
if len(before) == 0 {
|
||||||
if err := src.Walk(compileInto(func(dir []string) *folder { return madeFolder(g, dir) })); err != nil {
|
return nil, nil
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
mergeFolder(root, g)
|
after := standing(root)
|
||||||
provided := categoriesUnder(g)
|
var replaced []string
|
||||||
return func(p string) bool { return provided[p] }, nil
|
for p, was := range before {
|
||||||
|
if after[p] != was {
|
||||||
|
replaced = append(replaced, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return replaced, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// categoriesUnder is the path of every category under root, loaded or not.
|
// standing is what stands at the path of every category under root: its node, or its
|
||||||
func categoriesUnder(root *folder) map[string]bool {
|
// unloaded entry.
|
||||||
out := map[string]bool{}
|
func standing(root *folder) map[string]any {
|
||||||
|
out := map[string]any{}
|
||||||
for _, s := range categorySites(root) {
|
for _, s := range categorySites(root) {
|
||||||
out[s.path] = true
|
out[s.path] = s.n
|
||||||
}
|
}
|
||||||
unloaded := map[string]categoryAt{}
|
for p, c := range unloadedUnder(root) {
|
||||||
unloadedUnder(root, nil, unloaded)
|
out[p] = c.in.unloaded[c.name]
|
||||||
for p := range unloaded {
|
|
||||||
out[p] = true
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
@@ -75,7 +76,7 @@ func compileInto(place func(dir []string) *folder) func(datafiles.Category) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
place(c.Folders).children[c.Name] = n
|
place(c.Folders).put(c.Name, n)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -87,8 +88,7 @@ func madeFolder(root *folder, dir []string) *folder {
|
|||||||
sub, isFolder := g.children[seg].(*folder)
|
sub, isFolder := g.children[seg].(*folder)
|
||||||
if !isFolder {
|
if !isFolder {
|
||||||
sub = &folder{children: map[string]node{}}
|
sub = &folder{children: map[string]node{}}
|
||||||
g.children[seg] = sub
|
g.put(seg, sub)
|
||||||
delete(g.unloaded, seg)
|
|
||||||
}
|
}
|
||||||
g = sub
|
g = sub
|
||||||
}
|
}
|
||||||
@@ -103,8 +103,7 @@ func mergeFolder(dst, src *folder) {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
dst.children[k] = v
|
dst.put(k, v)
|
||||||
delete(dst.unloaded, k)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,10 +115,10 @@ type nodeSet func(fn func(label string, n node) error) error
|
|||||||
// folder's path, its name and its own dot path.
|
// folder's path, its name and its own dot path.
|
||||||
type categorySite struct {
|
type categorySite struct {
|
||||||
dir []string
|
dir []string
|
||||||
name string
|
|
||||||
path string
|
|
||||||
in *folder
|
in *folder
|
||||||
n node
|
n node
|
||||||
|
name string
|
||||||
|
path string
|
||||||
}
|
}
|
||||||
|
|
||||||
// categorySites lists every category under root, folders and names in sorted order.
|
// categorySites lists every category under root, folders and names in sorted order.
|
||||||
@@ -140,7 +139,7 @@ func categorySites(root *folder) []categorySite {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func siteIn(dir []string, in *folder, name string) categorySite {
|
func siteIn(dir []string, in *folder, name string) categorySite {
|
||||||
return categorySite{dir: dir, name: name, path: categoryPath(dir, name), in: in, n: in.children[name]}
|
return categorySite{dir: dir, in: in, n: in.children[name], name: name, path: categoryPath(dir, name)}
|
||||||
}
|
}
|
||||||
|
|
||||||
func categoryPath(dir []string, name string) string { return join(strings.Join(dir, "."), name) }
|
func categoryPath(dir []string, name string) string { return join(strings.Join(dir, "."), name) }
|
||||||
|
|||||||
@@ -179,3 +179,26 @@ Ruling: same
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Mid A | same | same | same | same | same |
|
| Mid A | same | same | same | same | same |
|
||||||
| Inherited architect | same | same | same | same | same |
|
| Inherited architect | same | same | same | same | same |
|
||||||
|
|
||||||
|
## 2026-10-09T17:21:57Z, PR #202 at 65b2de8, against 2652779
|
||||||
|
|
||||||
|
Ruling: worse
|
||||||
|
|
||||||
|
| Seat | Navigation | Locality | Shape | Self-sufficiency | Overall |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Mid A | same | worse | same | same | worse |
|
||||||
|
| Inherited architect | same | worse | worse | worse | worse |
|
||||||
|
|
||||||
|
Mid A, decided by:
|
||||||
|
|
||||||
|
- Locality, Overall: `data.go:13` `loadSources`: one pass mixes lazy and eager loading through before/after snapshots, a `provides` closure and a reverse graph search, and the skip of the first source is unexplained
|
||||||
|
- Locality: `data.go:107` `mergeFolder`: a category must sit in one of two maps, `children` or `unloaded`, kept by deletes in four functions across two files
|
||||||
|
- Locality: `internal/datafiles/datafiles.go:83` `embeddedManifests`: a process-wide cache in a package that was otherwise pure
|
||||||
|
|
||||||
|
Inherited architect, decided by:
|
||||||
|
|
||||||
|
- Locality, Self-sufficiency: `data.go:13` `loadSources`: nothing says why replaced paths drag their readers in, or what the `!after[p]` case covers
|
||||||
|
- Locality: `index.go:113` `loadReached`: its rollback claims the next call fails the same way, which holds only while no parent table was linked to a rolled-back child
|
||||||
|
- Shape: `index.go:159` `reaching`: four near-synonyms, `reaching`, `reachedFrom`, `loadReached` and `referenced`, and `referenced` and `categoryUnder` return loaded categories too
|
||||||
|
- Shape: `data.go:40` `place`: shares its name with `compileInto`'s parameter, and returns a closure answering one question two ways
|
||||||
|
- Locality: `internal/datafiles/datafiles.go:83` `embeddedManifests`: hidden global state for a small saving
|
||||||
|
|||||||
+7
-5
@@ -154,7 +154,7 @@ still lives in `data/`; `--data-path` layers over it.
|
|||||||
|
|
||||||
Valid while the shipped data stays under 100 MiB, which a gate test holds (`todo.md` item 192): `go get` fetches the whole Go module, and the `go` command refuses a Go module zip over 500 MiB (`MaxZipFile` in `golang.org/x/mod/zip`). Past it, `todo.md` item 193 moves the largest packages into nested Go modules that keep their import paths, each requiring the core release that drops the package, so no build sees it twice.
|
Valid while the shipped data stays under 100 MiB, which a gate test holds (`todo.md` item 192): `go get` fetches the whole Go module, and the `go` command refuses a Go module zip over 500 MiB (`MaxZipFile` in `golang.org/x/mod/zip`). Past it, `todo.md` item 193 moves the largest packages into nested Go modules that keep their import paths, each requiring the core release that drops the package, so no build sees it twice.
|
||||||
|
|
||||||
## A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads and every indexed category reaching what it replaces
|
## A source whose manifest carries an index loads each category on first reach; any other loads in `New`, with what it reads and every indexed category depending on what it provides or a later source replaced
|
||||||
|
|
||||||
2026-10-03, a restructure the maintainer approved; widened to any source on 2026-10-09.
|
2026-10-03, a restructure the maintainer approved; widened to any source on 2026-10-09.
|
||||||
|
|
||||||
@@ -168,10 +168,12 @@ load. `go generate` writes the shipped set's manifest from `data/`;
|
|||||||
and each shipped category alone into a fresh `New`, so none fails at first reach.
|
and each shipped category alone into a fresh `New`, so none fails at first reach.
|
||||||
|
|
||||||
Goal 6: a source without an index loads whole in `New`, so each mistake in it is `New`'s
|
Goal 6: a source without an index loads whole in `New`, so each mistake in it is `New`'s
|
||||||
error. So does every indexed category it reads, and every indexed category leading
|
error. So does every indexed category it reads, and every indexed category depending,
|
||||||
through references or table links to one that it or any later source replaced, so a
|
through references or table links, on a category it provides or a later source
|
||||||
replacement breaking a reader fails `New`. Goal 6.3: an indexed source from anyone else
|
replaced, so a replacement or a stand-in breaking a reader fails `New`. Goal 6.3: an
|
||||||
may fail at first reach; `Fake` then returns the load's error, the same every call.
|
indexed source from anyone else may fail at first reach; `Fake` then returns the load's
|
||||||
|
error wrapping `ErrLoad`, the same every call. A category whose index entry disagrees
|
||||||
|
with it fails there too, so a stale index never decides what loads.
|
||||||
|
|
||||||
Valid while CI proves the shipped set whole and each category alone.
|
Valid while CI proves the shipped set whole and each category alone.
|
||||||
|
|
||||||
|
|||||||
+2
-3
@@ -73,8 +73,7 @@ func WithoutShippedData() Option {
|
|||||||
// namespace segment. It errors on a missing directory, invalid JSON or invalid data; a
|
// namespace segment. It errors on a missing directory, invalid JSON or invalid data; a
|
||||||
// generator with no data at all renders templates that read none. A source whose
|
// generator with no data at all renders templates that read none. A source whose
|
||||||
// .fejkdata.json carries an index, as the shipped set's does, loads each category on the
|
// .fejkdata.json carries an index, as the shipped set's does, loads each category on the
|
||||||
// first call reaching it. Every other source loads here, with what it reads and every
|
// first call reaching it, and fails there with [ErrLoad].
|
||||||
// indexed category reaching what it replaces.
|
|
||||||
func New(opts ...Option) (*Generator, error) {
|
func New(opts ...Option) (*Generator, error) {
|
||||||
c := config{shipped: true}
|
c := config{shipped: true}
|
||||||
for _, opt := range opts {
|
for _, opt := range opts {
|
||||||
@@ -91,7 +90,7 @@ func New(opts ...Option) (*Generator, error) {
|
|||||||
return &Generator{drawState: drawstate.New(seed), root: root}, nil
|
return &Generator{drawState: drawstate.New(seed), root: root}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// load builds the tree New starts from. docs/decisions.md#a-source-whose-manifest-carries-an-index-loads-each-category-on-first-reach-any-other-loads-in-new-with-what-it-reads-and-every-indexed-category-reaching-what-it-replaces
|
// load builds the tree New starts from. docs/decisions.md#a-source-whose-manifest-carries-an-index-loads-each-category-on-first-reach-any-other-loads-in-new-with-what-it-reads-and-every-indexed-category-depending-on-what-it-provides-or-a-later-source-replaced
|
||||||
func (c config) load() (folder, error) {
|
func (c config) load() (folder, error) {
|
||||||
var sources []datafiles.Source
|
var sources []datafiles.Source
|
||||||
if c.shipped {
|
if c.shipped {
|
||||||
|
|||||||
@@ -1,17 +1,25 @@
|
|||||||
package fejkdata
|
package fejkdata
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"slices"
|
||||||
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/datafiles"
|
"github.com/larvit/fejkdata/internal/datafiles"
|
||||||
"github.com/larvit/fejkdata/internal/grammar"
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// ErrLoad marks the error of a category that fails to load on the first call reaching it,
|
||||||
|
// which only a source whose manifest carries an index defers to then.
|
||||||
|
var ErrLoad = errors.New("data fails to load")
|
||||||
|
|
||||||
// indexed is a category an index names and no call has reached yet, and the source holding it.
|
// indexed is a category an index names and no call has reached yet, and the source holding it.
|
||||||
type indexed struct {
|
type indexed struct {
|
||||||
src datafiles.Source
|
|
||||||
entry datafiles.IndexEntry
|
entry datafiles.IndexEntry
|
||||||
|
src datafiles.Source
|
||||||
}
|
}
|
||||||
|
|
||||||
// placeIndex sets every category index names unloaded in root, in place of what root holds
|
// placeIndex sets every category index names unloaded in root, in place of what root holds
|
||||||
@@ -19,17 +27,12 @@ type indexed struct {
|
|||||||
func placeIndex(root *folder, src datafiles.Source, index map[string]datafiles.IndexEntry) {
|
func placeIndex(root *folder, src datafiles.Source, index map[string]datafiles.IndexEntry) {
|
||||||
for _, p := range sortedNames(index) {
|
for _, p := range sortedNames(index) {
|
||||||
segs := strings.Split(p, ".")
|
segs := strings.Split(p, ".")
|
||||||
g := madeFolder(root, segs[:len(segs)-1])
|
madeFolder(root, segs[:len(segs)-1]).putUnloaded(segs[len(segs)-1], &indexed{entry: index[p], src: src})
|
||||||
name := segs[len(segs)-1]
|
|
||||||
delete(g.children, name)
|
|
||||||
if g.unloaded == nil {
|
|
||||||
g.unloaded = map[string]indexed{}
|
|
||||||
}
|
|
||||||
g.unloaded[name] = indexed{src: src, entry: index[p]}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// categoryAt is where a category sits: the folder holding it, that folder's path, and its name.
|
// categoryAt is where a category, loaded or not, sits: the folder holding it, that
|
||||||
|
// folder's path, and its name.
|
||||||
type categoryAt struct {
|
type categoryAt struct {
|
||||||
dir []string
|
dir []string
|
||||||
in *folder
|
in *folder
|
||||||
@@ -38,8 +41,8 @@ type categoryAt struct {
|
|||||||
|
|
||||||
func (c categoryAt) path() string { return categoryPath(c.dir, c.name) }
|
func (c categoryAt) path() string { return categoryPath(c.dir, c.name) }
|
||||||
|
|
||||||
// categoryUnder is the category, loaded or not, that a path from root names or descends into.
|
// categoryOn is the category, loaded or not, that a path from root names or passes.
|
||||||
func categoryUnder(root *folder, segs []string) (categoryAt, bool) {
|
func categoryOn(root *folder, segs []string) (categoryAt, bool) {
|
||||||
g, i := folderAt(root, segs)
|
g, i := folderAt(root, segs)
|
||||||
if i == len(segs) {
|
if i == len(segs) {
|
||||||
return categoryAt{}, false
|
return categoryAt{}, false
|
||||||
@@ -51,7 +54,7 @@ func categoryUnder(root *folder, segs []string) (categoryAt, bool) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// loadCallerPath strips the / a caller's path may start with, splits the path, and loads the
|
// loadCallerPath strips the / a caller's path may start with, splits the path, and loads the
|
||||||
// unloaded category it names or descends into. The caller walks it next, and a walk loads nothing.
|
// 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) {
|
func (f *Generator) loadCallerPath(path string) (string, []string, error) {
|
||||||
path, err := grammar.CallerPath(path)
|
path, err := grammar.CallerPath(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -61,16 +64,16 @@ func (f *Generator) loadCallerPath(path string) (string, []string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", nil, fmt.Errorf("fejkdata: %w", err)
|
return "", nil, fmt.Errorf("fejkdata: %w", err)
|
||||||
}
|
}
|
||||||
if c, ok := categoryUnder(&f.root, segs); ok {
|
if c, ok := categoryOn(&f.root, segs); ok {
|
||||||
if err := loadReached(&f.root, nil, []categoryAt{c}); err != nil {
|
if err := loadReached(&f.root, nil, []categoryAt{c}); err != nil {
|
||||||
return "", nil, fmt.Errorf("fejkdata: %w", err)
|
return "", nil, fmt.Errorf("fejkdata: %w: %w", ErrLoad, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return path, segs, nil
|
return path, segs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// referenced is every category the templates of nodes reference, each reference read from
|
// referenced is every category, loaded or not, that the templates of nodes reference, each
|
||||||
// the folder dir.
|
// reference read from the folder dir.
|
||||||
func referenced(root *folder, dir []string, nodes nodeSet) []categoryAt {
|
func referenced(root *folder, dir []string, nodes nodeSet) []categoryAt {
|
||||||
var out []categoryAt
|
var out []categoryAt
|
||||||
_ = nodes(func(_ string, n node) error {
|
_ = nodes(func(_ string, n node) error {
|
||||||
@@ -83,7 +86,7 @@ func referenced(root *folder, dir []string, nodes nodeSet) []categoryAt {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
continue // resolveRefs reports it
|
continue // resolveRefs reports it
|
||||||
}
|
}
|
||||||
if c, ok := categoryUnder(root, segs); ok {
|
if c, ok := categoryOn(root, segs); ok {
|
||||||
out = append(out, c)
|
out = append(out, c)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,12 +95,12 @@ func referenced(root *folder, dir []string, nodes nodeSet) []categoryAt {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// linkedTables is the table beside s that its parent column names, and every unloaded
|
// dependencies is every category, loaded or not, that s references, the table beside s
|
||||||
// table beside s whose parent s is.
|
// that its parent column names, and every unloaded table beside s whose parent s is.
|
||||||
func linkedTables(s categorySite) []categoryAt {
|
func dependencies(root *folder, s categorySite) []categoryAt {
|
||||||
var out []categoryAt
|
out := referenced(root, s.dir, siteNodes([]categorySite{s}))
|
||||||
if t, isTable := s.n.(*table); isTable && t.rows.Options().Parent != "" {
|
if parent := parentOf(s.n); parent != "" {
|
||||||
out = append(out, categoryAt{dir: s.dir, in: s.in, name: t.rows.Options().Parent})
|
out = append(out, categoryAt{dir: s.dir, in: s.in, name: parent})
|
||||||
}
|
}
|
||||||
for _, name := range sortedNames(s.in.unloaded) {
|
for _, name := range sortedNames(s.in.unloaded) {
|
||||||
if s.in.unloaded[name].entry.Parent == s.name {
|
if s.in.unloaded[name].entry.Parent == s.name {
|
||||||
@@ -107,61 +110,14 @@ func linkedTables(s categorySite) []categoryAt {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
// loadReached loads the unloaded categories among wanted, and every unloaded category they
|
// dependents is every unloaded category under root whose index entry leads to one of
|
||||||
// or sites reference or link to, then runs sites and all it loaded through a whole load's
|
// paths: through its reads, its parent or a child table, and through other unloaded
|
||||||
// pipeline. On an error it unloads again 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, reachedFrom(root, s)...)
|
|
||||||
}
|
|
||||||
var loaded []categorySite
|
|
||||||
var was []indexed
|
|
||||||
unload := func() {
|
|
||||||
for i, s := range loaded {
|
|
||||||
delete(s.in.children, s.name)
|
|
||||||
s.in.unloaded[s.name] = was[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for ; len(queue) > 0; queue = queue[1:] {
|
|
||||||
c := queue[0]
|
|
||||||
x, unloaded := c.in.unloaded[c.name]
|
|
||||||
if !unloaded {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err := x.src.Load(c.dir, c.name, compileInto(func([]string) *folder { return c.in })); err != nil {
|
|
||||||
unload()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
delete(c.in.unloaded, c.name)
|
|
||||||
s := siteIn(c.dir, c.in, c.name)
|
|
||||||
loaded, was = append(loaded, s), append(was, x)
|
|
||||||
queue = append(queue, reachedFrom(root, s)...)
|
|
||||||
}
|
|
||||||
sites = append(sites[:len(sites):len(sites)], loaded...)
|
|
||||||
if len(sites) == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if err := categoryPipeline(sites, root.children).run(); err != nil {
|
|
||||||
unload()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func reachedFrom(root *folder, s categorySite) []categoryAt {
|
|
||||||
return append(referenced(root, s.dir, siteNodes([]categorySite{s})), linkedTables(s)...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// reaching is every unloaded category under root whose index entry leads to a path in
|
|
||||||
// targets: through its reads, its parent or a child table, and through other unloaded
|
|
||||||
// categories.
|
// categories.
|
||||||
func reaching(root *folder, targets map[string]bool) []categoryAt {
|
func dependents(root *folder, paths []string) []categoryAt {
|
||||||
if len(targets) == 0 {
|
if len(paths) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
unloaded := map[string]categoryAt{}
|
unloaded := unloadedUnder(root)
|
||||||
unloadedUnder(root, nil, unloaded)
|
|
||||||
from := map[string][]string{} // a path, and the unloaded categories leading to it
|
from := map[string][]string{} // a path, and the unloaded categories leading to it
|
||||||
for _, p := range sortedNames(unloaded) {
|
for _, p := range sortedNames(unloaded) {
|
||||||
c := unloaded[p]
|
c := unloaded[p]
|
||||||
@@ -177,7 +133,9 @@ func reaching(root *folder, targets map[string]bool) []categoryAt {
|
|||||||
}
|
}
|
||||||
var out []categoryAt
|
var out []categoryAt
|
||||||
seen := map[string]bool{}
|
seen := map[string]bool{}
|
||||||
for queue := sortedNames(targets); len(queue) > 0; queue = queue[1:] {
|
queue := slices.Clone(paths)
|
||||||
|
sort.Strings(queue)
|
||||||
|
for ; len(queue) > 0; queue = queue[1:] {
|
||||||
for _, p := range from[queue[0]] {
|
for _, p := range from[queue[0]] {
|
||||||
c, isUnloaded := unloaded[p]
|
c, isUnloaded := unloaded[p]
|
||||||
if !isUnloaded || seen[p] {
|
if !isUnloaded || seen[p] {
|
||||||
@@ -191,14 +149,110 @@ func reaching(root *folder, targets map[string]bool) []categoryAt {
|
|||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func unloadedUnder(g *folder, dir []string, out map[string]categoryAt) {
|
func unloadedUnder(root *folder) map[string]categoryAt {
|
||||||
for name := range g.unloaded {
|
out := map[string]categoryAt{}
|
||||||
c := categoryAt{dir: dir, in: g, name: name}
|
var walk func(g *folder, dir []string)
|
||||||
out[c.path()] = c
|
walk = func(g *folder, dir []string) {
|
||||||
}
|
for name := range g.unloaded {
|
||||||
for name, n := range g.children {
|
c := categoryAt{dir: dir, in: g, name: name}
|
||||||
if sub, isFolder := n.(*folder); isFolder {
|
out[c.path()] = c
|
||||||
unloadedUnder(sub, append(dir[:len(dir):len(dir)], name), out)
|
}
|
||||||
|
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 depend on, 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. Checking
|
||||||
|
// each parent column against its entry before linking keeps that whole: a table then
|
||||||
|
// loads with its parent, so no table loaded earlier links to one put back.
|
||||||
|
func loadReached(root *folder, sites []categorySite, wanted []categoryAt) error {
|
||||||
|
queue := wanted
|
||||||
|
for _, s := range sites {
|
||||||
|
queue = append(queue, dependencies(root, s)...)
|
||||||
|
}
|
||||||
|
type load struct {
|
||||||
|
site categorySite
|
||||||
|
was *indexed
|
||||||
|
}
|
||||||
|
var loads []load
|
||||||
|
putBack := func() {
|
||||||
|
for _, l := range loads {
|
||||||
|
l.site.in.putUnloaded(l.site.name, l.was)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for ; len(queue) > 0; queue = queue[1:] {
|
||||||
|
c := queue[0]
|
||||||
|
x, unloaded := c.in.unloaded[c.name]
|
||||||
|
if !unloaded {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
err := x.src.Load(c.dir, c.name, compileInto(func([]string) *folder { return c.in }))
|
||||||
|
if err == nil {
|
||||||
|
loads = append(loads, load{site: siteIn(c.dir, c.in, c.name), was: x})
|
||||||
|
if parentOf(c.in.children[c.name]) != x.entry.Parent {
|
||||||
|
err = staleEntry(root, loads[len(loads)-1].site, x)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
putBack()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
queue = append(queue, dependencies(root, loads[len(loads)-1].site)...)
|
||||||
|
}
|
||||||
|
for _, l := range loads {
|
||||||
|
sites = append(sites[:len(sites):len(sites)], l.site)
|
||||||
|
}
|
||||||
|
if len(sites) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := categoryPipeline(sites, root.children).run()
|
||||||
|
for _, l := range loads {
|
||||||
|
if err == nil && !sameEntry(indexEntry(root, l.site), l.was.entry) {
|
||||||
|
err = staleEntry(root, l.site, l.was)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
putBack()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// indexEntry is what an index says of the loaded category s.
|
||||||
|
func indexEntry(root *folder, s categorySite) datafiles.IndexEntry {
|
||||||
|
e := datafiles.IndexEntry{Parent: parentOf(s.n), Paths: paths(s.n, false)}
|
||||||
|
sort.Strings(e.Paths)
|
||||||
|
seen := map[string]bool{s.path: true}
|
||||||
|
for _, c := range referenced(root, s.dir, siteNodes([]categorySite{s})) {
|
||||||
|
if p := c.path(); !seen[p] {
|
||||||
|
seen[p] = true
|
||||||
|
e.Reads = append(e.Reads, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(e.Reads)
|
||||||
|
return e
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameEntry(a, b datafiles.IndexEntry) bool {
|
||||||
|
return a.Parent == b.Parent && slices.Equal(a.Paths, b.Paths) && slices.Equal(a.Reads, b.Reads)
|
||||||
|
}
|
||||||
|
|
||||||
|
// staleEntry names the entry x holds for s, and the entry s calls for. Before linking,
|
||||||
|
// the paths and reads it names may still lack what s's family adds.
|
||||||
|
func staleEntry(root *folder, s categorySite, x *indexed) error {
|
||||||
|
want, _ := json.Marshal(indexEntry(root, s))
|
||||||
|
return fmt.Errorf("%s: the index entry for %s is stale; the category calls for %s", x.src.ManifestPath(), s.path, want)
|
||||||
|
}
|
||||||
|
|
||||||
|
func parentOf(n node) string {
|
||||||
|
if t, isTable := n.(*table); isTable {
|
||||||
|
return t.rows.Options().Parent
|
||||||
|
}
|
||||||
|
return ""
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func loadInline(root *folder, n node, label string, typedByGo bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if err := loadReached(root, nil, referenced(root, nil, nodes)); err != nil {
|
if err := loadReached(root, nil, referenced(root, nil, nodes)); err != nil {
|
||||||
return err
|
return fmt.Errorf("%w: %w", ErrLoad, err)
|
||||||
}
|
}
|
||||||
return pipeline{
|
return pipeline{
|
||||||
nodes: nodes,
|
nodes: nodes,
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package datafiles
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"embed"
|
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -12,7 +11,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/larvit/fejkdata/internal/grammar"
|
"github.com/larvit/fejkdata/internal/grammar"
|
||||||
"github.com/larvit/fejkdata/internal/jsonvalue"
|
"github.com/larvit/fejkdata/internal/jsonvalue"
|
||||||
@@ -79,47 +77,28 @@ type IndexEntry struct {
|
|||||||
Reads []string `json:"reads,omitempty"`
|
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.
|
// Manifest reads the manifest at the source's root, the zero Manifest where there is none.
|
||||||
func (s Source) Manifest() (Manifest, error) {
|
func (s Source) Manifest() (Manifest, error) {
|
||||||
e, embedded := s.fsys.(embed.FS)
|
if err := s.check(); err != nil {
|
||||||
key := embeddedManifest{fsys: e, base: s.base}
|
return Manifest{}, err
|
||||||
if m, read := embeddedManifests.Load(key); embedded && read {
|
|
||||||
return m.(Manifest), nil
|
|
||||||
}
|
}
|
||||||
m, err := s.readManifest()
|
b, err := fs.ReadFile(s.fsys, path.Join(s.base, ManifestFile))
|
||||||
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)
|
|
||||||
if errors.Is(err, fs.ErrNotExist) {
|
if errors.Is(err, fs.ErrNotExist) {
|
||||||
return Manifest{}, nil
|
return Manifest{}, nil
|
||||||
}
|
}
|
||||||
if err != 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)
|
m, err := decodeManifest(b)
|
||||||
if err != nil {
|
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
|
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) {
|
func decodeManifest(b []byte) (Manifest, error) {
|
||||||
dec := json.NewDecoder(bytes.NewReader(b))
|
dec := json.NewDecoder(bytes.NewReader(b))
|
||||||
dec.DisallowUnknownFields()
|
dec.DisallowUnknownFields()
|
||||||
@@ -130,36 +109,67 @@ func decodeManifest(b []byte) (Manifest, error) {
|
|||||||
if _, err := dec.Token(); err != io.EOF {
|
if _, err := dec.Token(); err != io.EOF {
|
||||||
return Manifest{}, fmt.Errorf("more than one JSON value")
|
return Manifest{}, fmt.Errorf("more than one JSON value")
|
||||||
}
|
}
|
||||||
for p := range m.Index {
|
for p, e := range m.Index {
|
||||||
for _, seg := range strings.Split(p, ".") {
|
for _, q := range append([]string{p}, e.Reads...) {
|
||||||
if err := grammar.CheckIdentifier(seg); err != nil {
|
if err := checkCategoryPath(q); err != nil {
|
||||||
return Manifest{}, fmt.Errorf("index names %q: %w", p, err)
|
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
|
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
|
// 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
|
// 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.
|
// category, though a category may name a hidden rows file beside it.
|
||||||
func (s Source) Walk(compile func(Category) error) error {
|
func (s Source) Walk(compile func(Category) error) error {
|
||||||
if s.onDisk {
|
if err := s.check(); err != nil {
|
||||||
if s.label == "" {
|
return err
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
_, err := s.walkDir(nil, compile)
|
_, err := s.walkDir(nil, compile)
|
||||||
return err
|
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.
|
// 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) {
|
func (s Source) walkDir(dir []string, compile func(Category) error) (bool, error) {
|
||||||
full := s.dirPath(dir)
|
full := s.dirPath(dir)
|
||||||
|
|||||||
@@ -20,13 +20,27 @@ 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).
|
||||||
|
// A name stands in children or in unloaded, never both; put and putUnloaded keep it so.
|
||||||
type folder struct {
|
type folder struct {
|
||||||
children map[string]node
|
children map[string]node
|
||||||
unloaded map[string]indexed // the categories here an index names and no call has reached yet
|
unloaded map[string]*indexed // the categories here an index names and no call has reached yet
|
||||||
}
|
}
|
||||||
|
|
||||||
func (*folder) isNode() {}
|
func (*folder) isNode() {}
|
||||||
|
|
||||||
|
func (g *folder) put(name string, n node) {
|
||||||
|
g.children[name] = n
|
||||||
|
delete(g.unloaded, name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *folder) putUnloaded(name string, x *indexed) {
|
||||||
|
delete(g.children, name)
|
||||||
|
if g.unloaded == nil {
|
||||||
|
g.unloaded = map[string]*indexed{}
|
||||||
|
}
|
||||||
|
g.unloaded[name] = x
|
||||||
|
}
|
||||||
|
|
||||||
// choice draws one of its items. cum holds cumulative weights for a weighted
|
// choice draws one of its items. cum holds cumulative weights for a weighted
|
||||||
// draw; when nil the choice is uniform and selection is O(1). shared is the set of
|
// draw; when nil the choice is uniform and selection is O(1). shared is the set of
|
||||||
// relative dot paths every item carries, counting paths inside a repeated template.
|
// relative dot paths every item carries, counting paths inside a repeated template.
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
`Bar = 9`
|
`Bar = 9`
|
||||||
|
|
||||||
`Next ID = 194`
|
`Next ID = 195`
|
||||||
|
|
||||||
| Goal | W |
|
| Goal | W |
|
||||||
|---|---|
|
|---|---|
|
||||||
@@ -106,6 +106,7 @@
|
|||||||
| 178 | 0.2.0 | | **Plan with the systems architect which `internal/` packages the engine splits into, by area, and how each type stops being written by more than one stage, then file the moves as items.** | 1 | 3 | 2 | 9 | 3, 3.2 | 18.4 |
|
| 178 | 0.2.0 | | **Plan with the systems architect which `internal/` packages the engine splits into, by area, and how each type stops being written by more than one stage, then file the moves as items.** | 1 | 3 | 2 | 9 | 3, 3.2 | 18.4 |
|
||||||
| 193 | 0.2.0 | decision | **Move the largest shipped packages into nested Go modules that keep their import paths, once item 192's gate fails.** | 6 | 6 | 6 | 7 | 2.3, 2 | 17.8 |
|
| 193 | 0.2.0 | decision | **Move the largest shipped packages into nested Go modules that keep their import paths, once item 192's gate fails.** | 6 | 6 | 6 | 7 | 2.3, 2 | 17.8 |
|
||||||
| 92 | 0.2.0 | | **Name the hyphenated field in a `calc`'s refusal, and say to rename it: today `{calc(postal-code * 2)}` reports `no field "postal"`.** | 1 | 2 | 5 | 6 | 6 | 17.5 |
|
| 92 | 0.2.0 | | **Name the hyphenated field in a `calc`'s refusal, and say to rename it: today `{calc(postal-code * 2)}` reports `no field "postal"`.** | 1 | 2 | 5 | 6 | 6 | 17.5 |
|
||||||
|
| 194 | 0.2.0 | | **Let a data author write and check their own `.fejkdata.json` index with the library or the CLI, and fail a load whose index leaves out a category file.** | 2 | 4 | 5 | 7 | 6.3, 9.1 | 17.5 |
|
||||||
| 53 | 0.2.0 | | **Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.** | 4 | 5 | 6 | 6 | 4 | 16.9 |
|
| 53 | 0.2.0 | | **Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.** | 4 | 5 | 6 | 6 | 4 | 16.9 |
|
||||||
| 55 | 0.2.0 | | **Document `NewRecordTemplate`, `ErrNoColumns` and `MaxRepeat` in the README's Library section.** | 1 | 2 | 6 | 7 | 12 | 16.8 |
|
| 55 | 0.2.0 | | **Document `NewRecordTemplate`, `ErrNoColumns` and `MaxRepeat` in the README's Library section.** | 1 | 2 | 6 | 7 | 12 | 16.8 |
|
||||||
| 18 | 0.2.0 | | **Give the address records one column set across countries: `region` and `municipality` as columns on `geo.SE.address` too.** | 4 | 4 | 6 | 6 | 5 | 16.6 |
|
| 18 | 0.2.0 | | **Give the address records one column set across countries: `region` and `municipality` as columns on `geo.SE.address` too.** | 4 | 4 | 6 | 6 | 5 | 16.6 |
|
||||||
@@ -422,6 +423,10 @@ Carries out the decision "The shipped data is Go packages a library imports by c
|
|||||||
|
|
||||||
A hyphen is always subtraction in a calc, and the shipped data has hyphenated fields (`postal-code` and `street-number` in `geo/SE/address`). The refusal names the first fragment, not the field the author meant.
|
A hyphen is always subtraction in a calc, and the shipped data has hyphenated fields (`postal-code` and `street-number` in `geo/SE/address`). The refusal names the first fragment, not the field the author meant.
|
||||||
|
|
||||||
|
### 194. Let a data author write and check their own `.fejkdata.json` index with the library or the CLI, and fail a load whose index leaves out a category file.
|
||||||
|
|
||||||
|
Goal 6.3 has an author prove their module, and goal 9.1 has fejkdata's data load as anyone's; today only a test of fejkdata's own writes and checks an index (`TestShippedManifestIsCurrent` over `indexEntry`). A category file the index leaves out never loads, silently. The principles audit of item 180's chunk found it.
|
||||||
|
|
||||||
### 53. Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.
|
### 53. Let a table column carry a `datatype`, so `--format json` writes `"safe": true` and `--format sql` a boolean.
|
||||||
|
|
||||||
Today only a JSON field takes one, so `misc.httpmethod`'s booleans are typed in Go and text everywhere else. Typing a shipped column changes what `json` and `sql` write, so the capability is a minor and applying it to `misc.httpmethod` is a major.
|
Today only a JSON field takes one, so `misc.httpmethod`'s booleans are typed in Go and text everywhere else. Typing a shipped column changes what `json` and `sql` write, so the capability is a minor and applying it to `misc.httpmethod` is a major.
|
||||||
|
|||||||
Reference in New Issue
Block a user