Require every choice variant to carry a path, and list only what Fake accepts
This commit is contained in:
@@ -141,7 +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).
|
dotted fields and folder segments (what the CLI's `-list` prints). It is exactly the
|
||||||
|
set `Fake` accepts.
|
||||||
|
|
||||||
A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
|
A `*Fakes` is **not** safe for concurrent use — create one per goroutine.
|
||||||
|
|
||||||
@@ -362,7 +363,10 @@ 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.
|
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 (so
|
||||||
|
`currency.symbol` works across all 16 currency variants), which keeps a path from
|
||||||
|
rendering on one call and failing on the next.
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
|
|||||||
@@ -76,39 +76,73 @@ func New(paths []string, opts ...Option) (*Fakes, error) {
|
|||||||
return &Fakes{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
return &Fakes{rand: newRand(c.seed, c.seeded), categories: cats}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// List returns the sorted dotted paths Fake can render: every category, the
|
// List returns the sorted dotted paths Fake can render: every category, the dotted
|
||||||
// dotted fields within a template, and folder segments — descending transparently
|
// fields within a template, and folder segments — descending transparently through
|
||||||
// through single-variant choices the way a reference does. A multi-variant choice
|
// single-variant choices the way a reference does. A choice consumes no segment, so
|
||||||
// is one path (its pick is random); its items are not separately addressable. It's
|
// a path continues through a multi-variant one only where every variant carries it,
|
||||||
// the discoverable map of what a loaded data set offers, powering the CLI's -list.
|
// which is the rule Fake applies too: List is the set of paths Fake accepts.
|
||||||
func (f *Fakes) List() []string {
|
func (f *Fakes) List() []string {
|
||||||
var out []string
|
var out []string
|
||||||
var walk func(prefix string, n node)
|
for _, name := range sortedNames(f.categories) {
|
||||||
walk = func(prefix string, n node) {
|
for _, p := range paths(f.categories[name]) {
|
||||||
|
out = append(out, join(name, p))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
sort.Strings(out)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// paths lists the dot paths addressable from n, relative to it, where "" is n
|
||||||
|
// itself. A group has no value of its own, so it contributes only its children's.
|
||||||
|
func paths(n node) []string {
|
||||||
switch n := n.(type) {
|
switch n := n.(type) {
|
||||||
case *group:
|
case *group:
|
||||||
for name, c := range n.children {
|
var out []string
|
||||||
walk(join(prefix, name), c)
|
for _, name := range sortedNames(n.children) {
|
||||||
|
for _, p := range paths(n.children[name]) {
|
||||||
|
out = append(out, join(name, p))
|
||||||
}
|
}
|
||||||
case *choice:
|
|
||||||
if len(n.items) == 1 { // a single-variant choice is a transparent wrapper
|
|
||||||
walk(prefix, n.items[0])
|
|
||||||
return
|
|
||||||
}
|
}
|
||||||
out = append(out, prefix)
|
return out
|
||||||
case *template:
|
case *template:
|
||||||
out = append(out, prefix)
|
out := []string{""}
|
||||||
for name, c := range n.fields {
|
for _, name := range sortedNames(n.fields) {
|
||||||
if isRef(name) { // a bound {..path} reference, not an authored field
|
if isRef(name) { // a bound {..path} reference, not an authored field
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
walk(join(prefix, name), c)
|
for _, p := range paths(n.fields[name]) {
|
||||||
|
out = append(out, join(name, p))
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
case *choice:
|
||||||
|
if len(n.items) == 1 {
|
||||||
|
return paths(n.items[0])
|
||||||
|
}
|
||||||
|
return append([]string{""}, sharedPaths(n.items)...)
|
||||||
case literal:
|
case literal:
|
||||||
out = append(out, prefix)
|
return []string{""}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// sharedPaths is the sub-paths every item carries — the only ones a path may step
|
||||||
|
// through a multi-variant choice to reach.
|
||||||
|
func sharedPaths(items []node) []string {
|
||||||
|
count := map[string]int{}
|
||||||
|
for _, it := range items {
|
||||||
|
for _, p := range paths(it) {
|
||||||
|
if p != "" {
|
||||||
|
count[p]++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for p, n := range count {
|
||||||
|
if n == len(items) {
|
||||||
|
out = append(out, p)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
walk("", &group{children: f.categories})
|
|
||||||
sort.Strings(out)
|
sort.Strings(out)
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,9 +28,11 @@ func (f *Fakes) Fake(path string) (string, error) {
|
|||||||
return render(f.rand, n), nil
|
return render(f.rand, n), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// descend walks named fields, resolving choices it meets along the way. It is
|
// descend walks named fields to the node a path names. It is the one render-side
|
||||||
// the one render-side step that can fail, because the path comes from the
|
// step that can fail, because the path comes from the caller and may name a field
|
||||||
// caller and may name a field that does not exist.
|
// that does not exist. A choice consumes no segment, so every variant must carry
|
||||||
|
// the rest of the path before one is picked at random — a path that resolves at
|
||||||
|
// all resolves on every call. A nil session validates without picking.
|
||||||
func descend(s *session, n node, segments []string) (node, error) {
|
func descend(s *session, n node, segments []string) (node, error) {
|
||||||
if len(segments) == 0 {
|
if len(segments) == 0 {
|
||||||
return n, nil
|
return n, nil
|
||||||
@@ -49,7 +51,22 @@ 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:
|
||||||
return descend(s, pick(s, n), segments) // a choice consumes no path segment
|
for i, item := range n.items {
|
||||||
|
_, err := descend(nil, item, segments)
|
||||||
|
if err == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(n.items) == 1 { // a single-variant choice is a transparent wrapper
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return nil, fmt.Errorf("variant %d of a %d-way choice: %w", i+1, len(n.items), err)
|
||||||
|
}
|
||||||
|
if s == nil {
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
return descend(s, pick(s, n), segments)
|
||||||
|
case literal:
|
||||||
|
return nil, fmt.Errorf("a plain string has no field %q", segments[0])
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0])
|
return nil, fmt.Errorf("cannot descend into %T at %q", n, segments[0])
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user