Skip to content
This repository was archived by the owner on May 14, 2026. It is now read-only.

Commit b790fdf

Browse files
authored
[ggj][nestedsig] feat: add DFS traversal to Trie (#625)
* fix: fix dep ordering in Bazel dedupe rules * feat: add a basic trie * fix: add hashCode() and equals() to Field * fix: add equals() and hashCode() for MethodArgument * feat: add a TriFunction * feat: add DFS traversal to Trie * fix: add Bazel dep for testutil
1 parent e6db415 commit b790fdf

3 files changed

Lines changed: 245 additions & 6 deletions

File tree

src/main/java/com/google/api/generator/util/Trie.java

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@
1414

1515
package com.google.api.generator.util;
1616

17-
import java.util.HashMap;
17+
import java.util.LinkedHashMap;
1818
import java.util.List;
1919
import java.util.Map;
20+
import java.util.function.BiFunction;
21+
import java.util.function.Function;
2022

2123
/**
2224
* A common-prefix trie. T represents the type of each "char" in a word (which is a T-typed list).
2325
*/
2426
public class Trie<T> {
2527
private class Node<T> {
2628
final T chr;
27-
Map<T, Node> children = new HashMap<>();
29+
// Maintain insertion order to enable deterministic test output.
30+
Map<T, Node<T>> children = new LinkedHashMap<>();
2831
boolean isLeaf;
2932

3033
Node() {
@@ -43,7 +46,7 @@ public Trie() {
4346
}
4447

4548
public void insert(List<T> word) {
46-
Map<T, Node> children = root.children;
49+
Map<T, Node<T>> children = root.children;
4750
for (int i = 0; i < word.size(); i++) {
4851
T chr = word.get(i);
4952
Node t;
@@ -71,8 +74,53 @@ public boolean hasPrefix(List<T> prefix) {
7174
return searchNode(prefix) != null;
7275
}
7376

77+
/**
78+
* Reduces the trie to a single value, via a DFS traversal.
79+
*
80+
* @param parentPreprocFn Transforms a parent node into an R-typed base value for consumption by
81+
* the child nodes. The rest of the children will compute their values using this as a base as
82+
* well, so it accumulates computational results as the traversal progresses. Does not handle
83+
* the root node (i.e. when {@code chr} is null).
84+
* @param leafReduceFn Transforms a child node into an R-typed value using the value computed by
85+
* the parent nodes' preprocessing functions.
86+
* @param parentPostprocFn Transforms the post-traversal result (from the child nodes) into
87+
* R-typed values, further building upon {@code baseValue}. Must handle the root node, i.e.
88+
* when {@code chr} is null.
89+
* @param baseValue The base value upon which subsequent reductions will be performed. Ensure this
90+
* is a type that can accumulate values, such as StringBuilder. An immutable type such as
91+
* String will not work here.
92+
*/
93+
public <R> R dfsTraverseAndReduce(
94+
Function<T, R> parentPreprocFn,
95+
TriFunction<T, R, R, R> parentPostprocFn,
96+
BiFunction<T, R, R> leafReduceFn,
97+
R baseValue) {
98+
return dfsTraverseAndReduce(root, parentPreprocFn, parentPostprocFn, leafReduceFn, baseValue);
99+
}
100+
101+
/** Traverses the trie DFS-style, reducing all values into a single one on {@code baseValue}. */
102+
private <R> R dfsTraverseAndReduce(
103+
Node<T> node,
104+
Function<T, R> parentPreprocFn,
105+
TriFunction<T, R, R, R> parentPostprocFn,
106+
BiFunction<T, R, R> leafReduceFn,
107+
R baseValue) {
108+
if (node.isLeaf) {
109+
return leafReduceFn.apply(node.chr, baseValue);
110+
}
111+
112+
R leafReducedValue = node.chr == null ? baseValue : parentPreprocFn.apply(node.chr);
113+
for (Map.Entry<T, Node<T>> e : node.children.entrySet()) {
114+
// Thread the parent value through each of the children, and accumulate it.
115+
leafReducedValue =
116+
dfsTraverseAndReduce(
117+
e.getValue(), parentPreprocFn, parentPostprocFn, leafReduceFn, leafReducedValue);
118+
}
119+
return parentPostprocFn.apply(node.chr, baseValue, leafReducedValue);
120+
}
121+
74122
private Node searchNode(List<T> word) {
75-
Map<T, Node> children = root.children;
123+
Map<T, Node<T>> children = root.children;
76124
Node t = null;
77125
for (int i = 0; i < word.size(); i++) {
78126
T chr = word.get(i);

src/test/java/com/google/api/generator/util/BUILD.bazel

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ filegroup(
1717
test_class = "com.google.api.generator.util.{0}".format(test_name),
1818
deps = [
1919
"//src/main/java/com/google/api/generator/util",
20+
"//src/test/java/com/google/api/generator/testutils",
2021
"@com_google_guava_guava//jar",
2122
"@com_google_truth_truth//jar",
2223
"@junit_junit//jar",

src/test/java/com/google/api/generator/util/TrieTest.java

Lines changed: 192 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,17 +14,20 @@
1414

1515
package com.google.api.generator.util;
1616

17+
import static junit.framework.Assert.assertEquals;
1718
import static junit.framework.Assert.assertFalse;
1819
import static junit.framework.Assert.assertTrue;
1920

21+
import com.google.api.generator.testutils.LineFormatter;
2022
import java.util.Arrays;
2123
import java.util.List;
24+
import java.util.function.BiFunction;
2225
import java.util.function.Function;
2326
import org.junit.Test;
2427

2528
public class TrieTest {
2629
@Test
27-
public void stringTrie() {
30+
public void insertAndSearch_stringTrie() {
2831
Trie<String> trie = new Trie<>();
2932

3033
Function<String, List<String>> wordToCharListFn = w -> Arrays.asList(w.split("(?!^)"));
@@ -43,7 +46,7 @@ public void stringTrie() {
4346
}
4447

4548
@Test
46-
public void multiStringTrie() {
49+
public void insertAndSearch_multiStringTrie() {
4750
Trie<String> trie = new Trie<>();
4851
assertFalse(trie.search(Arrays.asList("user", "identity", "name")));
4952

@@ -62,4 +65,191 @@ public void multiStringTrie() {
6265
assertFalse(trie.hasPrefix(Arrays.asList("identity")));
6366
assertFalse(trie.hasPrefix(Arrays.asList("contact")));
6467
}
68+
69+
@Test
70+
public void dfsTraverseAndReduce_emptyTrie() {
71+
// Add up points in the tree, where each parent gets (num child node points) * 2 + 1.
72+
int baseValue = 0;
73+
Function<String, Integer> parentPreprocFn = nodeVal -> new Integer(0);
74+
BiFunction<String, Integer, Integer> leafReduceFn =
75+
(nodeVal, accVal) -> new Integer(accVal + 1);
76+
TriFunction<String, Integer, Integer, Integer> parentPostprocFn =
77+
(nodeVal, baseVal, accVal) -> new Integer(nodeVal == null ? 0 : accVal * 2 + 1);
78+
79+
Trie<String> trie = new Trie<>();
80+
int finalValue = trie.dfsTraverseAndReduce(parentPreprocFn, parentPostprocFn, leafReduceFn, 0);
81+
assertEquals(0, finalValue);
82+
}
83+
84+
@Test
85+
public void dfsTraverseAndReduce_singleNodeTrie() {
86+
// Add up points in the tree, where each parent gets (num child node points) * 2 + 1.
87+
int baseValue = 0;
88+
Function<String, Integer> parentPreprocFn = nodeVal -> new Integer(0);
89+
BiFunction<String, Integer, Integer> leafReduceFn =
90+
(nodeVal, accVal) -> new Integer(accVal + 1);
91+
TriFunction<String, Integer, Integer, Integer> parentPostprocFn =
92+
(nodeVal, baseVal, accVal) -> new Integer(nodeVal == null ? accVal : accVal * 2 + 1);
93+
94+
Trie<String> trie = new Trie<>();
95+
trie.insert(Arrays.asList("user"));
96+
int finalValue = trie.dfsTraverseAndReduce(parentPreprocFn, parentPostprocFn, leafReduceFn, 0);
97+
assertEquals(1, finalValue);
98+
}
99+
100+
@Test
101+
public void dfsTraverseAndReduce_oneParentOneChildBranchTrie() {
102+
Function<String, String> toUpperCaseFn = s -> s.substring(0, 1).toUpperCase() + s.substring(1);
103+
Function<String, StringBuilder> parentPreprocFn =
104+
nodeVal ->
105+
new StringBuilder(String.format("%s.newBuilder()", toUpperCaseFn.apply(nodeVal)));
106+
BiFunction<String, StringBuilder, StringBuilder> leafReduceFn =
107+
(nodeVal, parentAccVal) -> {
108+
parentAccVal.append(
109+
String.format(
110+
".set%s(\"%s\")", toUpperCaseFn.apply(nodeVal.toString()), nodeVal.toString()));
111+
return parentAccVal;
112+
};
113+
TriFunction<String, StringBuilder, StringBuilder, StringBuilder> parentPostprocFn =
114+
(nodeVal, baseVal, accVal) -> {
115+
boolean isRootNode = nodeVal == null;
116+
if (!isRootNode) {
117+
baseVal.append(
118+
String.format(
119+
".set%s(%s.build())",
120+
toUpperCaseFn.apply(nodeVal.toString()), accVal.toString()));
121+
}
122+
return isRootNode ? accVal : baseVal;
123+
};
124+
StringBuilder baseVal = new StringBuilder("RequestType.newBuilder()");
125+
126+
Trie<String> trie = new Trie<>();
127+
trie.insert(Arrays.asList("user", "identity"));
128+
String finalValue =
129+
trie.dfsTraverseAndReduce(parentPreprocFn, parentPostprocFn, leafReduceFn, baseVal)
130+
.toString();
131+
132+
assertEquals(
133+
LineFormatter.lines(
134+
"RequestType.newBuilder()",
135+
".setUser(User.newBuilder()",
136+
".setIdentity(\"identity\").build())",
137+
".build();"),
138+
finalValue + ".build();");
139+
}
140+
141+
@Test
142+
public void dfsTraverseAndReduce_oneDeepBranchTrie() {
143+
// Add up points in the tree, where each parent gets (num child node points) * 2 + 1.
144+
int simpleBaseValue = 0;
145+
Function<String, Integer> simpleParentPreprocFn = nodeVal -> new Integer(0);
146+
BiFunction<String, Integer, Integer> simpleLeafReduceFn =
147+
(nodeVal, accVal) -> new Integer(accVal + 1);
148+
TriFunction<String, Integer, Integer, Integer> simpleParentPostprocFn =
149+
(nodeVal, baseVal, accVal) -> new Integer(nodeVal == null ? accVal : accVal * 2 + 1);
150+
151+
Trie<String> trie = new Trie<>();
152+
trie.insert(Arrays.asList("user", "identity", "name", "firstName"));
153+
int simpleFinalValue =
154+
trie.dfsTraverseAndReduce(
155+
simpleParentPreprocFn, simpleParentPostprocFn, simpleLeafReduceFn, simpleBaseValue);
156+
assertEquals(15, simpleFinalValue);
157+
158+
Function<String, String> toUpperCaseFn = s -> s.substring(0, 1).toUpperCase() + s.substring(1);
159+
Function<String, StringBuilder> parentPreprocFn =
160+
nodeVal ->
161+
new StringBuilder(String.format("%s.newBuilder()", toUpperCaseFn.apply(nodeVal)));
162+
BiFunction<String, StringBuilder, StringBuilder> leafReduceFn =
163+
(nodeVal, parentAccVal) -> {
164+
parentAccVal.append(
165+
String.format(
166+
".set%s(\"%s\")", toUpperCaseFn.apply(nodeVal.toString()), nodeVal.toString()));
167+
return parentAccVal;
168+
};
169+
TriFunction<String, StringBuilder, StringBuilder, StringBuilder> parentPostprocFn =
170+
(nodeVal, baseVal, accVal) -> {
171+
boolean isRootNode = nodeVal == null;
172+
if (!isRootNode) {
173+
baseVal.append(
174+
String.format(
175+
".set%s(%s.build())",
176+
toUpperCaseFn.apply(nodeVal.toString()), accVal.toString()));
177+
}
178+
return isRootNode ? accVal : baseVal;
179+
};
180+
StringBuilder baseVal = new StringBuilder("RequestType.newBuilder()");
181+
182+
String finalValue =
183+
trie.dfsTraverseAndReduce(parentPreprocFn, parentPostprocFn, leafReduceFn, baseVal)
184+
.toString();
185+
186+
assertEquals(
187+
LineFormatter.lines(
188+
"RequestType.newBuilder()",
189+
".setUser(User.newBuilder()",
190+
".setIdentity(Identity.newBuilder()",
191+
".setName(Name.newBuilder()",
192+
".setFirstName(\"firstName\").build())",
193+
".build())",
194+
".build())",
195+
".build();"),
196+
finalValue + ".build();");
197+
}
198+
199+
@Test
200+
public void dfsTraverseAndReduce_depthAndBreathTrie() {
201+
Function<String, String> toUpperCaseFn = s -> s.substring(0, 1).toUpperCase() + s.substring(1);
202+
Function<String, StringBuilder> parentPreprocFn =
203+
nodeVal ->
204+
new StringBuilder(String.format("%s.newBuilder()", toUpperCaseFn.apply(nodeVal)));
205+
BiFunction<String, StringBuilder, StringBuilder> leafReduceFn =
206+
(nodeVal, parentAccVal) -> {
207+
parentAccVal.append(
208+
String.format(
209+
".set%s(\"%s\")", toUpperCaseFn.apply(nodeVal.toString()), nodeVal.toString()));
210+
return parentAccVal;
211+
};
212+
TriFunction<String, StringBuilder, StringBuilder, StringBuilder> parentPostprocFn =
213+
(nodeVal, baseVal, accVal) -> {
214+
boolean isRootNode = nodeVal == null;
215+
if (!isRootNode) {
216+
baseVal.append(
217+
String.format(
218+
".set%s(%s.build())",
219+
toUpperCaseFn.apply(nodeVal.toString()), accVal.toString()));
220+
}
221+
return isRootNode ? accVal : baseVal;
222+
};
223+
StringBuilder baseVal = new StringBuilder("RequestType.newBuilder()");
224+
225+
Trie<String> trie = new Trie<>();
226+
trie.insert(Arrays.asList("user", "identity", "name", "firstName"));
227+
trie.insert(Arrays.asList("user", "identity", "name", "lastName"));
228+
trie.insert(Arrays.asList("user", "email"));
229+
trie.insert(Arrays.asList("user", "age"));
230+
trie.insert(Arrays.asList("user", "hobby", "hobbyName"));
231+
trie.insert(Arrays.asList("user", "hobby", "frequency"));
232+
233+
String finalValue =
234+
trie.dfsTraverseAndReduce(parentPreprocFn, parentPostprocFn, leafReduceFn, baseVal)
235+
.toString();
236+
237+
assertEquals(
238+
LineFormatter.lines(
239+
"RequestType.newBuilder()",
240+
".setUser(User.newBuilder()",
241+
".setIdentity(Identity.newBuilder()",
242+
".setName(Name.newBuilder()",
243+
".setFirstName(\"firstName\")",
244+
".setLastName(\"lastName\").build())", // Name.newBuilder().build().
245+
".build())", // Identity.newBuilder().build().
246+
".setEmail(\"email\")",
247+
".setAge(\"age\")",
248+
".setHobby(Hobby.newBuilder()",
249+
".setHobbyName(\"hobbyName\")",
250+
".setFrequency(\"frequency\").build())", // Hobby.newBuilder().build().
251+
".build())", // User.newBuilder().build().
252+
".build();"),
253+
finalValue + ".build();");
254+
}
65255
}

0 commit comments

Comments
 (0)