From e6fe9e1ed5772717006a75c68603c5b0f1014e9c Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 8 Feb 2022 09:37:55 -0300 Subject: [PATCH 1/9] Introducing OpenAPI Generator Signed-off-by: Ricardo Zanini --- deployment/pom.xml | 125 +- .../deployment/CodeGenConfiguration.java | 38 + .../codegen/OpenApiGeneratorCodeGenBase.java | 73 + .../codegen/OpenApiGeneratorJsonCodeGen.java | 14 + .../codegen/OpenApiGeneratorYamlCodeGen.java | 14 + .../codegen/OpenApiGeneratorYmlCodeGen.java | 14 + .../template/QuteTemplatingEngineAdapter.java | 73 + .../OpenApiClientGeneratorWrapper.java | 64 + .../wrapper/QuarkusCodegenConfigurator.java | 15 + .../wrapper/QuarkusJavaClientCodegen.java | 32 + .../io.quarkus.deployment.CodeGenProvider | 3 + .../org.openapitools.codegen.CodegenConfig | 1 + ...itools.codegen.api.TemplatingEngineAdapter | 1 + .../additionalEnumTypeAnnotations.qute | 1 + .../additionalModelTypeAnnotations.qute | 1 + .../src/main/resources/templates/api.qute | 52 + .../resources/templates/beanValidation.qute | 2 + .../templates/beanValidationCore.qute | 6 + .../templates/beanValidationHeaderParams.qute | 1 + .../main/resources/templates/bodyParams.qute | 1 + .../main/resources/templates/enumClass.qute | 39 + .../resources/templates/enumOuterClass.qute | 30 + .../main/resources/templates/formParams.qute | 1 + .../resources/templates/headerParams.qute | 1 + .../src/main/resources/templates/model.qute | 19 + .../main/resources/templates/pathParams.qute | 1 + .../src/main/resources/templates/pojo.qute | 145 ++ .../main/resources/templates/queryParams.qute | 1 + .../QuteTemplatingEngineAdapterTest.java | 37 + .../OpenApiClientGeneratorWrapperTest.java | 27 + .../test/OpenapiGeneratorDevModeTest.java | 2 +- .../generator/test/OpenapiGeneratorTest.java | 2 +- .../resources/openapi/petstore-openapi.json | 1225 +++++++++++++++++ .../src/test/resources/templates/hello.qute | 1 + integration-tests/pom.xml | 22 +- .../it/OpenapiGeneratorResource.java | 32 - .../src/main/resources/application.properties | 0 .../it/NativeOpenapiGeneratorResourceIT.java | 7 - .../it/OpenapiGeneratorResourceTest.java | 21 - .../openapi/generator/it/PetResource.java | 26 + .../openapi/generator/it/PetStoreTest.java | 34 + .../generator/it/WiremockPetStore.java | 42 + .../src/test/openapi/petstore.json | 1225 +++++++++++++++++ .../src/test/resources/application.properties | 2 + runtime/pom.xml | 14 +- .../openapi/generator/RuntimeConfig.java | 8 + .../resources/META-INF/quarkus-extension.yaml | 18 +- 47 files changed, 3437 insertions(+), 76 deletions(-) create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorJsonCodeGen.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYamlCodeGen.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYmlCodeGen.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapter.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusCodegenConfigurator.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java create mode 100644 deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider create mode 100644 deployment/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig create mode 100644 deployment/src/main/resources/META-INF/services/org.openapitools.codegen.api.TemplatingEngineAdapter create mode 100644 deployment/src/main/resources/templates/additionalEnumTypeAnnotations.qute create mode 100644 deployment/src/main/resources/templates/additionalModelTypeAnnotations.qute create mode 100644 deployment/src/main/resources/templates/api.qute create mode 100644 deployment/src/main/resources/templates/beanValidation.qute create mode 100644 deployment/src/main/resources/templates/beanValidationCore.qute create mode 100644 deployment/src/main/resources/templates/beanValidationHeaderParams.qute create mode 100644 deployment/src/main/resources/templates/bodyParams.qute create mode 100644 deployment/src/main/resources/templates/enumClass.qute create mode 100644 deployment/src/main/resources/templates/enumOuterClass.qute create mode 100644 deployment/src/main/resources/templates/formParams.qute create mode 100644 deployment/src/main/resources/templates/headerParams.qute create mode 100644 deployment/src/main/resources/templates/model.qute create mode 100644 deployment/src/main/resources/templates/pathParams.qute create mode 100644 deployment/src/main/resources/templates/pojo.qute create mode 100644 deployment/src/main/resources/templates/queryParams.qute create mode 100644 deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapterTest.java create mode 100644 deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java create mode 100644 deployment/src/test/resources/openapi/petstore-openapi.json create mode 100644 deployment/src/test/resources/templates/hello.qute delete mode 100644 integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResource.java delete mode 100644 integration-tests/src/main/resources/application.properties delete mode 100644 integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/NativeOpenapiGeneratorResourceIT.java delete mode 100644 integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResourceTest.java create mode 100644 integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java create mode 100644 integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java create mode 100644 integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java create mode 100644 integration-tests/src/test/openapi/petstore.json create mode 100644 integration-tests/src/test/resources/application.properties create mode 100644 runtime/src/main/java/io/quarkiverse/openapi/generator/RuntimeConfig.java diff --git a/deployment/pom.xml b/deployment/pom.xml index fa7e5fbb..147de13b 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 io.quarkiverse.openapi.generator @@ -9,16 +9,137 @@ quarkus-openapi-generator-deployment Quarkus - Openapi Generator - Deployment + + + 5.4.0 + 1.7.29 + 4.2.0 + 1.6.2 + 2.1.5 + + + io.quarkus - quarkus-arc-deployment + quarkus-core-deployment + + + io.quarkus + quarkus-rest-client-jackson-deployment + + + io.quarkus + quarkus-devtools-utilities + + ${quarkus.version} + + + io.quarkus + quarkus-qute-deployment io.quarkiverse.openapi.generator quarkus-openapi-generator ${project.version} + + + + org.openapitools + openapi-generator + ${version.org.openapitools} + + + org.slf4j + slf4j-simple + + + org.slf4j + slf4j-api + + + commons-cli + commons-cli + + + commons-logging + commons-logging + + + org.checkerframework + checker-qual + + + javax.validation + validation-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + + io.swagger.core.v3 + swagger-core + + + io.swagger + swagger-core + + + joda-time + joda-time + + + org.slf4j + slf4j-ext + + + com.github.jknack + * + + + + + + io.swagger.core.v3 + swagger-core + ${version.io.swagger.core.v3} + + + javax.validation + validation-api + + + jakarta.xml.bind + jakarta.xml.bind-api + + + + + io.swagger + swagger-core + ${version.io.swagger} + + + javax.validation + validation-api + + + + + org.slf4j + slf4j-ext + ${version.org.slf4j} + + + + com.github.jknack + handlebars-jackson2 + ${version.com.github.jknack} + + + io.quarkus quarkus-junit5-internal diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java new file mode 100644 index 00000000..ff2721e2 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java @@ -0,0 +1,38 @@ +package io.quarkiverse.openapi.generator.deployment; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME, prefix = "quarkus.openapi-generator") +public class CodeGenConfiguration { + /** + * Defines the base package name for the generated API classes + */ + @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.api") + String apiPackage; + + /** + * Defines the base package name for the generated Model classes + */ + @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.model") + String modelPackage; + + /** + * Increases the internal generator log output verbosity + */ + @ConfigItem(defaultValue = "false") + String verbose; + + public String getApiPackage() { + return apiPackage; + } + + public String getModelPackage() { + return modelPackage; + } + + public String getVerbose() { + return verbose; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java new file mode 100644 index 00000000..68c34d14 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java @@ -0,0 +1,73 @@ +package io.quarkiverse.openapi.generator.deployment.codegen; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiClientGeneratorWrapper; +import io.quarkus.bootstrap.prebuild.CodeGenException; +import io.quarkus.deployment.CodeGenContext; +import io.quarkus.deployment.CodeGenProvider; +import io.quarkus.utilities.OS; + +/** + * Code generation for OpenApi Client. Generates Java classes from OpenApi spec files located in src/main/openapi or + * src/test/openapi + *

+ * Wraps the OpenAPI Generator Client for Java + */ +public abstract class OpenApiGeneratorCodeGenBase implements CodeGenProvider { + + static final String YAML = ".yaml"; + static final String YML = ".yml"; + static final String JSON = ".json"; + + @Override + public String inputDirectory() { + return "openapi"; + } + + @Override + public boolean trigger(CodeGenContext context) throws CodeGenException { + final Path outDir = context.outDir(); + final Path openApiDir = context.inputDir(); + final String apiPackage = context.config().getConfigValue("quarkus.openapi-generator.codegen.api-package").getValue(); + final String modelPackage = context.config().getConfigValue("quarkus.openapi-generator.codegen.model-package") + .getValue(); + + try { + if (Files.isDirectory(openApiDir)) { + try (Stream openApiFilesPaths = Files.walk(openApiDir)) { + final List openApiFiles = openApiFilesPaths + .filter(Files::isRegularFile) + .map(Path::toString) + .filter(s -> s.endsWith(this.inputExtension())) + .map(this::escapeWhitespace) + .collect(Collectors.toList()); + for (String openApiFile : openApiFiles) { + final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(openApiFile, + outDir.toString()) + .withApiPackage(apiPackage) + .withModelPackage(modelPackage); + generator.generate(); + } + return true; + } + } + } catch (IOException e) { + throw new CodeGenException("Failed to generate java files from OpenApi files in " + openApiDir.toAbsolutePath(), e); + } + return false; + } + + private String escapeWhitespace(String path) { + if (OS.determineOS() == OS.LINUX) { + return path.replace(" ", "\\ "); + } else { + return path; + } + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorJsonCodeGen.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorJsonCodeGen.java new file mode 100644 index 00000000..373f7895 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorJsonCodeGen.java @@ -0,0 +1,14 @@ +package io.quarkiverse.openapi.generator.deployment.codegen; + +public class OpenApiGeneratorJsonCodeGen extends OpenApiGeneratorCodeGenBase { + + @Override + public String providerId() { + return "open-api-json"; + } + + @Override + public String inputExtension() { + return JSON; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYamlCodeGen.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYamlCodeGen.java new file mode 100644 index 00000000..69d57577 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYamlCodeGen.java @@ -0,0 +1,14 @@ +package io.quarkiverse.openapi.generator.deployment.codegen; + +public class OpenApiGeneratorYamlCodeGen extends OpenApiGeneratorCodeGenBase { + + @Override + public String providerId() { + return "open-api-yaml"; + } + + @Override + public String inputExtension() { + return YAML; + } +} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYmlCodeGen.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYmlCodeGen.java new file mode 100644 index 00000000..3ee549e4 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorYmlCodeGen.java @@ -0,0 +1,14 @@ +package io.quarkiverse.openapi.generator.deployment.codegen; + +public class OpenApiGeneratorYmlCodeGen extends OpenApiGeneratorCodeGenBase { + + @Override + public String providerId() { + return "open-api-yml"; + } + + @Override + public String inputExtension() { + return YML; + } +} \ No newline at end of file diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapter.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapter.java new file mode 100644 index 00000000..feaefcb2 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapter.java @@ -0,0 +1,73 @@ +package io.quarkiverse.openapi.generator.deployment.template; + +import java.io.IOException; +import java.util.Map; + +import org.openapitools.codegen.api.AbstractTemplatingEngineAdapter; +import org.openapitools.codegen.api.TemplatingExecutor; + +import io.quarkus.qute.Engine; +import io.quarkus.qute.ReflectionValueResolver; +import io.quarkus.qute.Template; + +public class QuteTemplatingEngineAdapter extends AbstractTemplatingEngineAdapter { + + public static final String IDENTIFIER = "qute"; + public static final String[] INCLUDE_TEMPLATES = { + "additionalEnumTypeAnnotations.qute", + "additionalModelTypeAnnotations.qute", + "beanValidation.qute", + "beanValidationCore.qute", + "beanValidationHeaderParams.qute", + "bodyParams.qute", + "enumClass.qute", + "enumOuterClass.qute", + "formParams.qute", + "headerParams.qute", + "pathParams.qute", + "pojo.qute", + "queryParams.qute" + }; + public final Engine engine; + + public QuteTemplatingEngineAdapter() { + this.engine = Engine.builder() + .addDefaults() + .addValueResolver(new ReflectionValueResolver()) + .removeStandaloneLines(true) + .strictRendering(false) + .build(); + } + + @Override + public String getIdentifier() { + return IDENTIFIER; + } + + @Override + public String[] getFileExtensions() { + return new String[] { IDENTIFIER }; + } + + @Override + public String compileTemplate(TemplatingExecutor executor, Map bundle, String templateFile) + throws IOException { + this.cacheTemplates(executor); + Template template = engine.getTemplate(templateFile); + if (template == null) { + template = engine.parse(executor.getFullTemplateContents(templateFile)); + engine.putTemplate(templateFile, template); + } + return template.data(bundle).render(); + } + + public void cacheTemplates(TemplatingExecutor executor) { + for (String templateId : INCLUDE_TEMPLATES) { + Template incTemplate = engine.getTemplate(templateId); + if (incTemplate == null) { + incTemplate = engine.parse(executor.getFullTemplateContents(templateId)); + engine.putTemplate(templateId, incTemplate); + } + } + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java new file mode 100644 index 00000000..75bc04b3 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapper.java @@ -0,0 +1,64 @@ +package io.quarkiverse.openapi.generator.deployment.wrapper; + +import static java.lang.Boolean.FALSE; +import static java.lang.Boolean.TRUE; + +import java.io.File; +import java.util.List; + +import org.openapitools.codegen.CodegenConstants; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.config.GlobalSettings; + +/** + * Wrapper for the OpenAPIGen tool. + * This is the same as calling the Maven plugin or the CLI. + * We are wrapping into a class to generate code that meet our requirements. + * + * @see OpenAPI Generator Client for Java + */ +public class OpenApiClientGeneratorWrapper { + + private static final String VERBOSE = "verbose"; + private static final String ONCE_LOGGER = "org.openapitools.codegen.utils.oncelogger.enabled"; + + private final QuarkusCodegenConfigurator configurator; + private final DefaultGenerator generator; + + public OpenApiClientGeneratorWrapper(final String specFilePath, final String outputDir) { + // do not generate docs nor tests + GlobalSettings.setProperty(CodegenConstants.API_DOCS, FALSE.toString()); + GlobalSettings.setProperty(CodegenConstants.API_TESTS, FALSE.toString()); + GlobalSettings.setProperty(CodegenConstants.MODEL_TESTS, FALSE.toString()); + GlobalSettings.setProperty(CodegenConstants.MODEL_DOCS, FALSE.toString()); + // generates every Api and Supporting files + // TODO: requires more testing to properly filter the generated classes + GlobalSettings.setProperty(CodegenConstants.APIS, ""); + GlobalSettings.setProperty(CodegenConstants.MODELS, ""); + GlobalSettings.setProperty(CodegenConstants.SUPPORTING_FILES, ""); + // logging + GlobalSettings.setProperty(VERBOSE, FALSE.toString()); + GlobalSettings.setProperty(ONCE_LOGGER, TRUE.toString()); + + this.configurator = new QuarkusCodegenConfigurator(); + this.configurator.setInputSpec(specFilePath); + this.configurator.setOutputDir(outputDir); + this.generator = new DefaultGenerator(); + } + + public OpenApiClientGeneratorWrapper withApiPackage(final String pkg) { + this.configurator.setApiPackage(pkg); + this.configurator.setInvokerPackage(pkg); + return this; + } + + public OpenApiClientGeneratorWrapper withModelPackage(final String pkg) { + this.configurator.setModelPackage(pkg); + return this; + } + + public List generate() { + return generator.opts(configurator.toClientOptInput()).generate(); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusCodegenConfigurator.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusCodegenConfigurator.java new file mode 100644 index 00000000..54d2bd77 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusCodegenConfigurator.java @@ -0,0 +1,15 @@ +package io.quarkiverse.openapi.generator.deployment.wrapper; + +import org.openapitools.codegen.config.CodegenConfigurator; +import org.openapitools.codegen.languages.JavaClientCodegen; + +public class QuarkusCodegenConfigurator extends CodegenConfigurator { + + public QuarkusCodegenConfigurator() { + // immutable properties + this.setGeneratorName("quarkus"); + this.setTemplatingEngineName("qute"); + this.setLibrary(JavaClientCodegen.MICROPROFILE); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java new file mode 100644 index 00000000..c740922e --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/wrapper/QuarkusJavaClientCodegen.java @@ -0,0 +1,32 @@ +package io.quarkiverse.openapi.generator.deployment.wrapper; + +import org.openapitools.codegen.languages.JavaClientCodegen; + +public class QuarkusJavaClientCodegen extends JavaClientCodegen { + + public QuarkusJavaClientCodegen() { + // TODO: immutable properties + this.setDateLibrary(JavaClientCodegen.JAVA8_MODE); + this.setTemplateDir("templates"); + // we are only interested in the main generated classes + this.projectFolder = ""; + this.projectTestFolder = ""; + this.sourceFolder = ""; + this.testFolder = ""; + } + + @Override + public String getName() { + return "quarkus"; + } + + @Override + public void processOpts() { + super.processOpts(); + supportingFiles.clear(); + apiTemplateFiles.clear(); + apiTemplateFiles.put("api.qute", ".java"); + modelTemplateFiles.clear(); + modelTemplateFiles.put("model.qute", ".java"); + } +} diff --git a/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider b/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider new file mode 100644 index 00000000..84a050e5 --- /dev/null +++ b/deployment/src/main/resources/META-INF/services/io.quarkus.deployment.CodeGenProvider @@ -0,0 +1,3 @@ +io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorJsonCodeGen +io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorYamlCodeGen +io.quarkiverse.openapi.generator.deployment.codegen.OpenApiGeneratorYmlCodeGen diff --git a/deployment/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig b/deployment/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig new file mode 100644 index 00000000..4940f583 --- /dev/null +++ b/deployment/src/main/resources/META-INF/services/org.openapitools.codegen.CodegenConfig @@ -0,0 +1 @@ +io.quarkiverse.openapi.generator.deployment.wrapper.QuarkusJavaClientCodegen diff --git a/deployment/src/main/resources/META-INF/services/org.openapitools.codegen.api.TemplatingEngineAdapter b/deployment/src/main/resources/META-INF/services/org.openapitools.codegen.api.TemplatingEngineAdapter new file mode 100644 index 00000000..f2d26a66 --- /dev/null +++ b/deployment/src/main/resources/META-INF/services/org.openapitools.codegen.api.TemplatingEngineAdapter @@ -0,0 +1 @@ +io.quarkiverse.openapi.generator.deployment.template.QuteTemplatingEngineAdapter diff --git a/deployment/src/main/resources/templates/additionalEnumTypeAnnotations.qute b/deployment/src/main/resources/templates/additionalEnumTypeAnnotations.qute new file mode 100644 index 00000000..1ee7c939 --- /dev/null +++ b/deployment/src/main/resources/templates/additionalEnumTypeAnnotations.qute @@ -0,0 +1 @@ +{#for annon in e.additionalEnumTypeAnnotations.orEmpty}{annon}{/for} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/additionalModelTypeAnnotations.qute b/deployment/src/main/resources/templates/additionalModelTypeAnnotations.qute new file mode 100644 index 00000000..0f55806e --- /dev/null +++ b/deployment/src/main/resources/templates/additionalModelTypeAnnotations.qute @@ -0,0 +1 @@ +{#for annon in m.additionalModelTypeAnnotations.orEmpty}{annon}{/for} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/api.qute b/deployment/src/main/resources/templates/api.qute new file mode 100644 index 00000000..df003c20 --- /dev/null +++ b/deployment/src/main/resources/templates/api.qute @@ -0,0 +1,52 @@ +package {package}; + +{#for imp in imports} +import {imp.import}; +{/for} + +import org.eclipse.microprofile.rest.client.inject.RegisterRestClient; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.List; +import java.util.Map; +import javax.ws.rs.*; +import javax.ws.rs.core.Response; +import javax.ws.rs.core.MediaType; + + +{#if appName} +/** + * {appName} + * {#if appDescription}

{appDescription}{/if} + */ +{/if} +@Path("{#if useAnnotatedBasePath}{contextPath}{/if}{commonPath}") +@RegisterRestClient +public interface {classname} { + + {#for op in operations.operation} + {#if op.summary} + /** + * {op.summary} + * + {#if op.notes} + * {op.notes} + * + {/if} + */ + {/if} + @{op.httpMethod} + {#if op.subresourceOperation} + @Path("{op.path}") + {/if} + {#if op.hasConsumes} + @Consumes(\{{#for consume in op.consumes}"{consume.mediaType}"{#if consume_hasNext}, {/if}{/for}\}) + {/if} + {#if op.hasProduces} + @Produces(\{{#for produce in op.produces}"{produce.mediaType}"{#if produce_hasNext}, {/if}{/for}\}) + {/if} + public {#if op.returnType}{op.returnType}{#else}void{/if} {op.nickname}({#for p in op.allParams}{#include pathParams.qute param=p/}{#include queryParams.qute param=p/}{#include bodyParams.qute param=p/}{#include formParams.qute param=p/}{#include headerParams.qute param=p/}{#if p_hasNext}, {/if}{/for}); + + {/for} +} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/beanValidation.qute b/deployment/src/main/resources/templates/beanValidation.qute new file mode 100644 index 00000000..609107ba --- /dev/null +++ b/deployment/src/main/resources/templates/beanValidation.qute @@ -0,0 +1,2 @@ +{#if p.required} @NotNull{/if} +{#include beanValidationCore.qute p=p/} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/beanValidationCore.qute b/deployment/src/main/resources/templates/beanValidationCore.qute new file mode 100644 index 00000000..2edfa82a --- /dev/null +++ b/deployment/src/main/resources/templates/beanValidationCore.qute @@ -0,0 +1,6 @@ +{#if p.pattern} @Pattern(regexp="{p.pattern}"){/if} +{#if p.minLength || p.minItems} @Size(min={p.minLength}{p.minItems}){/if} +{#if p.maxLength || p.maxItems} @Size(max={p.minLength}{p.minItems}){/if} +{#if p.isInteger}{#if p.minimum} @Min({p.minimum}){/if}{#if p.maximum} @Max({p.maximum}){/if}{/if} +{#if p.isLong}{#if p.minimum} @Min({p.minimum}L){/if}{#if p.maximum} @Max({p.maximum}L){/if}{/if} +{#if !p.isInteger && !p.isLong}{#if p.minimum} @DecimalMin("{minimum}"){/if}{#if p.maximum} @DecimalMax("{maximum}"){/if}{/if} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/beanValidationHeaderParams.qute b/deployment/src/main/resources/templates/beanValidationHeaderParams.qute new file mode 100644 index 00000000..15fa1752 --- /dev/null +++ b/deployment/src/main/resources/templates/beanValidationHeaderParams.qute @@ -0,0 +1 @@ +{#if param.required} @NotNull{/if}{#include beanValidationCore.qute p=param/} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/bodyParams.qute b/deployment/src/main/resources/templates/bodyParams.qute new file mode 100644 index 00000000..b3a7fe49 --- /dev/null +++ b/deployment/src/main/resources/templates/bodyParams.qute @@ -0,0 +1 @@ +{#if param.isBodyParam}{#if param.useBeanValidation}@Valid {/if}{param.dataType} {param.paramName}{/if} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/enumClass.qute b/deployment/src/main/resources/templates/enumClass.qute new file mode 100644 index 00000000..2b5aa8a2 --- /dev/null +++ b/deployment/src/main/resources/templates/enumClass.qute @@ -0,0 +1,39 @@ +{#if e.withXml} + @XmlType(name="{e.datatypeWithEnum}") + @XmlEnum({e.dataType}.class) +{/if} + {#include additionalEnumTypeAnnotations.qute e=e /}public enum {e.datatypeWithEnum} { + {#if e.allowableValues} + {#if e.withXml} + {#for v in e.allowableValues.enumVars}@XmlEnumValue({#if v.isInteger || v.isDouble || v.isLong || v.isFloat}"{/if}{v.value}{#if v.isInteger || v.isDouble || v.isLong || v.isFloat}"{/if}) {v.name}({e.dataType}.valueOf({v.value})){#if v_hasNext}, {#else}; {/if}{/for} + {#else} + {#for v in e.allowableValues.enumVars}{v.name}({e.dataType}.valueOf({v.value})){#if v_hasNext}, {#else};{/if}{/for} + {/if} + {/if} + + {e.dataType} value; + + {e.datatypeWithEnum} ({e.dataType} v) { + value = v; + } + + public {e.dataType} value() { + return value; + } + + @Override + public String toString() { + return String.valueOf(value); + } + + {#if e.withXml} + public static {e.datatypeWithEnum} fromValue(String, v) { + for ({#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if} b : {#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if}.values()) { + if (String.valueOf(b.value).equals(v)) { + return b; + } + } + {#if e.useNullForUnknownEnumValue}return null;{#else}throw new IllegalArgumentException("Unexpected value '" + v + "'");{/if} + } + {/if} + } \ No newline at end of file diff --git a/deployment/src/main/resources/templates/enumOuterClass.qute b/deployment/src/main/resources/templates/enumOuterClass.qute new file mode 100644 index 00000000..6511d430 --- /dev/null +++ b/deployment/src/main/resources/templates/enumOuterClass.qute @@ -0,0 +1,30 @@ +import com.fasterxml.jackson.annotation.JsonCreator; +import com.fasterxml.jackson.annotation.JsonValue; + +/** + * {#insert e.description}Gets or Sets {e.name}{/}{#if e.description}{description}{/} + */ +@JsonIgnoreProperties(ignoreUnknown = true) +{#include additionalEnumTypeAnnotations.qute e=e/}public enum {#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if} { + private {e.dataType} value; + + {#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if}({e.dataType} value){ + this.value = value; + } + + @Override + @JsonValue + public String toString() { + return String.valueOf(value); + } + + @JsonCreator + public static {#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if} fromValue(String text) { + for ({#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if} b : {#if e.datatypeWithEnum}{e.datatypeWithEnum}{#else}{e.classname}{/if}.values()) { + if (String.valueOf(b.value).equals(text)) { + return b; + } + } + {#if e.useNullForUnknownEnumValue}return null;{#else}throw new IllegalArgumentException("Unexpected value '" + text + "'");{/if} + } +} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/formParams.qute b/deployment/src/main/resources/templates/formParams.qute new file mode 100644 index 00000000..bbbabe46 --- /dev/null +++ b/deployment/src/main/resources/templates/formParams.qute @@ -0,0 +1 @@ +{#if param.isFormParam}{#insert param.isFile}@FormParam(value = "{param.baseName}"{#if param.required}, required = false{/if}) {param.dataType} {param.paramName}{/}{#if param.isFile}@FormParam(value = "{param.baseName}"{#if param.required}, required = false{/if}) Attachment {param.paramName}Detail{/if}{/if} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/headerParams.qute b/deployment/src/main/resources/templates/headerParams.qute new file mode 100644 index 00000000..83289946 --- /dev/null +++ b/deployment/src/main/resources/templates/headerParams.qute @@ -0,0 +1 @@ +{#if param.isHeaderParam}@HeaderParam("{param.baseName}"){#if param.useBeanValidation}{#include beanValidationHeaderParams.qute param=param/}{/if} {param.dataType} {param.paramName}{/if} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/model.qute b/deployment/src/main/resources/templates/model.qute new file mode 100644 index 00000000..3d6b21c2 --- /dev/null +++ b/deployment/src/main/resources/templates/model.qute @@ -0,0 +1,19 @@ +package {package}; + +{#for imp in imports} +import {imp.import}; +{/for} +{#if serializableModel} + +import java.io.Serializable; +{/if} +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +{#if useBeanValidation} + +import javax.validation.constraints.*; +import javax.validation.Valid; +{/if} +{#for m in models} +{#if m.model.isEnum}{#include enumOuterClass.qute e=m.model/} +{#else}{#include pojo.qute m=m.model withXml=withXml/}{/if} +{/for} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/pathParams.qute b/deployment/src/main/resources/templates/pathParams.qute new file mode 100644 index 00000000..b5f1d641 --- /dev/null +++ b/deployment/src/main/resources/templates/pathParams.qute @@ -0,0 +1 @@ +{#if param.isPathParam}@PathParam("{param.baseName}"){#if param.useBeanValidation}{#include beanValidationCore.qute p=param/}{/if} {param.dataType} {param.paramName}{/if} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/pojo.qute b/deployment/src/main/resources/templates/pojo.qute new file mode 100644 index 00000000..3fae0685 --- /dev/null +++ b/deployment/src/main/resources/templates/pojo.qute @@ -0,0 +1,145 @@ +{#if withXml} +import javax.xml.bind.annotation.XmlElement; +import javax.xml.bind.annotation.XmlRootElement; +import javax.xml.bind.annotation.XmlAccessType; +import javax.xml.bind.annotation.XmlAccessorType; +import javax.xml.bind.annotation.XmlType; +import javax.xml.bind.annotation.XmlEnum; +import javax.xml.bind.annotation.XmlEnumValue; +{#else} +import java.lang.reflect.Type; + +import com.fasterxml.jackson.annotation.JsonProperty; +{/if} + +{#if withXml} +@XmlAccessorType(XmlAccessType.FIELD) +{#if m.hasVars}@XmlType(name = "{m.classname}", propOrder = + { {#for var in m.vars}"{var.name}"{#if var_hasNext}, {/if}{/for} +}){#else} +@XmlType(name = "{m.classname}") +{/if} +{#if !m.parent || m.parent.isEmpty}@XmlRootElement(name = "{m.classname}"){/if} +{#else} +@JsonIgnoreProperties(ignoreUnknown = true) +{/if} +{#if m.description} +/** + * {m.description} + **/ +{/if} +{#include additionalModelTypeAnnotations.qute m=m/} +public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if m.serializableModel} implements Serializable{/if} { + + {#for v in m.vars} + {#if v.isEnum} + {#if v.isContainer && v.mostInnerItems} + {#include enumClass.qute e=v/} + {#else if !v.isContainer} + + {#include enumClass.qute e=v/}{/if} + {/if} + {#if withXml} + @XmlElement(name="{v.basename}"{#if v.required}, required = {v.required}{/if}) + {/if} + {#if m.description} + /** + * {m.description} + **/ + {/if} + {#if v.isContainer} + private {v.datatypeWithEnum} {v.name}{#if v.required} = {v.defaultValue}{#else} = null{/if}; + {#else} + private {v.datatypeWithEnum} {v.name}{#if v.defaultValue} = {v.defaultValue}{/if}; + {/if} + {/for} + + {#for v in m.vars} + /** + {#if v.description} + * {v.description} + {#else} + * Get {v.name} + {/if} + {#if v.minimum} + * minimum: {v.minimum} + {/if} + {#if v.maximum} + * maximum: {v.maximum} + {/if} + * @return {v.name} + **/ + {#if !withXml} + @JsonProperty("{v.baseName}") + {/if} + {#for ext in v.vendorExtensions.x-extra-annotation.orEmpty} + {ext} + {/for} + {#if v.useBeanValidation}{#include beanValidation.qute p=v/}{/if} + {#if v.isEnum && !v.isArray && !v.isMap}public {v.dataType} {v.getter}() { + if({v.name} == null) { + return null; + } + return {v.name}.value(); + }{#else if !withXml && v.isEnum && !v.isArray && !v.isMap}public {v.datatypeWithEnum} {v.getter}() { + return {v.name}; + }{#else if v.isEnum && (v.isArray || v.isMap)}public {v.datatypeWithEnum} {v.getter}() { + return {v.name}; + }{#else if !v.isEnum}public {v.datatypeWithEnum} {v.getter}() { + return {v.name}; + }{/if} + + {#if !v.isReadOnly} + /** + * Set {v.name} + **/ + public void {v.setter}({v.datatypeWithEnum} {v.name}) { + this.{v.name} = {v.name}; + } + + public {m.classname} {v.name}({v.datatypeWithEnum} {v.name}) { + this.{v.name} = {v.name}; + return this; + } + {#if v.isArray} + public {m.classname} add{v.nameInCamelCase}Item({v.items.datatypeWithEnum} {v.name}Item) { + this.{v.name}.add({v.name}Item); + return this; + } + {/if} + {#if v.isMap} + public {m.classname} put{v.nameInCamelCase}Item(String key, {v.items.datatypeWithEnum} {v.name}Item) { + this.{v.name}.put(key, {v.name}Item); + return this; + } + {/if} + {/if} + + {/for} + /** + * Create a string representation of this pojo. + **/ + @Override + public String toString() { + StringBuilder sb = new StringBuilder(); + sb.append("class {m.classname} {\n"); + {#if m.parent} + sb.append(" ").append(toIndentedString(super.toString())).append("\n");{/if} + {#for v in m.vars} + sb.append(" {v.name}: ").append(toIndentedString({v.name})).append("\n"); + {/for} + sb.append("}"); + return sb.toString(); + } + + /** + * Convert the given object to string with each line indented by 4 spaces + * (except the first line). + */ + private static String toIndentedString(Object o) { + if (o == null) { + return "null"; + } + return o.toString().replace("\n", "\n "); + } +} \ No newline at end of file diff --git a/deployment/src/main/resources/templates/queryParams.qute b/deployment/src/main/resources/templates/queryParams.qute new file mode 100644 index 00000000..c4e4ae70 --- /dev/null +++ b/deployment/src/main/resources/templates/queryParams.qute @@ -0,0 +1 @@ +{#if param.isQueryParam}@QueryParam("{param.baseName}"){#if param.useBeanValidation}{#include beanValidationCore.qute p=param/}{/if} {#if param.isContainer}{#if param.defaultValue}@DefaultValue("\{{param.defaultValue}\}"){/if}{/if}{param.dataType} {param.paramName}{/if} \ No newline at end of file diff --git a/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapterTest.java b/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapterTest.java new file mode 100644 index 00000000..bf2943f9 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/template/QuteTemplatingEngineAdapterTest.java @@ -0,0 +1,37 @@ +package io.quarkiverse.openapi.generator.deployment.template; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.fail; + +import java.io.File; +import java.io.IOException; +import java.nio.file.Files; +import java.util.Collections; + +import org.junit.jupiter.api.Test; +import org.openapitools.codegen.DefaultGenerator; +import org.openapitools.codegen.config.CodegenConfigurator; + +import io.quarkiverse.openapi.generator.deployment.wrapper.QuarkusCodegenConfigurator; + +public class QuteTemplatingEngineAdapterTest { + @Test + void checkTemplateGenerator() throws IOException { + final String petstoreOpenApi = this.getClass().getResource("/openapi/petstore-openapi.json").getPath(); + final DefaultGenerator generator = new DefaultGenerator(); + final CodegenConfigurator configurator = new QuarkusCodegenConfigurator(); + final File apiFile = File.createTempFile("api", "java"); + apiFile.deleteOnExit(); + configurator.setInputSpec(petstoreOpenApi); + generator.opts(configurator.toClientOptInput()); + + final File writtenFile = generator.getTemplateProcessor().write(Collections.singletonMap("name", "Jack"), "hello.qute", + apiFile); + if (writtenFile != null) { + writtenFile.deleteOnExit(); + assertEquals("Hello! My name is Jack", new String(Files.readAllBytes(writtenFile.toPath()))); + } else { + fail("Template failed to write to the file"); + } + } +} diff --git a/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java b/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java new file mode 100644 index 00000000..ec8faaa6 --- /dev/null +++ b/deployment/src/test/java/io/quarkiverse/openapi/generator/deployment/wrapper/OpenApiClientGeneratorWrapperTest.java @@ -0,0 +1,27 @@ +package io.quarkiverse.openapi.generator.deployment.wrapper; + +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.Paths; +import java.util.List; +import java.util.Objects; + +import org.junit.jupiter.api.Test; + +public class OpenApiClientGeneratorWrapperTest { + + @Test + void generatePetStore() throws URISyntaxException { + final String petstoreOpenApi = Objects.requireNonNull(this.getClass().getResource("/openapi/petstore-openapi.json")) + .getPath(); + final String targetPath = Paths.get(Objects.requireNonNull(getClass().getResource("/")).toURI()).getParent().toString() + + "/openapi-gen"; + final OpenApiClientGeneratorWrapper generatorWrapper = new OpenApiClientGeneratorWrapper(petstoreOpenApi, targetPath); + final List generatedFiles = generatorWrapper.generate(); + assertNotNull(generatedFiles); + assertFalse(generatedFiles.isEmpty()); + } +} diff --git a/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorDevModeTest.java b/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorDevModeTest.java index 3bc920fa..ce626178 100644 --- a/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorDevModeTest.java +++ b/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorDevModeTest.java @@ -13,7 +13,7 @@ public class OpenapiGeneratorDevModeTest { // Start hot reload (DevMode) test with your extension loaded @RegisterExtension static final QuarkusDevModeTest devModeTest = new QuarkusDevModeTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); @Test public void writeYourOwnDevModeTest() { diff --git a/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorTest.java b/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorTest.java index d382dea8..9956eca4 100644 --- a/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorTest.java +++ b/deployment/src/test/java/io/quarkiverse/openapi/generator/test/OpenapiGeneratorTest.java @@ -13,7 +13,7 @@ public class OpenapiGeneratorTest { // Start unit test with your extension loaded @RegisterExtension static final QuarkusUnitTest unitTest = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); + .setArchiveProducer(() -> ShrinkWrap.create(JavaArchive.class)); @Test public void writeYourOwnUnitTest() { diff --git a/deployment/src/test/resources/openapi/petstore-openapi.json b/deployment/src/test/resources/openapi/petstore-openapi.json new file mode 100644 index 00000000..74d203d7 --- /dev/null +++ b/deployment/src/test/resources/openapi/petstore-openapi.json @@ -0,0 +1,1225 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.5" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "/api/v3" + } + ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Operations about user" + }, + { + "name": "user", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { + "type": "string" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid pet value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "405": { + "description": "Invalid input" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "description": "date in UTC when toekn expires", + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "petId": { + "type": "integer", + "format": "int64", + "example": 198772 + }, + "quantity": { + "type": "integer", + "format": "int32", + "example": 7 + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "xml": { + "name": "order" + } + }, + "Customer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 100000 + }, + "username": { + "type": "string", + "example": "fehguy" + }, + "address": { + "type": "array", + "xml": { + "name": "addresses", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Address" + } + } + }, + "xml": { + "name": "customer" + } + }, + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "example": "437 Lytton" + }, + "city": { + "type": "string", + "example": "Palo Alto" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "94301" + } + }, + "xml": { + "name": "address" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "type": "string", + "example": "Dogs" + } + }, + "xml": { + "name": "category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "username": { + "type": "string", + "example": "theUser" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "James" + }, + "email": { + "type": "string", + "example": "john@email.com" + }, + "password": { + "type": "string", + "example": "12345" + }, + "phone": { + "type": "string", + "example": "12345" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32", + "example": 1 + } + }, + "xml": { + "name": "user" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "tag" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "doggie" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "photoUrls": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": "string", + "xml": { + "name": "photoUrl" + } + } + }, + "tags": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "xml": { + "name": "##default" + } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} diff --git a/deployment/src/test/resources/templates/hello.qute b/deployment/src/test/resources/templates/hello.qute new file mode 100644 index 00000000..ba8dac5f --- /dev/null +++ b/deployment/src/test/resources/templates/hello.qute @@ -0,0 +1 @@ +Hello! My name is {name} \ No newline at end of file diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index cc6651d5..ace581ed 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 io.quarkiverse.openapi.generator @@ -9,6 +9,11 @@ quarkus-openapi-generator-integration-tests Quarkus - Openapi Generator - Integration Tests + + + 2.32.0 + + io.quarkus @@ -21,7 +26,18 @@ io.quarkus - quarkus-junit5 + quarkus-junit5-internal + test + + + io.quarkus + quarkus-resteasy-jackson-deployment + test + + + com.github.tomakehurst + wiremock-jre8 + ${version.com.github.tomakehurst} test @@ -39,6 +55,8 @@ build + generate-code + generate-code-tests diff --git a/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResource.java b/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResource.java deleted file mode 100644 index 2f7ff4fe..00000000 --- a/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResource.java +++ /dev/null @@ -1,32 +0,0 @@ -/* -* Licensed to the Apache Software Foundation (ASF) under one or more -* contributor license agreements. See the NOTICE file distributed with -* this work for additional information regarding copyright ownership. -* The ASF licenses this file to You under the Apache License, Version 2.0 -* (the "License"); you may not use this file except in compliance with -* the License. You may obtain a copy of the License at -* -* http://www.apache.org/licenses/LICENSE-2.0 -* -* Unless required by applicable law or agreed to in writing, software -* distributed under the License is distributed on an "AS IS" BASIS, -* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -* See the License for the specific language governing permissions and -* limitations under the License. -*/ -package io.quarkiverse.openapi.generator.it; - -import javax.enterprise.context.ApplicationScoped; -import javax.ws.rs.GET; -import javax.ws.rs.Path; - -@Path("/openapi-generator") -@ApplicationScoped -public class OpenapiGeneratorResource { - // add some rest methods here - - @GET - public String hello() { - return "Hello openapi-generator"; - } -} diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties deleted file mode 100644 index e69de29b..00000000 diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/NativeOpenapiGeneratorResourceIT.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/NativeOpenapiGeneratorResourceIT.java deleted file mode 100644 index f9f5e638..00000000 --- a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/NativeOpenapiGeneratorResourceIT.java +++ /dev/null @@ -1,7 +0,0 @@ -package io.quarkiverse.openapi.generator.it; - -import io.quarkus.test.junit.NativeImageTest; - -@NativeImageTest -public class NativeOpenapiGeneratorResourceIT extends OpenapiGeneratorResourceTest { -} diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResourceTest.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResourceTest.java deleted file mode 100644 index b8c096d1..00000000 --- a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/OpenapiGeneratorResourceTest.java +++ /dev/null @@ -1,21 +0,0 @@ -package io.quarkiverse.openapi.generator.it; - -import static io.restassured.RestAssured.given; -import static org.hamcrest.Matchers.is; - -import org.junit.jupiter.api.Test; - -import io.quarkus.test.junit.QuarkusTest; - -@QuarkusTest -public class OpenapiGeneratorResourceTest { - - @Test - public void testHelloEndpoint() { - given() - .when().get("/openapi-generator") - .then() - .statusCode(200) - .body(is("Hello openapi-generator")); - } -} diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java new file mode 100644 index 00000000..3eb38c00 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java @@ -0,0 +1,26 @@ +package io.quarkiverse.openapi.generator.it; + +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; + +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.annotations.jaxrs.PathParam; +import org.openapitools.client.api.PetApi; + +@Produces(MediaType.APPLICATION_JSON) +@Path("/petstore") +public class PetResource { + + @RestClient + @Inject + PetApi petApi; + + @GET + @Path("/pet/name/{id}") + public String getPetName(@PathParam Long id) { + return petApi.getPetById(id).getName(); + } +} \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java new file mode 100644 index 00000000..4bbd8cbe --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java @@ -0,0 +1,34 @@ +package io.quarkiverse.openapi.generator.it; + +import static io.restassured.RestAssured.when; + +import org.jboss.shrinkwrap.api.ShrinkWrap; +import org.jboss.shrinkwrap.api.spec.JavaArchive; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusUnitTest; +import io.quarkus.test.common.QuarkusTestResource; + +@QuarkusTestResource(WiremockPetStore.class) +public class PetStoreTest { + + @RegisterExtension + static final QuarkusUnitTest config = new QuarkusUnitTest() + .setArchiveProducer(() -> ShrinkWrap + .create(JavaArchive.class) + .addClass(PetResource.class) + .addPackage("org.openapitools.client.model") + .addPackage("org.openapitools.client.api")); + + @Test + public void testGetPetById() { + final String petName = when() + .get("/petstore/pet/name/1234") + .then() + .statusCode(200) + .extract().asString(); + Assertions.assertEquals("Bidu", petName); + } +} diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java new file mode 100644 index 00000000..84184369 --- /dev/null +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java @@ -0,0 +1,42 @@ +package io.quarkiverse.openapi.generator.it; + +import static com.github.tomakehurst.wiremock.client.WireMock.aResponse; +import static com.github.tomakehurst.wiremock.client.WireMock.get; +import static com.github.tomakehurst.wiremock.client.WireMock.stubFor; +import static com.github.tomakehurst.wiremock.client.WireMock.urlEqualTo; + +import java.util.Collections; +import java.util.Map; + +import com.github.tomakehurst.wiremock.WireMockServer; + +import io.quarkus.test.common.QuarkusTestResourceLifecycleManager; + +public class WiremockPetStore implements QuarkusTestResourceLifecycleManager { + + private WireMockServer wireMockServer; + + @Override + public Map start() { + wireMockServer = new WireMockServer(); + wireMockServer.start(); + + stubFor(get(urlEqualTo("/pet/1234")) + .willReturn(aResponse() + .withStatus(200) + .withHeader("Content-Type", "application/json") + .withBody( + "{" + + "\"name\": \"Bidu\"," + + "\"status\": \"AVAILABLE\"" + + "}"))); + return Collections.singletonMap("org.openapitools.client.api.PetApi/mp-rest/url", wireMockServer.baseUrl()); + } + + @Override + public void stop() { + if (null != wireMockServer) { + wireMockServer.stop(); + } + } +} diff --git a/integration-tests/src/test/openapi/petstore.json b/integration-tests/src/test/openapi/petstore.json new file mode 100644 index 00000000..74d203d7 --- /dev/null +++ b/integration-tests/src/test/openapi/petstore.json @@ -0,0 +1,1225 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Swagger Petstore - OpenAPI 3.0", + "description": "This is a sample Pet Store Server based on the OpenAPI 3.0 specification. You can find out more about\nSwagger at [http://swagger.io](http://swagger.io). In the third iteration of the pet store, we've switched to the design first approach!\nYou can now help us improve the API whether it's by making changes to the definition itself or to the code.\nThat way, with time, we can improve the API in general, and expose some of the new features in OAS3.\n\nSome useful links:\n- [The Pet Store repository](https://github.com/swagger-api/swagger-petstore)\n- [The source API definition for the Pet Store](https://github.com/swagger-api/swagger-petstore/blob/master/src/main/resources/openapi.yaml)", + "termsOfService": "http://swagger.io/terms/", + "contact": { + "email": "apiteam@swagger.io" + }, + "license": { + "name": "Apache 2.0", + "url": "http://www.apache.org/licenses/LICENSE-2.0.html" + }, + "version": "1.0.5" + }, + "externalDocs": { + "description": "Find out more about Swagger", + "url": "http://swagger.io" + }, + "servers": [ + { + "url": "/api/v3" + } + ], + "tags": [ + { + "name": "pet", + "description": "Everything about your Pets", + "externalDocs": { + "description": "Find out more", + "url": "http://swagger.io" + } + }, + { + "name": "store", + "description": "Operations about user" + }, + { + "name": "user", + "description": "Access to Petstore orders", + "externalDocs": { + "description": "Find out more about our store", + "url": "http://swagger.io" + } + } + ], + "paths": { + "/pet": { + "put": { + "tags": [ + "pet" + ], + "summary": "Update an existing pet", + "description": "Update an existing pet by Id", + "operationId": "updatePet", + "requestBody": { + "description": "Update an existent pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + }, + "405": { + "description": "Validation exception" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Add a new pet to the store", + "description": "Add a new pet to the store", + "operationId": "addPet", + "requestBody": { + "description": "Create a new pet in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "required": true + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByStatus": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by status", + "description": "Multiple status values can be provided with comma separated strings", + "operationId": "findPetsByStatus", + "parameters": [ + { + "name": "status", + "in": "query", + "description": "Status values that need to be considered for filter", + "required": false, + "explode": true, + "schema": { + "type": "string", + "default": "available", + "enum": [ + "available", + "pending", + "sold" + ] + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid status value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/findByTags": { + "get": { + "tags": [ + "pet" + ], + "summary": "Finds Pets by tags", + "description": "Multiple tags can be provided with comma separated strings. Use tag1, tag2, tag3 for testing.", + "operationId": "findPetsByTags", + "parameters": [ + { + "name": "tags", + "in": "query", + "description": "Tags to filter by", + "required": false, + "explode": true, + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + }, + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Pet" + } + } + } + } + }, + "400": { + "description": "Invalid tag value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}": { + "get": { + "tags": [ + "pet" + ], + "summary": "Find pet by ID", + "description": "Returns a single pet", + "operationId": "getPetById", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to return", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Pet not found" + } + }, + "security": [ + { + "api_key": [] + }, + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "post": { + "tags": [ + "pet" + ], + "summary": "Updates a pet in the store with form data", + "description": "", + "operationId": "updatePetWithForm", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet that needs to be updated", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "name", + "in": "query", + "description": "Name of pet that needs to be updated", + "schema": { + "type": "string" + } + }, + { + "name": "status", + "in": "query", + "description": "Status of pet that needs to be updated", + "schema": { + "type": "string" + } + } + ], + "responses": { + "405": { + "description": "Invalid input" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + }, + "delete": { + "tags": [ + "pet" + ], + "summary": "Deletes a pet", + "description": "", + "operationId": "deletePet", + "parameters": [ + { + "name": "api_key", + "in": "header", + "description": "", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "petId", + "in": "path", + "description": "Pet id to delete", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid pet value" + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/pet/{petId}/uploadImage": { + "post": { + "tags": [ + "pet" + ], + "summary": "uploads an image", + "description": "", + "operationId": "uploadFile", + "parameters": [ + { + "name": "petId", + "in": "path", + "description": "ID of pet to update", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + }, + { + "name": "additionalMetadata", + "in": "query", + "description": "Additional Metadata", + "required": false, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "content": { + "application/octet-stream": { + "schema": { + "type": "string", + "format": "binary" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/ApiResponse" + } + } + } + } + }, + "security": [ + { + "petstore_auth": [ + "write:pets", + "read:pets" + ] + } + ] + } + }, + "/store/inventory": { + "get": { + "tags": [ + "store" + ], + "summary": "Returns pet inventories by status", + "description": "Returns a map of status codes to quantities", + "operationId": "getInventory", + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "type": "object", + "additionalProperties": { + "type": "integer", + "format": "int32" + } + } + } + } + } + }, + "security": [ + { + "api_key": [] + } + ] + } + }, + "/store/order": { + "post": { + "tags": [ + "store" + ], + "summary": "Place an order for a pet", + "description": "Place a new order in the store", + "operationId": "placeOrder", + "requestBody": { + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "405": { + "description": "Invalid input" + } + } + } + }, + "/store/order/{orderId}": { + "get": { + "tags": [ + "store" + ], + "summary": "Find purchase order by ID", + "description": "For valid response try integer IDs with value <= 5 or > 10. Other values will generated exceptions", + "operationId": "getOrderById", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of order that needs to be fetched", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Order" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/Order" + } + } + } + }, + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + }, + "delete": { + "tags": [ + "store" + ], + "summary": "Delete purchase order by ID", + "description": "For valid response try integer IDs with value < 1000. Anything above 1000 or nonintegers will generate API errors", + "operationId": "deleteOrder", + "parameters": [ + { + "name": "orderId", + "in": "path", + "description": "ID of the order that needs to be deleted", + "required": true, + "schema": { + "type": "integer", + "format": "int64" + } + } + ], + "responses": { + "400": { + "description": "Invalid ID supplied" + }, + "404": { + "description": "Order not found" + } + } + } + }, + "/user": { + "post": { + "tags": [ + "user" + ], + "summary": "Create user", + "description": "This can only be done by the logged in user.", + "operationId": "createUser", + "requestBody": { + "description": "Created user object", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + }, + "/user/createWithList": { + "post": { + "tags": [ + "user" + ], + "summary": "Creates list of users with given input array", + "description": "Creates list of users with given input array", + "operationId": "createUsersWithListInput", + "requestBody": { + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + }, + "responses": { + "200": { + "description": "Successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "default": { + "description": "successful operation" + } + } + } + }, + "/user/login": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs user into the system", + "description": "", + "operationId": "loginUser", + "parameters": [ + { + "name": "username", + "in": "query", + "description": "The user name for login", + "required": false, + "schema": { + "type": "string" + } + }, + { + "name": "password", + "in": "query", + "description": "The password for login in clear text", + "required": false, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "headers": { + "X-Rate-Limit": { + "description": "calls per hour allowed by the user", + "schema": { + "type": "integer", + "format": "int32" + } + }, + "X-Expires-After": { + "description": "date in UTC when toekn expires", + "schema": { + "type": "string", + "format": "date-time" + } + } + }, + "content": { + "application/xml": { + "schema": { + "type": "string" + } + }, + "application/json": { + "schema": { + "type": "string" + } + } + } + }, + "400": { + "description": "Invalid username/password supplied" + } + } + } + }, + "/user/logout": { + "get": { + "tags": [ + "user" + ], + "summary": "Logs out current logged in user session", + "description": "", + "operationId": "logoutUser", + "parameters": [], + "responses": { + "default": { + "description": "successful operation" + } + } + } + }, + "/user/{username}": { + "get": { + "tags": [ + "user" + ], + "summary": "Get user by user name", + "description": "", + "operationId": "getUserByName", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be fetched. Use user1 for testing. ", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "200": { + "description": "successful operation", + "content": { + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + }, + "put": { + "tags": [ + "user" + ], + "summary": "Update user", + "description": "This can only be done by the logged in user.", + "operationId": "updateUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "name that need to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "requestBody": { + "description": "Update an existent user in the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/User" + } + }, + "application/x-www-form-urlencoded": { + "schema": { + "$ref": "#/components/schemas/User" + } + } + } + }, + "responses": { + "default": { + "description": "successful operation" + } + } + }, + "delete": { + "tags": [ + "user" + ], + "summary": "Delete user", + "description": "This can only be done by the logged in user.", + "operationId": "deleteUser", + "parameters": [ + { + "name": "username", + "in": "path", + "description": "The name that needs to be deleted", + "required": true, + "schema": { + "type": "string" + } + } + ], + "responses": { + "400": { + "description": "Invalid username supplied" + }, + "404": { + "description": "User not found" + } + } + } + } + }, + "components": { + "schemas": { + "Order": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "petId": { + "type": "integer", + "format": "int64", + "example": 198772 + }, + "quantity": { + "type": "integer", + "format": "int32", + "example": 7 + }, + "shipDate": { + "type": "string", + "format": "date-time" + }, + "status": { + "type": "string", + "description": "Order Status", + "example": "approved", + "enum": [ + "placed", + "approved", + "delivered" + ] + }, + "complete": { + "type": "boolean" + } + }, + "xml": { + "name": "order" + } + }, + "Customer": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 100000 + }, + "username": { + "type": "string", + "example": "fehguy" + }, + "address": { + "type": "array", + "xml": { + "name": "addresses", + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Address" + } + } + }, + "xml": { + "name": "customer" + } + }, + "Address": { + "type": "object", + "properties": { + "street": { + "type": "string", + "example": "437 Lytton" + }, + "city": { + "type": "string", + "example": "Palo Alto" + }, + "state": { + "type": "string", + "example": "CA" + }, + "zip": { + "type": "string", + "example": "94301" + } + }, + "xml": { + "name": "address" + } + }, + "Category": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 1 + }, + "name": { + "type": "string", + "example": "Dogs" + } + }, + "xml": { + "name": "category" + } + }, + "User": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "username": { + "type": "string", + "example": "theUser" + }, + "firstName": { + "type": "string", + "example": "John" + }, + "lastName": { + "type": "string", + "example": "James" + }, + "email": { + "type": "string", + "example": "john@email.com" + }, + "password": { + "type": "string", + "example": "12345" + }, + "phone": { + "type": "string", + "example": "12345" + }, + "userStatus": { + "type": "integer", + "description": "User Status", + "format": "int32", + "example": 1 + } + }, + "xml": { + "name": "user" + } + }, + "Tag": { + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64" + }, + "name": { + "type": "string" + } + }, + "xml": { + "name": "tag" + } + }, + "Pet": { + "required": [ + "name", + "photoUrls" + ], + "type": "object", + "properties": { + "id": { + "type": "integer", + "format": "int64", + "example": 10 + }, + "name": { + "type": "string", + "example": "doggie" + }, + "category": { + "$ref": "#/components/schemas/Category" + }, + "photoUrls": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "type": "string", + "xml": { + "name": "photoUrl" + } + } + }, + "tags": { + "type": "array", + "xml": { + "wrapped": true + }, + "items": { + "$ref": "#/components/schemas/Tag" + } + }, + "status": { + "type": "string", + "description": "pet status in the store", + "enum": [ + "available", + "pending", + "sold" + ] + } + }, + "xml": { + "name": "pet" + } + }, + "ApiResponse": { + "type": "object", + "properties": { + "code": { + "type": "integer", + "format": "int32" + }, + "type": { + "type": "string" + }, + "message": { + "type": "string" + } + }, + "xml": { + "name": "##default" + } + } + }, + "requestBodies": { + "Pet": { + "description": "Pet object that needs to be added to the store", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + }, + "application/xml": { + "schema": { + "$ref": "#/components/schemas/Pet" + } + } + } + }, + "UserArray": { + "description": "List of user object", + "content": { + "application/json": { + "schema": { + "type": "array", + "items": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + }, + "securitySchemes": { + "petstore_auth": { + "type": "oauth2", + "flows": { + "implicit": { + "authorizationUrl": "https://petstore3.swagger.io/oauth/authorize", + "scopes": { + "write:pets": "modify pets in your account", + "read:pets": "read your pets" + } + } + } + }, + "api_key": { + "type": "apiKey", + "name": "api_key", + "in": "header" + } + } + } +} diff --git a/integration-tests/src/test/resources/application.properties b/integration-tests/src/test/resources/application.properties new file mode 100644 index 00000000..baf92b92 --- /dev/null +++ b/integration-tests/src/test/resources/application.properties @@ -0,0 +1,2 @@ +quarkus.openapi-generator.codegen.api-package=org.acme.openapi.api +quarkus.openapi-generator.codegen.model-package=org.acme.openapi.model \ No newline at end of file diff --git a/runtime/pom.xml b/runtime/pom.xml index 415aa0fb..48d6f996 100644 --- a/runtime/pom.xml +++ b/runtime/pom.xml @@ -1,6 +1,6 @@ + xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"> 4.0.0 io.quarkiverse.openapi.generator @@ -10,9 +10,19 @@ quarkus-openapi-generator Quarkus - Openapi Generator - Runtime + io.quarkus - quarkus-arc + quarkus-core + + + io.quarkus + quarkus-rest-client-jackson + + + io.quarkus + quarkus-qute + compile diff --git a/runtime/src/main/java/io/quarkiverse/openapi/generator/RuntimeConfig.java b/runtime/src/main/java/io/quarkiverse/openapi/generator/RuntimeConfig.java new file mode 100644 index 00000000..8c8ee8a2 --- /dev/null +++ b/runtime/src/main/java/io/quarkiverse/openapi/generator/RuntimeConfig.java @@ -0,0 +1,8 @@ +package io.quarkiverse.openapi.generator; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.RUN_TIME) +public class RuntimeConfig { +} diff --git a/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/runtime/src/main/resources/META-INF/quarkus-extension.yaml index f0920094..66041bb0 100644 --- a/runtime/src/main/resources/META-INF/quarkus-extension.yaml +++ b/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -1,9 +1,11 @@ -name: Openapi Generator -#description: Openapi Generator ... +name: "OpenAPI Generator - REST Client Generator" +artifact: ${project.groupId}:${project.artifactId}:${project.version} metadata: -# keywords: -# - openapi-generator -# guide: ... -# categories: -# - "miscellaneous" -# status: "preview" \ No newline at end of file + keywords: + - "openapi" + - "openapi-generator" + - "rest" + - "rest-client" + categories: + - "rest" + status: "preview" From 71c6bedf6b682229b818f810b72cfdc4cc1f6f52 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 8 Feb 2022 11:29:00 -0300 Subject: [PATCH 2/9] Fix properties reading from codegen Signed-off-by: Ricardo Zanini --- .../src/{test => main}/resources/application.properties | 0 .../java/io/quarkiverse/openapi/generator/it/PetResource.java | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) rename integration-tests/src/{test => main}/resources/application.properties (100%) diff --git a/integration-tests/src/test/resources/application.properties b/integration-tests/src/main/resources/application.properties similarity index 100% rename from integration-tests/src/test/resources/application.properties rename to integration-tests/src/main/resources/application.properties diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java index 3eb38c00..3c841e75 100644 --- a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java @@ -6,9 +6,9 @@ import javax.ws.rs.Produces; import javax.ws.rs.core.MediaType; +import org.acme.openapi.api.PetApi; import org.eclipse.microprofile.rest.client.inject.RestClient; import org.jboss.resteasy.annotations.jaxrs.PathParam; -import org.openapitools.client.api.PetApi; @Produces(MediaType.APPLICATION_JSON) @Path("/petstore") From b6039910afb83a1cc06cfc53f741844e01937f6b Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Tue, 8 Feb 2022 17:14:05 -0300 Subject: [PATCH 3/9] Produce BuildItems for the generated OpenAPI resources Signed-off-by: Ricardo Zanini --- README.md | 104 +++++- .../deployment/CodeGenConfiguration.java | 38 --- .../deployment/GeneratedOpenApiFile.java | 17 + .../GeneratedOpenApiModelBuildItem.java | 17 + .../GeneratedOpenApiRestClientBuildItem.java | 17 + .../OpenApiGeneratorConfiguration.java | 28 ++ .../deployment/OpenApiGeneratorProcessor.java | 39 +++ .../deployment/OpenapiGeneratorProcessor.java | 14 - .../generator/deployment/SpecConfig.java | 19 ++ .../codegen/OpenApiGeneratorCodeGenBase.java | 20 +- integration-tests/pom.xml | 8 +- .../openapi/generator/it/PetResource.java | 0 .../src/main/openapi/openweather.yaml | 320 ++++++++++++++++++ .../src/{test => main}/openapi/petstore.json | 0 .../src/main/resources/application.properties | 6 +- .../openapi/generator/it/PetStoreTest.java | 14 +- .../generator/it/WiremockPetStore.java | 2 +- 17 files changed, 573 insertions(+), 90 deletions(-) delete mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiModelBuildItem.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiRestClientBuildItem.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java delete mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenapiGeneratorProcessor.java create mode 100644 deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java rename integration-tests/src/{test => main}/java/io/quarkiverse/openapi/generator/it/PetResource.java (100%) create mode 100644 integration-tests/src/main/openapi/openweather.yaml rename integration-tests/src/{test => main}/openapi/petstore.json (100%) diff --git a/README.md b/README.md index e1c400a1..f5384f76 100644 --- a/README.md +++ b/README.md @@ -1,29 +1,109 @@ -# Quarkus - Openapi Generator +# Quarkus - OpenAPI Generator + [![All Contributors](https://img.shields.io/badge/all_contributors-1-orange.svg?style=flat-square)](#contributors-) -## Welcome to Quarkiverse! +Quarkus' extension for generation of [Rest Clients](https://quarkus.io/guides/rest-client) based on OpenAPI specification files. + +## Getting Started + +Add the following dependency to your project's `pom.xml` file: + +```xml + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator + 0.1.0 + +``` + +You will also need to add additional execution entries to the `quarkus-maven-plugin` configuration: + +```xml + + + io.quarkus + quarkus-maven-plugin + true + + + + build + generate-code + generate-code-tests + + + + +``` + +Now, create the directory `openapi` under your `src/main/` path and add the OpenAPI spec files there. We support JSON, YAML and YML extensions. + +To fine tune the configuration for each spec file, add the following entries to your properties file. In this example, our spec file is in `src/main/openapi/petstore.json`: + +```properties +quarkus.openapi-generator.spec."petstore.json".api-package=org.acme.openapi.api +quarkus.openapi-generator.spec."petstore.json".model-package=org.acme.openapi.model +``` + +Note that the file name is used to configure the specific information for each spec. + +Run `mvn compile` to generate your classes in `target/generated-sources/open-api-json` path: + +``` +- org.acme.openapi + - api + - PetApi.java + - StoreApi.java + - UserApi.java + - model + - Address.java + - Category.java + - Customer.java + - ModelApiResponse.java + - Order.java + - Pet.java + - Tag.java + - User.java +``` + +You can reference the generated code in your project, for example: -Congratulations and thank you for creating a new Quarkus extension project in Quarkiverse! +```java +import javax.inject.Inject; +import javax.ws.rs.GET; +import javax.ws.rs.Path; +import javax.ws.rs.Produces; +import javax.ws.rs.core.MediaType; -Feel free to replace this content with the proper description of your new project and necessary instructions how to use and contribute to it. +import org.acme.openapi.api.PetApi; +import org.eclipse.microprofile.rest.client.inject.RestClient; +import org.jboss.resteasy.annotations.jaxrs.PathParam; -You can find the basic info, Quarkiverse policies and conventions in [the Quarkiverse wiki](https://github.com/quarkiverse/quarkiverse/wiki). +@Produces(MediaType.APPLICATION_JSON) +@Path("/petstore") +public class PetResource { -In case you are creating a Quarkus extension project for the first time, please follow [Building My First Extension](https://quarkus.io/guides/building-my-first-extension) guide. + @RestClient + @Inject + PetApi petApi; +} +``` -Other useful articles related to Quarkus extension development can be found under the [Writing Extensions](https://quarkus.io/guides/#writing-extensions) guide category on the [Quarkus.io](http://quarkus.io) website. +See the [integration-tests](integration-tests) module for more information of how to use this extension. Please be advised that the extension is on experimental, early development stage. -Thanks again, good luck and have fun! +## Known Limitations -## Documentation +These are the known limitations of this pre-release version: -The documentation for this extension should be maintained as part of this repository and it is stored in the `docs/` directory. +- No authentication support (Basic, Bearer, OAuth2) +- No reactive support +- Only Jackson support -The layout should follow the [Antora's Standard File and Directory Set](https://docs.antora.org/antora/2.3/standard-directories/). +We will work in the next few releases to address these use cases, until there please provide feedback for the current state of this extension. We also love contributions :heart: -Once the docs are ready to be published, please open a PR including this repository in the [Quarkiverse Docs Antora playbook](https://github.com/quarkiverse/quarkiverse-docs/blob/main/antora-playbook.yml#L7). See an example [here](https://github.com/quarkiverse/quarkiverse-docs/pull/1). ## Contributors ✨ Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java deleted file mode 100644 index ff2721e2..00000000 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/CodeGenConfiguration.java +++ /dev/null @@ -1,38 +0,0 @@ -package io.quarkiverse.openapi.generator.deployment; - -import io.quarkus.runtime.annotations.ConfigItem; -import io.quarkus.runtime.annotations.ConfigPhase; -import io.quarkus.runtime.annotations.ConfigRoot; - -@ConfigRoot(phase = ConfigPhase.BUILD_TIME, prefix = "quarkus.openapi-generator") -public class CodeGenConfiguration { - /** - * Defines the base package name for the generated API classes - */ - @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.api") - String apiPackage; - - /** - * Defines the base package name for the generated Model classes - */ - @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.model") - String modelPackage; - - /** - * Increases the internal generator log output verbosity - */ - @ConfigItem(defaultValue = "false") - String verbose; - - public String getApiPackage() { - return apiPackage; - } - - public String getModelPackage() { - return modelPackage; - } - - public String getVerbose() { - return verbose; - } -} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java new file mode 100644 index 00000000..4bb3bd91 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java @@ -0,0 +1,17 @@ +package io.quarkiverse.openapi.generator.deployment; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +public class GeneratedOpenApiFile extends MultiBuildItem { + private final ClassInfo classInfo; + + public GeneratedOpenApiFile(final ClassInfo classInfo) { + this.classInfo = classInfo; + } + + public ClassInfo getClassInfo() { + return classInfo; + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiModelBuildItem.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiModelBuildItem.java new file mode 100644 index 00000000..2a5847db --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiModelBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkiverse.openapi.generator.deployment; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * {@link MultiBuildItem} produced for each domain Model generated by the OpenApi Generator tool containing the generated class + * information. + */ +public final class GeneratedOpenApiModelBuildItem extends GeneratedOpenApiFile { + + public GeneratedOpenApiModelBuildItem(final ClassInfo classInfo) { + super(classInfo); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiRestClientBuildItem.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiRestClientBuildItem.java new file mode 100644 index 00000000..0b6e0ac9 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiRestClientBuildItem.java @@ -0,0 +1,17 @@ +package io.quarkiverse.openapi.generator.deployment; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.builder.item.MultiBuildItem; + +/** + * {@link MultiBuildItem} produced for each Rest Client generated by the OpenApi Generator tool containing the generated class + * information. + */ +public final class GeneratedOpenApiRestClientBuildItem extends GeneratedOpenApiFile { + + public GeneratedOpenApiRestClientBuildItem(final ClassInfo classInfo) { + super(classInfo); + } + +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java new file mode 100644 index 00000000..df7437f3 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java @@ -0,0 +1,28 @@ +package io.quarkiverse.openapi.generator.deployment; + +import java.util.Map; + +import io.quarkus.runtime.annotations.ConfigItem; +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; + +@ConfigRoot(phase = ConfigPhase.BUILD_TIME, name = "openapi-generator") +public class OpenApiGeneratorConfiguration { + /** + * Fine tune the configuration for each OpenAPI spec file in `src/openapi` directory. + *

+ * The file name is used to index this configuration. For example: + * `quarkus.openapi-generator.spec."myfilespec.json".api-package=org.acme.api`. + *

+ * If you have more than one file to generate rest clients, it is **highly** recommended that you add this configuration + * to your properties file to make a distinction between the generated APIs. + **/ + @ConfigItem(name = "spec") + public Map specs; + + /** + * Increases the internal generator log output verbosity + */ + @ConfigItem(defaultValue = "false") + public String verbose; +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java new file mode 100644 index 00000000..8bf01079 --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java @@ -0,0 +1,39 @@ +package io.quarkiverse.openapi.generator.deployment; + +import org.jboss.jandex.ClassInfo; + +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.CombinedIndexBuildItem; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +class OpenApiGeneratorProcessor { + + private static final String FEATURE = "openapi-generator"; + + OpenApiGeneratorConfiguration configuration; + + @BuildStep + void discoverGeneratedApis(BuildProducer restClients, + BuildProducer models, + BuildProducer features, + CombinedIndexBuildItem index) { + + boolean hasGeneratedFiles = false; + for (SpecConfig spec : configuration.specs.values()) { + for (ClassInfo classInfo : index.getIndex().getKnownClasses()) { + if (classInfo.name().packagePrefix().equals(spec.apiPackage)) { + restClients.produce(new GeneratedOpenApiRestClientBuildItem(classInfo)); + hasGeneratedFiles = true; + } else if (classInfo.name().packagePrefix().equals(spec.modelPackage)) { + models.produce(new GeneratedOpenApiModelBuildItem(classInfo)); + hasGeneratedFiles = true; + } + } + } + + if (hasGeneratedFiles) { + features.produce(new FeatureBuildItem(FEATURE)); + } + } +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenapiGeneratorProcessor.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenapiGeneratorProcessor.java deleted file mode 100644 index af5a920c..00000000 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenapiGeneratorProcessor.java +++ /dev/null @@ -1,14 +0,0 @@ -package io.quarkiverse.openapi.generator.deployment; - -import io.quarkus.deployment.annotations.BuildStep; -import io.quarkus.deployment.builditem.FeatureBuildItem; - -class OpenapiGeneratorProcessor { - - private static final String FEATURE = "openapi-generator"; - - @BuildStep - FeatureBuildItem feature() { - return new FeatureBuildItem(FEATURE); - } -} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java new file mode 100644 index 00000000..02f3b39b --- /dev/null +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java @@ -0,0 +1,19 @@ +package io.quarkiverse.openapi.generator.deployment; + +import io.quarkus.runtime.annotations.ConfigGroup; +import io.quarkus.runtime.annotations.ConfigItem; + +@ConfigGroup +public class SpecConfig { + /** + * Defines the base package name for the generated API classes + */ + @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.api") + public String apiPackage; + + /** + * Defines the base package name for the generated Model classes + */ + @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.model") + public String modelPackage; +} diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java index 68c34d14..44d57765 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java @@ -3,6 +3,7 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; +import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -25,6 +26,8 @@ public abstract class OpenApiGeneratorCodeGenBase implements CodeGenProvider { static final String YML = ".yml"; static final String JSON = ".json"; + static final String CONFIG_PREFIX = "quarkus.openapi-generator"; + @Override public String inputDirectory() { return "openapi"; @@ -34,9 +37,6 @@ public String inputDirectory() { public boolean trigger(CodeGenContext context) throws CodeGenException { final Path outDir = context.outDir(); final Path openApiDir = context.inputDir(); - final String apiPackage = context.config().getConfigValue("quarkus.openapi-generator.codegen.api-package").getValue(); - final String modelPackage = context.config().getConfigValue("quarkus.openapi-generator.codegen.model-package") - .getValue(); try { if (Files.isDirectory(openApiDir)) { @@ -48,10 +48,11 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { .map(this::escapeWhitespace) .collect(Collectors.toList()); for (String openApiFile : openApiFiles) { + final String prop = getSpecPropertyPrefix(openApiFile); final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(openApiFile, outDir.toString()) - .withApiPackage(apiPackage) - .withModelPackage(modelPackage); + .withApiPackage(getRequiredIndexedProperty(prop + ".api-package", context)) + .withModelPackage(getRequiredIndexedProperty(prop + ".model-package", context)); generator.generate(); } return true; @@ -70,4 +71,13 @@ private String escapeWhitespace(String path) { return path; } } + + private String getSpecPropertyPrefix(final String openApiFile) { + return CONFIG_PREFIX + ".spec.\"" + Paths.get(openApiFile).getFileName().toString() + "\""; + } + + private String getRequiredIndexedProperty(final String propertyKey, final CodeGenContext context) { + // this is how we get a required property. The configSource will handle the exception for us. + return context.config().getValue(propertyKey, String.class); + } } diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index ace581ed..b2426dca 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -26,12 +26,7 @@ io.quarkus - quarkus-junit5-internal - test - - - io.quarkus - quarkus-resteasy-jackson-deployment + quarkus-junit5 test @@ -51,6 +46,7 @@ io.quarkus quarkus-maven-plugin + true diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java b/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/PetResource.java similarity index 100% rename from integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetResource.java rename to integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/PetResource.java diff --git a/integration-tests/src/main/openapi/openweather.yaml b/integration-tests/src/main/openapi/openweather.yaml new file mode 100644 index 00000000..289ae950 --- /dev/null +++ b/integration-tests/src/main/openapi/openweather.yaml @@ -0,0 +1,320 @@ +openapi: "3.0.2" +info: + title: "OpenWeatherMap API" + description: "Get the current weather, daily forecast for 16 days, and a three-hour-interval forecast for 5 days for your city. Helpful stats, graphics, and this day in history charts are available for your reference. Interactive maps show precipitation, clouds, pressure, wind around your location stations. Data is available in JSON, XML, or HTML format. **Note**: This sample Swagger file covers the `current` endpoint only from the OpenWeatherMap API.

**Note**: All parameters are optional, but you must select at least one parameter. Calling the API by city ID (using the `id` parameter) will provide the most precise location results." + version: "2.5" + termsOfService: "https://openweathermap.org/terms" + contact: + name: "OpenWeatherMap API" + url: "https://openweathermap.org/api" + email: "some_email@gmail.com" + license: + name: "CC Attribution-ShareAlike 4.0 (CC BY-SA 4.0)" + url: "https://openweathermap.org/price" + +servers: + - url: "https://api.openweathermap.org/data/2.5" + +paths: + /weather: + get: + tags: + - Current Weather Data + summary: "Call current weather data for one location" + description: "Access current weather data for any location on Earth including over 200,000 cities! Current weather is frequently updated based on global models and data from more than 40,000 weather stations." + operationId: CurrentWeatherData + parameters: + - $ref: '#/components/parameters/q' + - $ref: '#/components/parameters/id' + - $ref: '#/components/parameters/lat' + - $ref: '#/components/parameters/lon' + - $ref: '#/components/parameters/zip' + - $ref: '#/components/parameters/units' + - $ref: '#/components/parameters/lang' + - $ref: '#/components/parameters/mode' + + responses: + "200": + description: Successful response + content: + application/json: + schema: + $ref: '#/components/schemas/200' + "404": + description: Not found response + content: + text/plain: + schema: + title: Weather not found + type: string + example: Not found +security: + - app_id: [ ] + +tags: + - name: Current Weather Data + description: "Get current weather details" + +externalDocs: + description: API Documentation + url: https://openweathermap.org/api + +components: + + parameters: + q: + name: q + in: query + description: "**City name**. *Example: London*. You can call by city name, or by city name and country code. The API responds with a list of results that match a searching word. For the query value, type the city name and optionally the country code divided by a comma; use ISO 3166 country codes." + schema: + type: string + id: + name: id + in: query + description: "**City ID**. *Example: `2172797`*. You can call by city ID. The API responds with the exact result. The List of city IDs can be downloaded [here](http://bulk.openweathermap.org/sample/). You can include multiple cities in this parameter — just separate them by commas. The limit of locations is 20. *Note: A single ID counts as a one API call. So, if you have city IDs, it's treated as 3 API calls.*" + schema: + type: string + + lat: + name: lat + in: query + description: "**Latitude**. *Example: 35*. The latitude coordinate of the location of your interest. Must use with `lon`." + schema: + type: string + + lon: + name: lon + in: query + description: "**Longitude**. *Example: 139*. Longitude coordinate of the location of your interest. Must use with `lat`." + schema: + type: string + + zip: + name: zip + in: query + description: "**Zip code**. Search by zip code. *Example: 95050,us*. Please note that if the country is not specified, the search uses USA as a default." + schema: + type: string + + units: + name: units + in: query + description: '**Units**. *Example: imperial*. Possible values: `standard`, `metric`, and `imperial`. When you do not use the `units` parameter, the format is `standard` by default.' + schema: + type: string + enum: [ standard, metric, imperial ] + default: "imperial" + + lang: + name: lang + in: query + description: '**Language**. *Example: en*. You can use lang parameter to get the output in your language. We support the following languages that you can use with the corresponded lang values: Arabic - `ar`, Bulgarian - `bg`, Catalan - `ca`, Czech - `cz`, German - `de`, Greek - `el`, English - `en`, Persian (Farsi) - `fa`, Finnish - `fi`, French - `fr`, Galician - `gl`, Croatian - `hr`, Hungarian - `hu`, Italian - `it`, Japanese - `ja`, Korean - `kr`, Latvian - `la`, Lithuanian - `lt`, Macedonian - `mk`, Dutch - `nl`, Polish - `pl`, Portuguese - `pt`, Romanian - `ro`, Russian - `ru`, Swedish - `se`, Slovak - `sk`, Slovenian - `sl`, Spanish - `es`, Turkish - `tr`, Ukrainian - `ua`, Vietnamese - `vi`, Chinese Simplified - `zh_cn`, Chinese Traditional - `zh_tw`.' + schema: + type: string + enum: [ ar, bg, ca, cz, de, el, en, fa, fi, fr, gl, hr, hu, it, ja, kr, la, lt, mk, nl, pl, pt, ro, ru, se, sk, sl, es, tr, ua, vi, zh_cn, zh_tw ] + default: "en" + + mode: + name: mode + in: query + description: "**Mode**. *Example: html*. Determines the format of the response. Possible values are `json`, `xml`, and `html`. If the mode parameter is empty, the format is `json` by default." + schema: + type: string + enum: [ json, xml, html ] + default: "json" + + schemas: + "200": + title: Successful response + type: object + properties: + coord: + $ref: '#/components/schemas/Coord' + weather: + type: array + items: + $ref: '#/components/schemas/Weather' + description: (more info Weather condition codes) + base: + type: string + description: Internal parameter + example: cmc stations + main: + $ref: '#/components/schemas/Main' + visibility: + type: integer + description: Visibility, meter + example: 16093 + wind: + $ref: '#/components/schemas/Wind' + clouds: + $ref: '#/components/schemas/Clouds' + rain: + $ref: '#/components/schemas/Rain' + snow: + $ref: '#/components/schemas/Snow' + dt: + type: integer + description: Time of data calculation, unix, UTC + format: int32 + example: 1435658272 + sys: + $ref: '#/components/schemas/Sys' + id: + type: integer + description: City ID + format: int32 + example: 2172797 + name: + type: string + example: Cairns + cod: + type: integer + description: Internal parameter + format: int32 + example: 200 + Coord: + title: Coord + type: object + properties: + lon: + type: number + description: City geo location, longitude + example: 145.77000000000001 + lat: + type: number + description: City geo location, latitude + example: -16.920000000000002 + Weather: + title: Weather + type: object + properties: + id: + type: integer + description: Weather condition id + format: int32 + example: 803 + main: + type: string + description: Group of weather parameters (Rain, Snow, Extreme etc.) + example: Clouds + description: + type: string + description: Weather condition within the group + example: broken clouds + icon: + type: string + description: Weather icon id + example: 04n + Main: + title: Main + type: object + properties: + temp: + type: number + description: 'Temperature. Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.' + example: 293.25 + pressure: + type: integer + description: Atmospheric pressure (on the sea level, if there is no sea_level or grnd_level data), hPa + format: int32 + example: 1019 + humidity: + type: integer + description: Humidity, % + format: int32 + example: 83 + temp_min: + type: number + description: 'Minimum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally). Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.' + example: 289.81999999999999 + temp_max: + type: number + description: 'Maximum temperature at the moment. This is deviation from current temp that is possible for large cities and megalopolises geographically expanded (use these parameter optionally). Unit Default: Kelvin, Metric: Celsius, Imperial: Fahrenheit.' + example: 295.37 + sea_level: + type: number + description: Atmospheric pressure on the sea level, hPa + example: 984 + grnd_level: + type: number + description: Atmospheric pressure on the ground level, hPa + example: 990 + Wind: + title: Wind + type: object + properties: + speed: + type: number + description: 'Wind speed. Unit Default: meter/sec, Metric: meter/sec, Imperial: miles/hour.' + example: 5.0999999999999996 + deg: + type: integer + description: Wind direction, degrees (meteorological) + format: int32 + example: 150 + Clouds: + title: Clouds + type: object + properties: + all: + type: integer + description: Cloudiness, % + format: int32 + example: 75 + Rain: + title: Rain + type: object + properties: + 3h: + type: integer + description: Rain volume for the last 3 hours + format: int32 + example: 3 + Snow: + title: Snow + type: object + properties: + 3h: + type: number + description: Snow volume for the last 3 hours + example: 6 + Sys: + title: Sys + type: object + properties: + type: + type: integer + description: Internal parameter + format: int32 + example: 1 + id: + type: integer + description: Internal parameter + format: int32 + example: 8166 + message: + type: number + description: Internal parameter + example: 0.0166 + country: + type: string + description: Country code (GB, JP etc.) + example: AU + sunrise: + type: integer + description: Sunrise time, unix, UTC + format: int32 + example: 1435610796 + sunset: + type: integer + description: Sunset time, unix, UTC + format: int32 + example: 1435650870 + + securitySchemes: + app_id: + type: apiKey + description: "API key to authorize requests. (If you don't have an API key, get one at https://openweathermap.org/. See https://idratherbewriting.com/learnapidoc/docapis_get_auth_keys.html for details.)" + name: appid + in: query \ No newline at end of file diff --git a/integration-tests/src/test/openapi/petstore.json b/integration-tests/src/main/openapi/petstore.json similarity index 100% rename from integration-tests/src/test/openapi/petstore.json rename to integration-tests/src/main/openapi/petstore.json diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index baf92b92..ee2a6fe0 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -1,2 +1,4 @@ -quarkus.openapi-generator.codegen.api-package=org.acme.openapi.api -quarkus.openapi-generator.codegen.model-package=org.acme.openapi.model \ No newline at end of file +quarkus.openapi-generator.spec."petstore.json".api-package=org.acme.openapi.api +quarkus.openapi-generator.spec."petstore.json".model-package=org.acme.openapi.model +quarkus.openapi-generator.spec."openweather.yaml".api-package=org.acme.openapi.weather.api +quarkus.openapi-generator.spec."openweather.yaml".model-package=org.acme.openapi.weather.model \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java index 4bbd8cbe..86d25e15 100644 --- a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java @@ -2,26 +2,16 @@ import static io.restassured.RestAssured.when; -import org.jboss.shrinkwrap.api.ShrinkWrap; -import org.jboss.shrinkwrap.api.spec.JavaArchive; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.extension.RegisterExtension; -import io.quarkus.test.QuarkusUnitTest; import io.quarkus.test.common.QuarkusTestResource; +import io.quarkus.test.junit.QuarkusTest; @QuarkusTestResource(WiremockPetStore.class) +@QuarkusTest public class PetStoreTest { - @RegisterExtension - static final QuarkusUnitTest config = new QuarkusUnitTest() - .setArchiveProducer(() -> ShrinkWrap - .create(JavaArchive.class) - .addClass(PetResource.class) - .addPackage("org.openapitools.client.model") - .addPackage("org.openapitools.client.api")); - @Test public void testGetPetById() { final String petName = when() diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java index 84184369..3bd75eb0 100644 --- a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/WiremockPetStore.java @@ -30,7 +30,7 @@ public Map start() { "\"name\": \"Bidu\"," + "\"status\": \"AVAILABLE\"" + "}"))); - return Collections.singletonMap("org.openapitools.client.api.PetApi/mp-rest/url", wireMockServer.baseUrl()); + return Collections.singletonMap("org.acme.openapi.api.PetApi/mp-rest/url", wireMockServer.baseUrl()); } @Override From 34742c16fd647e6c1885be8ab00729fdd4327aa7 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 9 Feb 2022 12:33:42 -0300 Subject: [PATCH 4/9] Upgrade to Quarkus 2.7.1.Final Signed-off-by: Ricardo Zanini --- deployment/pom.xml | 3 +-- pom.xml | 2 +- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/deployment/pom.xml b/deployment/pom.xml index 147de13b..c3fb6f6d 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -31,8 +31,6 @@ io.quarkus quarkus-devtools-utilities - - ${quarkus.version} io.quarkus @@ -127,6 +125,7 @@ + org.slf4j slf4j-ext diff --git a/pom.xml b/pom.xml index a0156c2c..4bff5f77 100644 --- a/pom.xml +++ b/pom.xml @@ -21,7 +21,7 @@ 11 UTF-8 UTF-8 - 2.7.0.Final + 2.7.1.Final From d0cf428908094a9b2b0d2dc0285d4dd3cf21c4b7 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 9 Feb 2022 12:35:17 -0300 Subject: [PATCH 5/9] Make reused builditems abstract Signed-off-by: Ricardo Zanini --- .../openapi/generator/deployment/GeneratedOpenApiFile.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java index 4bb3bd91..cd0820b2 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/GeneratedOpenApiFile.java @@ -4,7 +4,7 @@ import io.quarkus.builder.item.MultiBuildItem; -public class GeneratedOpenApiFile extends MultiBuildItem { +public abstract class GeneratedOpenApiFile extends MultiBuildItem { private final ClassInfo classInfo; public GeneratedOpenApiFile(final ClassInfo classInfo) { From 18b965113d1b152b75b146b47f977afef6f848e0 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Wed, 9 Feb 2022 15:58:38 -0300 Subject: [PATCH 6/9] Review unit tests to use RegisterClient Signed-off-by: Ricardo Zanini --- .../src/main/resources/templates/pojo.qute | 13 ++++------ integration-tests/pom.xml | 5 ---- .../openapi/generator/it/PetResource.java | 26 ------------------- .../openapi/generator/it/PetStoreTest.java | 18 ++++++++----- 4 files changed, 16 insertions(+), 46 deletions(-) delete mode 100644 integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/PetResource.java diff --git a/deployment/src/main/resources/templates/pojo.qute b/deployment/src/main/resources/templates/pojo.qute index 3fae0685..604e4932 100644 --- a/deployment/src/main/resources/templates/pojo.qute +++ b/deployment/src/main/resources/templates/pojo.qute @@ -76,16 +76,13 @@ public class {m.classname} {#if m.parent}extends {m.parent}{/if}{#if m.serializa {ext} {/for} {#if v.useBeanValidation}{#include beanValidation.qute p=v/}{/if} - {#if v.isEnum && !v.isArray && !v.isMap}public {v.dataType} {v.getter}() { - if({v.name} == null) { - return null; - } - return {v.name}.value(); - }{#else if !withXml && v.isEnum && !v.isArray && !v.isMap}public {v.datatypeWithEnum} {v.getter}() { + {#if v.isEnum && !v.isArray && !v.isMap}public {v.datatypeWithEnum} {v.getter}() { + return {v.name}; + }{#else if !v.isEnum && !v.isArray && !v.isMap}public {v.datatype} {v.getter}() { return {v.name}; - }{#else if v.isEnum && (v.isArray || v.isMap)}public {v.datatypeWithEnum} {v.getter}() { + }{#else if !v.isEnum && (v.isArray || v.isMap)}public {v.datatype} {v.getter}() { return {v.name}; - }{#else if !v.isEnum}public {v.datatypeWithEnum} {v.getter}() { + }{#else if !v.isEnum}public {v.datatype} {v.getter}() { return {v.name}; }{/if} diff --git a/integration-tests/pom.xml b/integration-tests/pom.xml index b2426dca..957f3dee 100644 --- a/integration-tests/pom.xml +++ b/integration-tests/pom.xml @@ -35,11 +35,6 @@ ${version.com.github.tomakehurst} test - - io.rest-assured - rest-assured - test - diff --git a/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/PetResource.java b/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/PetResource.java deleted file mode 100644 index 3c841e75..00000000 --- a/integration-tests/src/main/java/io/quarkiverse/openapi/generator/it/PetResource.java +++ /dev/null @@ -1,26 +0,0 @@ -package io.quarkiverse.openapi.generator.it; - -import javax.inject.Inject; -import javax.ws.rs.GET; -import javax.ws.rs.Path; -import javax.ws.rs.Produces; -import javax.ws.rs.core.MediaType; - -import org.acme.openapi.api.PetApi; -import org.eclipse.microprofile.rest.client.inject.RestClient; -import org.jboss.resteasy.annotations.jaxrs.PathParam; - -@Produces(MediaType.APPLICATION_JSON) -@Path("/petstore") -public class PetResource { - - @RestClient - @Inject - PetApi petApi; - - @GET - @Path("/pet/name/{id}") - public String getPetName(@PathParam Long id) { - return petApi.getPetById(id).getName(); - } -} \ No newline at end of file diff --git a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java index 86d25e15..857dbb65 100644 --- a/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java +++ b/integration-tests/src/test/java/io/quarkiverse/openapi/generator/it/PetStoreTest.java @@ -1,7 +1,10 @@ package io.quarkiverse.openapi.generator.it; -import static io.restassured.RestAssured.when; +import javax.inject.Inject; +import org.acme.openapi.api.PetApi; +import org.acme.openapi.model.Pet; +import org.eclipse.microprofile.rest.client.inject.RestClient; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -12,13 +15,14 @@ @QuarkusTest public class PetStoreTest { + @RestClient + @Inject + PetApi petApi; + @Test public void testGetPetById() { - final String petName = when() - .get("/petstore/pet/name/1234") - .then() - .statusCode(200) - .extract().asString(); - Assertions.assertEquals("Bidu", petName); + final Pet pet = petApi.getPetById(1234L); + Assertions.assertEquals("Bidu", pet.getName()); + Assertions.assertEquals(Pet.StatusEnum.AVAILABLE, pet.getStatus()); } } From 50f08b749e49ef9e5ac21e5fb33b13074b2925a4 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Thu, 10 Feb 2022 16:17:07 -0300 Subject: [PATCH 7/9] Keeping only one package property for generated code Signed-off-by: Ricardo Zanini --- README.md | 5 ++-- .../OpenApiGeneratorConfiguration.java | 7 ++--- .../deployment/OpenApiGeneratorProcessor.java | 4 +-- .../generator/deployment/SpecConfig.java | 29 ++++++++++++++----- .../codegen/OpenApiGeneratorCodeGenBase.java | 18 +++++------- .../src/main/resources/application.properties | 6 ++-- 6 files changed, 38 insertions(+), 31 deletions(-) diff --git a/README.md b/README.md index f5384f76..71c387c4 100644 --- a/README.md +++ b/README.md @@ -41,11 +41,10 @@ You will also need to add additional execution entries to the `quarkus-maven-plu Now, create the directory `openapi` under your `src/main/` path and add the OpenAPI spec files there. We support JSON, YAML and YML extensions. -To fine tune the configuration for each spec file, add the following entries to your properties file. In this example, our spec file is in `src/main/openapi/petstore.json`: +To fine tune the configuration for each spec file, add the following entry to your properties file. In this example, our spec file is in `src/main/openapi/petstore.json`: ```properties -quarkus.openapi-generator.spec."petstore.json".api-package=org.acme.openapi.api -quarkus.openapi-generator.spec."petstore.json".model-package=org.acme.openapi.model +quarkus.openapi-generator.spec."petstore.json".base-package=org.acme.openapi ``` Note that the file name is used to configure the specific information for each spec. diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java index df7437f3..df0900d7 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorConfiguration.java @@ -8,14 +8,13 @@ @ConfigRoot(phase = ConfigPhase.BUILD_TIME, name = "openapi-generator") public class OpenApiGeneratorConfiguration { + static final String CONFIG_PREFIX = "quarkus.openapi-generator"; + /** * Fine tune the configuration for each OpenAPI spec file in `src/openapi` directory. *

* The file name is used to index this configuration. For example: - * `quarkus.openapi-generator.spec."myfilespec.json".api-package=org.acme.api`. - *

- * If you have more than one file to generate rest clients, it is **highly** recommended that you add this configuration - * to your properties file to make a distinction between the generated APIs. + * `quarkus.openapi-generator.spec."myfilespec.json".base-package=org.acme`. **/ @ConfigItem(name = "spec") public Map specs; diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java index 8bf01079..6d8996bd 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/OpenApiGeneratorProcessor.java @@ -22,10 +22,10 @@ void discoverGeneratedApis(BuildProducer re boolean hasGeneratedFiles = false; for (SpecConfig spec : configuration.specs.values()) { for (ClassInfo classInfo : index.getIndex().getKnownClasses()) { - if (classInfo.name().packagePrefix().equals(spec.apiPackage)) { + if (classInfo.name().packagePrefix().equals(spec.getApiPackage())) { restClients.produce(new GeneratedOpenApiRestClientBuildItem(classInfo)); hasGeneratedFiles = true; - } else if (classInfo.name().packagePrefix().equals(spec.modelPackage)) { + } else if (classInfo.name().packagePrefix().equals(spec.getModelPackage())) { models.produce(new GeneratedOpenApiModelBuildItem(classInfo)); hasGeneratedFiles = true; } diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java index 02f3b39b..b0c2a0b6 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java @@ -1,19 +1,32 @@ package io.quarkiverse.openapi.generator.deployment; +import static io.quarkiverse.openapi.generator.deployment.OpenApiGeneratorConfiguration.CONFIG_PREFIX; + +import java.nio.file.Paths; + import io.quarkus.runtime.annotations.ConfigGroup; import io.quarkus.runtime.annotations.ConfigItem; @ConfigGroup public class SpecConfig { - /** - * Defines the base package name for the generated API classes - */ - @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.api") - public String apiPackage; + public static final String API_PKG_SUFFIX = ".api"; + public static final String MODEL_PKG_SUFFIX = ".model"; /** - * Defines the base package name for the generated Model classes + * Defines the base package name for the generated classes. */ - @ConfigItem(defaultValue = "io.quarkiverse.openapi.generator.model") - public String modelPackage; + @ConfigItem + public String basePackage; + + public String getApiPackage() { + return String.format("%s%s", basePackage, API_PKG_SUFFIX); + } + + public String getModelPackage() { + return String.format("%s%s", basePackage, MODEL_PKG_SUFFIX); + } + + public static String getResolvedBasePackageProperty(final String openApiFile) { + return CONFIG_PREFIX + ".spec.\"" + Paths.get(openApiFile).getFileName().toString() + "\"" + ".base-package"; + } } diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java index 44d57765..cc0519d7 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java @@ -1,9 +1,12 @@ package io.quarkiverse.openapi.generator.deployment.codegen; +import static io.quarkiverse.openapi.generator.deployment.SpecConfig.API_PKG_SUFFIX; +import static io.quarkiverse.openapi.generator.deployment.SpecConfig.MODEL_PKG_SUFFIX; +import static io.quarkiverse.openapi.generator.deployment.SpecConfig.getResolvedBasePackageProperty; + import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.nio.file.Paths; import java.util.List; import java.util.stream.Collectors; import java.util.stream.Stream; @@ -26,8 +29,6 @@ public abstract class OpenApiGeneratorCodeGenBase implements CodeGenProvider { static final String YML = ".yml"; static final String JSON = ".json"; - static final String CONFIG_PREFIX = "quarkus.openapi-generator"; - @Override public String inputDirectory() { return "openapi"; @@ -48,11 +49,12 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { .map(this::escapeWhitespace) .collect(Collectors.toList()); for (String openApiFile : openApiFiles) { - final String prop = getSpecPropertyPrefix(openApiFile); + final String basePackage = getRequiredIndexedProperty(getResolvedBasePackageProperty(openApiFile), + context); final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(openApiFile, outDir.toString()) - .withApiPackage(getRequiredIndexedProperty(prop + ".api-package", context)) - .withModelPackage(getRequiredIndexedProperty(prop + ".model-package", context)); + .withApiPackage(basePackage + API_PKG_SUFFIX) + .withModelPackage(basePackage + MODEL_PKG_SUFFIX); generator.generate(); } return true; @@ -72,10 +74,6 @@ private String escapeWhitespace(String path) { } } - private String getSpecPropertyPrefix(final String openApiFile) { - return CONFIG_PREFIX + ".spec.\"" + Paths.get(openApiFile).getFileName().toString() + "\""; - } - private String getRequiredIndexedProperty(final String propertyKey, final CodeGenContext context) { // this is how we get a required property. The configSource will handle the exception for us. return context.config().getValue(propertyKey, String.class); diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index ee2a6fe0..9c0cae42 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -1,4 +1,2 @@ -quarkus.openapi-generator.spec."petstore.json".api-package=org.acme.openapi.api -quarkus.openapi-generator.spec."petstore.json".model-package=org.acme.openapi.model -quarkus.openapi-generator.spec."openweather.yaml".api-package=org.acme.openapi.weather.api -quarkus.openapi-generator.spec."openweather.yaml".model-package=org.acme.openapi.weather.model \ No newline at end of file +quarkus.openapi-generator.spec."petstore.json".base-package=org.acme.openapi +quarkus.openapi-generator.spec."openweather.yaml".base-package=org.acme.openapi.weather \ No newline at end of file From 0f054081ddd0563bb583f46a4114c2ad45e05794 Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Fri, 11 Feb 2022 17:03:56 -0300 Subject: [PATCH 8/9] handle spaces in spec name Signed-off-by: Ricardo Zanini --- deployment/pom.xml | 43 +------------- .../generator/deployment/SpecConfig.java | 5 +- .../codegen/OpenApiGeneratorCodeGenBase.java | 56 +++++++++---------- .../{openweather.yaml => open weather.yaml} | 0 .../src/main/resources/application.properties | 3 +- 5 files changed, 31 insertions(+), 76 deletions(-) rename integration-tests/src/main/openapi/{openweather.yaml => open weather.yaml} (100%) diff --git a/deployment/pom.xml b/deployment/pom.xml index c3fb6f6d..5475936d 100644 --- a/deployment/pom.xml +++ b/deployment/pom.xml @@ -13,9 +13,7 @@ 5.4.0 1.7.29 - 4.2.0 - 1.6.2 - 2.1.5 + 4.2.1 @@ -76,14 +74,6 @@ jakarta.xml.bind jakarta.xml.bind-api - - io.swagger.core.v3 - swagger-core - - - io.swagger - swagger-core - joda-time joda-time @@ -92,37 +82,6 @@ org.slf4j slf4j-ext - - com.github.jknack - * - - - - - - io.swagger.core.v3 - swagger-core - ${version.io.swagger.core.v3} - - - javax.validation - validation-api - - - jakarta.xml.bind - jakarta.xml.bind-api - - - - - io.swagger - swagger-core - ${version.io.swagger} - - - javax.validation - validation-api - diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java index b0c2a0b6..7698d754 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java @@ -26,7 +26,8 @@ public String getModelPackage() { return String.format("%s%s", basePackage, MODEL_PKG_SUFFIX); } - public static String getResolvedBasePackageProperty(final String openApiFile) { - return CONFIG_PREFIX + ".spec.\"" + Paths.get(openApiFile).getFileName().toString() + "\"" + ".base-package"; + public static String getResolvedBasePackageProperty(final String openApiFilePath) { + final String fileName = Paths.get(openApiFilePath).getFileName().toString().replace(" ", "\\u0020"); + return CONFIG_PREFIX + ".spec.\"" + fileName + "\"" + ".base-package"; } } diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java index cc0519d7..a82b1795 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/codegen/OpenApiGeneratorCodeGenBase.java @@ -7,15 +7,12 @@ import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; -import java.util.List; -import java.util.stream.Collectors; import java.util.stream.Stream; import io.quarkiverse.openapi.generator.deployment.wrapper.OpenApiClientGeneratorWrapper; import io.quarkus.bootstrap.prebuild.CodeGenException; import io.quarkus.deployment.CodeGenContext; import io.quarkus.deployment.CodeGenProvider; -import io.quarkus.utilities.OS; /** * Code generation for OpenApi Client. Generates Java classes from OpenApi spec files located in src/main/openapi or @@ -39,39 +36,36 @@ public boolean trigger(CodeGenContext context) throws CodeGenException { final Path outDir = context.outDir(); final Path openApiDir = context.inputDir(); - try { - if (Files.isDirectory(openApiDir)) { - try (Stream openApiFilesPaths = Files.walk(openApiDir)) { - final List openApiFiles = openApiFilesPaths - .filter(Files::isRegularFile) - .map(Path::toString) - .filter(s -> s.endsWith(this.inputExtension())) - .map(this::escapeWhitespace) - .collect(Collectors.toList()); - for (String openApiFile : openApiFiles) { - final String basePackage = getRequiredIndexedProperty(getResolvedBasePackageProperty(openApiFile), - context); - final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper(openApiFile, - outDir.toString()) - .withApiPackage(basePackage + API_PKG_SUFFIX) - .withModelPackage(basePackage + MODEL_PKG_SUFFIX); - generator.generate(); - } - return true; - } + if (Files.isDirectory(openApiDir)) { + try (Stream openApiFilesPaths = Files.walk(openApiDir)) { + openApiFilesPaths + .filter(Files::isRegularFile) + .map(Path::toString) + .filter(s -> s.endsWith(this.inputExtension())) + .map(this::encodeURISpaces) + .forEach(openApiFilePath -> { + final String basePackage = getRequiredIndexedProperty( + getResolvedBasePackageProperty(openApiFilePath), context); + final OpenApiClientGeneratorWrapper generator = new OpenApiClientGeneratorWrapper( + "file:" + openApiFilePath, outDir.toString()) + .withApiPackage(basePackage + API_PKG_SUFFIX) + .withModelPackage(basePackage + MODEL_PKG_SUFFIX); + generator.generate(); + }); + } catch (IOException e) { + throw new CodeGenException("Failed to generate java files from OpenApi files in " + openApiDir.toAbsolutePath(), + e); } - } catch (IOException e) { - throw new CodeGenException("Failed to generate java files from OpenApi files in " + openApiDir.toAbsolutePath(), e); + return true; } return false; } - private String escapeWhitespace(String path) { - if (OS.determineOS() == OS.LINUX) { - return path.replace(" ", "\\ "); - } else { - return path; - } + private String encodeURISpaces(String path) { + // the underlying swagger parser library handles the path as a URI, and in previous versions (the one used by the openapi-generator) + // it has a bug, not handling these paths correctly. That's why we manually encode the paths here, so the file can be accessible and validated against + // these libraries + return path.replaceAll(" ", "%20"); } private String getRequiredIndexedProperty(final String propertyKey, final CodeGenContext context) { diff --git a/integration-tests/src/main/openapi/openweather.yaml b/integration-tests/src/main/openapi/open weather.yaml similarity index 100% rename from integration-tests/src/main/openapi/openweather.yaml rename to integration-tests/src/main/openapi/open weather.yaml diff --git a/integration-tests/src/main/resources/application.properties b/integration-tests/src/main/resources/application.properties index 9c0cae42..99cba4d4 100644 --- a/integration-tests/src/main/resources/application.properties +++ b/integration-tests/src/main/resources/application.properties @@ -1,2 +1,3 @@ quarkus.openapi-generator.spec."petstore.json".base-package=org.acme.openapi -quarkus.openapi-generator.spec."openweather.yaml".base-package=org.acme.openapi.weather \ No newline at end of file +# since the file name has a space, we use the URI representation of this space here to not break the properties file +quarkus.openapi-generator.spec."open%20weather.yaml".base-package=org.acme.openapi.weather \ No newline at end of file From b853d356d0556fe4644c3de2f747aeb1aa03861d Mon Sep 17 00:00:00 2001 From: Ricardo Zanini Date: Mon, 14 Feb 2022 09:30:13 -0300 Subject: [PATCH 9/9] small fix to not replace ascii space code Signed-off-by: Ricardo Zanini --- .../io/quarkiverse/openapi/generator/deployment/SpecConfig.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java index 7698d754..3cac39f4 100644 --- a/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java +++ b/deployment/src/main/java/io/quarkiverse/openapi/generator/deployment/SpecConfig.java @@ -27,7 +27,7 @@ public String getModelPackage() { } public static String getResolvedBasePackageProperty(final String openApiFilePath) { - final String fileName = Paths.get(openApiFilePath).getFileName().toString().replace(" ", "\\u0020"); + final String fileName = Paths.get(openApiFilePath).getFileName().toString(); return CONFIG_PREFIX + ".spec.\"" + fileName + "\"" + ".base-package"; } }