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

Commit 36c6aad

Browse files
authored
@see tag support (#188)
* Support for @see tag for methods and fields * regen docs * typo fix
1 parent bd2cce4 commit 36c6aad

File tree

9 files changed

+163
-35
lines changed

9 files changed

+163
-35
lines changed

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

Lines changed: 43 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import static capital.scalable.restdocs.constraints.ConstraintReader.OPTIONAL_ATTRIBUTE;
2222
import static capital.scalable.restdocs.util.FieldUtil.fromGetter;
2323
import static capital.scalable.restdocs.util.FieldUtil.isGetter;
24+
import static capital.scalable.restdocs.util.FormatUtil.addDot;
2425
import static capital.scalable.restdocs.util.TypeUtil.isPrimitive;
2526
import static org.apache.commons.lang3.StringUtils.isBlank;
2627
import static org.apache.commons.lang3.StringUtils.isNotBlank;
@@ -72,6 +73,7 @@ public void addField(InternalFieldInfo info, String jsonType) {
7273
Class<?> javaBaseClass = info.getJavaBaseClass();
7374
String javaFieldName = info.getJavaFieldName();
7475
String comment = resolveComment(javaBaseClass, javaFieldName);
76+
comment = attachTags(comment, resolveSeeTag(javaBaseClass, javaFieldName));
7577

7678
FieldDescriptor fieldDescriptor = fieldWithPath(jsonFieldPath)
7779
.type(jsonType)
@@ -99,19 +101,6 @@ private boolean isPresent(String jsonFieldPath, String javaFieldTypeName) {
99101
return false;
100102
}
101103

102-
private String resolveComment(Class<?> javaBaseClass, String javaFieldName) {
103-
String comment = javadocReader.resolveFieldComment(javaBaseClass, javaFieldName);
104-
if (isBlank(comment)) {
105-
// fallback if fieldName is getter method and comment is on the method itself
106-
comment = javadocReader.resolveMethodComment(javaBaseClass, javaFieldName);
107-
}
108-
if (isBlank(comment) && isGetter(javaFieldName)) {
109-
// fallback if fieldName is getter method but comment is on field itself
110-
comment = javadocReader.resolveFieldComment(javaBaseClass, fromGetter(javaFieldName));
111-
}
112-
return comment;
113-
}
114-
115104
private Attribute constraintAttribute(Class<?> javaBaseClass, String javaFieldName) {
116105
return new Attribute(CONSTRAINTS_ATTRIBUTE,
117106
resolveConstraintDescriptions(javaBaseClass, javaFieldName));
@@ -178,4 +167,45 @@ private String resolveDeprecatedMessage(Class<?> javaBaseClass, String javaField
178167
return null;
179168
}
180169
}
170+
171+
private String attachTags(String comment, String... tagComments) {
172+
for (String tagComment : tagComments) {
173+
if (isNotBlank(tagComment)) {
174+
comment += "<br>" + addDot(tagComment);
175+
}
176+
}
177+
return comment;
178+
}
179+
180+
private String resolveSeeTag(Class<?> javaBaseClass, String javaFieldName) {
181+
String comment = resolveTag(javaBaseClass, javaFieldName, "see");
182+
return isNotBlank(comment) ? "See " + comment : "";
183+
}
184+
185+
private String resolveComment(Class<?> javaBaseClass, String javaFieldName) {
186+
String comment = javadocReader.resolveFieldComment(javaBaseClass, javaFieldName);
187+
if (isBlank(comment)) {
188+
// fallback if fieldName is getter method and comment is on the method itself
189+
comment = javadocReader.resolveMethodComment(javaBaseClass, javaFieldName);
190+
}
191+
if (isBlank(comment) && isGetter(javaFieldName)) {
192+
// fallback if fieldName is getter method but comment is on field itself
193+
comment = javadocReader.resolveFieldComment(javaBaseClass, fromGetter(javaFieldName));
194+
}
195+
return comment;
196+
}
197+
198+
private String resolveTag(Class<?> javaBaseClass, String javaFieldName, String tagName) {
199+
String comment = javadocReader.resolveFieldTag(javaBaseClass, javaFieldName, tagName);
200+
if (isBlank(comment)) {
201+
// fallback if fieldName is getter method and comment is on the method itself
202+
comment = javadocReader.resolveMethodTag(javaBaseClass, javaFieldName, tagName);
203+
}
204+
if (isBlank(comment) && isGetter(javaFieldName)) {
205+
// fallback if fieldName is getter method but comment is on field itself
206+
comment = javadocReader.resolveFieldTag(javaBaseClass, fromGetter(javaFieldName),
207+
tagName);
208+
}
209+
return comment;
210+
}
181211
}

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/javadoc/JavadocUtil.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,9 @@ public static String convertFromJavadoc(String javadoc, TemplateFormatting templ
2727
.replace("<b>", templateFormatting.getBold())
2828
.replace("</b>", templateFormatting.getBold())
2929
.replace("<i>", templateFormatting.getItalics())
30-
.replace("</i>", templateFormatting.getItalics());
30+
.replace("</i>", templateFormatting.getItalics())
31+
.replaceAll("<a\\s+href\\s*=\\s*[\"\'](.*?)[\"\']\\s*>(.*?)</a>",
32+
templateFormatting.link());
3133

3234
if (converted.isEmpty()) {
3335
return "";

spring-auto-restdocs-core/src/main/java/capital/scalable/restdocs/misc/DescriptionSnippet.java

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,8 +50,9 @@ protected Map<String, Object> createModel(Operation operation) {
5050

5151
JavadocReader javadocReader = getJavadocReader(operation);
5252
String methodComment = resolveComment(handlerMethod, javadocReader);
53+
String tagComment = resolveSeeTag(handlerMethod, javadocReader);
5354
String deprecated = resolveDeprecated(handlerMethod, javadocReader);
54-
String description = convertFromJavadoc(deprecated + methodComment,
55+
String description = convertFromJavadoc(deprecated + methodComment + tagComment,
5556
determineTemplateFormatting(operation));
5657

5758
model.put("description", description);
@@ -76,6 +77,12 @@ private String resolveComment(HandlerMethod handlerMethod, JavadocReader javadoc
7677
return capitalize(addDot(methodComment));
7778
}
7879

80+
private String resolveSeeTag(HandlerMethod handlerMethod, JavadocReader javadocReader) {
81+
String comment = javadocReader.resolveMethodTag(handlerMethod.getBeanType(),
82+
handlerMethod.getMethod().getName(), "see");
83+
return isNotBlank(comment) ? "<p>See " + addDot(comment) : "";
84+
}
85+
7986
private Map<String, Object> defaultModel() {
8087
Map<String, Object> model = new HashMap<>();
8188
model.put("description", "");
Lines changed: 17 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,27 +1,31 @@
11
package capital.scalable.restdocs.util;
22

33
public class TemplateFormatting {
4-
private static final String LINE_BREAK_ASCIIDOC = " +\n";
5-
private static final String LINE_BREAK_MARKDOWN = "<br>";
6-
private static final String BOLD_ASCIIDOC = "**";
7-
private static final String BOLD_MARKDOWN = "**";
8-
private static final String ITALICS_ASCIIDOC = "__";
9-
private static final String ITALICS_MARKDOWN = "*";
4+
private static final String LINE_BREAK_ADOC = " +\n";
5+
private static final String LINE_BREAK_MD = "<br>";
6+
private static final String BOLD_ADOC = "**";
7+
private static final String BOLD_MD = "**";
8+
private static final String ITALICS_ADOC = "__";
9+
private static final String ITALICS_MD = "*";
10+
private static final String LINK_ADOC = "link:$1[$2]";
11+
private static final String LINK_MD = "[$2]($1)";
1012

1113
public static TemplateFormatting ASCIIDOC =
12-
new TemplateFormatting(LINE_BREAK_ASCIIDOC, BOLD_ASCIIDOC, ITALICS_ASCIIDOC);
14+
new TemplateFormatting(LINE_BREAK_ADOC, BOLD_ADOC, ITALICS_ADOC, LINK_ADOC);
1315

1416
public static TemplateFormatting MARKDOWN =
15-
new TemplateFormatting(LINE_BREAK_MARKDOWN, BOLD_MARKDOWN, ITALICS_MARKDOWN);
17+
new TemplateFormatting(LINE_BREAK_MD, BOLD_MD, ITALICS_MD, LINK_MD);
1618

1719
private final String lineBreak;
1820
private final String bold;
1921
private final String italics;
22+
private final String link;
2023

21-
private TemplateFormatting(String lineBreak, String bold, String italics) {
24+
private TemplateFormatting(String lineBreak, String bold, String italics, String link) {
2225
this.lineBreak = lineBreak;
2326
this.bold = bold;
2427
this.italics = italics;
28+
this.link = link;
2529
}
2630

2731
public String getLineBreak() {
@@ -35,4 +39,8 @@ public String getBold() {
3539
public String getItalics() {
3640
return italics;
3741
}
42+
43+
public String link() {
44+
return link;
45+
}
3846
}

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

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,32 @@ public void testGenerateDocumentationForJacksonSubTypes() throws Exception {
429429
is(descriptor("base1Sub2", "String", "A base 1 sub 2", "true")));
430430
}
431431

432+
@Test
433+
public void testGenerateDocumentationWithTags() throws Exception {
434+
// given
435+
ObjectMapper mapper = createMapper();
436+
mockFieldComment(BasicTypes.class, "string", "A string");
437+
mockFieldTag(BasicTypes.class, "string", "see", "this");
438+
mockFieldComment(BasicTypes.class, "bool", "A boolean");
439+
mockFieldTag(BasicTypes.class, "bool", "see", "<a href=\"xyz\">docs</a>");
440+
441+
FieldDocumentationGenerator generator =
442+
new FieldDocumentationGenerator(mapper.writer(), mapper.getDeserializationConfig(),
443+
javadocReader, constraintReader);
444+
Type type = BasicTypes.class;
445+
446+
// when
447+
List<ExtendedFieldDescriptor> result = cast(generator
448+
.generateDocumentation(type, mapper.getTypeFactory()));
449+
// then
450+
assertThat(result.size(), is(4));
451+
assertThat(result.get(0),
452+
is(descriptor("string", "String", "A string<br>See this.", "true")));
453+
assertThat(result.get(1),
454+
is(descriptor("bool", "Boolean", "A boolean<br>See <a href=\"xyz\">docs</a>.",
455+
"true")));
456+
}
457+
432458
private OngoingStubbing<List<String>> mockOptional(Class<?> javaBaseClass, String fieldName,
433459
String value) {
434460
return when(constraintReader.getOptionalMessages(javaBaseClass, fieldName))
@@ -445,6 +471,12 @@ private void mockFieldComment(Class<?> javaBaseClass, String fieldName, String v
445471
.thenReturn(value);
446472
}
447473

474+
private void mockFieldTag(Class<?> javaBaseClass, String fieldName, String tagName,
475+
String value) {
476+
when(javadocReader.resolveFieldTag(javaBaseClass, fieldName, tagName))
477+
.thenReturn(value);
478+
}
479+
448480
private ObjectMapper createMapper() {
449481
ObjectMapper mapper = new ObjectMapper();
450482
mapper.setVisibility(mapper.getSerializationConfig().getDefaultVisibilityChecker()

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/javadoc/JavadocUtilTest.java

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,4 +103,20 @@ public void convertStylingMarkdown() {
103103
assertThat(JavadocUtil.convertFromJavadoc(actual, TemplateFormatting.MARKDOWN),
104104
is(expected));
105105
}
106+
107+
@Test
108+
public void convertLinkAsciidoc() {
109+
String actual = "<a href=\"https://github.com\">GitHub</a>";
110+
String expected = "link:https://github.com[GitHub]";
111+
assertThat(JavadocUtil.convertFromJavadoc(actual, TemplateFormatting.ASCIIDOC),
112+
is(expected));
113+
}
114+
115+
@Test
116+
public void convertLinkMarkdown() {
117+
String actual = "<a href=\"https://github.com\">GitHub</a>";
118+
String expected = "[GitHub](https://github.com)";
119+
assertThat(JavadocUtil.convertFromJavadoc(actual, TemplateFormatting.MARKDOWN),
120+
is(expected));
121+
}
106122
}

spring-auto-restdocs-core/src/test/java/capital/scalable/restdocs/misc/DescriptionSnippetTest.java

Lines changed: 33 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
import org.springframework.web.method.HandlerMethod;
2929

3030
public class DescriptionSnippetTest extends AbstractSnippetTests {
31+
private JavadocReader javadocReader = mock(JavadocReader.class);
3132

3233
public DescriptionSnippetTest(String name, TemplateFormat templateFormat) {
3334
super(name, templateFormat);
@@ -36,11 +37,8 @@ public DescriptionSnippetTest(String name, TemplateFormat templateFormat) {
3637
@Test
3738
public void description() throws Exception {
3839
HandlerMethod handlerMethod = new HandlerMethod(new TestResource(), "testDescription");
39-
JavadocReader javadocReader = mock(JavadocReader.class);
40-
when(javadocReader.resolveMethodComment(TestResource.class, "testDescription"))
41-
.thenReturn("sample method comment");
42-
when(javadocReader.resolveMethodTag(TestResource.class, "testDescription", "deprecated"))
43-
.thenReturn("");
40+
mockMethodComment(TestResource.class, "testDescription", "sample method comment");
41+
mockMethodTag(TestResource.class, "testDescription", "deprecated", "");
4442

4543
this.snippets.expect(DESCRIPTION).withContents(equalTo("Sample method comment."));
4644

@@ -54,12 +52,8 @@ public void description() throws Exception {
5452
@Test
5553
public void descriptionWithDeprecated() throws Exception {
5654
HandlerMethod handlerMethod = new HandlerMethod(new TestResource(), "testDescription");
57-
JavadocReader javadocReader = mock(JavadocReader.class);
58-
when(javadocReader.resolveMethodComment(TestResource.class, "testDescription"))
59-
.thenReturn("sample method comment");
60-
when(javadocReader
61-
.resolveMethodTag(TestResource.class, "testDescription", "deprecated"))
62-
.thenReturn("use different one");
55+
mockMethodComment(TestResource.class, "testDescription", "sample method comment");
56+
mockMethodTag(TestResource.class, "testDescription", "deprecated", "use different one");
6357

6458
this.snippets.expect(DESCRIPTION).withContents(equalTo(
6559
"**Deprecated.** Use different one.\n\nSample method comment."));
@@ -71,6 +65,22 @@ public void descriptionWithDeprecated() throws Exception {
7165
.build());
7266
}
7367

68+
@Test
69+
public void descriptionWithSeeTag() throws Exception {
70+
HandlerMethod handlerMethod = new HandlerMethod(new TestResource(), "testDescription");
71+
mockMethodComment(TestResource.class, "testDescription", "sample method comment");
72+
mockMethodTag(TestResource.class, "testDescription", "see", "something");
73+
74+
this.snippets.expect(DESCRIPTION).withContents(equalTo(
75+
"Sample method comment.\n\nSee something."));
76+
77+
new DescriptionSnippet().document(operationBuilder
78+
.attribute(HandlerMethod.class.getName(), handlerMethod)
79+
.attribute(JavadocReader.class.getName(), javadocReader)
80+
.request("http://localhost/test")
81+
.build());
82+
}
83+
7484
@Test
7585
public void noHandlerMethod() throws Exception {
7686
this.snippets.expect(DESCRIPTION).withContents(equalTo(""));
@@ -80,6 +90,18 @@ public void noHandlerMethod() throws Exception {
8090
.build());
8191
}
8292

93+
private void mockMethodComment(Class<TestResource> javaBaseClass, String methodName,
94+
String comment) {
95+
when(javadocReader.resolveMethodComment(javaBaseClass, methodName))
96+
.thenReturn(comment);
97+
}
98+
99+
private void mockMethodTag(Class<TestResource> javaBaseClass, String methodName, String tagName,
100+
String comment) {
101+
when(javadocReader.resolveMethodTag(javaBaseClass, methodName, tagName))
102+
.thenReturn(comment);
103+
}
104+
83105
private static class TestResource {
84106

85107
public void testDescription() {

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

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,8 @@ public HttpEntity<ItemResponse> updateItem(@PathVariable("id") @Id String id,
148148
* An example of using path variables.
149149
*
150150
* @param id Item ID
151+
* @see <a href="https://scacap.github.io/spring-auto-restdocs/#snippets-path-parameters">path
152+
* parameters documentation</a>
151153
*/
152154
@DeleteMapping("{id}")
153155
public void deleteItem(@PathVariable("id") @Id String id) {
@@ -162,6 +164,8 @@ public void deleteItem(@PathVariable("id") @Id String id) {
162164
* @param id Item ID.
163165
* @param childId Child ID.
164166
* @return response
167+
* @see <a href="https://scacap.github.io/spring-auto-restdocs/#constraints">constraints
168+
* documentation</a>
165169
*/
166170
@GetMapping("{id}/{child}")
167171
public ItemResponse getChild(@PathVariable @Id String id,
@@ -183,6 +187,7 @@ public ItemResponse getChild(@PathVariable @Id String id,
183187
* @param descMatch Lookup on description field.
184188
* @param hint Lookup hint.
185189
* @return response
190+
* @see <a href="https://scacap.github.io/spring-auto-restdocs/#paging">paging documentation</a>
186191
*/
187192
@GetMapping("search")
188193
public Page<ItemResponse> searchItem(

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,12 @@ class ItemResponse {
5454

5555
/**
5656
* Metadata.
57+
* <p>
58+
* An example of JsonSubType support.
59+
*
60+
* @see
61+
* <a href="https://github.com/FasterXML/jackson-annotations/wiki/Jackson-Annotations#type-handling">
62+
* Jackson type documentation</a>
5763
*/
5864
private Metadata meta;
5965

0 commit comments

Comments
 (0)