Skip to content
This repository was archived by the owner on Dec 19, 2023. It is now read-only.

Commit 9955e08

Browse files
authored
support for recursive structures (#131)
1 parent 7547e0d commit 9955e08

File tree

6 files changed

+82
-43
lines changed

6 files changed

+82
-43
lines changed

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationArrayVisitor.java

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package capital.scalable.restdocs.jackson;
1818

19+
import java.util.Set;
20+
1921
import com.fasterxml.jackson.databind.JavaType;
2022
import com.fasterxml.jackson.databind.JsonMappingException;
2123
import com.fasterxml.jackson.databind.SerializerProvider;
@@ -27,20 +29,22 @@ public class FieldDocumentationArrayVisitor extends JsonArrayFormatVisitor.Base
2729

2830
private final FieldDocumentationVisitorContext context;
2931
private final String path;
32+
private final Set<JavaType> visited;
3033

3134
public FieldDocumentationArrayVisitor(SerializerProvider provider,
32-
FieldDocumentationVisitorContext context, String path) {
35+
FieldDocumentationVisitorContext context, String path, Set<JavaType> visited) {
3336
super(provider);
3437
this.context = context;
3538
this.path = path;
39+
this.visited = visited;
3640
}
3741

3842
@Override
3943
public void itemsFormat(JsonFormatVisitable handler, JavaType elementType)
4044
throws JsonMappingException {
4145
String elementPath = path + "[]";
42-
JsonFormatVisitorWrapper visitor =
43-
new FieldDocumentationVisitorWrapper(getProvider(), context, elementPath, null);
46+
JsonFormatVisitorWrapper visitor = new FieldDocumentationVisitorWrapper(getProvider(),
47+
context, elementPath, null, visited);
4448
handler.acceptJsonFormatVisitor(visitor, elementType);
4549
}
4650
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationObjectVisitor.java

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@
1616

1717
package capital.scalable.restdocs.jackson;
1818

19+
import java.util.Set;
20+
1921
import com.fasterxml.jackson.databind.BeanProperty;
2022
import com.fasterxml.jackson.databind.JavaType;
2123
import com.fasterxml.jackson.databind.JsonMappingException;
@@ -29,12 +31,14 @@ public class FieldDocumentationObjectVisitor extends JsonObjectFormatVisitor.Bas
2931

3032
private final FieldDocumentationVisitorContext context;
3133
private final String path;
34+
private final Set<JavaType> visited;
3235

3336
public FieldDocumentationObjectVisitor(SerializerProvider provider,
34-
FieldDocumentationVisitorContext context, String path) {
37+
FieldDocumentationVisitorContext context, String path, Set<JavaType> visited) {
3538
super(provider);
3639
this.context = context;
3740
this.path = path;
41+
this.visited = visited;
3842
}
3943

4044
@Override
@@ -57,11 +61,11 @@ public void optionalProperty(BeanProperty prop) throws JsonMappingException {
5761
Class<?> javaBaseClass = prop.getMember().getDeclaringClass();
5862
boolean shouldExpand = shouldExpand(prop);
5963

60-
InternalFieldInfo fieldInfo =
61-
new InternalFieldInfo(javaBaseClass, fieldName, fieldPath, shouldExpand);
64+
InternalFieldInfo fieldInfo = new InternalFieldInfo(javaBaseClass, fieldName, fieldPath,
65+
shouldExpand);
6266

63-
JsonFormatVisitorWrapper visitor =
64-
new FieldDocumentationVisitorWrapper(getProvider(), context, fieldPath, fieldInfo);
67+
JsonFormatVisitorWrapper visitor = new FieldDocumentationVisitorWrapper(getProvider(),
68+
context, fieldPath, fieldInfo, visited);
6569

6670
ser.acceptJsonFormatVisitor(visitor, type);
6771
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationVisitorContext.java

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -53,32 +53,32 @@ public List<FieldDescriptor> getFields() {
5353
}
5454

5555
public void addField(InternalFieldInfo info, String jsonType) {
56-
Class<?> javaFieldClass = info.getJavaBaseClass();
56+
Class<?> javaBaseClass = info.getJavaBaseClass();
5757
String javaFieldName = info.getJavaFieldName();
5858

59-
String comment = resolveComment(javaFieldClass, javaFieldName);
59+
String comment = resolveComment(javaBaseClass, javaFieldName);
6060
String jsonFieldPath = info.getJsonFieldPath();
6161

6262
FieldDescriptor fieldDescriptor = fieldWithPath(jsonFieldPath)
6363
.type(jsonType)
6464
.description(comment);
6565

66-
Attribute constraints = constraintAttribute(javaFieldClass, javaFieldName);
67-
Attribute optionals = optionalAttribute(javaFieldClass, javaFieldName);
66+
Attribute constraints = constraintAttribute(javaBaseClass, javaFieldName);
67+
Attribute optionals = optionalAttribute(javaBaseClass, javaFieldName);
6868
fieldDescriptor.attributes(constraints, optionals);
6969

7070
fields.add(fieldDescriptor);
7171
}
7272

73-
private String resolveComment(Class<?> javaFieldClass, String javaFieldName) {
74-
String comment = javadocReader.resolveFieldComment(javaFieldClass, javaFieldName);
73+
private String resolveComment(Class<?> javaBaseClass, String javaFieldName) {
74+
String comment = javadocReader.resolveFieldComment(javaBaseClass, javaFieldName);
7575
if (isBlank(comment)) {
7676
// fallback if fieldName is getter method and comment is on the method itself
77-
comment = javadocReader.resolveMethodComment(javaFieldClass, javaFieldName);
77+
comment = javadocReader.resolveMethodComment(javaBaseClass, javaFieldName);
7878
}
7979
if (isBlank(comment) && isGetter(javaFieldName)) {
8080
// fallback if fieldName is getter method but comment is on field itself
81-
comment = javadocReader.resolveFieldComment(javaFieldClass, fromGetter(javaFieldName));
81+
comment = javadocReader.resolveFieldComment(javaBaseClass, fromGetter(javaFieldName));
8282
}
8383
return comment;
8484
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/jackson/FieldDocumentationVisitorWrapper.java

Lines changed: 26 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,9 @@
1616

1717
package capital.scalable.restdocs.jackson;
1818

19+
import java.util.HashSet;
20+
import java.util.Set;
21+
1922
import capital.scalable.restdocs.constraints.ConstraintReader;
2023
import capital.scalable.restdocs.javadoc.JavadocReader;
2124
import com.fasterxml.jackson.databind.DeserializationConfig;
@@ -38,25 +41,28 @@ public class FieldDocumentationVisitorWrapper implements JsonFormatVisitorWrappe
3841
private final FieldDocumentationVisitorContext context;
3942
private final String path;
4043
private final InternalFieldInfo fieldInfo;
44+
private final Set<JavaType> visited;
4145

4246
FieldDocumentationVisitorWrapper(FieldDocumentationVisitorContext context, String path,
43-
InternalFieldInfo fieldInfo) {
44-
this(null, context, path, fieldInfo);
47+
InternalFieldInfo fieldInfo, Set<JavaType> visited) {
48+
this(null, context, path, fieldInfo, visited);
4549
}
4650

4751
FieldDocumentationVisitorWrapper(SerializerProvider provider,
48-
FieldDocumentationVisitorContext context, String path, InternalFieldInfo fieldInfo) {
52+
FieldDocumentationVisitorContext context, String path, InternalFieldInfo fieldInfo,
53+
Set<JavaType> visited) {
4954
this.provider = provider;
5055
this.context = context;
5156
this.path = path;
5257
this.fieldInfo = fieldInfo;
58+
this.visited = visited;
5359
}
5460

5561
public static FieldDocumentationVisitorWrapper create(JavadocReader javadocReader,
5662
ConstraintReader constraintReader, DeserializationConfig deserializationConfig) {
5763
return new FieldDocumentationVisitorWrapper(
5864
new FieldDocumentationVisitorContext(javadocReader, constraintReader,
59-
deserializationConfig), "", null);
65+
deserializationConfig), "", null, new HashSet<JavaType>());
6066
}
6167

6268
@Override
@@ -72,8 +78,9 @@ public void setProvider(SerializerProvider provider) {
7278
@Override
7379
public JsonObjectFormatVisitor expectObjectFormat(JavaType type) throws JsonMappingException {
7480
addFieldIfPresent("Object");
75-
if (shouldExpand()) {
76-
return new FieldDocumentationObjectVisitor(provider, context, path);
81+
if (shouldExpand() && !wasVisited(type)) {
82+
return new FieldDocumentationObjectVisitor(provider, context, path,
83+
withVisitedType(type));
7784
} else {
7885
return new JsonObjectFormatVisitor.Base();
7986
}
@@ -82,8 +89,9 @@ public JsonObjectFormatVisitor expectObjectFormat(JavaType type) throws JsonMapp
8289
@Override
8390
public JsonArrayFormatVisitor expectArrayFormat(JavaType type) throws JsonMappingException {
8491
addFieldIfPresent("Array");
85-
if (shouldExpand()) {
86-
return new FieldDocumentationArrayVisitor(provider, context, path);
92+
if (shouldExpand() && !wasVisited(type)) {
93+
return new FieldDocumentationArrayVisitor(provider, context, path,
94+
withVisitedType(type));
8795
} else {
8896
return new JsonArrayFormatVisitor.Base();
8997
}
@@ -144,4 +152,14 @@ private void addFieldIfPresent(String jsonType) {
144152
private boolean shouldExpand() {
145153
return fieldInfo == null || fieldInfo.shouldExpand();
146154
}
155+
156+
private Set<JavaType> withVisitedType(JavaType type) {
157+
Set<JavaType> result = new HashSet<>(visited);
158+
result.add(type);
159+
return result;
160+
}
161+
162+
private boolean wasVisited(JavaType type) {
163+
return visited.contains(type);
164+
}
147165
}

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/jackson/FieldDocumentationGeneratorTest.java

Lines changed: 32 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -250,13 +250,6 @@ public void testGenerateDocumentationForRecursiveTypes() throws Exception {
250250
// given
251251
ObjectMapper mapper = createMapper();
252252
JavadocReader javadocReader = mock(JavadocReader.class);
253-
when(javadocReader.resolveFieldComment(RecursiveType.class, "value"))
254-
.thenReturn("Type value");
255-
when(javadocReader.resolveFieldComment(RecursiveType.class, "children"))
256-
.thenReturn("Child types");
257-
when(javadocReader.resolveFieldComment(RecursiveType.class, "sibling"))
258-
.thenReturn("Sibling type");
259-
260253
ConstraintReader constraintReader = mock(ConstraintReader.class);
261254

262255
FieldDocumentationGenerator generator =
@@ -269,13 +262,19 @@ public void testGenerateDocumentationForRecursiveTypes() throws Exception {
269262
.generateDocumentation(type, mapper.getTypeFactory()));
270263

271264
// then
272-
assertThat(fieldDescriptions.size(), is(3));
273-
assertThat(fieldDescriptions.get(0),
274-
is(descriptor("value", "String", "Type value", "true")));
275-
assertThat(fieldDescriptions.get(1),
276-
is(descriptor("children", "Array", "Child types", "true")));
277-
assertThat(fieldDescriptions.get(2),
278-
is(descriptor("sibling", "Object", "Sibling type", "true")));
265+
assertThat(fieldDescriptions.size(), is(12));
266+
assertThat(fieldDescriptions.get(0), is(descriptor("sub1", "Array", null, "true")));
267+
assertThat(fieldDescriptions.get(1), is(descriptor("sub2", "Object", null, "true")));
268+
assertThat(fieldDescriptions.get(2), is(descriptor("sub3", "Array", null, "true")));
269+
assertThat(fieldDescriptions.get(3), is(descriptor("sub4", "Object", null, "true")));
270+
assertThat(fieldDescriptions.get(4), is(descriptor("sub5", "Array", null, "true")));
271+
assertThat(fieldDescriptions.get(5), is(descriptor("sub6", "Object", null, "true")));
272+
assertThat(fieldDescriptions.get(6), is(descriptor("sub7", "Array", null, "true")));
273+
assertThat(fieldDescriptions.get(7), is(descriptor("sub7[].sub1", "Object", null, "true")));
274+
assertThat(fieldDescriptions.get(8), is(descriptor("sub7[].sub2", "Object", null, "true")));
275+
assertThat(fieldDescriptions.get(9), is(descriptor("sub8", "Object", null, "true")));
276+
assertThat(fieldDescriptions.get(10), is(descriptor("sub8.sub1", "Object", null, "true")));
277+
assertThat(fieldDescriptions.get(11), is(descriptor("sub8.sub2", "Object", null, "true")));
279278
}
280279

281280
@Test
@@ -584,10 +583,26 @@ private static class ConstraintField {
584583
}
585584

586585
private static class RecursiveType {
587-
private String value;
586+
// explicitly prevented expansion
587+
@RestdocsNotExpanded
588+
private List<RecursiveType> sub1;
588589
@RestdocsNotExpanded
589-
private List<RecursiveType> children;
590+
private RecursiveType sub2;
590591
@RestdocsNotExpanded
591-
private RecursiveType sibling;
592+
private List<RecursiveType2> sub3;
593+
@RestdocsNotExpanded
594+
private RecursiveType2 sub4;
595+
596+
// implicitly prevented recursion
597+
private List<RecursiveType> sub5;
598+
private RecursiveType sub6;
599+
600+
private List<RecursiveType2> sub7;
601+
private RecursiveType2 sub8;
602+
}
603+
604+
private static class RecursiveType2 {
605+
private RecursiveType sub1;
606+
private RecursiveType2 sub2;
592607
}
593608
}

spring-auto-restdocs-example/src/main/java/capital/scalable/restdocs/example/items/ItemResponse.java

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@
2626
import java.math.BigDecimal;
2727
import java.util.List;
2828

29-
import capital.scalable.restdocs.jackson.RestdocsNotExpanded;
3029
import com.fasterxml.jackson.annotation.JsonIgnore;
3130
import com.fasterxml.jackson.annotation.JsonUnwrapped;
3231
import lombok.AllArgsConstructor;
@@ -64,7 +63,6 @@ class ItemResponse {
6463
*/
6564
@Valid
6665
@NotEmpty
67-
@RestdocsNotExpanded
6866
private List<ItemResponse> children;
6967

7068
/**

0 commit comments

Comments
 (0)