Avoid problems caused by | characters in table cell content
Previously, a | character in a cell of an Asciidoctor table would break that table's formatting as it would be interpretted as a delimiter for the cell. The problem also affects Markdown tables where the | character is not within backticks. This commit addresses the problem with Asciidoctor tables by introducing a JMustache Lambda that escapes | characters by prefixing them with a backslash. Unfortunately, a complete solution to the problem when using Markdown is not as straightforward. Markdown only requires a | character to be espaced when it is not within backticks. Determing this isn't straightforward as the text would have to be parsed, taking into account the possibility of backticks themselves being escaped. Instead, this commit attempts to avoid the problem in a different way. The most likely source of a | character is when documenting a `@Pattern` constraint that uses a | character in its regular expression. To improve the readability of the regular expression this commit wraps it in backticks, thereby formatting it in a monospaced font in both Asciidoctor and Markdown and also avoiding any escaping problems with the latter. Closes gh-232 See gh-230
This commit is contained in:
@@ -36,7 +36,9 @@ import org.springframework.restdocs.snippet.StandardWriterResolver;
|
||||
import org.springframework.restdocs.snippet.WriterResolver;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.instanceOf;
|
||||
@@ -164,6 +166,34 @@ public class RestDocumentationConfigurerTests {
|
||||
is(equalTo(TemplateFormats.markdown())));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void asciidoctorTableCellContentLambaIsInstalledWhenUsingAsciidoctorTemplateFormat() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
this.configurer.apply(configuration, createContext());
|
||||
TemplateEngine templateEngine = (TemplateEngine) configuration
|
||||
.get(TemplateEngine.class.getName());
|
||||
MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine;
|
||||
Map<String, Object> templateContext = (Map<String, Object>) ReflectionTestUtils
|
||||
.getField(mustacheTemplateEngine, "context");
|
||||
assertThat(templateContext, hasEntry(equalTo("tableCellContent"),
|
||||
instanceOf(AsciidoctorTableCellContentLambda.class)));
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
@Test
|
||||
public void asciidoctorTableCellContentLambaIsNotInstalledWhenUsingNonAsciidoctorTemplateFormat() {
|
||||
Map<String, Object> configuration = new HashMap<>();
|
||||
this.configurer.snippetConfigurer.withTemplateFormat(TemplateFormats.markdown());
|
||||
this.configurer.apply(configuration, createContext());
|
||||
TemplateEngine templateEngine = (TemplateEngine) configuration
|
||||
.get(TemplateEngine.class.getName());
|
||||
MustacheTemplateEngine mustacheTemplateEngine = (MustacheTemplateEngine) templateEngine;
|
||||
Map<String, Object> templateContext = (Map<String, Object>) ReflectionTestUtils
|
||||
.getField(mustacheTemplateEngine, "context");
|
||||
assertThat(templateContext.size(), equalTo(0));
|
||||
}
|
||||
|
||||
private RestDocumentationContext createContext() {
|
||||
ManualRestDocumentation manualRestDocumentation = new ManualRestDocumentation(
|
||||
"build");
|
||||
|
||||
@@ -118,7 +118,7 @@ public class ConstraintDescriptionsTests {
|
||||
@Test
|
||||
public void pattern() {
|
||||
assertThat(this.constraintDescriptions.descriptionsForProperty("pattern"),
|
||||
contains("Must match the regular expression '[A-Z][a-z]+'"));
|
||||
contains("Must match the regular expression `[A-Z][a-z]+`"));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -137,7 +137,7 @@ public class ResourceBundleConstraintDescriptionResolverTests {
|
||||
@Test
|
||||
public void defaultMessagePattern() {
|
||||
assertThat(constraintDescriptionForField("pattern"),
|
||||
is(equalTo("Must match the regular expression '[A-Z][a-z]+'")));
|
||||
is(equalTo("Must match the regular expression `[A-Z][a-z]+`")));
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Test;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
import org.springframework.restdocs.test.OperationBuilder;
|
||||
@@ -165,4 +166,23 @@ public class RequestHeadersSnippetTests extends AbstractSnippetTests {
|
||||
.header("Connection", "keep-alive").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tableCellContentIsEscapedWhenNecessary() throws IOException {
|
||||
this.snippet.expectRequestHeaders("request-with-escaped-headers").withContents(
|
||||
tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"),
|
||||
escapeIfNecessary("one|two")));
|
||||
new RequestHeadersSnippet(
|
||||
Arrays.asList(headerWithName("Foo|Bar").description("one|two")))
|
||||
.document(operationBuilder("request-with-escaped-headers")
|
||||
.request("http://localhost").header("Foo|Bar", "baz")
|
||||
.build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Test;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
import org.springframework.restdocs.test.OperationBuilder;
|
||||
@@ -155,4 +156,22 @@ public class ResponseHeadersSnippetTests extends AbstractSnippetTests {
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tableCellContentIsEscapedWhenNecessary() throws IOException {
|
||||
this.snippet.expectResponseHeaders("response-with-escaped-headers").withContents(
|
||||
tableWithHeader("Name", "Description").row(escapeIfNecessary("`Foo|Bar`"),
|
||||
escapeIfNecessary("one|two")));
|
||||
new ResponseHeadersSnippet(
|
||||
Arrays.asList(headerWithName("Foo|Bar").description("one|two")))
|
||||
.document(operationBuilder("response-with-escaped-headers")
|
||||
.response().header("Foo|Bar", "baz").build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Test;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
|
||||
@@ -171,4 +172,22 @@ public class LinksSnippetTests extends AbstractSnippetTests {
|
||||
.and(new LinkDescriptor("b").description("two"))
|
||||
.document(operationBuilder("additional-descriptors").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void tableCellContentIsEscapedWhenNecessary() throws IOException {
|
||||
this.snippet.expectLinks("links-with-escaped-content")
|
||||
.withContents(tableWithHeader("Relation", "Description").row(
|
||||
escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two")));
|
||||
new LinksSnippet(new StubLinkExtractor().withLinks(new Link("Foo|Bar", "foo")),
|
||||
Arrays.asList(new LinkDescriptor("Foo|Bar").description("one|two")))
|
||||
.document(operationBuilder("links-with-escaped-content").build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
|
||||
@@ -227,4 +228,25 @@ public class RequestFieldsSnippetTests extends AbstractSnippetTests {
|
||||
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestWithFieldsWithEscapedContent() throws IOException {
|
||||
this.snippet.expectRequestFields("request-fields-with-escaped-content")
|
||||
.withContents(tableWithHeader("Path", "Type", "Description").row(
|
||||
escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"),
|
||||
escapeIfNecessary("three|four")));
|
||||
|
||||
new RequestFieldsSnippet(Arrays.asList(
|
||||
fieldWithPath("Foo|Bar").type("one|two").description("three|four")))
|
||||
.document(operationBuilder("request-fields-with-escaped-content")
|
||||
.request("http://localhost").content("{\"Foo|Bar\": 5}")
|
||||
.build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
|
||||
@@ -283,4 +284,24 @@ public class ResponseFieldsSnippetTests extends AbstractSnippetTests {
|
||||
.content("{\"a\": {\"b\": 5, \"c\": \"charlie\"}}").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void responseWithFieldsWithEscapedContent() throws IOException {
|
||||
this.snippet.expectResponseFields("response-fields-with-escaped-content")
|
||||
.withContents(tableWithHeader("Path", "Type", "Description").row(
|
||||
escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("`one|two`"),
|
||||
escapeIfNecessary("three|four")));
|
||||
|
||||
new ResponseFieldsSnippet(Arrays.asList(
|
||||
fieldWithPath("Foo|Bar").type("one|two").description("three|four")))
|
||||
.document(operationBuilder("response-fields-with-escaped-content")
|
||||
.response().content("{\"Foo|Bar\": 5}").build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -192,9 +192,37 @@ public class PathParametersSnippetTests extends AbstractSnippetTests {
|
||||
.build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void pathParametersWithEscapedContent() throws IOException {
|
||||
this.snippet.expectPathParameters("path-parameters-with-escaped-content")
|
||||
.withContents(tableWithTitleAndHeader(getTitle("{Foo|Bar}"), "Parameter",
|
||||
"Description").row(escapeIfNecessary("`Foo|Bar`"),
|
||||
escapeIfNecessary("one|two")));
|
||||
|
||||
RequestDocumentation
|
||||
.pathParameters(parameterWithName("Foo|Bar").description("one|two"))
|
||||
.document(operationBuilder("path-parameters-with-escaped-content")
|
||||
.attribute(RestDocumentationGenerator.ATTRIBUTE_NAME_URL_TEMPLATE,
|
||||
"{Foo|Bar}")
|
||||
.build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
private String getTitle() {
|
||||
return this.templateFormat == TemplateFormats.asciidoctor() ? "/{a}/{b}"
|
||||
: "`/{a}/{b}`";
|
||||
return getTitle("/{a}/{b}");
|
||||
}
|
||||
|
||||
private String getTitle(String title) {
|
||||
if (this.templateFormat.equals(TemplateFormats.asciidoctor())) {
|
||||
return title;
|
||||
}
|
||||
return "`" + title + "`";
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Test;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
|
||||
@@ -194,4 +195,23 @@ public class RequestParametersSnippetTests extends AbstractSnippetTests {
|
||||
.param("b", "bravo").build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestParametersWithEscapedContent() throws IOException {
|
||||
this.snippet.expectRequestParameters("request-parameters-with-escaped-content")
|
||||
.withContents(tableWithHeader("Parameter", "Description").row(
|
||||
escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two")));
|
||||
|
||||
RequestDocumentation
|
||||
.requestParameters(parameterWithName("Foo|Bar").description("one|two"))
|
||||
.document(operationBuilder("request-parameters-with-escaped-content")
|
||||
.request("http://localhost").param("Foo|Bar", "baz").build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -24,6 +24,7 @@ import org.junit.Test;
|
||||
import org.springframework.restdocs.AbstractSnippetTests;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.TemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
|
||||
@@ -180,4 +181,23 @@ public class RequestPartsSnippetTests extends AbstractSnippetTests {
|
||||
.part("b", "bravo".getBytes()).build());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void requestPartsWithEscapedContent() throws IOException {
|
||||
this.snippet.expectRequestParts("request-parts-with-escaped-content")
|
||||
.withContents(tableWithHeader("Part", "Description").row(
|
||||
escapeIfNecessary("`Foo|Bar`"), escapeIfNecessary("one|two")));
|
||||
|
||||
RequestDocumentation.requestParts(partWithName("Foo|Bar").description("one|two"))
|
||||
.document(operationBuilder("request-parts-with-escaped-content")
|
||||
.request("http://localhost").part("Foo|Bar", "baz".getBytes())
|
||||
.build());
|
||||
}
|
||||
|
||||
private String escapeIfNecessary(String input) {
|
||||
if (this.templateFormat.equals(TemplateFormats.markdown())) {
|
||||
return input;
|
||||
}
|
||||
return input.replace("|", "\\|");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,57 @@
|
||||
/*
|
||||
* Copyright 2014-2016 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.restdocs.templates.mustache;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.io.StringWriter;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.restdocs.mustache.Template.Fragment;
|
||||
|
||||
import static org.hamcrest.CoreMatchers.equalTo;
|
||||
import static org.hamcrest.CoreMatchers.is;
|
||||
import static org.junit.Assert.assertThat;
|
||||
import static org.mockito.BDDMockito.given;
|
||||
import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* Tests for {@link AsciidoctorTableCellContentLambda}.
|
||||
*
|
||||
* @author Andy Wilkinson
|
||||
*/
|
||||
public class AsciidoctorTableCellContentLambdaTests {
|
||||
|
||||
@Test
|
||||
public void verticalBarCharactersAreEscaped() throws IOException {
|
||||
Fragment fragment = mock(Fragment.class);
|
||||
given(fragment.execute()).willReturn("|foo|bar|baz|");
|
||||
StringWriter writer = new StringWriter();
|
||||
new AsciidoctorTableCellContentLambda().execute(fragment, writer);
|
||||
assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|")));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void escapedVerticalBarCharactersAreNotEscapedAgain() throws IOException {
|
||||
Fragment fragment = mock(Fragment.class);
|
||||
given(fragment.execute()).willReturn("\\|foo|bar\\|baz|");
|
||||
StringWriter writer = new StringWriter();
|
||||
new AsciidoctorTableCellContentLambda().execute(fragment, writer);
|
||||
assertThat(writer.toString(), is(equalTo("\\|foo\\|bar\\|baz\\|")));
|
||||
}
|
||||
|
||||
}
|
||||
@@ -29,6 +29,7 @@ import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.restdocs.ManualRestDocumentation;
|
||||
import org.springframework.restdocs.RestDocumentationContext;
|
||||
import org.springframework.restdocs.mustache.Mustache;
|
||||
import org.springframework.restdocs.operation.Operation;
|
||||
import org.springframework.restdocs.operation.OperationRequest;
|
||||
import org.springframework.restdocs.operation.OperationRequestFactory;
|
||||
@@ -45,6 +46,7 @@ import org.springframework.restdocs.templates.StandardTemplateResourceResolver;
|
||||
import org.springframework.restdocs.templates.TemplateEngine;
|
||||
import org.springframework.restdocs.templates.TemplateFormat;
|
||||
import org.springframework.restdocs.templates.TemplateFormats;
|
||||
import org.springframework.restdocs.templates.mustache.AsciidoctorTableCellContentLambda;
|
||||
import org.springframework.restdocs.templates.mustache.MustacheTemplateEngine;
|
||||
|
||||
/**
|
||||
@@ -93,9 +95,13 @@ public class OperationBuilder {
|
||||
|
||||
public Operation build() {
|
||||
if (this.attributes.get(TemplateEngine.class.getName()) == null) {
|
||||
Map<String, Object> templateContext = new HashMap<>();
|
||||
templateContext.put("tableCellContent",
|
||||
new AsciidoctorTableCellContentLambda());
|
||||
this.attributes.put(TemplateEngine.class.getName(),
|
||||
new MustacheTemplateEngine(
|
||||
new StandardTemplateResourceResolver(this.templateFormat)));
|
||||
new StandardTemplateResourceResolver(this.templateFormat),
|
||||
Mustache.compiler().escapeHTML(false), templateContext));
|
||||
}
|
||||
RestDocumentationContext context = createContext();
|
||||
this.attributes.put(RestDocumentationContext.class.getName(), context);
|
||||
|
||||
Reference in New Issue
Block a user