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) {
|
func TestBoundPathIsReachableByFake(t *testing.T) {
|
||||||
// Binding changes how a format reads a sibling, not what List and Fake offer:
|
// Binding changes how a format reads a sibling, not what List and Fake offer:
|
||||||
// the sub-fields stay addressable on their own.
|
// the sub-fields stay addressable on their own.
|
||||||
|
|||||||
@@ -139,9 +139,12 @@ func expand(s *session, t *template) string {
|
|||||||
b.Grow(t.grow)
|
b.Grow(t.grow)
|
||||||
// One draw per bound head, held for this expansion only: a nested template and
|
// 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.
|
// 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 {
|
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 {
|
for i := range t.ops {
|
||||||
o := &t.ops[i]
|
o := &t.ops[i]
|
||||||
@@ -159,30 +162,42 @@ func expand(s *session, t *template) string {
|
|||||||
return b.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
|
// 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
|
// 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
|
// bound into fields too. A field the format addresses by dotted path is bound: it
|
||||||
// and held in bound, so {place.postal-code} and {place.locality} read one row;
|
// is drawn once for the expansion, so {place.postal-code} and {place.locality}
|
||||||
// the arm's tail then walks into that draw. checkTokens, checkPath and linkRefs
|
// read one row and either read twice gives one value. checkTokens, checkPath and
|
||||||
// prove every step, so this cannot fail.
|
// linkRefs prove every step, so this cannot fail.
|
||||||
func resolve(s *session, arms []arm, t *template, bound map[string]node) string {
|
func resolve(s *session, arms []arm, t *template, bound *draws) string {
|
||||||
a := arms[s.IntN(len(arms))]
|
a := arms[s.IntN(len(arms))]
|
||||||
n := t.fields[a.key]
|
if !t.bound[a.key] {
|
||||||
if t.bound[a.key] {
|
return render(s, t.fields[a.key])
|
||||||
held, drew := bound[a.key]
|
|
||||||
if !drew {
|
|
||||||
held = drawn(s, n)
|
|
||||||
bound[a.key] = held
|
|
||||||
}
|
}
|
||||||
n = held
|
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 {
|
if len(a.tail) > 0 {
|
||||||
var err error
|
var err error
|
||||||
if n, err = descend(s, n, a.tail); err != nil {
|
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
|
// 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
|
// 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).
|
// the arm a bound draw: its head is drawn once per expansion (see boundHeads).
|
||||||
type arm struct {
|
type arm struct {
|
||||||
|
name string // as written, and the key a bound draw's value is held under
|
||||||
key string
|
key string
|
||||||
tail []string
|
tail []string
|
||||||
}
|
}
|
||||||
@@ -185,13 +186,13 @@ type arm struct {
|
|||||||
// dots — linkRefs binds it whole — so only a sibling name reads as a path.
|
// dots — linkRefs binds it whole — so only a sibling name reads as a path.
|
||||||
func splitArm(name string) arm {
|
func splitArm(name string) arm {
|
||||||
if isRef(name) {
|
if isRef(name) {
|
||||||
return arm{key: name}
|
return arm{name: name, key: name}
|
||||||
}
|
}
|
||||||
head, tail, dotted := strings.Cut(name, ".")
|
head, tail, dotted := strings.Cut(name, ".")
|
||||||
if !dotted {
|
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.
|
// splitArms splits a token body's '|' alternatives.
|
||||||
|
|||||||
Reference in New Issue
Block a user