One spelling per shape: reject a one-item choice, a repeated item and an inert object; reshape the data

This commit is contained in:
2026-09-02 12:38:59 +02:00
parent de8c1d13e9
commit 054f99c94f
49 changed files with 620 additions and 593 deletions
+51 -19
View File
@@ -1,6 +1,7 @@
package fejkdata
import (
"encoding/json"
"fmt"
"math"
"sort"
@@ -107,6 +108,12 @@ func compileChoice(items []any) (node, error) {
if len(items) == 0 {
return nil, fmt.Errorf("empty choice")
}
if len(items) == 1 {
return nil, fmt.Errorf("a one-item choice is its item; write the item")
}
if err := checkNoRepeatedItem(items); err != nil {
return nil, err
}
c := &choice{items: make([]node, len(items))}
cum := make([]float64, len(items))
var total float64
@@ -133,14 +140,33 @@ func compileChoice(items []any) (node, error) {
}
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)
}
// 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)
return c, nil
}
// checkNoRepeatedItem rejects a choice that lists one item twice: a pick is even
// over the items, so a repeat is a second spelling of weight. The error names the
// spelling that does skew a pick.
func checkNoRepeatedItem(items []any) error {
seen := make(map[string]int, len(items))
for i, raw := range items {
key, err := json.Marshal(raw)
if err != nil {
return err
}
if j, dup := seen[string(key)]; dup {
if s, isString := raw.(string); isString {
return fmt.Errorf("choice item %q is repeated; skew the odds with a weight instead: { \"format\": %q, \"weight\": 2 }", s, s)
}
return fmt.Errorf("choice item %d repeats item %d; skew the odds with a weight on one of them instead", i, j)
}
seen[string(key)] = i
}
return nil
}
func compileTemplate(m map[string]any) (node, error) {
format, ok := m["format"].(string)
if !ok {
@@ -181,6 +207,9 @@ func compileTemplate(m map[string]any) (node, error) {
}
t.fields[k] = n
}
if _, weighted := m["weight"]; len(t.fields) == 0 && repeat == 1 && !weighted {
return nil, fmt.Errorf("an object holding only a format is a string; write %q", format)
}
if err := checkTokens(format, t.fields); err != nil {
return nil, err
}
@@ -212,27 +241,24 @@ func checkPath(n node, tail []string, level string) error {
}
return checkPath(child, tail[1:], level+"."+tail[0])
case *choice:
if len(n.items) > 1 {
if want := strings.Join(tail, "."); !n.shared[want] {
return unreachableInChoice(n, want)
}
// Reachability is settled; each variant still answers for itself, so a
// rule about the level (its repeat) holds behind a choice as in front.
for _, item := range n.items {
if err := checkPath(item, tail, level); err != nil {
return err
}
}
return nil
if want := strings.Join(tail, "."); !n.shared[want] {
return unreachableInChoice(n, want)
}
return checkPath(n.items[0], tail, level)
// Reachability is settled; each variant still answers for itself, so a
// rule about the level (its repeat) holds behind a choice as in front.
for _, item := range n.items {
if err := checkPath(item, tail, level); err != nil {
return err
}
}
return nil
default:
return fmt.Errorf("cannot descend into %T at %q", n, tail[0])
}
}
// repeatOf reads a template's "repeat" (default 1): how many times its format
// is rendered and concatenated. A present one must be a positive integer.
// is rendered and concatenated. A present one must be an integer above 1.
func repeatOf(m map[string]any) (int, error) {
rv, ok := m["repeat"]
if !ok {
@@ -245,6 +271,9 @@ func repeatOf(m map[string]any) (int, error) {
if math.IsNaN(r) || math.IsInf(r, 0) || r < 1 || r != math.Trunc(r) {
return 0, fmt.Errorf("repeat must be a positive integer, got %v", rv)
}
if r == 1 {
return 0, fmt.Errorf("repeat 1 is the default, so it has no effect; drop it")
}
if r > maxLen { // cap so a fat-fingered repeat can't build a multi-GB string
return 0, fmt.Errorf("repeat %v exceeds the maximum %d", rv, maxLen)
}
@@ -269,6 +298,9 @@ func weightOf(raw any) (float64, error) {
if w < 0 || math.IsNaN(w) || math.IsInf(w, 0) {
return 0, fmt.Errorf("weight must be finite and non-negative, got %v", w)
}
if w == 1 {
return 0, fmt.Errorf("weight 1 is the default, so it has no effect; drop it")
}
return w, nil
}