diff --git a/README.md b/README.md index d466e37ac..d3eced9a5 100644 --- a/README.md +++ b/README.md @@ -16,6 +16,11 @@ It currently consists of # Release Notes BOAT is still under development and subject to change. +## 0.18.1 +* Spring generator: fixed `@ExampleObject` rendering in `api.mustache` by unwrapping escaped quotes in example payloads. +* Added `unwrapEscapedQuotes` lambda to `boat-spring` generator templates to prevent malformed annotation values (for example `value = "\"{...}"`). +* Added a regression test to verify generated Spring API interfaces include valid `@ExampleObject` annotation values and remain parseable Java code. + ## 0.18.0 * openapi-generator `7.20.0` baseline (Spring Boot 4, Jackson 3) * moved Java and JavaSpring templates to `7.20.0` adding remaing custom features diff --git a/boat-engine/pom.xml b/boat-engine/pom.xml index 776bed88a..89746680a 100644 --- a/boat-engine/pom.xml +++ b/boat-engine/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-engine jar diff --git a/boat-maven-plugin/pom.xml b/boat-maven-plugin/pom.xml index 3c8e8aae0..00069a4aa 100644 --- a/boat-maven-plugin/pom.xml +++ b/boat-maven-plugin/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-maven-plugin diff --git a/boat-quay/boat-quay-lint/pom.xml b/boat-quay/boat-quay-lint/pom.xml index 14df40cd7..db9b4ebf6 100644 --- a/boat-quay/boat-quay-lint/pom.xml +++ b/boat-quay/boat-quay-lint/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss boat-quay - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-quay-lint diff --git a/boat-quay/boat-quay-rules/pom.xml b/boat-quay/boat-quay-rules/pom.xml index 0f48c13c5..609a524fd 100644 --- a/boat-quay/boat-quay-rules/pom.xml +++ b/boat-quay/boat-quay-rules/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss boat-quay - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-quay-rules diff --git a/boat-quay/pom.xml b/boat-quay/pom.xml index 7a2d7f695..277df7c25 100644 --- a/boat-quay/pom.xml +++ b/boat-quay/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT diff --git a/boat-scaffold/pom.xml b/boat-scaffold/pom.xml index 479e70c16..6186671b7 100644 --- a/boat-scaffold/pom.xml +++ b/boat-scaffold/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-scaffold @@ -102,7 +102,7 @@ com.backbase.oss boat-trail-resources - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT test diff --git a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java index 633651516..bd03dc4b1 100644 --- a/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java +++ b/boat-scaffold/src/main/java/com/backbase/oss/codegen/java/BoatSpringCodeGen.java @@ -48,6 +48,7 @@ public class BoatSpringCodeGen extends SpringCodegen { public static final String ADD_SERVLET_REQUEST = "addServletRequest"; public static final String ADD_BINDING_RESULT = "addBindingResult"; + public static final String UNWRAP_ESCAPED_QUOTES = "unwrapEscapedQuotes"; private static final String VENDOR_EXTENSION_NOT_NULL = "x-not-null"; @@ -147,6 +148,23 @@ protected String postProcessLine(String line) { } } + static class UnwrapEscapedQuotes implements Mustache.Lambda { + + @Override + public void execute(Fragment frag, Writer out) throws IOException { + String text = frag.execute(); + if (text == null) { + return; + } + String normalized = text.replace("\\\\\"", "\\\""); + if (normalized.length() >= 4 && normalized.startsWith("\\\"") && normalized.endsWith("\\\"")) { + out.write(normalized.substring(2, normalized.length() - 2)); + return; + } + out.write(normalized); + } + } + /** * Adds a HttpServletRequest object to the API definition method. */ @@ -332,6 +350,7 @@ public void processOpts() { this.additionalProperties.put("newLine8", new NewLineIndent(8, " ")); this.additionalProperties.put("toOneLine", new FormatToOneLine()); this.additionalProperties.put("trimAndIndent4", new TrimAndIndent(4, " ")); + this.additionalProperties.put(UNWRAP_ESCAPED_QUOTES, new UnwrapEscapedQuotes()); } @Override diff --git a/boat-scaffold/src/main/templates/boat-spring/api.mustache b/boat-scaffold/src/main/templates/boat-spring/api.mustache index 9f3ac9721..41500ef63 100644 --- a/boat-scaffold/src/main/templates/boat-spring/api.mustache +++ b/boat-scaffold/src/main/templates/boat-spring/api.mustache @@ -187,7 +187,7 @@ public interface {{classname}} { {{#examples}} @ExampleObject( name = "{{{exampleName}}}", - value = "{{{exampleValue}}}" + value = "{{#unwrapEscapedQuotes}}{{{exampleValue}}}{{/unwrapEscapedQuotes}}" ){{^-last}},{{/-last}} {{/examples}} {{#-last}} diff --git a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java index c0c52fb20..1d24004c4 100644 --- a/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java +++ b/boat-scaffold/src/test/java/com/backbase/oss/codegen/java/BoatSpringCodeGenTests.java @@ -6,6 +6,7 @@ import static org.hamcrest.Matchers.is; import static org.hamcrest.Matchers.isA; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; @@ -48,6 +49,7 @@ import java.util.Set; import java.util.UUID; import java.util.stream.Collectors; +import java.util.stream.Stream; import org.apache.commons.io.FileUtils; import org.apache.commons.lang.UnhandledException; import org.hamcrest.Matchers; @@ -55,7 +57,9 @@ import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; import org.junit.jupiter.params.provider.CsvSource; +import org.junit.jupiter.params.provider.MethodSource; import org.openapitools.codegen.CliOption; import org.openapitools.codegen.ClientOptInput; import org.openapitools.codegen.CodegenOperation; @@ -97,6 +101,20 @@ void newLineIndent() throws IOException { assertThat(output.toString(), equalTo(String.format("__%n__Good%n__ morning,%n__ Dave%n"))); } + @ParameterizedTest + @MethodSource("unwrapEscapedQuotesCases") + void unwrapEscapedQuotes_execute_shouldHandleAllScenarios(String input, String expectedOutput) throws IOException { + final BoatSpringCodeGen.UnwrapEscapedQuotes lambda = new BoatSpringCodeGen.UnwrapEscapedQuotes(); + final StringWriter output = new StringWriter(); + final Fragment frag = mock(Fragment.class); + + when(frag.execute()).thenReturn(input); + + lambda.execute(frag, output); + + assertThat(output.toString(), equalTo(expectedOutput)); + } + @Test void addServletRequestTestFromOperation(){ final BoatSpringCodeGen gen = new BoatSpringCodeGen(); @@ -139,6 +157,45 @@ void multipartWithFileAndObject() throws IOException { assertThat(filesParam.getTypeAsString(), equalTo("List")); } + @Test + void shouldGenerateValidExampleObjectAnnotation() throws IOException { + var codegen = new BoatSpringCodeGen(); + var input = new File("src/test/resources/openapi-with-examples/openapi-with-multiple-permissions.yaml"); + codegen.setLibrary("spring-boot"); + codegen.setInterfaceOnly(true); + codegen.setSkipDefaultInterface(true); + codegen.setOutputDir(TEST_OUTPUT + "/example-object"); + codegen.setInputSpec(input.getAbsolutePath()); + codegen.additionalProperties().put(SpringCodegen.USE_SPRING_BOOT3, Boolean.TRUE.toString()); + + var openApiInput = new OpenAPIParser().readLocation(input.getAbsolutePath(), null, new ParseOptions()) + .getOpenAPI(); + var clientOptInput = new ClientOptInput(); + clientOptInput.config(codegen); + clientOptInput.openAPI(openApiInput); + + List files = new DefaultGenerator().opts(clientOptInput).generate(); + + File apiFile = files.stream() + .filter(file -> file.getName().endsWith("Api.java")) + .filter(file -> { + try { + return Files.readString(file.toPath()).contains("@ExampleObject("); + } catch (IOException e) { + throw new UnhandledException(e); + } + }) + .findFirst() + .orElseThrow(); + + String apiContent = Files.readString(apiFile.toPath()); + assertTrue(apiContent.contains("@ExampleObject(")); + assertTrue(apiContent.contains("Value Exceeded. Must be between {min} and {max}.")); + assertTrue(apiContent.contains("Bad Request")); + assertFalse(apiContent.contains("value = \"\\\"{")); + StaticJavaParser.parse(apiFile); + } + @Test void testReplaceBeanValidationCollectionType() { var codegen = new BoatSpringCodeGen(); @@ -552,4 +609,12 @@ private static void assertMethodCollectionReturnType(MethodDeclaration method, S .getTypeArguments().get().getFirst().get(); assertEquals(itemType, collectionItemType.getName().toString()); } + + static Stream unwrapEscapedQuotesCases() { + return Stream.of( + Arguments.of((String) null, ""), + Arguments.of("\\\"{\\\"message\\\":\\\"Bad Request\\\"}\\\"", "{\\\"message\\\":\\\"Bad Request\\\"}"), + Arguments.of("prefix\\\\\"quoted\\\\\"suffix", "prefix\\\"quoted\\\"suffix"), + Arguments.of("\\\"", "\\\"")); + } } diff --git a/boat-trail-resources/pom.xml b/boat-trail-resources/pom.xml index b3e0c7f8f..082a6947e 100644 --- a/boat-trail-resources/pom.xml +++ b/boat-trail-resources/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT boat-trail-resources diff --git a/pom.xml b/pom.xml index bce873435..b140018c7 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT pom Backbase Open Api Tools is a collection of tools to work with Open API diff --git a/tests/pom.xml b/tests/pom.xml index 90eee5d9c..116bc34b9 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.0-SNAPSHOT + 0.18.1-SNAPSHOT tests