Read one value when a bound path is read twice
This commit is contained in:
@@ -176,6 +176,32 @@ func TestDottedTokenErrors(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSamePathReadTwiceReadsOneValue(t *testing.T) {
|
||||
// A path is drawn once per expansion, so reading it twice reads one value —
|
||||
// what a name shown in a display form and again in an address needs.
|
||||
f := engine(7)
|
||||
for i := 0; i < 300; i++ {
|
||||
got := mustRender(t, f, `{"format":"{p.first}|{p.first}",
|
||||
"p":[{"format":"{first}","first":["Anna","Astrid","Elin","Karin"]}]}`)
|
||||
parts := strings.Split(got, "|")
|
||||
if parts[0] != parts[1] {
|
||||
t.Fatalf("draw %d = %q, want one value read twice", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestDifferentTailsUnderOneHeadShareTheRow(t *testing.T) {
|
||||
// Two tails of one head stay in the same variant, each keeping its own value.
|
||||
f := engine(8)
|
||||
for i := 0; i < 300; i++ {
|
||||
got := mustRender(t, f, `{"format":"{p.a}{p.b}",
|
||||
"p":[{"format":"x","a":"A","b":"1"},{"format":"y","a":"B","b":"2"}]}`)
|
||||
if got != "A1" && got != "B2" {
|
||||
t.Fatalf("draw %d = %q, want a row's own pair", i, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBoundPathIsReachableByFake(t *testing.T) {
|
||||
// Binding changes how a format reads a sibling, not what List and Fake offer:
|
||||
// the sub-fields stay addressable on their own.
|
||||
|
||||
@@ -139,9 +139,12 @@ func expand(s *session, t *template) string {
|
||||
b.Grow(t.grow)
|
||||
// One draw per bound head, held for this expansion only: a nested template and
|
||||
// each repeat iteration get their own, since each is its own expansion.
|
||||
var bound map[string]node
|
||||
var bound *draws
|
||||
if len(t.bound) > 0 {
|
||||
bound = make(map[string]node, len(t.bound))
|
||||
bound = &draws{
|
||||
variant: make(map[string]node, len(t.bound)),
|
||||
value: make(map[string]string, len(t.bound)),
|
||||
}
|
||||
}
|
||||
for i := range t.ops {
|
||||
o := &t.ops[i]
|
||||
@@ -159,30 +162,42 @@ func expand(s *session, t *template) string {
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// draws is what an expansion has already drawn for its bound heads: the variant
|
||||
// each head was drawn as, so every path under it reads one row, and the value each
|
||||
// path read, so the same path read twice reads one value.
|
||||
type draws struct {
|
||||
variant map[string]node
|
||||
value map[string]string
|
||||
}
|
||||
|
||||
// resolve renders one field alternation: the '|' alternatives, one picked at
|
||||
// random. An arm's key is a sibling field or a {..path} reference, which linkRefs
|
||||
// bound into fields too. A head the format addresses by dotted path is drawn once
|
||||
// and held in bound, so {place.postal-code} and {place.locality} read one row;
|
||||
// the arm's tail then walks into that draw. checkTokens, checkPath and linkRefs
|
||||
// prove every step, so this cannot fail.
|
||||
func resolve(s *session, arms []arm, t *template, bound map[string]node) string {
|
||||
// bound into fields too. A field the format addresses by dotted path is bound: it
|
||||
// is drawn once for the expansion, so {place.postal-code} and {place.locality}
|
||||
// read one row and either read twice gives one value. checkTokens, checkPath and
|
||||
// linkRefs prove every step, so this cannot fail.
|
||||
func resolve(s *session, arms []arm, t *template, bound *draws) string {
|
||||
a := arms[s.IntN(len(arms))]
|
||||
n := t.fields[a.key]
|
||||
if t.bound[a.key] {
|
||||
held, drew := bound[a.key]
|
||||
if !drew {
|
||||
held = drawn(s, n)
|
||||
bound[a.key] = held
|
||||
}
|
||||
n = held
|
||||
if !t.bound[a.key] {
|
||||
return render(s, t.fields[a.key])
|
||||
}
|
||||
if v, read := bound.value[a.name]; read {
|
||||
return v
|
||||
}
|
||||
n, drew := bound.variant[a.key]
|
||||
if !drew {
|
||||
n = drawn(s, t.fields[a.key])
|
||||
bound.variant[a.key] = n
|
||||
}
|
||||
if len(a.tail) > 0 {
|
||||
var err error
|
||||
if n, err = descend(s, n, a.tail); err != nil {
|
||||
panic(fmt.Sprintf("fakes: %s: %v", strings.Join(append([]string{a.key}, a.tail...), "."), err))
|
||||
panic(fmt.Sprintf("fakes: %s: %v", a.name, err))
|
||||
}
|
||||
}
|
||||
return render(s, n)
|
||||
v := render(s, n)
|
||||
bound.value[a.name] = v
|
||||
return v
|
||||
}
|
||||
|
||||
// drawn resolves a choice to one variant, so a bound head is a concrete node the
|
||||
|
||||
+4
-3
@@ -177,6 +177,7 @@ func fieldTokens(format string) []string {
|
||||
// bound under) and the tail of a dotted path into it. A non-empty tail is what makes
|
||||
// the arm a bound draw: its head is drawn once per expansion (see boundHeads).
|
||||
type arm struct {
|
||||
name string // as written, and the key a bound draw's value is held under
|
||||
key string
|
||||
tail []string
|
||||
}
|
||||
@@ -185,13 +186,13 @@ type arm struct {
|
||||
// dots — linkRefs binds it whole — so only a sibling name reads as a path.
|
||||
func splitArm(name string) arm {
|
||||
if isRef(name) {
|
||||
return arm{key: name}
|
||||
return arm{name: name, key: name}
|
||||
}
|
||||
head, tail, dotted := strings.Cut(name, ".")
|
||||
if !dotted {
|
||||
return arm{key: name}
|
||||
return arm{name: name, key: name}
|
||||
}
|
||||
return arm{key: head, tail: strings.Split(tail, ".")}
|
||||
return arm{name: name, key: head, tail: strings.Split(tail, ".")}
|
||||
}
|
||||
|
||||
// splitArms splits a token body's '|' alternatives.
|
||||
|
||||
Reference in New Issue
Block a user