From fe2c468b365f8b6ef12ac83c5e065a5a6df797c8 Mon Sep 17 00:00:00 2001 From: Marcos Freire <164157879+MarcosFreireSngular@users.noreply.github.com> Date: Mon, 1 Apr 2024 10:10:53 +0200 Subject: [PATCH 1/4] 333 generated avro class imports use model package instead of avro schema namespace (#335) * Logic changed in AsyncApiGenerator processExternalAvro function to always use Avro contained namespace. Test for file generation with external Avro schemas changed by adding new avro schema with namespace outside of the model package. * README.md changed to reflect new behaviour * Added new exception, InvalidAvroException, for the case where an Avro file contains no namespace attribute as well as a test for said exception in AsyncApiGeneratorTest, testExceptionForTestIssueInvalidAvro(). Also Updated README to reflect behaviour. * README changed to display correct version (5.3.6) * Changed version number from 5.3.6 to 5.4.0 * Readme column width fixed * Removed references to disashop present in some tests. Now the name referenced is testshop. * Added developer information to the pom file --- README.md | 16 +-- multiapi-engine/pom.xml | 4 +- .../plugin/asyncapi/AsyncApiGenerator.java | 14 +-- .../exception/InvalidAvroException.java | 14 +++ .../asyncapi/AsyncApiGeneratorFixtures.java | 20 +++- .../asyncapi/AsyncApiGeneratorTest.java | 6 + .../assets/IPublishOperation.java | 2 +- .../assets/ISubscribeReceiptExternalAvro.java | 8 ++ .../assets/Producer.java | 2 +- .../assets/Subscriber.java | 11 +- .../avro/Order.avsc | 2 +- .../avro/Receipt.avsc | 11 ++ .../event-api.yml | 6 + .../assets/IPublishOperation.java | 8 ++ .../ISubscribeOperationExternalAvro.java | 8 ++ .../assets/ModelClassException.java | 10 ++ .../testIssueInvalidAvro/assets/Producer.java | 24 ++++ .../assets/Subscriber.java | 24 ++++ .../testIssueInvalidAvro/avro/Order.avsc | 105 ++++++++++++++++++ .../testIssueInvalidAvro/event-api.yml | 101 +++++++++++++++++ scs-multiapi-gradle-plugin/build.gradle | 6 +- scs-multiapi-maven-plugin/pom.xml | 17 ++- 22 files changed, 390 insertions(+), 29 deletions(-) create mode 100644 multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/exception/InvalidAvroException.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/ISubscribeReceiptExternalAvro.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Receipt.avsc create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/IPublishOperation.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ISubscribeOperationExternalAvro.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ModelClassException.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Producer.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Subscriber.java create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/avro/Order.avsc create mode 100644 multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/event-api.yml diff --git a/README.md b/README.md index 42f5d89f..1926fbe6 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ As commented above, they both could be used at the same time, setting a double com.sngular scs-multiapi-maven-plugin - 5.3.5 + 5.4.0 asyncapi @@ -114,7 +114,7 @@ Apply the plugin in the `build.gradle` file and invoke the task. ```groovy plugins { id "java" - id "com.sngular.scs-multiapi-gradle-plugin' version '5.3.5" + id "com.sngular.scs-multiapi-gradle-plugin' version '5.4.0" openapimodel { @@ -153,7 +153,7 @@ which the plugin is designed. com.sngular scs-multiapi-maven-plugin - 5.3.5 + 5.4.0 generate-sources @@ -368,10 +368,10 @@ order/createCommand: $ref: '#/components/messages/com.sngular.apigenerator.asyncapi.model.CreateOrder' ``` -- **Namespace from Avro**: If the user doesn't provide a package name, and the - entity is defined by an Avro Schema, the plugin will check for a `namespace` - attribute defined in the Avro file, and if there is, it will use it. The plugin - expects to receive a relative path from the `yml` file folder. +- **Namespace from Avro**: The plugin will check for a `namespace` + attribute defined in the Avro file and use it, if a namespace is + not defined it will throw an exception. The plugin expects to receive + a relative path from the `yml` file folder. ```yaml order/created: @@ -584,7 +584,7 @@ file. Here is an example of a basic configuration: com.sngular scs-multiapi-maven-plugin - 5.3.5 + 5.4.0 diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index e2345f0a..92903312 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 5.3.5 + 5.4.0 jar @@ -63,7 +63,7 @@ org.projectlombok lombok - 1.18.26 + 1.18.30 org.slf4j diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java index bc0d3431..a766b61e 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGenerator.java @@ -28,12 +28,7 @@ import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.dataformat.yaml.YAMLFactory; import com.sngular.api.generator.plugin.PluginConstants; -import com.sngular.api.generator.plugin.asyncapi.exception.ChannelNameException; -import com.sngular.api.generator.plugin.asyncapi.exception.DuplicateClassException; -import com.sngular.api.generator.plugin.asyncapi.exception.DuplicatedOperationException; -import com.sngular.api.generator.plugin.asyncapi.exception.ExternalRefComponentNotFoundException; -import com.sngular.api.generator.plugin.asyncapi.exception.FileSystemException; -import com.sngular.api.generator.plugin.asyncapi.exception.InvalidAsyncAPIException; +import com.sngular.api.generator.plugin.asyncapi.exception.*; import com.sngular.api.generator.plugin.asyncapi.model.ProcessBindingsResult; import com.sngular.api.generator.plugin.asyncapi.model.ProcessBindingsResult.ProcessBindingsResultBuilder; import com.sngular.api.generator.plugin.asyncapi.model.ProcessMethodResult; @@ -591,8 +586,11 @@ private String processExternalAvro(final String modelPackage, final FileLocation final ObjectMapper mapper = new ObjectMapper(); try { final JsonNode fileTree = mapper.readTree(avroFile); - final String fullNamespace = fileTree.get("namespace").asText() + PACKAGE_SEPARATOR + fileTree.get("name").asText(); - namespace = processModelPackage(fullNamespace, modelPackage); + final JsonNode avroNamespace = fileTree.get("namespace"); + + if (avroNamespace == null) throw new InvalidAvroException(avroFilePath); + + namespace = avroNamespace.asText() + PACKAGE_SEPARATOR + fileTree.get("name").asText();;//processModelPackage(fullNamespace, avroPackage); } catch (final IOException e) { throw new FileSystemException(e); } diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/exception/InvalidAvroException.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/exception/InvalidAvroException.java new file mode 100644 index 00000000..f35e3a20 --- /dev/null +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/asyncapi/exception/InvalidAvroException.java @@ -0,0 +1,14 @@ +/* + * This Source Code Form is subject to the terms of the Mozilla Public + * * License, v. 2.0. If a copy of the MPL was not distributed with this + * * file, You can obtain one at https://mozilla.org/MPL/2.0/. + */ + +package com.sngular.api.generator.plugin.asyncapi.exception; + +public class InvalidAvroException extends RuntimeException { + private static final String ERROR_MESSAGE = "AsyncApi -> Avro schema at path %s lacks a namespace."; + public InvalidAvroException(final String enumName) { + super(String.format(ERROR_MESSAGE, enumName)); + } +} \ No newline at end of file diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorFixtures.java index 6e4d7fad..69ba8db5 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorFixtures.java @@ -168,7 +168,7 @@ public class AsyncApiGeneratorFixtures { .builder() .filePath("src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/event-api.yml") .consumer(OperationParameterObject.builder() - .ids("subscribeOperationExternalAvro") + .ids("subscribeOperationExternalAvro,subscribeReceiptExternalAvro") .apiPackage("com.sngular.scsplugin.externalavro.model.event.consumer") .modelPackage("com.sngular.scsplugin.externalavro.model.event") .build()) @@ -180,6 +180,23 @@ public class AsyncApiGeneratorFixtures { .build() ); + final static List TEST_ISSUE_INVALID_AVRO = List.of( + SpecFile + .builder() + .filePath("src/test/resources/asyncapigenerator/testIssueInvalidAvro/event-api.yml") + .consumer(OperationParameterObject.builder() + .ids("subscribeOperationExternalAvro") + .apiPackage("com.sngular.scsplugin.issueAvro.model.event.consumer") + .modelPackage("com.sngular.scsplugin.issueAvro.model.event") + .build()) + .supplier(OperationParameterObject.builder() + .ids("publishOperationExternalAvro") + .apiPackage("com.sngular.scsplugin.issueAvro.model.event.producer") + .modelPackage("com.sngular.scsplugin.issueAvro.model.event") + .build()) + .build() + ); + final static List TEST_FILE_GENERATION_STREAM_BRIDGE = List.of( SpecFile .builder() @@ -743,6 +760,7 @@ static Function validateTestFileGenerationExternalAvro() { final List expectedConsumerFiles = List.of( ASSETS_PATH + "ISubscribeOperationExternalAvro.java", + ASSETS_PATH + "ISubscribeReceiptExternalAvro.java", ASSETS_PATH + "Subscriber.java" ); diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorTest.java index e6db8fc9..5ec114e6 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/asyncapi/AsyncApiGeneratorTest.java @@ -12,6 +12,7 @@ import java.util.function.Function; import java.util.stream.Stream; +import com.sngular.api.generator.plugin.asyncapi.exception.InvalidAvroException; import com.sngular.api.generator.plugin.asyncapi.parameter.SpecFile; import com.sngular.api.generator.plugin.exception.InvalidAPIException; import org.assertj.core.api.Assertions; @@ -91,4 +92,9 @@ void testExceptionForTestGenerationWithNoOperationConfiguration() { Assertions.assertThatThrownBy(() -> asyncApiGenerator.processFileSpec(AsyncApiGeneratorFixtures.TEST_FILE_GENERATION_NO_CONFIG)).isInstanceOf(InvalidAPIException.class); } + @Test + void testExceptionForTestIssueInvalidAvro() { + Assertions.assertThatThrownBy(() -> asyncApiGenerator.processFileSpec(AsyncApiGeneratorFixtures.TEST_ISSUE_INVALID_AVRO)).isInstanceOf(InvalidAvroException.class); + } + } diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/IPublishOperation.java b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/IPublishOperation.java index 1990353c..84843e4c 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/IPublishOperation.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/IPublishOperation.java @@ -1,6 +1,6 @@ package com.sngular.scsplugin.externalavro.model.event.producer; -import com.sngular.scsplugin.externalavro.model.event.Order; +import com.sngular.testshop.business_model.model.event.Order; public interface IPublishOperationExternalAvro { diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/ISubscribeReceiptExternalAvro.java b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/ISubscribeReceiptExternalAvro.java new file mode 100644 index 00000000..4c063ab3 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/ISubscribeReceiptExternalAvro.java @@ -0,0 +1,8 @@ +package com.sngular.scsplugin.externalavro.model.event.consumer; + +import com.sngular.testshop.commons.Receipt; + +public interface ISubscribeReceiptExternalAvro { + + void subscribeReceiptExternalAvro(final Receipt value); +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Producer.java b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Producer.java index 8c20fcc7..f55befd9 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Producer.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Producer.java @@ -4,7 +4,7 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import com.sngular.scsplugin.externalavro.model.event.Order; +import com.sngular.testshop.business_model.model.event.Order; @Configuration public class Producer { diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Subscriber.java b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Subscriber.java index 824d7591..450ca07a 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Subscriber.java +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/assets/Subscriber.java @@ -4,17 +4,26 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import com.sngular.testshop.commons.Receipt; import com.sngular.scsplugin.externalavro.model.event.CreateOrder; @Configuration public class Subscriber { + private final ISubscribeReceiptExternalAvro subscribeReceiptExternalAvro; + private final ISubscribeOperationExternalAvro subscribeOperationExternalAvro; - protected Subscriber(final ISubscribeOperationExternalAvro subscribeOperationExternalAvro) { + protected Subscriber(final ISubscribeReceiptExternalAvro subscribeReceiptExternalAvro, final ISubscribeOperationExternalAvro subscribeOperationExternalAvro) { + this.subscribeReceiptExternalAvro = subscribeReceiptExternalAvro; this.subscribeOperationExternalAvro = subscribeOperationExternalAvro; } + @Bean + public Consumer subscribeReceiptExternalAvro() { + return value -> subscribeReceiptExternalAvro.subscribeReceiptExternalAvro(value); + } + @Bean public Consumer subscribeOperationExternalAvro() { return value -> subscribeOperationExternalAvro.subscribeOperationExternalAvro(value); diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Order.avsc b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Order.avsc index cee8185f..45c68950 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Order.avsc +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Order.avsc @@ -1,7 +1,7 @@ { "type": "record", "name": "Order", - "namespace": "com.sngular.disashop.business_model.model.event", + "namespace": "com.sngular.testshop.business_model.model.event", "fields": [ { "name": "ref", diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Receipt.avsc b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Receipt.avsc new file mode 100644 index 00000000..3760cf04 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/avro/Receipt.avsc @@ -0,0 +1,11 @@ +{ + "type": "record", + "name": "Receipt", + "namespace": "com.sngular.testshop.commons", + "fields": [ + { + "name": "ref", + "type": "string" + } + ] +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/event-api.yml b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/event-api.yml index bcf59949..48015a5c 100644 --- a/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/event-api.yml +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testFileGenerationExternalAvro/event-api.yml @@ -20,6 +20,12 @@ servers: protocol: kafka protocolVersion: 0.9.1 channels: + order/receipt: + subscribe: + operationId: "subscribeReceiptExternalAvro" + message: + payload: + $ref: 'avro/Receipt.avsc' order/created: publish: operationId: "publishOperationExternalAvro" diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/IPublishOperation.java b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/IPublishOperation.java new file mode 100644 index 00000000..1cfa54c1 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/IPublishOperation.java @@ -0,0 +1,8 @@ +package com.sngular.scsplugin.issueAvro.model.event.producer; + +import com.sngular.testshop.business_model.model.event.Order; + +public interface IPublishOperationExternalAvro { + + Order publishOperationExternalAvro(); +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ISubscribeOperationExternalAvro.java b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ISubscribeOperationExternalAvro.java new file mode 100644 index 00000000..3eaff574 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ISubscribeOperationExternalAvro.java @@ -0,0 +1,8 @@ +package com.sngular.scsplugin.issueAvro.model.event.consumer; + +import com.sngular.scsplugin.issueAvro.model.event.CreateOrder; + +public interface ISubscribeOperationExternalAvro { + + void subscribeOperationExternalAvro(final CreateOrder value); +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ModelClassException.java b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ModelClassException.java new file mode 100644 index 00000000..1d57cdf5 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/ModelClassException.java @@ -0,0 +1,10 @@ +package com.sngular.scsplugin.issueAvro.model.event.exception; + +public class ModelClassException extends RuntimeException { + + private static final String ERROR_MESSAGE = "There are some problems related to the entity called %s. Maybe could be caused by required fields or anyOf/oneOf restrictions"; + + public ModelClassException(final String modelEntity) { + super(String.format(ERROR_MESSAGE, modelEntity)); + } +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Producer.java b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Producer.java new file mode 100644 index 00000000..e30b4119 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Producer.java @@ -0,0 +1,24 @@ +package com.sngular.scsplugin.issueAvro.model.event.producer; + +import java.util.function.Supplier; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.sngular.testshop.business_model.model.event.Order; + +@Configuration +public class Producer { + + private final IPublishOperationExternalAvro publishOperationExternalAvro; + + protected Producer(final IPublishOperationExternalAvro publishOperationExternalAvro) { + this.publishOperationExternalAvro = publishOperationExternalAvro; + } + + @Bean + public Supplier publishOperationExternalAvro() { + return () -> publishOperationExternalAvro.publishOperationExternalAvro(); + } + + +} diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Subscriber.java b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Subscriber.java new file mode 100644 index 00000000..faa1c95c --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/assets/Subscriber.java @@ -0,0 +1,24 @@ +package com.sngular.scsplugin.issueAvro.model.event.consumer; + +import java.util.function.Consumer; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import com.sngular.scsplugin.issueAvro.model.event.CreateOrder; + +@Configuration +public class Subscriber { + + private final ISubscribeOperationExternalAvro subscribeOperationExternalAvro; + + protected Subscriber(final ISubscribeOperationExternalAvro subscribeOperationExternalAvro) { + this.subscribeOperationExternalAvro = subscribeOperationExternalAvro; + } + + @Bean + public Consumer subscribeOperationExternalAvro() { + return value -> subscribeOperationExternalAvro.subscribeOperationExternalAvro(value); + } + + +} diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/avro/Order.avsc b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/avro/Order.avsc new file mode 100644 index 00000000..6dfe4c2c --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/avro/Order.avsc @@ -0,0 +1,105 @@ +{ + "type": "record", + "name": "Order", + "fields": [ + { + "name": "ref", + "type": "string" + }, + { + "name": "clientRef", + "type": "string" + }, + { + "name": "amount", + "type": { + "type": "bytes", + "logicalType": "decimal", + "precision": 10, + "scale": 2 + } + }, + { + "name": "lines", + "type": { + "type": "array", + "items": { + "name": "OrderLine", + "type": "record", + "fields": [ + { + "name": "ref", + "type": "string" + }, + { + "name": "products", + "type": { + "type": "array", + "items": { + "type": "record", + "name": "OrderProduct", + "fields": [ + { + "name": "ref", + "type": "string" + }, + { + "name": "productRef", + "type": "string" + }, + { + "name": "price", + "type": { + "type": "bytes", + "logicalType": "decimal", + "precision": 10, + "scale": 2 + } + }, + { + "name": "quantity", + "type": { + "type": "bytes", + "logicalType": "decimal", + "precision": 10, + "scale": 2 + } + } + ] + } + } + } + ] + } + } + }, + { + "name": "promotions", + "type": { + "type": "array", + "items": { + "name": "Promotion", + "type": "record", + "fields": [ + { + "name": "ref", + "type": "string" + }, + { + "name": "name", + "type": "string" + }, + { + "name": "discount", + "type": "boolean" + }, + { + "name": "amount", + "type": "double" + } + ] + } + } + } + ] +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/event-api.yml b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/event-api.yml new file mode 100644 index 00000000..e9774cb5 --- /dev/null +++ b/multiapi-engine/src/test/resources/asyncapigenerator/testIssueInvalidAvro/event-api.yml @@ -0,0 +1,101 @@ +asyncapi: 2.3.0 +info: + title: Order Service + version: 1.0.0 + description: Order management Service +servers: + development: + url: development.gigantic-server.com + description: Development server + protocol: kafka + protocolVersion: 0.9.1 + staging: + url: staging.gigantic-server.com + description: Staging server + protocol: kafka + protocolVersion: 0.9.1 + production: + url: api.gigantic-server.com + description: Production server + protocol: kafka + protocolVersion: 0.9.1 +channels: + order/created: + publish: + operationId: "publishOperationExternalAvro" + message: + payload: + $ref: 'avro/Order.avsc' + order/createCommand: + subscribe: + operationId: "subscribeOperationExternalAvro" + message: + $ref: '#/components/messages/CreateOrder' +components: + messages: + OrderCreated: + payload: + $ref: '#/components/schemas/Order' + CreateOrder: + payload: + type: object + properties: + order: + $ref: '#/components/schemas/Order' + waiter: + $ref: '#/components/schemas/Waiter' + schemas: + Waiter: + type: object + properties: + ref: + type: string + timestamp: + type: string + format: 'dd/mm/yyyy hh:MM:sss' + table: + type: string + Order: + type: object + properties: + ref: + type: string + clientRef: + type: string + amount: + type: string + format: decimal + lines: + type: array + items: + $ref: '#/components/schemas/OrderLine' + OrderLine: + type: object + required: + - ref + - products + properties: + ref: + type: string + products: + type: array + items: + $ref: '#/components/schemas/OrderProduct' + OrderProduct: + type: object + required: + - ref + - productRef + - price + - quantity + properties: + ref: + type: string + productRef: + type: string + price: + type: string + format: decimal + quantity: + type: string + format: decimal diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index 38acf355..a01a38ef 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -20,7 +20,7 @@ repositories { } group = 'com.sngular' -version = '5.3.5' +version = '5.4.0' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -30,7 +30,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:5.3.5' + implementation 'com.sngular:multiapi-engine:5.4.0' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' } @@ -98,7 +98,7 @@ testing { integrationTest(JvmTestSuite) { dependencies { - implementation 'com.sngular:scs-multiapi-gradle-plugin:5.3.5' + implementation 'com.sngular:scs-multiapi-gradle-plugin:5.4.0' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index e554ee14..5d7778d8 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 5.3.5 + 5.4.0 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -196,6 +196,17 @@ Europe/Madrid + + MarcosFreireSngular + Marcos Freire Patiño + marcos.freire@sngular.com + Sngular + https://www.sngular.com + + Software Developer - Trainee + + Europe/Madrid + @@ -237,13 +248,13 @@ org.projectlombok lombok - 1.18.26 + 1.18.30 provided com.sngular multiapi-engine - 5.3.5 + 5.4.0 org.apache.maven From e5dbf2c1b5187e85bf5de00b21198b399c3ccffc Mon Sep 17 00:00:00 2001 From: carlosaf-sngular <165148531+carlosaf-sngular@users.noreply.github.com> Date: Mon, 1 Apr 2024 12:55:33 +0200 Subject: [PATCH 2/4] 336 fix responses with no body return void (#337) * Fix responses with no body and add test to check for no content * Update version to 5.3.6 * Change scs-multiapi-maven-plugin lombok version * Add developer information * Resolve conflicts in README.md --- README.md | 8 ++--- multiapi-engine/pom.xml | 2 +- .../openapi/templateCallRestClient.ftlh | 4 +-- .../openapi/OpenApiGeneratorFixtures.java | 2 +- .../testRestClientApiGeneration/api-test.yml | 22 +++++++++++++ .../assets/TestApi.java | 32 +++++++++++++++++++ scs-multiapi-gradle-plugin/build.gradle | 6 ++-- scs-multiapi-maven-plugin/pom.xml | 10 ++++-- 8 files changed, 73 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 1926fbe6..77713f08 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ As commented above, they both could be used at the same time, setting a double com.sngular scs-multiapi-maven-plugin - 5.4.0 + 5.4.1 asyncapi @@ -114,7 +114,7 @@ Apply the plugin in the `build.gradle` file and invoke the task. ```groovy plugins { id "java" - id "com.sngular.scs-multiapi-gradle-plugin' version '5.4.0" + id "com.sngular.scs-multiapi-gradle-plugin' version '5.4.1" openapimodel { @@ -153,7 +153,7 @@ which the plugin is designed. com.sngular scs-multiapi-maven-plugin - 5.4.0 + 5.4.1 generate-sources @@ -584,7 +584,7 @@ file. Here is an example of a basic configuration: com.sngular scs-multiapi-maven-plugin - 5.4.0 + 5.4.1 diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index 92903312..0a28a2bb 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 5.4.0 + 5.4.1 jar diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh index a66feb72..e645e26e 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh @@ -107,8 +107,8 @@ public class ${className?cap_first}Api { */ public <@compress single_line=true><#if operation.responseObjects[0].contentObjects[0]??> ${operation.responseObjects[0].contentObjects[0].dataType} - <#else>Void ${operation.operationId}<#compress>(<#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter>${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content><#list path.parameterObjects as parameter>${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if operation.requestObjects?has_content><#list operation.requestObjects as request><#list request.contentObjects as content>${content.dataType} ${content.dataType?api.getVariableNameString()} <#if content?has_next>, ) throws RestClientException { - return ${operation.operationId}WithHttpInfo(<#compress><#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter>${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content><#list path.parameterObjects as parameter>${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if operation.requestObjects?has_content><#list operation.requestObjects as request><#list request.contentObjects as content>${content.dataType?api.getVariableNameString()}<#if content?has_next>, )<#if operation.responseObjects[0].contentObjects[0]??>.getBody(); + <#else>void ${operation.operationId}<#compress>(<#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter>${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content><#list path.parameterObjects as parameter>${parameter.dataType} ${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if operation.requestObjects?has_content><#list operation.requestObjects as request><#list request.contentObjects as content>${content.dataType} ${content.dataType?api.getVariableNameString()} <#if content?has_next>, ) throws RestClientException { + <#if operation.responseObjects[0].contentObjects[0]??>return ${operation.operationId}WithHttpInfo(<#compress><#if operation.parameterObjects?has_content><#list operation.parameterObjects as parameter>${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if path.parameterObjects?has_content><#list path.parameterObjects as parameter>${parameter.name}<#if parameter?has_next || operation.requestObjects?has_content>, <#if operation.requestObjects?has_content><#list operation.requestObjects as request><#list request.contentObjects as content>${content.dataType?api.getVariableNameString()}<#if content?has_next>, )<#if operation.responseObjects[0].contentObjects[0]??>.getBody(); } public <@compress single_line=true><#if operation.responseObjects[0].contentObjects[0]??> diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java index 565a8d21..cee9c43c 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java @@ -199,7 +199,7 @@ public final class OpenApiGeneratorFixtures { static final List TEST_REST_CLIENT_GENERATION = List.of( SpecFile .builder() - .filePath("openapigenerator/testClientPackageWebClientApiGeneration/api-test.yml") + .filePath("openapigenerator/testRestClientApiGeneration/api-test.yml") .apiPackage("com.sngular.multifileplugin.restclient") .modelPackage("com.sngular.multifileplugin.restclient.model") .clientPackage("com.sngular.multifileplugin.restclient.client") diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/api-test.yml index ee194201..4e795317 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/api-test.yml +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/api-test.yml @@ -61,6 +61,28 @@ paths: application/json: schema: $ref: "#/components/schemas/Error" + delete: + summary: Info for a specific test + operationId: deleteTestById + tags: + - test + parameters: + - name: testId + in: path + required: true + description: The id of the test to retrieve + schema: + type: integer + format: int32 + responses: + '204': + description: No content response + default: + description: unexpected error + content: + application/json: + schema: + $ref: "#/components/schemas/Error" security: - BasicAuth: [] components: diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/assets/TestApi.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/assets/TestApi.java index f7293c1d..6d8f3fd5 100644 --- a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/assets/TestApi.java +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiGeneration/assets/TestApi.java @@ -104,4 +104,36 @@ public ResponseEntity showTestByIdWithHttpInfo(Integer testId) t return apiRestClient.invokeAPI("http://localhost:8080/v1","/test/{testId}", HttpMethod.GET, uriVariables, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); } + /** + * DELETE /test/{testId}: Info for a specific test + * @param testId The id of the test to retrieve true + * @return No content response; (status code 204) + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public void deleteTestById(Integer testId) throws RestClientException { + deleteTestByIdWithHttpInfo(testId); + } + + public ResponseEntity deleteTestByIdWithHttpInfo(Integer testId) throws RestClientException { + + Object postBody = null; + final Map uriVariables = new HashMap(); + + uriVariables.put("testId", testId); + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + + final String[] localVarAccepts = {"application/json"}; + final List localVarAccept = apiRestClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {}; + final MediaType localVarContentType = apiRestClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {"BasicAuth"}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiRestClient.invokeAPI("http://localhost:8080/v1","/test/{testId}", HttpMethod.DELETE, uriVariables, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + } + } \ No newline at end of file diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index a01a38ef..c71a92b1 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -20,7 +20,7 @@ repositories { } group = 'com.sngular' -version = '5.4.0' +version = '5.4.1' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -30,7 +30,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:5.4.0' + implementation 'com.sngular:multiapi-engine:5.4.1' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' } @@ -98,7 +98,7 @@ testing { integrationTest(JvmTestSuite) { dependencies { - implementation 'com.sngular:scs-multiapi-gradle-plugin:5.4.0' + implementation 'com.sngular:scs-multiapi-gradle-plugin:5.4.1' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index 5d7778d8..ac9a63c4 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 5.4.0 + 5.4.1 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -196,6 +196,12 @@ Europe/Madrid + + carlosaf-sngular + Carlos Alonso Ferreira + carlos.alonso@sngular.com + https://sngular.github.io/ + MarcosFreireSngular Marcos Freire Patiño @@ -254,7 +260,7 @@ com.sngular multiapi-engine - 5.4.0 + 5.4.1 org.apache.maven From 996f205c19802d2687295c7a45106d3e8bf26860 Mon Sep 17 00:00:00 2001 From: oscar-ares <160604756+oscar-ares@users.noreply.github.com> Date: Tue, 7 May 2024 12:30:16 +0200 Subject: [PATCH 3/4] added support for multipart and x-www-form-encoded when callmode is true (#341) --- README.md | 8 +- multiapi-engine/pom.xml | 2 +- .../plugin/openapi/utils/MapperPathUtil.java | 42 +- .../openapi/templateCallRestClient.ftlh | 7 + .../openapi/OpenApiGeneratorFixtures.java | 45 ++ .../plugin/openapi/OpenApiGeneratorTest.java | 2 + .../api-test.yml | 75 +++ .../assets/ApiErrorDTO.java | 111 +++++ .../assets/ApiTestDTO.java | 111 +++++ .../assets/ApiTestInfoDTO.java | 122 +++++ .../assets/ModelClassException.java | 10 + .../assets/TestApi.java | 114 +++++ .../assets/client/ApiRestClient.java | 461 ++++++++++++++++++ .../assets/client/auth/Authentication.java | 9 + .../assets/client/auth/HttpBasicAuth.java | 37 ++ scs-multiapi-gradle-plugin/build.gradle | 6 +- scs-multiapi-maven-plugin/pom.xml | 15 +- 17 files changed, 1141 insertions(+), 36 deletions(-) create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/api-test.yml create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiErrorDTO.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestDTO.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestInfoDTO.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ModelClassException.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/TestApi.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/ApiRestClient.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/Authentication.java create mode 100644 multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/HttpBasicAuth.java diff --git a/README.md b/README.md index 77713f08..ed77c1c8 100644 --- a/README.md +++ b/README.md @@ -60,7 +60,7 @@ As commented above, they both could be used at the same time, setting a double com.sngular scs-multiapi-maven-plugin - 5.4.1 + 5.4.2 asyncapi @@ -114,7 +114,7 @@ Apply the plugin in the `build.gradle` file and invoke the task. ```groovy plugins { id "java" - id "com.sngular.scs-multiapi-gradle-plugin' version '5.4.1" + id "com.sngular.scs-multiapi-gradle-plugin' version '5.4.2" openapimodel { @@ -153,7 +153,7 @@ which the plugin is designed. com.sngular scs-multiapi-maven-plugin - 5.4.1 + 5.4.2 generate-sources @@ -584,7 +584,7 @@ file. Here is an example of a basic configuration: com.sngular scs-multiapi-maven-plugin - 5.4.1 + 5.4.2 diff --git a/multiapi-engine/pom.xml b/multiapi-engine/pom.xml index 0a28a2bb..4f8ed86b 100644 --- a/multiapi-engine/pom.xml +++ b/multiapi-engine/pom.xml @@ -4,7 +4,7 @@ com.sngular multiapi-engine - 5.4.1 + 5.4.2 jar diff --git a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java index e8e87ed3..de1fcb7a 100644 --- a/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java +++ b/multiapi-engine/src/main/java/com/sngular/api/generator/plugin/openapi/utils/MapperPathUtil.java @@ -6,37 +6,22 @@ package com.sngular.api.generator.plugin.openapi.utils; -import java.nio.file.Path; -import java.util.ArrayList; -import java.util.Collection; -import java.util.HashMap; -import java.util.Iterator; -import java.util.List; -import java.util.Map; -import java.util.Map.Entry; -import java.util.Objects; -import java.util.function.BiConsumer; - import com.fasterxml.jackson.databind.JsonNode; import com.sngular.api.generator.plugin.common.tools.ApiTool; import com.sngular.api.generator.plugin.common.tools.SchemaUtil; import com.sngular.api.generator.plugin.openapi.exception.DuplicatedOperationException; import com.sngular.api.generator.plugin.openapi.exception.InvalidOpenAPIException; -import com.sngular.api.generator.plugin.openapi.model.AuthSchemaObject; -import com.sngular.api.generator.plugin.openapi.model.ContentObject; -import com.sngular.api.generator.plugin.openapi.model.GlobalObject; +import com.sngular.api.generator.plugin.openapi.model.*; import com.sngular.api.generator.plugin.openapi.model.GlobalObject.GlobalObjectBuilder; -import com.sngular.api.generator.plugin.openapi.model.OperationObject; -import com.sngular.api.generator.plugin.openapi.model.ParameterObject; -import com.sngular.api.generator.plugin.openapi.model.PathObject; -import com.sngular.api.generator.plugin.openapi.model.RequestObject; -import com.sngular.api.generator.plugin.openapi.model.ResponseObject; -import com.sngular.api.generator.plugin.openapi.model.SchemaFieldObjectType; -import com.sngular.api.generator.plugin.openapi.model.TypeConstants; import com.sngular.api.generator.plugin.openapi.parameter.SpecFile; import org.apache.commons.collections4.IteratorUtils; import org.apache.commons.lang3.StringUtils; +import java.nio.file.Path; +import java.util.*; +import java.util.Map.Entry; +import java.util.function.BiConsumer; + public class MapperPathUtil { public static final String INLINE_PARAMETER = "InlineParameter"; @@ -352,17 +337,22 @@ private static List mapContentObject( final Path baseDir) { final List contentObjects = new ArrayList<>(); if (Objects.nonNull(content)) { - for (Iterator it = content.fieldNames(); it.hasNext();) { + for (Iterator it = content.fieldNames(); it.hasNext(); ) { final String mediaType = it.next(); final var schema = ApiTool.getNode(ApiTool.getNode(content, mediaType), SCHEMA); final String pojoName = preparePojoName(inlineObject, schema, specFile); final SchemaFieldObjectType dataType = getSchemaType(schema, pojoName, specFile, globalObject, baseDir); final String importName = getImportFromType(dataType); + SchemaObject schemaObject = null; + if (mediaType.equals("application/x-www-form-urlencoded") || mediaType.equals("multipart/form-data")) { + schemaObject = MapperContentUtil.mapComponentToSchemaObject(globalObject.getSchemaMap(), new HashMap(), schema, dataType.getBaseType(), specFile, baseDir).get("object"); + } contentObjects.add(ContentObject.builder() - .dataType(dataType) - .name(mediaType) - .importName(importName) - .build()); + .dataType(dataType) + .name(mediaType) + .importName(importName) + .schemaObject(schemaObject) + .build()); } } return contentObjects; diff --git a/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh b/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh index e645e26e..e4bbc8ef 100644 --- a/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh +++ b/multiapi-engine/src/main/resources/templates/openapi/templateCallRestClient.ftlh @@ -141,6 +141,13 @@ public class ${className?cap_first}Api { final HttpHeaders headerParams = new HttpHeaders(); final MultiValueMap cookieParams = new LinkedMultiValueMap(); final MultiValueMap formParams = new LinkedMultiValueMap(); + <#if operation.requestObjects?has_content> + <#if operation.requestObjects[0].contentObjects[0].schemaObject?has_content> + <#list operation.requestObjects[0].contentObjects[0].schemaObject.fieldObjectList as field> + formParams.put("${field.baseName}", List.of(${operation.requestObjects[0].contentObjects[0].dataType?api.getVariableNameString()}.get${field.baseName?cap_first}())); + + + <#if operation.parameterObjects?has_content> <#list operation.parameterObjects as parameter> diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java index cee9c43c..9097c333 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorFixtures.java @@ -210,6 +210,20 @@ public final class OpenApiGeneratorFixtures { .build() ); + static final List TEST_REST_CLIENT_API_WITH_REQUEST_OBJECTS_GENERATION = List.of( + SpecFile + .builder() + .filePath("openapigenerator/testRestClientApiWithRequestObjectGeneration/api-test.yml") + .apiPackage("com.sngular.multifileplugin.restclientWithRequestObjects") + .modelPackage("com.sngular.multifileplugin.restclientWithRequestObjects.model") + .clientPackage("com.sngular.multifileplugin.restclientWithRequestObjects.client") + .modelNamePrefix("Api") + .modelNameSuffix("DTO") + .useLombokModelAnnotation(false) + .callMode(true) + .build() + ); + static final List TEST_ENUMS_GENERATION = List.of( SpecFile .builder() @@ -491,6 +505,7 @@ public final class OpenApiGeneratorFixtures { .build() ); + static Function validateOneOfInResponse() { final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/testoneofinresponse"; @@ -911,6 +926,36 @@ static Function validateRestClientGeneration() { commonTest(path, expectedTestClientApiFile, expectedTestClientAuthModelFiles, CLIENT_TARGET_API, CLIENT_MODEL_API, Collections.emptyList(), null); } + static Function validateRestClientWithRequestBodyGeneration() { + + final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/restclientWithRequestObjects"; + + final String CLIENT_TARGET_API = "generated/com/sngular/multifileplugin/restclientWithRequestObjects/client"; + + final String CLIENT_MODEL_API = "generated/com/sngular/multifileplugin/restclientWithRequestObjects/client/auth"; + + final String COMMON_PATH = "openapigenerator/testRestClientApiWithRequestObjectGeneration/"; + + final String ASSETS_PATH = COMMON_PATH + "assets/"; + + List expectedTestApiFile = List.of( + ASSETS_PATH + "TestApi.java" + ); + + List expectedTestClientApiFile = List.of( + ASSETS_PATH + "client/ApiRestClient.java" + ); + + List expectedTestClientAuthModelFiles = List.of( + ASSETS_PATH + "client/auth/Authentication.java", + ASSETS_PATH + "client/auth/HttpBasicAuth.java" + ); + + return (path) -> + commonTest(path, expectedTestApiFile, Collections.emptyList(), DEFAULT_TARGET_API, null, Collections.emptyList(), null) && + commonTest(path, expectedTestClientApiFile, expectedTestClientAuthModelFiles, CLIENT_TARGET_API, CLIENT_MODEL_API, Collections.emptyList(), null); + } + static Function validateEnumsGeneration() { final String DEFAULT_TARGET_API = "generated/com/sngular/multifileplugin/enumgeneration"; diff --git a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java index 76cfeee3..f5ded4be 100644 --- a/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java +++ b/multiapi-engine/src/test/java/com/sngular/api/generator/plugin/openapi/OpenApiGeneratorTest.java @@ -74,6 +74,8 @@ static Stream fileSpecToProcess() { OpenApiGeneratorFixtures.validateClientPackageWebClientGeneration()), Arguments.of("testRestClientApiGeneration", OpenApiGeneratorFixtures.TEST_REST_CLIENT_GENERATION, OpenApiGeneratorFixtures.validateRestClientGeneration()), + Arguments.of("testRestClientApiWithRequestObjectGeneration", OpenApiGeneratorFixtures.TEST_REST_CLIENT_API_WITH_REQUEST_OBJECTS_GENERATION, + OpenApiGeneratorFixtures.validateRestClientWithRequestBodyGeneration()), Arguments.of("testApiEnumsGeneration", OpenApiGeneratorFixtures.TEST_ENUMS_GENERATION, OpenApiGeneratorFixtures.validateEnumsGeneration()), Arguments.of("testApiEnumsLombokGeneration", OpenApiGeneratorFixtures.TEST_ENUMS_LOMBOK_GENERATION, diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/api-test.yml b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/api-test.yml new file mode 100644 index 00000000..a888ead4 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/api-test.yml @@ -0,0 +1,75 @@ +--- +openapi: "3.0.0" +info: + version: 1.0.0 + title: Sngular Test Api + license: + name: MIT +servers: + - url: http://localhost:8080/v1 +paths: + /test/form_url_encoded: + summary: test + get: + summary: Test url form encoded + operationId: test_form_url_encoded + tags: + - test + requestBody: + required: true + content: + application/x-www-form-urlencoded: + schema: + $ref: "#/components/schemas/TestInput" + responses: + '200': + description: Test response + content: + application/json: + schema: + $ref: "#/components/schemas/TestResponse" + /test/multipart: + summary: test + get: + summary: Test multipart + operationId: test_multipart + tags: + - test + requestBody: + required: true + content: + multipart/form-data: + schema: + $ref: "#/components/schemas/TestInput" + responses: + '200': + description: Test response + content: + application/json: + schema: + $ref: "#/components/schemas/TestResponse" + +components: + schemas: + TestInput: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int32 + name: + type: string + TestResponse: + type: object + required: + - id + - name + properties: + id: + type: integer + format: int32 + name: + type: string \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiErrorDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiErrorDTO.java new file mode 100644 index 00000000..113772c2 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiErrorDTO.java @@ -0,0 +1,111 @@ +package com.sngular.multifileplugin.restclient.model; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import com.sngular.multifileplugin.restclient.model.exception.ModelClassException; + +public class ApiErrorDTO { + + @JsonProperty(value ="code") + private final Integer code; + @JsonProperty(value ="message") + private final String message; + + private ApiErrorDTO(Integer code, String message) { + this.code = code; + this.message = message; + + validateRequiredAttributes(); + } + + private ApiErrorDTO(ApiErrorDTOBuilder builder) { + this.code = builder.code; + this.message = builder.message; + + validateRequiredAttributes(); + } + + public static ApiErrorDTO.ApiErrorDTOBuilder builder() { + return new ApiErrorDTO.ApiErrorDTOBuilder(); + } + + public static class ApiErrorDTOBuilder { + + private Integer code; + private String message; + + public ApiErrorDTO.ApiErrorDTOBuilder code(Integer code) { + this.code = code; + return this; + } + + public ApiErrorDTO.ApiErrorDTOBuilder message(String message) { + this.message = message; + return this; + } + + public ApiErrorDTO build() { + ApiErrorDTO apiErrorDTO = new ApiErrorDTO(this); + return apiErrorDTO; + } + } + + + @Schema(name = "code", required = true) + public Integer getCode() { + return code; + } + + + @Schema(name = "message", required = true) + public String getMessage() { + return message; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiErrorDTO apiErrorDTO = (ApiErrorDTO) o; + return Objects.equals(this.code, apiErrorDTO.code) && Objects.equals(this.message, apiErrorDTO.message); + } + + @Override + public int hashCode() { + return Objects.hash(code, message); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ApiErrorDTO{"); + sb.append(" code:").append(code).append(","); + sb.append(" message:").append(message); + sb.append("}"); + return sb.toString(); + } + + + + + private void validateRequiredAttributes() { + boolean satisfiedCondition = true; + + if (!Objects.nonNull(this.code)) { + satisfiedCondition = false; + } else if (!Objects.nonNull(this.message)) { + satisfiedCondition = false; + } + + if (!satisfiedCondition) { + throw new ModelClassException("ApiErrorDTO"); + } + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestDTO.java new file mode 100644 index 00000000..2a010065 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestDTO.java @@ -0,0 +1,111 @@ +package com.sngular.multifileplugin.restclient.model; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import com.sngular.multifileplugin.restclient.model.exception.ModelClassException; + +public class ApiTestDTO { + + @JsonProperty(value ="name") + private final String name; + @JsonProperty(value ="id") + private final Integer id; + + private ApiTestDTO(String name, Integer id) { + this.name = name; + this.id = id; + + validateRequiredAttributes(); + } + + private ApiTestDTO(ApiTestDTOBuilder builder) { + this.name = builder.name; + this.id = builder.id; + + validateRequiredAttributes(); + } + + public static ApiTestDTO.ApiTestDTOBuilder builder() { + return new ApiTestDTO.ApiTestDTOBuilder(); + } + + public static class ApiTestDTOBuilder { + + private String name; + private Integer id; + + public ApiTestDTO.ApiTestDTOBuilder name(String name) { + this.name = name; + return this; + } + + public ApiTestDTO.ApiTestDTOBuilder id(Integer id) { + this.id = id; + return this; + } + + public ApiTestDTO build() { + ApiTestDTO apiTestDTO = new ApiTestDTO(this); + return apiTestDTO; + } + } + + + @Schema(name = "name", required = true) + public String getName() { + return name; + } + + + @Schema(name = "id", required = true) + public Integer getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiTestDTO apiTestDTO = (ApiTestDTO) o; + return Objects.equals(this.name, apiTestDTO.name) && Objects.equals(this.id, apiTestDTO.id); + } + + @Override + public int hashCode() { + return Objects.hash(name, id); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ApiTestDTO{"); + sb.append(" name:").append(name).append(","); + sb.append(" id:").append(id); + sb.append("}"); + return sb.toString(); + } + + + + + private void validateRequiredAttributes() { + boolean satisfiedCondition = true; + + if (!Objects.nonNull(this.name)) { + satisfiedCondition = false; + } else if (!Objects.nonNull(this.id)) { + satisfiedCondition = false; + } + + if (!satisfiedCondition) { + throw new ModelClassException("ApiTestDTO"); + } + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestInfoDTO.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestInfoDTO.java new file mode 100644 index 00000000..c2f14d4d --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ApiTestInfoDTO.java @@ -0,0 +1,122 @@ +package com.sngular.multifileplugin.restclient.model; + +import java.util.Objects; + +import com.fasterxml.jackson.annotation.JsonProperty; +import io.swagger.v3.oas.annotations.media.Schema; +import java.util.List; +import java.util.ArrayList; +import com.sngular.multifileplugin.restclient.model.exception.ModelClassException; + +public class ApiTestInfoDTO { + + @JsonProperty(value ="testers") + private List testers = new ArrayList(); + @JsonProperty(value ="testName") + private final String testName; + + private ApiTestInfoDTO(List testers, String testName) { + this.testers = testers; + this.testName = testName; + + validateRequiredAttributes(); + } + + private ApiTestInfoDTO(ApiTestInfoDTOBuilder builder) { + this.testers = builder.testers; + this.testName = builder.testName; + + validateRequiredAttributes(); + } + + public static ApiTestInfoDTO.ApiTestInfoDTOBuilder builder() { + return new ApiTestInfoDTO.ApiTestInfoDTOBuilder(); + } + + public static class ApiTestInfoDTOBuilder { + + private List testers = new ArrayList(); + private String testName; + public ApiTestInfoDTO.ApiTestInfoDTOBuilder testers(List testers) { + if (!testers.isEmpty()) { + this.testers.addAll(testers); + } + return this; + } + + public ApiTestInfoDTO.ApiTestInfoDTOBuilder tester(String tester) { + if (tester != null) { + this.testers.add(tester); + } + return this; + } + + public ApiTestInfoDTO.ApiTestInfoDTOBuilder testName(String testName) { + this.testName = testName; + return this; + } + + public ApiTestInfoDTO build() { + ApiTestInfoDTO apiTestInfoDTO = new ApiTestInfoDTO(this); + return apiTestInfoDTO; + } + } + + + @Schema(name = "testers", required = false) + public List getTesters() { + return testers; + } + public void setTesters(List testers) { + this.testers = testers; + } + + + @Schema(name = "testName", required = true) + public String getTestName() { + return testName; + } + + @Override + public boolean equals(Object o) { + if (this == o) { + return true; + } + if (o == null || getClass() != o.getClass()) { + return false; + } + ApiTestInfoDTO apiTestInfoDTO = (ApiTestInfoDTO) o; + return Objects.equals(this.testers, apiTestInfoDTO.testers) && Objects.equals(this.testName, apiTestInfoDTO.testName); + } + + @Override + public int hashCode() { + return Objects.hash(testers, testName); + } + + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("ApiTestInfoDTO{"); + sb.append(" testers:").append(testers).append(","); + sb.append(" testName:").append(testName); + sb.append("}"); + return sb.toString(); + } + + + + + private void validateRequiredAttributes() { + boolean satisfiedCondition = true; + + if (!Objects.nonNull(this.testName)) { + satisfiedCondition = false; + } + + if (!satisfiedCondition) { + throw new ModelClassException("ApiTestInfoDTO"); + } + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ModelClassException.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ModelClassException.java new file mode 100644 index 00000000..003ffa8f --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/ModelClassException.java @@ -0,0 +1,10 @@ +package com.sngular.multifileplugin.testrestclient.model.exception; + +public class ModelClassException extends RuntimeException { + + private static final String ERROR_MESSAGE = "There are some problems related to the entity called %s. Maybe could be caused by required fields or anyOf/oneOf restrictions"; + + public ModelClassException(final String modelEntity) { + super(String.format(ERROR_MESSAGE, modelEntity)); + } +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/TestApi.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/TestApi.java new file mode 100644 index 00000000..533b5c2b --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/TestApi.java @@ -0,0 +1,114 @@ +package com.sngular.multifileplugin.restclientWithRequestObjects; + +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.sngular.multifileplugin.restclientWithRequestObjects.client.ApiRestClient; + +import com.sngular.multifileplugin.restclientWithRequestObjects.model.ApiTestInputDTO; +import com.sngular.multifileplugin.restclientWithRequestObjects.model.ApiTestResponseDTO; + +import com.sngular.multifileplugin.restclientWithRequestObjects.client.auth.Authentication; + +import org.springframework.stereotype.Component; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.HttpClientErrorException; + +@Component() +public class TestApi { + + private ApiRestClient apiRestClient; + + private Map authenticationsApi; + + public TestApi() { + this.init(); + } + + protected void init() { + this.authenticationsApi = new HashMap(); + this.apiRestClient = new ApiRestClient(authenticationsApi); + } + + /** + * GET /test/form_url_encoded: Test url form encoded + * @param apiTestInputDTO (required) + * @return Test response; (status code 200) + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ApiTestResponseDTO test_form_url_encoded(ApiTestInputDTO apiTestInputDTO ) throws RestClientException { + return test_form_url_encodedWithHttpInfo(apiTestInputDTO).getBody(); + } + + public ResponseEntity test_form_url_encodedWithHttpInfo(ApiTestInputDTO apiTestInputDTO) throws RestClientException { + + Object postBody = apiTestInputDTO; + if (apiTestInputDTO == null) { + throw new RestClientException(HttpStatus.BAD_REQUEST + " Missing the required parameter ''apiTestInputDTO'' when calling test_form_url_encoded"); + } + final Map uriVariables = new HashMap(); + + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + formParams.put("name", List.of(apiTestInputDTO.getName())); + formParams.put("id", List.of(apiTestInputDTO.getId())); + + final String[] localVarAccepts = {"application/json"}; + final List localVarAccept = apiRestClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {"application/x-www-form-urlencoded"}; + final MediaType localVarContentType = apiRestClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiRestClient.invokeAPI("http://localhost:8080/v1","/test/form_url_encoded", HttpMethod.GET, uriVariables, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + } + + /** + * GET /test/multipart: Test multipart + * @param apiTestInputDTO (required) + * @return Test response; (status code 200) + * @throws RestClientException if an error occurs while attempting to invoke the API + */ + public ApiTestResponseDTO test_multipart(ApiTestInputDTO apiTestInputDTO ) throws RestClientException { + return test_multipartWithHttpInfo(apiTestInputDTO).getBody(); + } + + public ResponseEntity test_multipartWithHttpInfo(ApiTestInputDTO apiTestInputDTO) throws RestClientException { + + Object postBody = apiTestInputDTO; + if (apiTestInputDTO == null) { + throw new RestClientException(HttpStatus.BAD_REQUEST + " Missing the required parameter ''apiTestInputDTO'' when calling test_multipart"); + } + final Map uriVariables = new HashMap(); + + final MultiValueMap queryParams = new LinkedMultiValueMap(); + final HttpHeaders headerParams = new HttpHeaders(); + final MultiValueMap cookieParams = new LinkedMultiValueMap(); + final MultiValueMap formParams = new LinkedMultiValueMap(); + formParams.put("name", List.of(apiTestInputDTO.getName())); + formParams.put("id", List.of(apiTestInputDTO.getId())); + + final String[] localVarAccepts = {"application/json"}; + final List localVarAccept = apiRestClient.selectHeaderAccept(localVarAccepts); + final String[] localVarContentTypes = {"multipart/form-data"}; + final MediaType localVarContentType = apiRestClient.selectHeaderContentType(localVarContentTypes); + + String[] localVarAuthNames = new String[] {}; + + ParameterizedTypeReference localVarReturnType = new ParameterizedTypeReference() {}; + return apiRestClient.invokeAPI("http://localhost:8080/v1","/test/multipart", HttpMethod.GET, uriVariables, queryParams, postBody, headerParams, cookieParams, formParams, localVarAccept, localVarContentType, localVarAuthNames, localVarReturnType); + } + +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/ApiRestClient.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/ApiRestClient.java new file mode 100644 index 00000000..55860558 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/ApiRestClient.java @@ -0,0 +1,461 @@ +package com.sngular.multifileplugin.restclientWithRequestObjects.client; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.io.UnsupportedEncodingException; +import java.net.URI; +import java.net.URISyntaxException; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; +import java.text.DateFormat; +import java.text.FieldPosition; +import java.text.ParsePosition; +import java.time.OffsetDateTime; +import java.time.format.DateTimeFormatter; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.Date; +import java.util.GregorianCalendar; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Map.Entry; +import java.util.Objects; +import java.util.TimeZone; + +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.util.StdDateFormat; +import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.springframework.core.ParameterizedTypeReference; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpRequest; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.http.RequestEntity; +import org.springframework.http.RequestEntity.BodyBuilder; +import org.springframework.http.ResponseEntity; +import org.springframework.http.client.BufferingClientHttpRequestFactory; +import org.springframework.http.client.ClientHttpRequestExecution; +import org.springframework.http.client.ClientHttpRequestInterceptor; +import org.springframework.http.client.ClientHttpResponse; +import org.springframework.stereotype.Component; +import org.springframework.util.CollectionUtils; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.util.StringUtils; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import org.springframework.web.util.DefaultUriBuilderFactory; +import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.http.converter.HttpMessageConverter; +import org.springframework.http.converter.json.AbstractJackson2HttpMessageConverter; + +import com.sngular.multifileplugin.restclientWithRequestObjects.client.auth.Authentication; + +@Component +public class ApiRestClient { + public enum CollectionFormat { + CSV(","), TSV("\t"), SSV(" "), PIPES("|"), MULTI(null); + + private final String separator; + private CollectionFormat(final String separator) { + this.separator = separator; + } + + private String collectionToString(final Collection collection) { + return StringUtils.collectionToDelimitedString(collection, separator); + } + } + + private final HttpHeaders defaultHeaders = new HttpHeaders(); + private final MultiValueMap defaultCookies = new LinkedMultiValueMap(); + private final RestTemplate restTemplate; + private final DateFormat dateFormat; + private final Map authentications; + + public ApiRestClient() { + this.dateFormat = createDefaultDateFormat(); + addDefaultHeader("User-Agent", "Java-SDK"); + this.restTemplate = buildRestTemplate(); + authentications = Collections.unmodifiableMap(new HashMap()); + } + + public ApiRestClient(final Map authentications) { + this.dateFormat = createDefaultDateFormat(); + addDefaultHeader("User-Agent", "Java-SDK"); + this.restTemplate = buildRestTemplate(); + this.authentications = Collections.unmodifiableMap(authentications); + } + + public static DateFormat createDefaultDateFormat() { + final DateFormat dateFormat = new DateFormat() { + private final StdDateFormat fmt = new StdDateFormat() + .withTimeZone(TimeZone.getTimeZone("UTC")) + .withColonInTimeZone(true); + + @Override + public Date parse(final String source) { + return parse(source, new ParsePosition(0)); + } + + @Override + public StringBuffer format(final Date date, final StringBuffer toAppendTo, final FieldPosition fieldPosition) { + return fmt.format(date, toAppendTo, fieldPosition); + } + + @Override + public Date parse(final String source, final ParsePosition pos) { + return fmt.parse(source, pos); + } + }; + dateFormat.setCalendar(new GregorianCalendar()); + dateFormat.setTimeZone(TimeZone.getTimeZone("UTC")); + return dateFormat; + } + + private static void createDefaultObjectMapper(final DateFormat defaultDateFormat, final AbstractJackson2HttpMessageConverter converter) { + DateFormat dateFormat = defaultDateFormat; + if (Objects.isNull(defaultDateFormat)) { + dateFormat = createDefaultDateFormat(); + } + final ObjectMapper mapper = converter.getObjectMapper(); + mapper.setDateFormat(dateFormat); + mapper.registerModule(new JavaTimeModule()); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + } + + protected RestTemplate buildRestTemplate() { + final RestTemplate restTemplate = new RestTemplate(); + for(HttpMessageConverter converter:restTemplate.getMessageConverters()) { + if(converter instanceof AbstractJackson2HttpMessageConverter) { + createDefaultObjectMapper(this.dateFormat, (AbstractJackson2HttpMessageConverter) converter); + } + } + restTemplate.setRequestFactory(new BufferingClientHttpRequestFactory(restTemplate.getRequestFactory())); + return restTemplate; + } + + public ApiRestClient addDefaultHeader(final String name, final String value) { + if (defaultHeaders.containsKey(name)) { + defaultHeaders.remove(name); + } + defaultHeaders.add(name, value); + return this; + } + + public ApiRestClient addDefaultCookie(final String name, final String value) { + if (defaultCookies.containsKey(name)) { + defaultCookies.remove(name); + } + defaultCookies.add(name, value); + return this; + } + + public String parameterToString(final Object param) { + if (param == null) { + return ""; + } else if (param instanceof Date) { + return dateFormat.format((Date) param); + } else if (param instanceof OffsetDateTime) { + return formatOffsetDateTime((OffsetDateTime) param); + } else if (param instanceof Collection) { + StringBuilder b = new StringBuilder(); + for (Object o : (Collection) param) { + if(b.length() > 0) { + b.append(","); + } + b.append(String.valueOf(o)); + } + return b.toString(); + } else { + return String.valueOf(param); + } + } + + private String formatOffsetDateTime(final OffsetDateTime offsetDateTime) { + return DateTimeFormatter.ISO_OFFSET_DATE_TIME.format(offsetDateTime); + } + + public String collectionPathParameterToString(final CollectionFormat collectionFormat, final Collection values) { + String result; + if (CollectionFormat.MULTI.equals(collectionFormat)) { + result = parameterToString(values); + } + if(collectionFormat == null) { + result = CollectionFormat.CSV.collectionToString(values); + } else { + result = collectionFormat.collectionToString(values); + } + return result; + } + + public MultiValueMap parameterToMultiValueMap(final CollectionFormat collectionFormat, final String name, final Object value) { + final MultiValueMap params = new LinkedMultiValueMap(); + CollectionFormat colFormat = collectionFormat; + if (name == null || name.isEmpty() || value == null) { + return params; + } + + if (colFormat == null) { + colFormat = CollectionFormat.CSV; + } + + if (value instanceof Map) { + @SuppressWarnings("unchecked") + final Map valuesMap = (Map) value; + for (final Entry entry : valuesMap.entrySet()) { + params.add(entry.getKey(), parameterToString(entry.getValue())); + } + return params; + } + + Collection valueCollection = null; + if (value instanceof Collection) { + valueCollection = (Collection) value; + } else { + params.add(name, parameterToString(value)); + return params; + } + + if (valueCollection.isEmpty()) { + return params; + } + + if (colFormat.equals(CollectionFormat.MULTI)) { + for (Object item : valueCollection) { + params.add(name, parameterToString(item)); + } + return params; + } + + List values = new ArrayList(); + for (Object o : valueCollection) { + values.add(parameterToString(o)); + } + params.add(name, colFormat.collectionToString(values)); + + return params; + } + + private boolean isJsonMime(final MediaType mediaType) { + return mediaType != null && (MediaType.APPLICATION_JSON.isCompatibleWith(mediaType) || mediaType.getSubtype().matches("^.*\\+json[;]?\\s*$")); + } + + public List selectHeaderAccept(final String[] accepts) { + if (accepts.length == 0) { + return null; + } + for (String accept : accepts) { + MediaType mediaType = MediaType.parseMediaType(accept); + if (isJsonMime(mediaType) && !"application/problem+json".equalsIgnoreCase(accept)) { + return Collections.singletonList(mediaType); + } + } + return MediaType.parseMediaTypes(StringUtils.arrayToCommaDelimitedString(accepts)); + } + + public MediaType selectHeaderContentType(final String[] contentTypes) { + if (contentTypes.length == 0) { + return MediaType.APPLICATION_JSON; + } + for (String contentType : contentTypes) { + MediaType mediaType = MediaType.parseMediaType(contentType); + if (isJsonMime(mediaType)) { + return mediaType; + } + } + return MediaType.parseMediaType(contentTypes[0]); + } + + public String expandPath(final String pathTemplate, final Map variables) { + final DefaultUriBuilderFactory uriBuilderFactory = new DefaultUriBuilderFactory(); + uriBuilderFactory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.NONE); + final RestTemplate restTemplate = new RestTemplate(); + restTemplate.setUriTemplateHandler(uriBuilderFactory); + return restTemplate.getUriTemplateHandler().expand(pathTemplate, variables).toString(); + } + + protected Object selectBody(final Object obj, final MultiValueMap formParams, final MediaType contentType) { + boolean isForm = MediaType.MULTIPART_FORM_DATA.isCompatibleWith(contentType) || MediaType.APPLICATION_FORM_URLENCODED.isCompatibleWith(contentType); + return isForm ? formParams : obj; + } + + public String generateQueryUri(final MultiValueMap queryParams, final Map uriParams) { + final StringBuilder queryBuilder = new StringBuilder(); + queryParams.forEach((name, values) -> { + try { + final String encodedName = URLEncoder.encode(name.toString(), "UTF-8"); + if (CollectionUtils.isEmpty(values)) { + if (queryBuilder.length() != 0) { + queryBuilder.append('&'); + } + queryBuilder.append(encodedName); + } else { + int valueItemCounter = 0; + for (Object value : values) { + if (queryBuilder.length() != 0) { + queryBuilder.append('&'); + } + queryBuilder.append(encodedName); + if (value != null) { + String templatizedKey = encodedName + valueItemCounter++; + final String encodedValue = URLEncoder.encode(value.toString(), "UTF-8"); + uriParams.put(templatizedKey, encodedValue); + queryBuilder.append('=').append("{").append(templatizedKey).append("}"); + } + } + } + } catch (UnsupportedEncodingException e) { + + } + }); + return queryBuilder.toString(); + } + + public ResponseEntity invokeAPI(final String basePath, final String path, final HttpMethod method, final Map pathParams, + final MultiValueMap queryParams, final Object body, final HttpHeaders headerParams, final MultiValueMap cookieParams, final MultiValueMap formParams, + final List accept, final MediaType contentType, final String[] authNames, final ParameterizedTypeReference returnType) throws RestClientException { + + updateParamsForAuth(authNames, queryParams, headerParams, cookieParams); + Map uriParams = new HashMap<>(); + uriParams.putAll(pathParams); + + String finalUri = path; + + if (queryParams != null && !queryParams.isEmpty()) { + String queryUri = generateQueryUri(queryParams, uriParams); + finalUri += "?" + queryUri; + } + String expandedPath = this.expandPath(finalUri, uriParams); + final UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(basePath).path(expandedPath); + + URI uri; + try { + uri = new URI(builder.build().toUriString()); + } catch(URISyntaxException ex) { + throw new RestClientException("Could not build URL: " + builder.toUriString(), ex); + } + + final BodyBuilder requestBuilder = RequestEntity.method(method, uri); + if(accept != null) { + requestBuilder.accept(accept.toArray(new MediaType[accept.size()])); + } + if(contentType != null) { + requestBuilder.contentType(contentType); + } + + addHeadersToRequest(headerParams, requestBuilder); + addHeadersToRequest(defaultHeaders, requestBuilder); + addCookiesToRequest(cookieParams, requestBuilder); + addCookiesToRequest(defaultCookies, requestBuilder); + + RequestEntity requestEntity = requestBuilder.body(selectBody(body, formParams, contentType)); + + ResponseEntity responseEntity = restTemplate.exchange(requestEntity, returnType); + + if (responseEntity.getStatusCode().is2xxSuccessful()) { + return responseEntity; + } else { + throw new RestClientException("API returned " + responseEntity.getStatusCode() + " and it wasn't handled by the RestTemplate error handler"); + } + } + + protected void addHeadersToRequest(final HttpHeaders headers, final BodyBuilder requestBuilder) { + for (Entry> entry : headers.entrySet()) { + List values = entry.getValue(); + for(String value : values) { + if (value != null) { + requestBuilder.header(entry.getKey(), value); + } + } + } + } + + protected void addCookiesToRequest(final MultiValueMap cookies, final BodyBuilder requestBuilder) { + if (!cookies.isEmpty()) { + requestBuilder.header("Cookie", buildCookieHeader(cookies)); + } + } + + private String buildCookieHeader(final MultiValueMap cookies) { + final StringBuilder cookieValue = new StringBuilder(); + String delimiter = ""; + for (final Map.Entry> entry : cookies.entrySet()) { + final String value = entry.getValue().get(entry.getValue().size() - 1); + cookieValue.append(String.format("%s%s=%s", delimiter, entry.getKey(), value)); + delimiter = "; "; + } + return cookieValue.toString(); + } + + private void updateParamsForAuth(final String[] authNames, final MultiValueMap queryParams, final HttpHeaders headerParams, final MultiValueMap cookieParams) { + for (String authName : authNames) { + Authentication auth = authentications.get(authName); + if (auth == null) { + throw new RestClientException("Authentication undefined: " + authName); + } + auth.applyToParams(queryParams, headerParams, cookieParams); + } + } + + private class ApiClientHttpRequestInterceptor implements ClientHttpRequestInterceptor { + private final Log log = LogFactory.getLog(ApiClientHttpRequestInterceptor.class); + + @Override + public ClientHttpResponse intercept(final HttpRequest request, final byte[] body, final ClientHttpRequestExecution execution) throws IOException { + logRequest(request, body); + ClientHttpResponse response = execution.execute(request, body); + logResponse(response); + return response; + } + + private void logRequest(final HttpRequest request, final byte[] body) throws UnsupportedEncodingException { + log.info("URI: " + request.getURI()); + log.info("HTTP Method: " + request.getMethod()); + log.info("HTTP Headers: " + headersToString(request.getHeaders())); + log.info("Request Body: " + new String(body, StandardCharsets.UTF_8)); + } + + private void logResponse(final ClientHttpResponse response) throws IOException { + log.info("HTTP Status Code: " + response.getRawStatusCode()); + log.info("Status Text: " + response.getStatusText()); + log.info("HTTP Headers: " + headersToString(response.getHeaders())); + log.info("Response Body: " + bodyToString(response.getBody())); + } + + private String headersToString(final HttpHeaders headers) { + final StringBuilder builder = new StringBuilder(); + for(Entry> entry : headers.entrySet()) { + builder.append(entry.getKey()).append("=["); + for(String value : entry.getValue()) { + builder.append(value).append(","); + } + builder.setLength(builder.length() - 1); + builder.append("],"); + } + builder.setLength(builder.length() - 1); + return builder.toString(); + } + + private String bodyToString(final InputStream body) throws IOException { + final StringBuilder builder = new StringBuilder(); + final BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(body, StandardCharsets.UTF_8)); + String line = bufferedReader.readLine(); + while (line != null) { + builder.append(line).append(System.lineSeparator()); + line = bufferedReader.readLine(); + } + bufferedReader.close(); + return builder.toString(); + } + } + +} diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/Authentication.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/Authentication.java new file mode 100644 index 00000000..c9d14ed4 --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/Authentication.java @@ -0,0 +1,9 @@ +package com.sngular.multifileplugin.restclientWithRequestObjects.client.auth; + +import org.springframework.http.HttpHeaders; +import org.springframework.util.MultiValueMap; + +public interface Authentication { + + public void applyToParams(MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams); +} \ No newline at end of file diff --git a/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/HttpBasicAuth.java b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/HttpBasicAuth.java new file mode 100644 index 00000000..22b1afbb --- /dev/null +++ b/multiapi-engine/src/test/resources/openapigenerator/testRestClientApiWithRequestObjectGeneration/assets/client/auth/HttpBasicAuth.java @@ -0,0 +1,37 @@ +package com.sngular.multifileplugin.restclientWithRequestObjects.client.auth; + +import java.io.UnsupportedEncodingException; +import java.nio.charset.StandardCharsets; +import org.springframework.http.HttpHeaders; +import org.springframework.util.Base64Utils; +import org.springframework.util.MultiValueMap; + +public class HttpBasicAuth implements Authentication { + private String username; + private String password; + + public String getUsername() { + return username; + } + + public void setUsername(String username) { + this.username = username; + } + + public String getPassword() { + return password; + } + + public void setPassword(String password) { + this.password = password; + } + + @Override + public void applyToParams(MultiValueMap queryParams, HttpHeaders headerParams, MultiValueMap cookieParams) { + if (username == null && password == null) { + return; + } + String str = (username == null ? "" : username) + ":" + (password == null ? "" : password); + headerParams.add(HttpHeaders.AUTHORIZATION, "Basic " + Base64Utils.encodeToString(str.getBytes(StandardCharsets.UTF_8))); + } +} \ No newline at end of file diff --git a/scs-multiapi-gradle-plugin/build.gradle b/scs-multiapi-gradle-plugin/build.gradle index c71a92b1..cbbb8b4a 100644 --- a/scs-multiapi-gradle-plugin/build.gradle +++ b/scs-multiapi-gradle-plugin/build.gradle @@ -20,7 +20,7 @@ repositories { } group = 'com.sngular' -version = '5.4.1' +version = '5.4.2' def SCSMultiApiPluginGroupId = group def SCSMultiApiPluginVersion = version @@ -30,7 +30,7 @@ dependencies { shadow localGroovy() shadow gradleApi() - implementation 'com.sngular:multiapi-engine:5.4.1' + implementation 'com.sngular:multiapi-engine:5.4.2' testImplementation 'org.assertj:assertj-core:3.24.2' testImplementation 'com.puppycrawl.tools:checkstyle:10.12.3' } @@ -98,7 +98,7 @@ testing { integrationTest(JvmTestSuite) { dependencies { - implementation 'com.sngular:scs-multiapi-gradle-plugin:5.4.1' + implementation 'com.sngular:scs-multiapi-gradle-plugin:5.4.2' implementation 'org.assertj:assertj-core:3.24.2' } diff --git a/scs-multiapi-maven-plugin/pom.xml b/scs-multiapi-maven-plugin/pom.xml index ac9a63c4..3c60dc4a 100644 --- a/scs-multiapi-maven-plugin/pom.xml +++ b/scs-multiapi-maven-plugin/pom.xml @@ -4,7 +4,7 @@ com.sngular scs-multiapi-maven-plugin - 5.4.1 + 5.4.2 maven-plugin AsyncApi - OpenApi Code Generator Maven Plugin @@ -213,6 +213,17 @@ Europe/Madrid + + oscar-ares + Oscar Ares Bascon + oscar.ares@sngular.com + Sngular + https://www.sngular.com + + Software Developer - Trainee + + Europe/Madrid + @@ -260,7 +271,7 @@ com.sngular multiapi-engine - 5.4.1 + 5.4.2 org.apache.maven From f1c42c54172dde1abc2e789844655e83c8405dc0 Mon Sep 17 00:00:00 2001 From: gtjarks <4910285+gtjarks@users.noreply.github.com> Date: Sun, 4 Aug 2024 18:28:04 +0200 Subject: [PATCH 4/4] Fix use wrong suffix varialble in stream templates (#345) --- .../main/resources/templates/asyncapi/templateStreamBridge.ftlh | 2 +- .../asyncapi/templateStreamBridgeWithKafkaBindings.ftlh | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridge.ftlh b/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridge.ftlh index f404a3c3..6ec14a6a 100644 --- a/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridge.ftlh +++ b/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridge.ftlh @@ -16,7 +16,7 @@ public class ${streamBridgeClassName?cap_first} { } <#list streamBridgeMethods as method> - public void ${method.operationId?uncap_first}(final ${method.className}<#if streamBridgeEntitiesSuffix?has_content>${subscribeEntitiesSuffix} ${method.className?uncap_first}) { + public void ${method.operationId?uncap_first}(final ${method.className}<#if streamBridgeEntitiesSuffix?has_content>${streamBridgeEntitiesSuffix} ${method.className?uncap_first}) { streamBridge.send("${method.channelName}", ${method.className?uncap_first}); } diff --git a/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridgeWithKafkaBindings.ftlh b/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridgeWithKafkaBindings.ftlh index e504aa91..4cc5dc1d 100644 --- a/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridgeWithKafkaBindings.ftlh +++ b/multiapi-engine/src/main/resources/templates/asyncapi/templateStreamBridgeWithKafkaBindings.ftlh @@ -19,7 +19,7 @@ public class ${streamBridgeClassName?cap_first} { } <#list streamBridgeMethods as method> - public void ${method.operationId?uncap_first}(final ${method.className}<#if streamBridgeEntitiesSuffix?has_content>${subscribeEntitiesSuffix} ${method.className?uncap_first}, final ${method.keyClassName}<#if streamBridgeEntitiesSuffix?has_content>${subscribeEntitiesSuffix} ${method.keyClassName?uncap_first}) { + public void ${method.operationId?uncap_first}(final ${method.className}<#if streamBridgeEntitiesSuffix?has_content>${streamBridgeEntitiesSuffix} ${method.className?uncap_first}, final ${method.keyClassName}<#if streamBridgeEntitiesSuffix?has_content>${streamBridgeEntitiesSuffix} ${method.keyClassName?uncap_first}) { final var message = MessageBuilder.withPayload(${method.className?uncap_first}).setHeader("key", ${method.keyClassName?uncap_first}).build(); streamBridge.send("${method.channelName}", message); }