Skip to content

Commit

Permalink
Avoid generating uncompilable response body in Spring's API template (#…
Browse files Browse the repository at this point in the history
…2903)

* Moved example string to a dedicated variable in Spring's methodBody template

* Created a new exampleString template for JavaSpring

* Added a new mustache lambda to trim whitespace in fragments

* Added a new lambda to split long fragments into compilable strings

* Use newly introduced lambdas in Spring's API template to avoid generating uncompilable example code
  • Loading branch information
smasset authored and wing328 committed Nov 12, 2019
1 parent 21a291f commit 99204ec
Show file tree
Hide file tree
Showing 70 changed files with 715 additions and 195 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
import org.openapitools.codegen.languages.features.BeanValidationFeatures;
import org.openapitools.codegen.languages.features.OptionalFeatures;
import org.openapitools.codegen.languages.features.PerformBeanValidationFeatures;
import org.openapitools.codegen.templating.mustache.SplitStringLambda;
import org.openapitools.codegen.templating.mustache.TrimWhitespaceLambda;
import org.openapitools.codegen.utils.URLPathUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
Expand Down Expand Up @@ -463,6 +465,10 @@ public void processOpts() {
(Mustache.Lambda) (fragment, writer) -> writer.write(fragment.execute().replaceAll("\"", Matcher.quoteReplacement("\\\""))));
additionalProperties.put("lambdaRemoveLineBreak",
(Mustache.Lambda) (fragment, writer) -> writer.write(fragment.execute().replaceAll("\\r|\\n", "")));

additionalProperties.put("lambdaTrimWhitespace", new TrimWhitespaceLambda());

additionalProperties.put("lambdaSplitString", new SplitStringLambda());
}

@Override
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* 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.openapitools.codegen.templating.mustache;

import java.io.IOException;
import java.io.Writer;
import java.util.Locale;

import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template.Fragment;

