diff --git a/README.md b/README.md index 8088051bd..0a22c5eb6 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.2 +* 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.1 * Use Jackson3 imports in `BigDecimalCustomSerializer.class` when using `useJackson3` set to `true` diff --git a/boat-engine/pom.xml b/boat-engine/pom.xml index 89746680a..caf355b9e 100644 --- a/boat-engine/pom.xml +++ b/boat-engine/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.1-SNAPSHOT + 0.18.2-SNAPSHOT boat-engine jar diff --git a/boat-maven-plugin/pom.xml b/boat-maven-plugin/pom.xml index 00069a4aa..472f7384c 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.1-SNAPSHOT + 0.18.2-SNAPSHOT boat-maven-plugin diff --git a/boat-quay/boat-quay-lint/pom.xml b/boat-quay/boat-quay-lint/pom.xml index db9b4ebf6..c99f58764 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.1-SNAPSHOT + 0.18.2-SNAPSHOT boat-quay-lint diff --git a/boat-quay/boat-quay-rules/pom.xml b/boat-quay/boat-quay-rules/pom.xml index 609a524fd..0122aa3d7 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.1-SNAPSHOT + 0.18.2-SNAPSHOT boat-quay-rules diff --git a/boat-quay/pom.xml b/boat-quay/pom.xml index 277df7c25..ed2296493 100644 --- a/boat-quay/pom.xml +++ b/boat-quay/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.1-SNAPSHOT + 0.18.2-SNAPSHOT diff --git a/boat-scaffold/pom.xml b/boat-scaffold/pom.xml index 6186671b7..ad1c32de3 100644 --- a/boat-scaffold/pom.xml +++ b/boat-scaffold/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.1-SNAPSHOT + 0.18.2-SNAPSHOT boat-scaffold @@ -102,7 +102,7 @@ com.backbase.oss boat-trail-resources - 0.18.1-SNAPSHOT + 0.18.2-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 9fa741474..ec2c7448f 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"; private static final String JSON_SERIALIZE = "JsonSerialize"; @@ -148,6 +149,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. */ @@ -338,6 +356,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 082a6947e..118708d3a 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.1-SNAPSHOT + 0.18.2-SNAPSHOT boat-trail-resources diff --git a/pom.xml b/pom.xml index b140018c7..58a4a55b2 100644 --- a/pom.xml +++ b/pom.xml @@ -4,7 +4,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.1-SNAPSHOT + 0.18.2-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 116bc34b9..61dbd512c 100644 --- a/tests/pom.xml +++ b/tests/pom.xml @@ -5,7 +5,7 @@ com.backbase.oss backbase-openapi-tools - 0.18.1-SNAPSHOT + 0.18.2-SNAPSHOT tests