Advertise only spellable paths, and name what a choice's variants share
This commit is contained in:
committed by
lilleman-tw
parent
595809bf38
commit
362c484825
@@ -141,8 +141,8 @@ av == bv // true
|
|||||||
```
|
```
|
||||||
|
|
||||||
`f.List()` returns the sorted paths the loaded data offers — the categories, their
|
`f.List()` returns the sorted paths the loaded data offers — the categories, their
|
||||||
dotted fields and folder segments (what the CLI's `-list` prints). It is exactly the
|
dotted fields and folder segments (what the CLI's `-list` prints). Every path it
|
||||||
set `Fake` accepts.
|
lists renders.
|
||||||
|
|
||||||
A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
|
A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
|
||||||
|
|
||||||
@@ -364,9 +364,9 @@ no tokens at all, use a bare string node (`"100 Main St"`), emitted verbatim.
|
|||||||
This yields e.g. `Anna Eriksson`, `Erik Berg`, or rarely `dr Astrid von Flemming`.
|
This yields e.g. `Anna Eriksson`, `Erik Berg`, or rarely `dr Astrid von Flemming`.
|
||||||
Any field is reachable by dotted path — `Fake("person.last")` renders just a
|
Any field is reachable by dotted path — `Fake("person.last")` renders just a
|
||||||
surname; choices along the path are resolved at random. A path may continue
|
surname; choices along the path are resolved at random. A path may continue
|
||||||
*through* a choice only where every variant carries the rest of it — `misc`'s
|
*through* a choice only where every variant carries the rest of it — every
|
||||||
`currency` has 16 variants and every one carries `symbol`, so `currency.symbol`
|
`currency` variant carries `symbol`, so `currency.symbol` resolves — which keeps a
|
||||||
resolves — which keeps a path from rendering on one call and failing on the next.
|
path from rendering on one call and failing on the next.
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ func TestPathThroughChoice(t *testing.T) {
|
|||||||
dir := writeData(t, map[string]string{
|
dir := writeData(t, map[string]string{
|
||||||
"every": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
"every": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"]}]`,
|
||||||
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
"notall": `[{"format":"{f}","f":["1"]},["plain"]]`,
|
||||||
|
"some": `[{"format":"{f}","f":["1"]},{"format":"{f}","f":["2"],"extra":["x"]}]`,
|
||||||
})
|
})
|
||||||
f := newFakes(t, dir, WithSeed(1))
|
f := newFakes(t, dir, WithSeed(1))
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
@@ -104,6 +105,10 @@ func TestPathThroughChoice(t *testing.T) {
|
|||||||
t.Fatalf("every.f = %q, want 1 or 2", got)
|
t.Fatalf("every.f = %q, want 1 or 2", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// A path only some variants carry is reported against what all of them carry.
|
||||||
|
if _, err := f.Fake("some.extra"); err == nil || !strings.Contains(err.Error(), "all carry [f]") {
|
||||||
|
t.Errorf("Fake(some.extra) = %v, want it to name what every variant carries", err)
|
||||||
|
}
|
||||||
var first string
|
var first string
|
||||||
for i := 0; i < 200; i++ {
|
for i := 0; i < 200; i++ {
|
||||||
_, err := f.Fake("notall.f")
|
_, err := f.Fake("notall.f")
|
||||||
|
|||||||
@@ -21,8 +21,8 @@ import (
|
|||||||
"encoding/binary"
|
"encoding/binary"
|
||||||
"fmt"
|
"fmt"
|
||||||
"math/rand/v2"
|
"math/rand/v2"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
// Fakes generates fake data from a loaded namespace tree. Create one with [New].
|
||||||
@@ -90,7 +90,15 @@ func (f *Fakes) List() []string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
sort.Strings(out)
|
sort.Strings(out)
|
||||||
return slices.Compact(out)
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// addressable reports whether a name can be one segment of a dot path. A dot would
|
||||||
|
// split it into two segments and an empty name into none, so a path through such a
|
||||||
|
// name cannot be spelled — List must not offer one, and it must not count towards
|
||||||
|
// what a choice's variants share.
|
||||||
|
func addressable(name string) bool {
|
||||||
|
return name != "" && !strings.Contains(name, ".")
|
||||||
}
|
}
|
||||||
|
|
||||||
// paths lists the dot paths addressable from n, relative to it, where "" is n
|
// paths lists the dot paths addressable from n, relative to it, where "" is n
|
||||||
@@ -100,6 +108,9 @@ func paths(n node) []string {
|
|||||||
case *group:
|
case *group:
|
||||||
var out []string
|
var out []string
|
||||||
for _, name := range sortedNames(n.children) {
|
for _, name := range sortedNames(n.children) {
|
||||||
|
if !addressable(name) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
for _, p := range paths(n.children[name]) {
|
for _, p := range paths(n.children[name]) {
|
||||||
out = append(out, join(name, p))
|
out = append(out, join(name, p))
|
||||||
}
|
}
|
||||||
@@ -108,7 +119,7 @@ func paths(n node) []string {
|
|||||||
case *template:
|
case *template:
|
||||||
out := []string{""}
|
out := []string{""}
|
||||||
for _, name := range sortedNames(n.fields) {
|
for _, name := range sortedNames(n.fields) {
|
||||||
if isRef(name) { // a bound {..path} reference, not an authored field
|
if isRef(name) || !addressable(name) { // a binding, or unreachable by path
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
for _, p := range paths(n.fields[name]) {
|
for _, p := range paths(n.fields[name]) {
|
||||||
@@ -132,29 +143,35 @@ func paths(n node) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// sharedPaths is the sub-paths every item carries — the only ones a path may step
|
// sharedPaths is the sub-paths every item carries — the only ones a path may step
|
||||||
// through a multi-variant choice to reach. An item is counted once per path, since
|
// through a multi-variant choice to reach. It intersects, bailing as soon as the set
|
||||||
// one item offering a path twice does not make it shared.
|
// is empty, which is immediate for a choice of plain strings.
|
||||||
func sharedPaths(items []node) map[string]bool {
|
func sharedPaths(items []node) map[string]bool {
|
||||||
count := map[string]int{}
|
shared := subPaths(items[0])
|
||||||
for _, it := range items {
|
for _, it := range items[1:] {
|
||||||
seen := map[string]bool{}
|
if len(shared) == 0 {
|
||||||
for _, p := range paths(it) {
|
return nil
|
||||||
if p == "" || seen[p] {
|
|
||||||
continue
|
|
||||||
}
|
}
|
||||||
seen[p] = true
|
next := subPaths(it)
|
||||||
count[p]++
|
for p := range shared {
|
||||||
|
if !next[p] {
|
||||||
|
delete(shared, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
shared := map[string]bool{}
|
|
||||||
for p, n := range count {
|
|
||||||
if n == len(items) {
|
|
||||||
shared[p] = true
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return shared
|
return shared
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// subPaths is paths(n) as a set, without the empty path that means n itself.
|
||||||
|
func subPaths(n node) map[string]bool {
|
||||||
|
out := map[string]bool{}
|
||||||
|
for _, p := range paths(n) {
|
||||||
|
if p != "" {
|
||||||
|
out[p] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
func join(prefix, name string) string {
|
func join(prefix, name string) string {
|
||||||
switch {
|
switch {
|
||||||
case prefix == "":
|
case prefix == "":
|
||||||
|
|||||||
@@ -91,7 +91,11 @@ func compileChoice(items []any) (node, error) {
|
|||||||
}
|
}
|
||||||
c.cum = cum
|
c.cum = cum
|
||||||
}
|
}
|
||||||
|
if len(c.items) > 1 {
|
||||||
|
// Safe to precompute: a choice's items come from one file, so no group can
|
||||||
|
// appear inside one, and neither mergeChildren nor linkRefs can reach in.
|
||||||
c.shared = sharedPaths(c.items)
|
c.shared = sharedPaths(c.items)
|
||||||
|
}
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package fakes
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"slices"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
@@ -52,10 +51,11 @@ func descend(s *session, n node, segments []string) (node, error) {
|
|||||||
}
|
}
|
||||||
return descend(s, child, segments[1:])
|
return descend(s, child, segments[1:])
|
||||||
case *choice:
|
case *choice:
|
||||||
want := strings.Join(segments, ".")
|
if len(n.items) > 1 {
|
||||||
if !n.shared[want] {
|
if want := strings.Join(segments, "."); !n.shared[want] {
|
||||||
return nil, unreachableInChoice(n, want)
|
return nil, unreachableInChoice(n, want)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return descend(s, pick(s, n), segments)
|
return descend(s, pick(s, n), segments)
|
||||||
case literal:
|
case literal:
|
||||||
return nil, fmt.Errorf("a plain string has no field %q", segments[0])
|
return nil, fmt.Errorf("a plain string has no field %q", segments[0])
|
||||||
@@ -64,17 +64,19 @@ func descend(s *session, n node, segments []string) (node, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// unreachableInChoice names the first variant that cannot address want. It walks
|
// unreachableInChoice reports that a path cannot step through this choice, listing
|
||||||
// the variants, which only a failing path does.
|
// what every variant does carry. It reads the precomputed set, so a failing path
|
||||||
|
// costs no more than a rendering one.
|
||||||
func unreachableInChoice(c *choice, want string) error {
|
func unreachableInChoice(c *choice, want string) error {
|
||||||
if len(c.items) > 1 {
|
if len(c.shared) == 0 {
|
||||||
for i, item := range c.items {
|
return fmt.Errorf("no variant of this %d-way choice carries %q", len(c.items), want)
|
||||||
if !slices.Contains(paths(item), want) {
|
|
||||||
return fmt.Errorf("variant %d of a %d-way choice cannot reach %q", i+1, len(c.items), want)
|
|
||||||
}
|
}
|
||||||
|
offered := make([]string, 0, len(c.shared))
|
||||||
|
for p := range c.shared {
|
||||||
|
offered = append(offered, p)
|
||||||
}
|
}
|
||||||
}
|
sort.Strings(offered)
|
||||||
return fmt.Errorf("cannot reach %q", want)
|
return fmt.Errorf("not every variant of this %d-way choice carries %q; all carry %v", len(c.items), want, offered)
|
||||||
}
|
}
|
||||||
|
|
||||||
// render evaluates a compiled node to a string. compile validates every node up
|
// render evaluates a compiled node to a string. compile validates every node up
|
||||||
|
|||||||
Reference in New Issue
Block a user