/**
* Splits long fragments into smaller strings and uses a StringBuilder to merge
* them back.
*
* Register:
*
* <pre>
* additionalProperties.put("lambdaSplitString", new SplitStringLambda());
* </pre>
*
* Use:
*
* <pre>
* {{#lambdaSplitString}}{{summary}}{{/lambdaSplitString}}
* </pre>
*/
public class SplitStringLambda implements Mustache.Lambda {
private static final int DEFAULT_MAX_LENGTH = 65535;

private static final String SPLIT_INIT = "new StringBuilder(%d)";

private static final String SPLIT_PART = ".append(\"%s\")";

private static final String SPLIT_SUFFIX = ".toString()";

private final int maxLength;

public SplitStringLambda() {
this(DEFAULT_MAX_LENGTH);
}

public SplitStringLambda(int maxLength) {
this.maxLength = maxLength;
}

@Override
public void execute(Fragment fragment, Writer writer) throws IOException {
String input = fragment.execute();
int inputLength = input.length();

StringBuilder builder = new StringBuilder();
if (inputLength > maxLength) {

// Initialize a StringBuilder
builder.append(String.format(Locale.ROOT, SPLIT_INIT, inputLength));

int currentPosition = 0;
int currentStringLength = 0;
char currentLastChar = '\\';

// Split input into parts of at most maxLength and not ending with an escape character
// Append each part to the StringBuilder
while (currentPosition + maxLength < input.length()) {
currentStringLength = maxLength;
currentLastChar = input.charAt(currentPosition + currentStringLength - 1);
if (currentLastChar == '\\') {
--currentStringLength;
}

builder.append(String.format(Locale.ROOT, SPLIT_PART, input.substring(currentPosition, currentPosition + currentStringLength)));
currentPosition += currentStringLength;
}

// Append last part if necessary
if (currentPosition < input.length()) {
builder.append(String.format(Locale.ROOT, SPLIT_PART, input.substring(currentPosition)));
}

// Close the builder and merge everything back to a string
builder.append(SPLIT_SUFFIX);
} else {
builder.append(String.format(Locale.ROOT, "\"%s\"", input));
}

writer.write(builder.toString());
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* 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.openapitools.codegen.templating.mustache;

import java.io.IOException;
import java.io.Writer;

import com.samskivert.mustache.Mustache;
import com.samskivert.mustache.Template.Fragment;

/**
* Replaces duplicate whitespace characters in a fragment with single space.
*
* Register:
* <pre>
* additionalProperties.put("lambdaTrimWhitespace", new TrimWhitespaceLambda());
* </pre>
*
* Use:
* <pre>
* {{#lambdaTrimWhitespace}}{{name}}{{/lambdaTrimWhitespace}}
* </pre>
*/
public class TrimWhitespaceLambda implements Mustache.Lambda {
private static final String SINGLE_SPACE = " ";

private static final String WHITESPACE_REGEX = "\\s+";

@Override
public void execute(Fragment fragment, Writer writer) throws IOException {
writer.write(fragment.execute().replaceAll(WHITESPACE_REGEX, SINGLE_SPACE));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{{#lambdaSplitString}}{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{#lambdaTrimWhitespace}}{{{example}}}{{/lambdaTrimWhitespace}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}{{/lambdaSplitString}}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@ return CompletableFuture.supplyAsync(()-> {
{{#async}} {{/async}} {{/jdk8}}for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
{{/-first}}
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}");
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} String exampleString = {{>exampleString}};
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} ApiUtil.setExampleResponse(request, "{{{contentType}}}", exampleString);
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} break;
{{#async}} {{/async}}{{^async}}{{#jdk8}} {{/jdk8}}{{/async}} }
{{#-last}}
Expand Down Expand Up @@ -36,7 +37,8 @@ Mono<Void> result = Mono.empty();
for (MediaType mediaType : exchange.getRequest().getHeaders().getAccept()) {
{{/-first}}
if (mediaType.isCompatibleWith(MediaType.valueOf("{{{contentType}}}"))) {
result = ApiUtil.getExampleResponse(exchange, "{{#lambdaRemoveLineBreak}}{{#lambdaEscapeDoubleQuote}}{{{example}}}{{/lambdaEscapeDoubleQuote}}{{/lambdaRemoveLineBreak}}");
String exampleString = {{>exampleString}};
result = ApiUtil.getExampleResponse(exchange, exampleString);
break;
}
{{#-last}}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* 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.openapitools.codegen.templating.mustache;

import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;

import java.io.IOException;
import java.io.StringWriter;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;

import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.samskivert.mustache.Template.Fragment;

public class SplitStringLambdaTest {
private static final String INPUT_STRING = "1112223334";

private static final Map<Integer, String> EXPECTED_OUTPUTS;
static {
EXPECTED_OUTPUTS = new HashMap<>();
EXPECTED_OUTPUTS.put(2,
String.format(
Locale.ROOT,
"new StringBuilder(%d).append(\"11\").append(\"12\").append(\"22\").append(\"33\").append(\"34\").toString()",
INPUT_STRING.length()));
EXPECTED_OUTPUTS.put(3,
String.format(
Locale.ROOT,
"new StringBuilder(%d).append(\"111\").append(\"222\").append(\"333\").append(\"4\").toString()",
INPUT_STRING.length()));
}

private static final String INPUT_QUOTED_STRING = "1\\\"11\\\"2223\\\"334";
private static final String INPUT_QUOTED_OUTPUT = String.format(
Locale.ROOT,
"new StringBuilder(%d).append(\"1\\\"\").append(\"11\").append(\"\\\"2\").append(\"223\").append(\"\\\"3\").append(\"34\").toString()",
INPUT_QUOTED_STRING.length());

@Mock
private Fragment fragment;

@BeforeMethod
public void init() {
MockitoAnnotations.initMocks(this);
}

@AfterMethod
public void reset() {
Mockito.reset(fragment);
}

private void testString(String input, int maxLength, String expected) throws IOException {
when(fragment.execute()).thenReturn(input);

StringWriter output = new StringWriter();
new SplitStringLambda(maxLength).execute(fragment, output);
assertEquals(output.toString(), expected);
}

@Test
public void testSplitGroupsOf2() throws IOException {
int maxLength = 2;
testString(INPUT_STRING, maxLength, EXPECTED_OUTPUTS.get(maxLength));
}

@Test
public void testSplitGroupsOf3() throws IOException {
int maxLength = 3;
testString(INPUT_STRING, maxLength, EXPECTED_OUTPUTS.get(maxLength));
}

@Test
public void testSplitQuotedString() throws IOException {
int maxLength = 3;
testString(INPUT_QUOTED_STRING, maxLength, INPUT_QUOTED_OUTPUT);
}

@Test
public void testShortString() throws IOException {
testString(INPUT_STRING, INPUT_STRING.length(), String.format(Locale.ROOT, "\"%s\"", INPUT_STRING));
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
/*
* Copyright 2018 OpenAPI-Generator Contributors (https://openapi-generator.tech)
* Copyright 2018 SmartBear Software
*
* 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.openapitools.codegen.templating.mustache;

import static org.mockito.Mockito.when;
import static org.testng.Assert.assertEquals;

import java.io.IOException;
import java.io.StringWriter;

import org.mockito.Mock;
import org.mockito.Mockito;
import org.mockito.MockitoAnnotations;
import org.testng.annotations.AfterMethod;
import org.testng.annotations.BeforeMethod;
import org.testng.annotations.Test;

import com.samskivert.mustache.Template.Fragment;

public class TrimWhitespaceLambdaTest {

@Mock
private Fragment fragment;

@BeforeMethod
public void init() {
MockitoAnnotations.initMocks(this);
}

@AfterMethod
public void reset() {
Mockito.reset(fragment);
}

@Test
public void testTrimWhitespace() throws IOException {
when(fragment.execute()).thenReturn("\t a b\t\tc \t");

StringWriter output = new StringWriter();
new TrimWhitespaceLambda().execute(fragment, output);
assertEquals(output.toString(), " a b c ");
}

}
Loading

0 comments on commit 99204ec

Please sign in to comment.