From c356a86b5ec497deffee5a04b45fcc8c3f595c40 Mon Sep 17 00:00:00 2001 From: Francisco Javier Tirado Sarti <65240126+fjtirado@users.noreply.github.com> Date: Wed, 19 Mar 2025 21:18:25 +0100 Subject: [PATCH 1/2] Merge pull request #826 from mcruzdev/mock-module Add moqu and wiremock module --- client/deployment/pom.xml | 1 - .../images/moqu-devui-card-framework.png | Bin 0 -> 40731 bytes .../ROOT/assets/images/table-wiremock.png | Bin 0 -> 37474 bytes docs/modules/ROOT/nav.adoc | 3 +- ...arkus-openapi-generator-moqu-wiremock.adoc | 32 + ...enapi-generator-moqu-wiremock_quarkus.adoc | 32 + ...qu-wiremock_quarkus.openapi-generator.adoc | 32 + .../quarkus-openapi-generator_quarkus.adoc | 1199 +++++++++++++++++ docs/modules/ROOT/pages/moqu.adoc | 206 +++ moqu/core/pom.xml | 54 + .../io/quarkiverse/openapi/moqu/Moqu.java | 37 + .../openapi/moqu/MoquImporter.java | 17 + .../quarkiverse/openapi/moqu/MoquMapper.java | 19 + .../openapi/moqu/OpenAPIMoquImporter.java | 253 ++++ .../openapi/moqu/ParameterType.java | 43 + .../openapi/moqu/SchemaReader.java | 64 + .../moqu/marshall/ObjectMapperFactory.java | 15 + .../openapi/moqu/model/Header.java | 12 + .../openapi/moqu/model/Operation.java | 10 + .../openapi/moqu/model/Parameter.java | 14 + .../openapi/moqu/model/Request.java | 16 + .../moqu/model/RequestResponsePair.java | 10 + .../openapi/moqu/model/Response.java | 20 + .../moqu/wiremock/mapper/WiremockMapper.java | 41 + .../moqu/wiremock/model/WiremockMapping.java | 4 + .../moqu/wiremock/model/WiremockRequest.java | 6 + .../moqu/wiremock/model/WiremockResponse.java | 8 + .../openapi/moqu/OpenAPIMoquImporterTest.java | 181 +++ .../quarkiverse/openapi/moqu/TestUtils.java | 20 + .../wiremock/mapper/WiremockMapperTest.java | 65 + .../mapper/WiremockPathParamTest.java | 164 +++ .../core/src/test/resources/wiremock/full.yml | 45 + .../should_map_one_wiremock_definition.yml | 28 + .../should_map_two_wiremock_definition.yml | 33 + .../wiremock/one_example_in_the_same_path.yml | 28 + .../wiremock/path_param_one_path_param.yml | 23 + ...th_param_two_params_but_different_path.yml | 39 + ...path_param_two_path_params_combination.yml | 35 + ..._two_path_params_only_one_with_example.yml | 25 + .../resources/wiremock/response_from_ref.yml | 31 + .../wiremock/response_from_ref_and_noref.yml | 38 + .../wiremock/response_from_ref_array.yml | 37 + .../two_examples_in_the_same_path.yml | 33 + moqu/deployment/pom.xml | 66 + .../generator/MoquProjectProcessor.java | 102 ++ .../generator/MoquWiremockProcessor.java | 12 + .../openapi/generator/devui/MoquModel.java | 5 + .../devui/MoquWiremockDevUIProcessor.java | 81 ++ .../generator/items/MoquBuildItem.java | 37 + .../generator/items/MoquProjectBuildItem.java | 22 + .../src/main/resources/dev-ui/qwc-moqu.js | 96 ++ .../generator/MoquProjectProcessorTest.java | 47 + moqu/deployment/src/test/resources/api.yaml | 35 + moqu/deployment/src/test/resources/apiv2.json | 60 + moqu/pom.xml | 19 + moqu/runtime/pom.xml | 77 ++ .../openapi/generator/moqu/MoquConfig.java | 19 + .../moqu/recorder/MoquRoutesRecorder.java | 47 + .../resources/META-INF/quarkus-extension.yaml | 11 + pom.xml | 4 + 60 files changed, 3711 insertions(+), 2 deletions(-) create mode 100644 docs/modules/ROOT/assets/images/moqu-devui-card-framework.png create mode 100644 docs/modules/ROOT/assets/images/table-wiremock.png create mode 100644 docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock.adoc create mode 100644 docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock_quarkus.adoc create mode 100644 docs/modules/ROOT/pages/includes/quarkus-openapi-generator-moqu-wiremock_quarkus.openapi-generator.adoc create mode 100644 docs/modules/ROOT/pages/includes/quarkus-openapi-generator_quarkus.adoc create mode 100644 docs/modules/ROOT/pages/moqu.adoc create mode 100644 moqu/core/pom.xml create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/Moqu.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquImporter.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquMapper.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporter.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/ParameterType.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/SchemaReader.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/marshall/ObjectMapperFactory.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Header.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Operation.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Parameter.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Request.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/RequestResponsePair.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Response.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapper.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockMapping.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockRequest.java create mode 100644 moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockResponse.java create mode 100644 moqu/core/src/test/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporterTest.java create mode 100644 moqu/core/src/test/java/io/quarkiverse/openapi/moqu/TestUtils.java create mode 100644 moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapperTest.java create mode 100644 moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockPathParamTest.java create mode 100644 moqu/core/src/test/resources/wiremock/full.yml create mode 100644 moqu/core/src/test/resources/wiremock/mapper/should_map_one_wiremock_definition.yml create mode 100644 moqu/core/src/test/resources/wiremock/mapper/should_map_two_wiremock_definition.yml create mode 100644 moqu/core/src/test/resources/wiremock/one_example_in_the_same_path.yml create mode 100644 moqu/core/src/test/resources/wiremock/path_param_one_path_param.yml create mode 100644 moqu/core/src/test/resources/wiremock/path_param_two_params_but_different_path.yml create mode 100644 moqu/core/src/test/resources/wiremock/path_param_two_path_params_combination.yml create mode 100644 moqu/core/src/test/resources/wiremock/path_param_two_path_params_only_one_with_example.yml create mode 100644 moqu/core/src/test/resources/wiremock/response_from_ref.yml create mode 100644 moqu/core/src/test/resources/wiremock/response_from_ref_and_noref.yml create mode 100644 moqu/core/src/test/resources/wiremock/response_from_ref_array.yml create mode 100644 moqu/core/src/test/resources/wiremock/two_examples_in_the_same_path.yml create mode 100644 moqu/deployment/pom.xml create mode 100644 moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquProjectProcessor.java create mode 100644 moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquWiremockProcessor.java create mode 100644 moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquModel.java create mode 100644 moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquWiremockDevUIProcessor.java create mode 100644 moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquBuildItem.java create mode 100644 moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquProjectBuildItem.java create mode 100644 moqu/deployment/src/main/resources/dev-ui/qwc-moqu.js create mode 100644 moqu/deployment/src/test/java/io/quarkiverse/openapi/generator/MoquProjectProcessorTest.java create mode 100644 moqu/deployment/src/test/resources/api.yaml create mode 100644 moqu/deployment/src/test/resources/apiv2.json create mode 100644 moqu/pom.xml create mode 100644 moqu/runtime/pom.xml create mode 100644 moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/MoquConfig.java create mode 100644 moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/recorder/MoquRoutesRecorder.java create mode 100644 moqu/runtime/src/main/resources/META-INF/quarkus-extension.yaml diff --git a/client/deployment/pom.xml b/client/deployment/pom.xml index 04eccecbd..a995eb63d 100644 --- a/client/deployment/pom.xml +++ b/client/deployment/pom.xml @@ -14,7 +14,6 @@ 7.12.0 2.0.17 4.3.1 - 2.1.25 diff --git a/docs/modules/ROOT/assets/images/moqu-devui-card-framework.png b/docs/modules/ROOT/assets/images/moqu-devui-card-framework.png new file mode 100644 index 0000000000000000000000000000000000000000..e9b7d0e405eac7e60b018dd35748c7c7341d9251 GIT binary patch literal 40731 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

IfaN^_u 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 literal 0 HcmV?d00001 diff --git a/docs/modules/ROOT/assets/images/table-wiremock.png b/docs/modules/ROOT/assets/images/table-wiremock.png new file mode 100644 index 0000000000000000000000000000000000000000..7113b186b663e47320866d27b38ad33595f21c2b GIT binary patch literal 37474 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 +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-import-mappings-import-mappings]] [.property-path]##link:#quarkus-openapi-generator_quarkus-import-mappings-import-mappings[`quarkus.import-mappings."import-mappings"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.import-mappings."import-mappings"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Import Mapping is an OpenAPI Generator configuration specifying which Java types (the values) should be imported when a given OAS datatype (the keys of this map) is used + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_IMPORT_MAPPINGS__IMPORT_MAPPINGS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_IMPORT_MAPPINGS__IMPORT_MAPPINGS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-schema-mappings-schema-mappings]] [.property-path]##link:#quarkus-openapi-generator_quarkus-schema-mappings-schema-mappings[`quarkus.schema-mappings."schema-mappings"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.schema-mappings."schema-mappings"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Schema Mapping is an OpenAPI Generator configuration specifying which Java types (the values) should be imported when a given schema type (the keys of this map) is used + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SCHEMA_MAPPINGS__SCHEMA_MAPPINGS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SCHEMA_MAPPINGS__SCHEMA_MAPPINGS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-additional-model-type-annotations]] [.property-path]##link:#quarkus-openapi-generator_quarkus-additional-model-type-annotations[`quarkus.additional-model-type-annotations`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.additional-model-type-annotations+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The specified annotations will be added to the generated model files + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_ADDITIONAL_MODEL_TYPE_ANNOTATIONS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_ADDITIONAL_MODEL_TYPE_ANNOTATIONS+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-additional-enum-type-unexpected-member]] [.property-path]##link:#quarkus-openapi-generator_quarkus-additional-enum-type-unexpected-member[`quarkus.additional-enum-type-unexpected-member`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.additional-enum-type-unexpected-member+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines if the enums should have an `UNEXPECTED` member to convey values that cannot be parsed. Default is `false`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_ADDITIONAL_ENUM_TYPE_UNEXPECTED_MEMBER+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_ADDITIONAL_ENUM_TYPE_UNEXPECTED_MEMBER+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-additional-api-type-annotations]] [.property-path]##link:#quarkus-openapi-generator_quarkus-additional-api-type-annotations[`quarkus.additional-api-type-annotations`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.additional-api-type-annotations+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The specified annotations will be added to the generated api files + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_ADDITIONAL_API_TYPE_ANNOTATIONS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_ADDITIONAL_API_TYPE_ANNOTATIONS+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-additional-request-args]] [.property-path]##link:#quarkus-openapi-generator_quarkus-additional-request-args[`quarkus.additional-request-args`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.additional-request-args+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Add custom/additional HTTP Headers or other args to every request + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_ADDITIONAL_REQUEST_ARGS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_ADDITIONAL_REQUEST_ARGS+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-return-response]] [.property-path]##link:#quarkus-openapi-generator_quarkus-return-response[`quarkus.return-response`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.return-response+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines if the methods should return `jakarta.ws.rs.core.Response` or a model. Default is `false`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_RETURN_RESPONSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_RETURN_RESPONSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-enable-security-generation]] [.property-path]##link:#quarkus-openapi-generator_quarkus-enable-security-generation[`quarkus.enable-security-generation`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.enable-security-generation+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines if security support classes should be generated + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_ENABLE_SECURITY_GENERATION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_ENABLE_SECURITY_GENERATION+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-open-api-normalizer-normalizer]] [.property-path]##link:#quarkus-openapi-generator_quarkus-open-api-normalizer-normalizer[`quarkus.open-api-normalizer."normalizer"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.open-api-normalizer."normalizer"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines the normalizer options. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_OPEN_API_NORMALIZER__NORMALIZER_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_OPEN_API_NORMALIZER__NORMALIZER_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-mutiny]] [.property-path]##link:#quarkus-openapi-generator_quarkus-mutiny[`quarkus.mutiny`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.mutiny+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable SmallRye Mutiny support. If you set this to `true`, all return types will be wrapped in `io.smallrye.mutiny.Uni`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_MUTINY+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_MUTINY+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-mutiny-return-response]] [.property-path]##link:#quarkus-openapi-generator_quarkus-mutiny-return-response[`quarkus.mutiny.return-response`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.mutiny.return-response+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines with SmallRye Mutiny enabled if methods should return `jakarta.ws.rs.core.Response` or a model. Default is `false`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_MUTINY_RETURN_RESPONSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_MUTINY_RETURN_RESPONSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-mutiny-operation-ids-mutiny-multi-operation-ids]] [.property-path]##link:#quarkus-openapi-generator_quarkus-mutiny-operation-ids-mutiny-multi-operation-ids[`quarkus.mutiny.operation-ids."mutiny-multi-operation-ids"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.mutiny.operation-ids."mutiny-multi-operation-ids"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Handles the return type for each operation, depending on the configuration. The following cases are supported: + +1. If `mutiny` is enabled and the operation ID is specified to return `Multi`: - The return type will be wrapped in `io.smallrye.mutiny.Multi`. - If `mutiny.return-response` is enabled, the return type will be `io.smallrye.mutiny.Multi`. - If the operation has a void return type, it will return `io.smallrye.mutiny.Multi`. - Otherwise, it will return `io.smallrye.mutiny.Multi`. + +2. If `mutiny` is enabled and the operation ID is specified to return `Uni`: - The return type will be wrapped in `io.smallrye.mutiny.Uni`. - If `mutiny.return-response` is enabled, the return type will be `io.smallrye.mutiny.Uni`. - If the operation has a void return type, it will return `io.smallrye.mutiny.Uni`. - Otherwise, it will return `io.smallrye.mutiny.Uni`. + +3. If `mutiny` is enabled but no specific operation ID is configured for `Multi` or `Uni`: - The return type defaults to `Uni`. - If `mutiny.return-response` is enabled, the return type will be `io.smallrye.mutiny.Uni`. - If the operation has a void return type, it will return `io.smallrye.mutiny.Uni`. - Otherwise, it will return `io.smallrye.mutiny.Uni``. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_MUTINY_OPERATION_IDS__MUTINY_MULTI_OPERATION_IDS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_MUTINY_OPERATION_IDS__MUTINY_MULTI_OPERATION_IDS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-generate-part-filename]] [.property-path]##link:#quarkus-openapi-generator_quarkus-generate-part-filename[`quarkus.generate-part-filename`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.generate-part-filename+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines, whether the `PartFilename` (`org.jboss.resteasy.reactive.PartFilename` or `org.jboss.resteasy.annotations.providers.multipart.PartFilename`) annotation should be generated for MultipartForm POJOs. By setting to `false`, the annotation will not be generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GENERATE_PART_FILENAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GENERATE_PART_FILENAME+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-part-filename-value]] [.property-path]##link:#quarkus-openapi-generator_quarkus-part-filename-value[`quarkus.part-filename-value`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.part-filename-value+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines the filename for a part in case the `PartFilename` annotation (`org.jboss.resteasy.reactive.PartFilename` or `org.jboss.resteasy.annotations.providers.multipart.PartFilename`) is generated. In case no value is set, the default one is `File` or `file`, depending on the `CommonItemConfig++#++useFieldNameInPartFilename` configuration. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_PART_FILENAME_VALUE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_PART_FILENAME_VALUE+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-use-field-name-in-part-filename]] [.property-path]##link:#quarkus-openapi-generator_quarkus-use-field-name-in-part-filename[`quarkus.use-field-name-in-part-filename`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.use-field-name-in-part-filename+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines, whether the filename should also include the property name in case the `PartFilename` annotation (`org.jboss.resteasy.reactive.PartFilename` or `org.jboss.resteasy.annotations.providers.multipart.PartFilename`) is generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_USE_FIELD_NAME_IN_PART_FILENAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_USE_FIELD_NAME_IN_PART_FILENAME+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-use-bean-validation]] [.property-path]##link:#quarkus-openapi-generator_quarkus-use-bean-validation[`quarkus.use-bean-validation`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.use-bean-validation+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable bean validation. If you set this to `true`, validation annotations are added to generated sources E.g. `@Size`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_USE_BEAN_VALIDATION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_USE_BEAN_VALIDATION+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-generate-apis]] [.property-path]##link:#quarkus-openapi-generator_quarkus-generate-apis[`quarkus.generate-apis`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.generate-apis+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the generation of APIs. If you set this to `false`, APIs will not be generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GENERATE_APIS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GENERATE_APIS+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-generate-models]] [.property-path]##link:#quarkus-openapi-generator_quarkus-generate-models[`quarkus.generate-models`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.generate-models+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the generation of models. If you set this to `false`, models will not be generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_GENERATE_MODELS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_GENERATE_MODELS+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-equals-hashcode]] [.property-path]##link:#quarkus-openapi-generator_quarkus-equals-hashcode[`quarkus.equals-hashcode`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.equals-hashcode+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the generation of equals and hashcode in models. If you set this to `false`, the models will not have equals and hashcode. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_EQUALS_HASHCODE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_EQUALS_HASHCODE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-verbose]] [.property-path]##link:#quarkus-openapi-generator_quarkus-verbose[`quarkus.verbose`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.verbose+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Whether to log the internal generator codegen process in the default output or not. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_VERBOSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_VERBOSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`false` + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-input-base-dir]] [.property-path]##link:#quarkus-openapi-generator_quarkus-input-base-dir[`quarkus.input-base-dir`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.input-base-dir+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Option to change the directory where OpenAPI files must be found. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_INPUT_BASE_DIR+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_INPUT_BASE_DIR+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-template-base-dir]] [.property-path]##link:#quarkus-openapi-generator_quarkus-template-base-dir[`quarkus.template-base-dir`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.template-base-dir+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Option to change the directory where template files must be found. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_TEMPLATE_BASE_DIR+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_TEMPLATE_BASE_DIR+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-validatespec]] [.property-path]##link:#quarkus-openapi-generator_quarkus-validatespec[`quarkus.validateSpec`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.validateSpec+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Whether or not to skip validating the input spec prior to generation. By default, invalid specifications will result in an error. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_VALIDATESPEC+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_VALIDATESPEC+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +|`true` + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-include]] [.property-path]##link:#quarkus-openapi-generator_quarkus-include[`quarkus.include`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.include+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Option to specify files for which generation should be executed only + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_INCLUDE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_INCLUDE+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-exclude]] [.property-path]##link:#quarkus-openapi-generator_quarkus-exclude[`quarkus.exclude`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.exclude+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Option to exclude file from generation + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_EXCLUDE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_EXCLUDE+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-default-security-scheme]] [.property-path]##link:#quarkus-openapi-generator_quarkus-default-security-scheme[`quarkus.default-security-scheme`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.default-security-scheme+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Create security for the referenced security scheme + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_DEFAULT_SECURITY_SCHEME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_DEFAULT_SECURITY_SCHEME+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-skip-form-model]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-skip-form-model[`quarkus.spec."spec-item".skip-form-model`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".skip-form-model+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Whether to skip the generation of models for form parameters + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__SKIP_FORM_MODEL+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__SKIP_FORM_MODEL+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-type-mappings-type-mappings]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-type-mappings-type-mappings[`quarkus.spec."spec-item".type-mappings."type-mappings"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".type-mappings."type-mappings"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Type Mapping is an OpenAPI Generator configuration specifying which Java types (the values) should be used for a given OAS datatype (the keys of this map) + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__TYPE_MAPPINGS__TYPE_MAPPINGS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__TYPE_MAPPINGS__TYPE_MAPPINGS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-import-mappings-import-mappings]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-import-mappings-import-mappings[`quarkus.spec."spec-item".import-mappings."import-mappings"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".import-mappings."import-mappings"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Import Mapping is an OpenAPI Generator configuration specifying which Java types (the values) should be imported when a given OAS datatype (the keys of this map) is used + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__IMPORT_MAPPINGS__IMPORT_MAPPINGS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__IMPORT_MAPPINGS__IMPORT_MAPPINGS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-schema-mappings-schema-mappings]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-schema-mappings-schema-mappings[`quarkus.spec."spec-item".schema-mappings."schema-mappings"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".schema-mappings."schema-mappings"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Schema Mapping is an OpenAPI Generator configuration specifying which Java types (the values) should be imported when a given schema type (the keys of this map) is used + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__SCHEMA_MAPPINGS__SCHEMA_MAPPINGS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__SCHEMA_MAPPINGS__SCHEMA_MAPPINGS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-additional-model-type-annotations]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-additional-model-type-annotations[`quarkus.spec."spec-item".additional-model-type-annotations`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".additional-model-type-annotations+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The specified annotations will be added to the generated model files + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_MODEL_TYPE_ANNOTATIONS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_MODEL_TYPE_ANNOTATIONS+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-additional-enum-type-unexpected-member]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-additional-enum-type-unexpected-member[`quarkus.spec."spec-item".additional-enum-type-unexpected-member`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".additional-enum-type-unexpected-member+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines if the enums should have an `UNEXPECTED` member to convey values that cannot be parsed. Default is `false`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_ENUM_TYPE_UNEXPECTED_MEMBER+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_ENUM_TYPE_UNEXPECTED_MEMBER+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-additional-api-type-annotations]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-additional-api-type-annotations[`quarkus.spec."spec-item".additional-api-type-annotations`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".additional-api-type-annotations+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +The specified annotations will be added to the generated api files + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_API_TYPE_ANNOTATIONS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_API_TYPE_ANNOTATIONS+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-additional-request-args]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-additional-request-args[`quarkus.spec."spec-item".additional-request-args`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".additional-request-args+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Add custom/additional HTTP Headers or other args to every request + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_REQUEST_ARGS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__ADDITIONAL_REQUEST_ARGS+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-return-response]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-return-response[`quarkus.spec."spec-item".return-response`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".return-response+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines if the methods should return `jakarta.ws.rs.core.Response` or a model. Default is `false`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__RETURN_RESPONSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__RETURN_RESPONSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-enable-security-generation]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-enable-security-generation[`quarkus.spec."spec-item".enable-security-generation`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".enable-security-generation+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines if security support classes should be generated + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__ENABLE_SECURITY_GENERATION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__ENABLE_SECURITY_GENERATION+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-open-api-normalizer-normalizer]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-open-api-normalizer-normalizer[`quarkus.spec."spec-item".open-api-normalizer."normalizer"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".open-api-normalizer."normalizer"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines the normalizer options. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__OPEN_API_NORMALIZER__NORMALIZER_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__OPEN_API_NORMALIZER__NORMALIZER_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-mutiny]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-mutiny[`quarkus.spec."spec-item".mutiny`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".mutiny+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable SmallRye Mutiny support. If you set this to `true`, all return types will be wrapped in `io.smallrye.mutiny.Uni`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__MUTINY+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__MUTINY+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-mutiny-return-response]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-mutiny-return-response[`quarkus.spec."spec-item".mutiny.return-response`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".mutiny.return-response+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines with SmallRye Mutiny enabled if methods should return `jakarta.ws.rs.core.Response` or a model. Default is `false`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__MUTINY_RETURN_RESPONSE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__MUTINY_RETURN_RESPONSE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-mutiny-operation-ids-mutiny-multi-operation-ids]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-mutiny-operation-ids-mutiny-multi-operation-ids[`quarkus.spec."spec-item".mutiny.operation-ids."mutiny-multi-operation-ids"`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".mutiny.operation-ids."mutiny-multi-operation-ids"+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Handles the return type for each operation, depending on the configuration. The following cases are supported: + +1. If `mutiny` is enabled and the operation ID is specified to return `Multi`: - The return type will be wrapped in `io.smallrye.mutiny.Multi`. - If `mutiny.return-response` is enabled, the return type will be `io.smallrye.mutiny.Multi`. - If the operation has a void return type, it will return `io.smallrye.mutiny.Multi`. - Otherwise, it will return `io.smallrye.mutiny.Multi`. + +2. If `mutiny` is enabled and the operation ID is specified to return `Uni`: - The return type will be wrapped in `io.smallrye.mutiny.Uni`. - If `mutiny.return-response` is enabled, the return type will be `io.smallrye.mutiny.Uni`. - If the operation has a void return type, it will return `io.smallrye.mutiny.Uni`. - Otherwise, it will return `io.smallrye.mutiny.Uni`. + +3. If `mutiny` is enabled but no specific operation ID is configured for `Multi` or `Uni`: - The return type defaults to `Uni`. - If `mutiny.return-response` is enabled, the return type will be `io.smallrye.mutiny.Uni`. - If the operation has a void return type, it will return `io.smallrye.mutiny.Uni`. - Otherwise, it will return `io.smallrye.mutiny.Uni``. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__MUTINY_OPERATION_IDS__MUTINY_MULTI_OPERATION_IDS_+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__MUTINY_OPERATION_IDS__MUTINY_MULTI_OPERATION_IDS_+++` +endif::add-copy-button-to-env-var[] +-- +|Map +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-generate-part-filename]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-generate-part-filename[`quarkus.spec."spec-item".generate-part-filename`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".generate-part-filename+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines, whether the `PartFilename` (`org.jboss.resteasy.reactive.PartFilename` or `org.jboss.resteasy.annotations.providers.multipart.PartFilename`) annotation should be generated for MultipartForm POJOs. By setting to `false`, the annotation will not be generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__GENERATE_PART_FILENAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__GENERATE_PART_FILENAME+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-part-filename-value]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-part-filename-value[`quarkus.spec."spec-item".part-filename-value`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".part-filename-value+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines the filename for a part in case the `PartFilename` annotation (`org.jboss.resteasy.reactive.PartFilename` or `org.jboss.resteasy.annotations.providers.multipart.PartFilename`) is generated. In case no value is set, the default one is `File` or `file`, depending on the `CommonItemConfig++#++useFieldNameInPartFilename` configuration. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__PART_FILENAME_VALUE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__PART_FILENAME_VALUE+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-use-field-name-in-part-filename]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-use-field-name-in-part-filename[`quarkus.spec."spec-item".use-field-name-in-part-filename`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".use-field-name-in-part-filename+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Defines, whether the filename should also include the property name in case the `PartFilename` annotation (`org.jboss.resteasy.reactive.PartFilename` or `org.jboss.resteasy.annotations.providers.multipart.PartFilename`) is generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__USE_FIELD_NAME_IN_PART_FILENAME+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__USE_FIELD_NAME_IN_PART_FILENAME+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-use-bean-validation]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-use-bean-validation[`quarkus.spec."spec-item".use-bean-validation`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".use-bean-validation+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable bean validation. If you set this to `true`, validation annotations are added to generated sources E.g. `@Size`. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__USE_BEAN_VALIDATION+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__USE_BEAN_VALIDATION+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-generate-apis]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-generate-apis[`quarkus.spec."spec-item".generate-apis`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".generate-apis+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the generation of APIs. If you set this to `false`, APIs will not be generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__GENERATE_APIS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__GENERATE_APIS+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-generate-models]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-generate-models[`quarkus.spec."spec-item".generate-models`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".generate-models+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the generation of models. If you set this to `false`, models will not be generated. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__GENERATE_MODELS+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__GENERATE_MODELS+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-equals-hashcode]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-equals-hashcode[`quarkus.spec."spec-item".equals-hashcode`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".equals-hashcode+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Enable the generation of equals and hashcode in models. If you set this to `false`, the models will not have equals and hashcode. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__EQUALS_HASHCODE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__EQUALS_HASHCODE+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-base-package]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-base-package[`quarkus.spec."spec-item".base-package`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".base-package+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Base package for where the generated code for the given OpenAPI specification will be added. + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__BASE_PACKAGE+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__BASE_PACKAGE+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-api-name-suffix]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-api-name-suffix[`quarkus.spec."spec-item".api-name-suffix`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".api-name-suffix+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Suffix name for generated api classes + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__API_NAME_SUFFIX+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__API_NAME_SUFFIX+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-model-name-suffix]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-model-name-suffix[`quarkus.spec."spec-item".model-name-suffix`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".model-name-suffix+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Suffix name for generated model classes + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__MODEL_NAME_SUFFIX+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__MODEL_NAME_SUFFIX+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-model-name-prefix]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-model-name-prefix[`quarkus.spec."spec-item".model-name-prefix`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".model-name-prefix+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Prefix name for generated model classes + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__MODEL_NAME_PREFIX+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__MODEL_NAME_PREFIX+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-remove-operation-id-prefix]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-remove-operation-id-prefix[`quarkus.spec."spec-item".remove-operation-id-prefix`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".remove-operation-id-prefix+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Remove operation id prefix + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__REMOVE_OPERATION_ID_PREFIX+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__REMOVE_OPERATION_ID_PREFIX+++` +endif::add-copy-button-to-env-var[] +-- +|boolean +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-remove-operation-id-prefix-delimiter]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-remove-operation-id-prefix-delimiter[`quarkus.spec."spec-item".remove-operation-id-prefix-delimiter`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".remove-operation-id-prefix-delimiter+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Remove operation id prefix + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__REMOVE_OPERATION_ID_PREFIX_DELIMITER+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__REMOVE_OPERATION_ID_PREFIX_DELIMITER+++` +endif::add-copy-button-to-env-var[] +-- +|string +| + +a|icon:lock[title=Fixed at build time] [[quarkus-openapi-generator_quarkus-spec-spec-item-remove-operation-id-prefix-count]] [.property-path]##link:#quarkus-openapi-generator_quarkus-spec-spec-item-remove-operation-id-prefix-count[`quarkus.spec."spec-item".remove-operation-id-prefix-count`]## +ifdef::add-copy-button-to-config-props[] +config_property_copy_button:+++quarkus.spec."spec-item".remove-operation-id-prefix-count+++[] +endif::add-copy-button-to-config-props[] + + +[.description] +-- +Remove operation id prefix + + +ifdef::add-copy-button-to-env-var[] +Environment variable: env_var_with_copy_button:+++QUARKUS_SPEC__SPEC_ITEM__REMOVE_OPERATION_ID_PREFIX_COUNT+++[] +endif::add-copy-button-to-env-var[] +ifndef::add-copy-button-to-env-var[] +Environment variable: `+++QUARKUS_SPEC__SPEC_ITEM__REMOVE_OPERATION_ID_PREFIX_COUNT+++` +endif::add-copy-button-to-env-var[] +-- +|int +| + +|=== + diff --git a/docs/modules/ROOT/pages/moqu.adoc b/docs/modules/ROOT/pages/moqu.adoc new file mode 100644 index 000000000..846cd9bbc --- /dev/null +++ b/docs/modules/ROOT/pages/moqu.adoc @@ -0,0 +1,206 @@ += Quarkus - Open API Generator - Moqu + +include::./includes/attributes.adoc[] + +The **OpenAPI Generator Moqu extension** converts an OpenAPI specification into a mock representation. This mock can then be mapped to the link:https://wiremock.org/[WireMock] for further use, providing a way to simulate APIs for testing and development purposes. + +[NOTE] +==== +Currently, this extension supports only link:https://wiremock.org/[WireMock] definitions. +==== + +[[getting-started]] +== Getting Started + +[source,xml] +---- + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu + {project-version} + +---- + +Now, create the following OpenAPI specification file under your `src/resources/openapi` directory: + +[source,yaml] +.src/main/openapi/hello.yaml +---- +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Get framework by ID +paths: + "/frameworks/{id}": + get: + parameters: + - name: id + in: path + examples: + quarkus: + value: 1 + responses: + 200: + content: + "application/json": + examples: + quarkus: + $ref: "#/components/schemas/Framework" + description: Ok +components: + schemas: + Framework: + type: object + properties: + name: + type: string + example: "Quarkus" + versions: + type: array + example: ["999-SNAPSHOT", "3.15.1"] + supportsJava: + type: boolean + example: true + contributors: + type: integer + example: 1000 + rules: + type: object + example: + hello: world +---- + +Execute now your application on Dev mode, and access the Dev UI for getting you wiremock stubbing: + +image::moqu-devui-card-framework.png[] + +Click on `Moqu Wiremock`, you will se a table containing all wiremock definitions: + +image::table-wiremock.png[] + +Now, you can `see` or `download` the Wiremock stubbing. + +== Request matching + +The Moqu extension uses the request and response examples defined in the OpenAPI Specification to determine the appropriate response for a specific request, creating a corresponding request/response pair. + +Example: + +[source,yaml] +---- +openapi: 3.0.3 +info: + title: "Users API" + version: 1.0.0-alpha +servers: + - url: http://localhost:8888 +paths: + /users/{id}: + get: + description: Get user by ID + parameters: + - name: id + in: path + required: true + schema: + type: number + examples: + john: <1> + value: 1 <2> + responses: + "200": + description: Ok + content: + "application/json": + examples: + john: <3> + value: + '{"id": 1, "name": "John Doe"}' +---- + +<1> Defines an example named `john` for request +<2> Maps the request for path `/users/1` should use the response named as `john` +<3> Defines an example named `john` for response + + +In other words, if the user accesses `/users/1`, the response will be the one mapped for the `john` example in response. + +The Wiremock definition using the OpenAPI specification above, looks something like this: + +[source,json] +---- +{ + "mappings": [ + { + "request": { + "method": "GET", + "url": "/users/1" + }, + "response": { + "status": 200, + "body": "{\"name\":\"John\",\"age\": 80}", + "headers": {} + } + } + ] +} +---- + +=== Response as Schema + +You can use the `$ref` to reference a schema for mapping a response: + +[source,yaml] +---- +paths: + "/users/{id}": + get: + parameters: + - name: id + in: path + examples: + alice: + value: 1 + responses: + 200: + content: + "application/json": + examples: + alice: + $ref: "#/components/schemas/User" + description: Ok +components: + schemas: + User: + type: object + properties: + name: + type: string + example: "Alice" + age: + type: number + example: 80 +---- + +The Wiremock definition using the OpenAPI specification above, looks something like this: + +[source,json] +---- +{ + "mappings": [ + { + "request": { + "method": "GET", + "url": "/users/1" + }, + "response": { + "status": 200, + "body": "{\"name\":\"Alice\",\"age\":80}", + "headers": {} + } + } + ] +} +---- \ No newline at end of file diff --git a/moqu/core/pom.xml b/moqu/core/pom.xml new file mode 100644 index 000000000..06bd3b4a2 --- /dev/null +++ b/moqu/core/pom.xml @@ -0,0 +1,54 @@ + + + 4.0.0 + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu-parent + 3.0.0-SNAPSHOT + + + quarkus-openapi-generator-moqu-core + Quarkus :: Openapi Generator :: Moqu :: Core + + + 2.16.1 + + + + + io.swagger.parser.v3 + swagger-parser + ${version.io.swagger.parser} + + + org.assertj + assertj-core + + + org.junit.jupiter + junit-jupiter-api + test + + + org.junit.jupiter + junit-jupiter-params + test + + + org.junit.jupiter + junit-jupiter-engine + test + + + commons-io + commons-io + ${commons.io.version} + + + org.jboss.logmanager + jboss-logmanager + + + diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/Moqu.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/Moqu.java new file mode 100644 index 000000000..1d0024cad --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/Moqu.java @@ -0,0 +1,37 @@ +package io.quarkiverse.openapi.moqu; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Objects; + +import io.quarkiverse.openapi.moqu.model.RequestResponsePair; + +/** + * Represents a collection of request-response pairs, providing methods to access + * these pairs in an immutable list. + */ +public class Moqu { + + private List requestResponsePairs = new ArrayList<>(); + + /** + * Constructs a {@code Moqu} instance with the provided list of request-response pairs. + * + * @param requestResponsePairs the list of {@link RequestResponsePair} objects to initialize + * the collection. Must not be {@code null}. + * @throws NullPointerException if {@code requestResponsePairs} is null. + */ + public Moqu(List requestResponsePairs) { + this.requestResponsePairs = Objects.requireNonNull(requestResponsePairs); + } + + /** + * Returns an unmodifiable list of request-response pairs. + * + * @return an immutable list of {@link RequestResponsePair}. + */ + public List getRequestResponsePairs() { + return Collections.unmodifiableList(requestResponsePairs); + } +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquImporter.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquImporter.java new file mode 100644 index 000000000..16d63747d --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquImporter.java @@ -0,0 +1,17 @@ +package io.quarkiverse.openapi.moqu; + +/** + * {@link MoquImporter} aims to convert a specification into a {@link Moqu} model. + * It provides a method to parse the content, typically from an OpenAPI specification, + * and generate a corresponding {@link Moqu} instance. + */ +public interface MoquImporter { + + /** + * Parses the provided OpenAPI content and generates a new {@link Moqu} instance. + * + * @param content the OpenAPI content as a string, which will be parsed into a {@link Moqu} model. + * @return a new {@link Moqu} instance based on the provided content. + */ + Moqu parse(String content); +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquMapper.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquMapper.java new file mode 100644 index 000000000..53ed02bbe --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/MoquMapper.java @@ -0,0 +1,19 @@ +package io.quarkiverse.openapi.moqu; + +import java.util.List; + +/** + * A generic interface for mapping a {@link Moqu} instance to a list of objects of type {@code T}. + * + * @param the type of objects to which the {@link Moqu} instance will be mapped. + */ +public interface MoquMapper { + + /** + * Maps the given {@link Moqu} instance to a list of objects of type {@code T}. + * + * @param moqu the {@link Moqu} instance to be mapped. + * @return a list of mapped objects of type {@code T}. + */ + List map(Moqu moqu); +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporter.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporter.java new file mode 100644 index 000000000..a969d8582 --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporter.java @@ -0,0 +1,253 @@ +package io.quarkiverse.openapi.moqu; + +import static io.swagger.v3.parser.util.SchemaTypeUtil.INTEGER_TYPE; +import static io.swagger.v3.parser.util.SchemaTypeUtil.OBJECT_TYPE; +import static io.swagger.v3.parser.util.SchemaTypeUtil.STRING_TYPE; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Collectors; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.google.common.base.Strings; +import com.google.common.collect.ArrayListMultimap; +import com.google.common.collect.Multimap; + +import io.quarkiverse.openapi.moqu.model.Header; +import io.quarkiverse.openapi.moqu.model.Request; +import io.quarkiverse.openapi.moqu.model.RequestResponsePair; +import io.quarkiverse.openapi.moqu.model.Response; +import io.swagger.v3.oas.models.OpenAPI; +import io.swagger.v3.oas.models.Operation; +import io.swagger.v3.oas.models.PathItem; +import io.swagger.v3.oas.models.examples.Example; +import io.swagger.v3.oas.models.media.MediaType; +import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.parameters.Parameter; +import io.swagger.v3.oas.models.responses.ApiResponse; +import io.swagger.v3.parser.OpenAPIV3Parser; +import io.swagger.v3.parser.core.models.SwaggerParseResult; + +public class OpenAPIMoquImporter implements MoquImporter { + + private static final Logger LOGGER = LoggerFactory.getLogger(OpenAPIMoquImporter.class); + private static final String HTTP_HEADER_ACCEPT = "Accept"; + private static final String REFERENCE_PREFIX = "#/components/schemas/"; + + @Override + public Moqu parse(String content) { + + SwaggerParseResult swaggerParseResult = new OpenAPIV3Parser().readContents(content); + + if (LOGGER.isDebugEnabled()) { + for (String message : swaggerParseResult.getMessages()) { + LOGGER.debug("[context:SwaggerParseResult] {}", message); + } + } + + OpenAPI openAPI = swaggerParseResult.getOpenAPI(); + + if (Objects.isNull(openAPI)) { + throw new IllegalArgumentException("Cannot parse OpenAPI V3 content: " + content); + } + + return new Moqu( + getRequestResponsePairs(openAPI)); + } + + private List getRequestResponsePairs(OpenAPI openAPI) { + Map requestResponsePairs = new HashMap<>(); + + Map localSchemas = getSchemas(openAPI); + + Set> entries = Optional.ofNullable(openAPI.getPaths()) + .orElseThrow(IllegalArgumentException::new) + .entrySet(); + + for (Map.Entry entry : entries) { + + for (Map.Entry httpMethodOperation : entry.getValue().readOperationsMap() + .entrySet()) { + + if (!Objects.isNull(httpMethodOperation.getValue().getResponses())) { + + Set> statusApiResponses = httpMethodOperation.getValue().getResponses() + .entrySet(); + + for (Map.Entry statusApiResponse : statusApiResponses) { + + if (Objects.isNull(statusApiResponse.getValue())) { + continue; + } + + Map> examplesOnPath = extractParameters(httpMethodOperation.getValue(), + ParameterType.PATH); + + requestResponsePairs.putAll(getContentRequestResponsePairs(statusApiResponse, examplesOnPath, + httpMethodOperation.getKey(), entry.getKey(), localSchemas)); + } + } + } + } + + return requestResponsePairs.entrySet().stream().map(entry -> new RequestResponsePair(entry.getKey(), entry.getValue())) + .collect(Collectors.toList()); + } + + private Map getSchemas(OpenAPI openAPI) { + if (openAPI.getComponents() == null) { + return Map.of(); + } + return Objects.requireNonNullElse(openAPI.getComponents().getSchemas(), Map.of()); + } + + private int tryGetStatusCode(Map.Entry statusApiResponse) { + try { + return Integer.parseInt(statusApiResponse.getKey()); + } catch (NumberFormatException e) { + throw new IllegalArgumentException("Invalid status code: " + statusApiResponse.getKey()); + } + } + + private Map> extractParameters(Operation operation, ParameterType parameterType) { + List parameters = Optional.ofNullable(operation.getParameters()).orElse(Collections.emptyList()); + Map> finalParameters = new HashMap<>(); + + for (Parameter parameter : parameters) { + if (isEligibleForExtraction(parameter, parameterType)) { + + Set exampleNames = parameter.getExamples().keySet(); + for (String exampleName : exampleNames) { + + Example example = parameter.getExamples().get(exampleName); + + Object object = example.getValue(); + String value = resolveContent(object); + finalParameters.computeIfAbsent(exampleName, + k -> ArrayListMultimap.create()).put(parameter.getName(), value); + } + } + } + + return finalParameters; + } + + private boolean isEligibleForExtraction(Parameter parameter, ParameterType type) { + return parameter.getIn().equals(type.value()) && !Objects.isNull(parameter.getExamples()); + } + + private Map getContentRequestResponsePairs(Map.Entry statusApiResponse, + Map> parametersOnPath, PathItem.HttpMethod httpMethod, String url, + Map localSchemas) { + Map requestResponseMap = new HashMap<>(); + + ApiResponse apiResponse = statusApiResponse.getValue(); + + int statusCode = tryGetStatusCode(statusApiResponse); + + for (Map.Entry entry : apiResponse.getContent().entrySet()) { + String contentType = entry.getKey(); + MediaType mediaType = entry.getValue(); + Map examples = Optional.ofNullable(mediaType.getExamples()).orElse(Collections.emptyMap()); + + examples.forEach((exampleName, example) -> { + + String content = resolveContent(localSchemas, example); + + Response response = new Response( + exampleName, + mediaType, + statusCode, + content, + List.of()); + + Multimap onPath = parametersOnPath.get(exampleName); + List reqParams = new ArrayList<>(); + + if (onPath != null) { + for (Map.Entry paramEntry : onPath.entries()) { + io.quarkiverse.openapi.moqu.model.Parameter parameter = new io.quarkiverse.openapi.moqu.model.Parameter( + paramEntry.getKey(), + paramEntry.getValue(), + ParameterType.PATH); + reqParams.add(parameter); + } + } + + List parameters = reqParams.stream() + .filter(reqParam -> reqParam.where().equals(ParameterType.PATH)).toList(); + String finalUrl = resolveUrlParameters(url, parameters); + Request request = new Request( + finalUrl, + httpMethod.name(), + exampleName, + new Header(HTTP_HEADER_ACCEPT, List.of(contentType)), + reqParams); + requestResponseMap.put(request, response); + }); + } + + return requestResponseMap; + } + + private String resolveContent(Map localSchemas, Example example) { + if (!Strings.isNullOrEmpty(example.get$ref())) { + return resolveRef(example.get$ref(), localSchemas); + } else { + return resolveContent(example.getValue()); + } + } + + private String resolveUrlParameters(String url, List parameters) { + for (io.quarkiverse.openapi.moqu.model.Parameter parameter : parameters) { + String placeholder = "{%s}".formatted(parameter.key()); + url = url.replace(placeholder, parameter.value()); + } + return url; + } + + private String resolveRef(String ref, Map localSchemas) { + if (!ref.startsWith(REFERENCE_PREFIX)) { + throw new IllegalArgumentException( + "There is no support for external $ref schemas. Please, configure the %s as local schema" + .formatted(ref)); + } + + String refName = ref.substring(REFERENCE_PREFIX.length(), ref.length()); + + Schema schema = localSchemas.get(refName); + + if (schema == null) { + throw new IllegalArgumentException("Schema not found: " + refName); + } + + return generateResponseBodyFromRefSchema(schema); + } + + private String resolveContent(Object object) { + if (object instanceof String) { + return (String) object; + } + if (object instanceof Integer) { + return String.valueOf((Integer) object); + } + throw new IllegalArgumentException("Object is not a String"); + } + + private static String generateResponseBodyFromRefSchema(final Schema schema) { + String schemaType = Optional.ofNullable(schema.getType()).orElse(OBJECT_TYPE); + return switch (schemaType) { + case STRING_TYPE, INTEGER_TYPE -> (String) schema.getExample(); + case OBJECT_TYPE -> SchemaReader.readObjectExample(schema); + default -> ""; + }; + } +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/ParameterType.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/ParameterType.java new file mode 100644 index 000000000..0c053f55b --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/ParameterType.java @@ -0,0 +1,43 @@ +package io.quarkiverse.openapi.moqu; + +/** + * Enum representing the type of a parameter in an HTTP request, indicating its location. + * The parameter can be part of the path, query string, or headers. + */ +public enum ParameterType { + + /** + * Indicates that the parameter is part of the URL path. + */ + PATH("path"), + + /** + * Indicates that the parameter is part of the query string. + */ + QUERY("query"), + + /** + * Indicates that the parameter is part of the HTTP headers. + */ + HEADER("header"); + + private final String value; + + /** + * Constructs a {@code ParameterType} with the given string value representing the parameter location. + * + * @param value the string value corresponding to the parameter type. + */ + ParameterType(String value) { + this.value = value; + } + + /** + * Returns the string value associated with this {@code ParameterType}. + * + * @return the string representation of the parameter type. + */ + public String value() { + return this.value; + } +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/SchemaReader.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/SchemaReader.java new file mode 100644 index 000000000..7b330f0e5 --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/SchemaReader.java @@ -0,0 +1,64 @@ +package io.quarkiverse.openapi.moqu; + +import static io.swagger.v3.parser.util.SchemaTypeUtil.OBJECT_TYPE; + +import java.util.HashMap; +import java.util.Map; +import java.util.Optional; + +import com.fasterxml.jackson.core.JsonProcessingException; + +import io.quarkiverse.openapi.moqu.marshall.ObjectMapperFactory; +import io.swagger.v3.oas.models.media.Schema; + +/** + * Utility class for reading schema examples and converting them into JSON representations. + * This class provides methods to extract example data from a given schema and serialize it + * into JSON format. + */ +public class SchemaReader { + + static String EMPTY_JSON_OBJECT = "{}"; + + /** + * Reads the example object from the provided schema and converts it to a JSON string. + * + * @param schema the schema from which to extract the example object. + * @return a JSON string representation of the example object, or an empty JSON object + * if an error occurs during processing. + */ + static String readObjectExample(Schema schema) { + try { + Map map = mapObjectExample(schema); + return ObjectMapperFactory.getInstance().writeValueAsString(map); + } catch (JsonProcessingException e) { + return EMPTY_JSON_OBJECT; + } + } + + /** + * Recursively maps the properties of the provided schema to a map. + * + * @param schema the schema from which to map properties. + * @return a map representing the example properties of the schema. + */ + private static Map mapObjectExample(Schema schema) { + Map currentRoot = new HashMap<>(); + + Optional.ofNullable(schema.getProperties()) + .orElse(Map.of()) + .forEach((key, value) -> { + if (value.getType().equals(OBJECT_TYPE)) { + if (value.getExample() != null) { + currentRoot.put(key, value.getExample()); + } else { + currentRoot.put(key, mapObjectExample(value)); + } + } else { + currentRoot.put(key, value.getExample()); + } + }); + + return currentRoot; + } +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/marshall/ObjectMapperFactory.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/marshall/ObjectMapperFactory.java new file mode 100644 index 000000000..68957a738 --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/marshall/ObjectMapperFactory.java @@ -0,0 +1,15 @@ +package io.quarkiverse.openapi.moqu.marshall; + +import com.fasterxml.jackson.databind.ObjectMapper; + +/** + * Responsible for providing a Single of {@link ObjectMapper} instance. + */ +public class ObjectMapperFactory { + + private static final ObjectMapper objectMapper = new ObjectMapper(); + + public static ObjectMapper getInstance() { + return objectMapper; + } +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Header.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Header.java new file mode 100644 index 000000000..476bd63ab --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Header.java @@ -0,0 +1,12 @@ +package io.quarkiverse.openapi.moqu.model; + +import java.util.List; + +/** + * Represents an HTTP header with a name and a set of associated values. + * + * @param name the name of the HTTP header (e.g., "Accept", "Content-Type"). + * @param value the set of values associated with the header, allowing multiple values (e.g., "application/json", "text/html"). + */ +public record Header(String name, List value) { +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Operation.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Operation.java new file mode 100644 index 000000000..bac321819 --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Operation.java @@ -0,0 +1,10 @@ +package io.quarkiverse.openapi.moqu.model; + +/** + * Represents an HTTP operation. + *

+ * + * @param httpMethod the HTTP verb used for the current {@link Operation}. + */ +public record Operation(String httpMethod) { +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Parameter.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Parameter.java new file mode 100644 index 000000000..ee87f789b --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Parameter.java @@ -0,0 +1,14 @@ +package io.quarkiverse.openapi.moqu.model; + +import io.quarkiverse.openapi.moqu.ParameterType; + +/** + * Represents an HTTP request parameter with a key, value, and location indicating where the parameter is used. + * + * @param key the key of the parameter (e.g., "id", "query"). + * @param value the value of the parameter associated with the key. + * @param where the location of the parameter in the request (e.g., query string, path, header), defined by + * {@link ParameterType}. + */ +public record Parameter(String key, String value, ParameterType where) { +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Request.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Request.java new file mode 100644 index 000000000..ff1b8fe8e --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Request.java @@ -0,0 +1,16 @@ +package io.quarkiverse.openapi.moqu.model; + +import java.util.Collection; + +/** + * Represents an HTTP request with essential details such as URL, HTTP method, + * example name, accepted header, and parameters. + * + * @param url the URL to which the request is sent. + * @param httpMethod the HTTP method (GET, POST, PUT, DELETE, etc.) used for the request. + * @param exampleName the name of the example associated with the request. + * @param accept the "Accept" header, which specifies the expected response format. + * @param parameters the list of parameters to be included in the request. + */ +public record Request(String url, String httpMethod, String exampleName, Header accept, Collection parameters) { +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/RequestResponsePair.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/RequestResponsePair.java new file mode 100644 index 000000000..214cb92de --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/RequestResponsePair.java @@ -0,0 +1,10 @@ +package io.quarkiverse.openapi.moqu.model; + +/** + * Represents a pair of an HTTP request and its corresponding response. + * + * @param request the HTTP request that was sent. + * @param response the HTTP response received for the given request. + */ +public record RequestResponsePair(Request request, Response response) { +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Response.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Response.java new file mode 100644 index 000000000..532345b9f --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/model/Response.java @@ -0,0 +1,20 @@ +package io.quarkiverse.openapi.moqu.model; + +import java.util.List; + +import io.swagger.v3.oas.models.media.MediaType; + +/** + * Represents an HTTP response with details such as the example name, media type, + * status code, content, and headers. + * + * @param exampleName the name of the example associated with this response. + * @param mediaType the media type of the response content (e.g., application/json, text/html), + * represented by {@link MediaType}. + * @param statusCode the HTTP status code of the response (e.g., 200, 404). + * @param content the body of the response as a string. + * @param headers the list of headers included in the response. + */ +public record Response(String exampleName, MediaType mediaType, int statusCode, + String content, List

headers) { +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapper.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapper.java new file mode 100644 index 000000000..83bf92e58 --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapper.java @@ -0,0 +1,41 @@ +package io.quarkiverse.openapi.moqu.wiremock.mapper; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import io.quarkiverse.openapi.moqu.Moqu; +import io.quarkiverse.openapi.moqu.MoquMapper; +import io.quarkiverse.openapi.moqu.model.Request; +import io.quarkiverse.openapi.moqu.model.RequestResponsePair; +import io.quarkiverse.openapi.moqu.model.Response; +import io.quarkiverse.openapi.moqu.wiremock.model.WiremockMapping; +import io.quarkiverse.openapi.moqu.wiremock.model.WiremockRequest; +import io.quarkiverse.openapi.moqu.wiremock.model.WiremockResponse; + +public class WiremockMapper implements MoquMapper { + + @Override + public List map(Moqu moqu) { + ArrayList definitions = new ArrayList<>(); + for (RequestResponsePair pair : moqu.getRequestResponsePairs()) { + + Request mockRequest = pair.request(); + Response mockResponse = pair.response(); + + WiremockRequest request = new WiremockRequest(mockRequest.httpMethod(), mockRequest.url()); + + Map headers = new HashMap<>(); + + mockResponse.headers().forEach(item -> headers.put(item.name(), item.value())); + + WiremockResponse response = new WiremockResponse(mockResponse.statusCode(), mockResponse.content(), headers); + + definitions.add(new WiremockMapping( + request, response)); + } + + return definitions; + } +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockMapping.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockMapping.java new file mode 100644 index 000000000..bf703d7f9 --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockMapping.java @@ -0,0 +1,4 @@ +package io.quarkiverse.openapi.moqu.wiremock.model; + +public record WiremockMapping(WiremockRequest request, WiremockResponse response) { +} \ No newline at end of file diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockRequest.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockRequest.java new file mode 100644 index 000000000..3d663e79a --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockRequest.java @@ -0,0 +1,6 @@ +package io.quarkiverse.openapi.moqu.wiremock.model; + +public record WiremockRequest( + String method, + String url) { +} diff --git a/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockResponse.java b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockResponse.java new file mode 100644 index 000000000..bfbcb6eef --- /dev/null +++ b/moqu/core/src/main/java/io/quarkiverse/openapi/moqu/wiremock/model/WiremockResponse.java @@ -0,0 +1,8 @@ +package io.quarkiverse.openapi.moqu.wiremock.model; + +import java.util.Map; + +public record WiremockResponse(Integer status, + String body, + Map headers) { +} diff --git a/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporterTest.java b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporterTest.java new file mode 100644 index 000000000..8267b1ddd --- /dev/null +++ b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/OpenAPIMoquImporterTest.java @@ -0,0 +1,181 @@ +package io.quarkiverse.openapi.moqu; + +import static io.quarkiverse.openapi.moqu.TestUtils.readContentFromFile; + +import java.util.List; +import java.util.Map; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.quarkiverse.openapi.moqu.marshall.ObjectMapperFactory; + +class OpenAPIMoquImporterTest { + + private final MoquImporter sut = new OpenAPIMoquImporter(); + + @Test + @DisplayName("Should create a new definition from OpenAPI specification") + void shouldCreateANewDefinitionFromOpenAPISpecification() { + // act + String content = readContentFromFile("wiremock/one_example_in_the_same_path.yml"); + Moqu moqu = sut.parse(content); + + // assert + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(moqu.getRequestResponsePairs()).isNotEmpty(); + softly.assertThat(moqu.getRequestResponsePairs()).hasSize(1); + softly.assertThat(moqu.getRequestResponsePairs().get(0)).satisfies(requestResponsePair -> { + softly.assertThat(requestResponsePair.request().accept().name()).isEqualTo("Accept"); + softly.assertThat(requestResponsePair.request().accept().value()).contains("application/json"); + softly.assertThat(requestResponsePair.request().exampleName()).isEqualTo("john"); + softly.assertThat(requestResponsePair.request().parameters()).hasSize(1); + softly.assertThat(requestResponsePair.request().parameters()).anySatisfy(parameters -> { + softly.assertThat(parameters.key()).isEqualTo("id"); + softly.assertThat(parameters.value()).isEqualTo("1"); + }); + }); + }); + } + + @Test + @DisplayName("Should throws exception when the OpenAPI is invalid") + void shouldThrowsExceptionWhenTheOpenAPIIsInvalid() { + // act, assert + Assertions.assertThrows(IllegalArgumentException.class, () -> { + sut.parse(""" + openapi: 3.0.3 + info: + version: 999-SNAPSHOT + """); + }); + } + + @Test + @DisplayName("Should handle OpenAPI with two path params") + void shouldHandleOpenAPIWithTwoPathParams() { + + // act + String content = readContentFromFile("wiremock/two_examples_in_the_same_path.yml"); + Moqu moqu = sut.parse(content); + + // assert + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(moqu.getRequestResponsePairs()).isNotEmpty(); + softly.assertThat(moqu.getRequestResponsePairs()).hasSize(2); + softly.assertThat(moqu.getRequestResponsePairs()).allSatisfy(requestResponsePair -> { + softly.assertThat(requestResponsePair.request().accept().name()).isEqualTo("Accept"); + softly.assertThat(requestResponsePair.request().accept().value()).contains("application/json"); + }); + softly.assertThat(moqu.getRequestResponsePairs()).anySatisfy(requestResponsePair -> { + softly.assertThat(requestResponsePair.request().exampleName()).isEqualTo("john"); + softly.assertThat(requestResponsePair.request().parameters()).hasSize(1); + }); + softly.assertThat(moqu.getRequestResponsePairs()).anySatisfy(requestResponsePair -> { + softly.assertThat(requestResponsePair.request().exampleName()).isEqualTo("mary"); + softly.assertThat(requestResponsePair.request().parameters()).hasSize(1); + }); + }); + } + + @Test + @DisplayName("Should generate a response from ref") + void shouldGenerateAResponseFromRef() { + String content = readContentFromFile("wiremock/response_from_ref.yml"); + + Moqu moqu = sut.parse(content); + + // assert + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(moqu.getRequestResponsePairs()).isNotEmpty(); + softly.assertThat(moqu.getRequestResponsePairs()).hasSize(1); + softly.assertThat(moqu.getRequestResponsePairs().get(0)).satisfies(requestResponsePair -> { + softly.assertThat(requestResponsePair.request().accept().name()).isEqualTo("Accept"); + softly.assertThat(requestResponsePair.request().accept().value()).contains("application/json"); + softly.assertThat(requestResponsePair.request().exampleName()).isEqualTo("quarkus"); + softly.assertThat(requestResponsePair.request().parameters()).hasSize(1); + softly.assertThat(requestResponsePair.request().parameters()).anySatisfy(parameters -> { + softly.assertThat(parameters.key()).isEqualTo("id"); + softly.assertThat(parameters.value()).isEqualTo("1"); + }); + }); + }); + } + + @Test + @DisplayName("Should generate a response from ref as array") + void shouldGenerateAResponseFromRefAsArray() { + String content = readContentFromFile("wiremock/response_from_ref_array.yml"); + Moqu moqu = sut.parse(content); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(moqu.getRequestResponsePairs()).isNotEmpty(); + softly.assertThat(moqu.getRequestResponsePairs()).hasSize(1); + softly.assertThat(moqu.getRequestResponsePairs().get(0)).satisfies(requestResponsePair -> { + Map map = ObjectMapperFactory.getInstance().readValue( + requestResponsePair.response() + .content(), + Map.class); + softly.assertThat((List) map.get("versions")) + .hasSize(2); + + softly.assertThat(map.get("supportsJava")).isEqualTo(true); + + }); + }); + } + + @Test + @DisplayName("Should generate a response from $ref and with no $ref") + void shouldGenerateAResponseFromRefAndNoRef() { + String content = readContentFromFile("wiremock/response_from_ref_and_noref.yml"); + Moqu moqu = sut.parse(content); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(moqu.getRequestResponsePairs()).isNotEmpty(); + softly.assertThat(moqu.getRequestResponsePairs()).hasSize(2); + softly.assertThat(moqu.getRequestResponsePairs()).anySatisfy(requestResponsePair -> { + Map map = ObjectMapperFactory.getInstance().readValue( + requestResponsePair.response() + .content(), + Map.class); + softly.assertThat((List) map.get("versions")) + .hasSize(2); + }); + + softly.assertThat(moqu.getRequestResponsePairs()).anySatisfy(requestResponsePair -> { + Map map = ObjectMapperFactory.getInstance().readValue( + requestResponsePair.response() + .content(), + Map.class); + softly.assertThat((List) map.get("versions")) + .hasSize(1); + }); + }); + } + + @Test + @DisplayName("Should generate a full OpenAPI specification") + void shouldGenerateAFullResponse() { + String content = readContentFromFile("wiremock/full.yml"); + Moqu moqu = sut.parse(content); + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(moqu.getRequestResponsePairs()).isNotEmpty(); + softly.assertThat(moqu.getRequestResponsePairs()).hasSize(1); + softly.assertThat(moqu.getRequestResponsePairs().get(0)).satisfies(requestResponsePair -> { + Map map = ObjectMapperFactory.getInstance().readValue( + requestResponsePair.response() + .content(), + Map.class); + softly.assertThat((List) map.get("versions")) + .hasSize(2); + + softly.assertThat(map.get("supportsJava")).isEqualTo(true); + + softly.assertThat(map.get("contributors")).isEqualTo(1000); + + softly.assertThat(((Map) map.get("rules")).get("hello")).isEqualTo("world"); + }); + }); + } +} diff --git a/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/TestUtils.java b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/TestUtils.java new file mode 100644 index 000000000..2f8b1b571 --- /dev/null +++ b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/TestUtils.java @@ -0,0 +1,20 @@ +package io.quarkiverse.openapi.moqu; + +import java.io.IOException; +import java.net.URISyntaxException; +import java.net.URL; +import java.nio.file.Files; +import java.nio.file.Path; + +public class TestUtils { + + public static String readContentFromFile(String resourcePath) { + URL url = Thread.currentThread().getContextClassLoader().getResource((resourcePath)); + assert url != null; + try { + return Files.readString(Path.of(url.toURI())); + } catch (IOException | URISyntaxException e) { + return null; + } + } +} diff --git a/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapperTest.java b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapperTest.java new file mode 100644 index 000000000..9a8611bab --- /dev/null +++ b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockMapperTest.java @@ -0,0 +1,65 @@ +package io.quarkiverse.openapi.moqu.wiremock.mapper; + +import static io.quarkiverse.openapi.moqu.TestUtils.readContentFromFile; + +import java.util.List; + +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.quarkiverse.openapi.moqu.Moqu; +import io.quarkiverse.openapi.moqu.OpenAPIMoquImporter; +import io.quarkiverse.openapi.moqu.wiremock.model.WiremockMapping; + +class WiremockMapperTest { + + private final OpenAPIMoquImporter importer = new OpenAPIMoquImporter(); + private final WiremockMapper sut = new WiremockMapper(); + + @Test + @DisplayName("Should map one Wiremock definition") + void shouldMapOneWiremockDefinition() { + String content = readContentFromFile("wiremock/mapper/should_map_one_wiremock_definition.yml"); + + Moqu moqu = importer.parse(content); + + List definitions = sut.map(moqu); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(definitions).isNotEmpty(); + softly.assertThat(definitions).hasSize(1); + softly.assertThat(definitions).anySatisfy(definition -> { + softly.assertThat(definition.request().method()).isEqualTo("GET"); + softly.assertThat(definition.request().url()).isEqualTo("/users/1"); + softly.assertThat(definition.response().body()).isEqualTo("{\"id\": 1, \"name\": \"John Doe\"}"); + }); + }); + } + + @Test + @DisplayName("Should map two Wiremock definitions") + void shouldMapTwoWiremockDefinitions() { + String content = readContentFromFile("wiremock/mapper/should_map_two_wiremock_definition.yml"); + + Moqu mock = importer.parse(content); + + List definitions = sut.map(mock); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(definitions).isNotEmpty(); + softly.assertThat(definitions).hasSize(2); + softly.assertThat(definitions).anySatisfy(definition -> { + softly.assertThat(definition.request().method()).isEqualTo("GET"); + softly.assertThat(definition.request().url()).isEqualTo("/users/1"); + softly.assertThat(definition.response().body()).isEqualTo("{\"id\": 1, \"name\": \"John Doe\"}"); + }); + + softly.assertThat(definitions).anySatisfy(definition -> { + softly.assertThat(definition.request().method()).isEqualTo("GET"); + softly.assertThat(definition.request().url()).isEqualTo("/users/2"); + softly.assertThat(definition.response().body()).isEqualTo("{\"id\": 2, \"name\": \"Mary Doe\"}"); + }); + }); + } +} diff --git a/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockPathParamTest.java b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockPathParamTest.java new file mode 100644 index 000000000..abff5f59a --- /dev/null +++ b/moqu/core/src/test/java/io/quarkiverse/openapi/moqu/wiremock/mapper/WiremockPathParamTest.java @@ -0,0 +1,164 @@ +package io.quarkiverse.openapi.moqu.wiremock.mapper; + +import java.util.List; + +import org.assertj.core.api.Assertions; +import org.assertj.core.api.SoftAssertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import io.quarkiverse.openapi.moqu.Moqu; +import io.quarkiverse.openapi.moqu.OpenAPIMoquImporter; +import io.quarkiverse.openapi.moqu.TestUtils; +import io.quarkiverse.openapi.moqu.wiremock.model.WiremockMapping; + +public class WiremockPathParamTest { + + @Test + @DisplayName("Should convert a OpenAPI with a single path param correctly") + void shouldMapOneWiremockDefinition() { + + String content = TestUtils.readContentFromFile("wiremock/path_param_one_path_param.yml"); + if (content == null) { + Assertions.fail("Was not possible to read the file!"); + } + + OpenAPIMoquImporter importer = new OpenAPIMoquImporter(); + + Moqu mock = importer.parse(content); + + WiremockMapper wiremockMapper = new WiremockMapper(); + + List definitions = wiremockMapper.map(mock); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(definitions).hasSize(1); + WiremockMapping definition = definitions.get(0); + + softly.assertThat(definition).satisfies(wiremockDefinition -> { + // request + softly.assertThat(wiremockDefinition.request().url()).isEqualTo("/users/1"); + softly.assertThat(wiremockDefinition.request().method()).isEqualTo("GET"); + // response + softly.assertThat(wiremockDefinition.response().status()).isEqualTo(200); + softly.assertThat(wiremockDefinition.response().body()).isEqualTo("{\"name\": \"Quarkus\"}"); + }); + }); + } + + @Test + @DisplayName("Should convert with a two OpenAPI#paths each one with one path param") + void shouldMapTwoWiremockDefinitions() { + + String content = TestUtils.readContentFromFile("wiremock/path_param_two_params_but_different_path.yml"); + if (content == null) { + Assertions.fail("Was not possible to read the file!"); + } + + OpenAPIMoquImporter importer = new OpenAPIMoquImporter(); + + Moqu mock = importer.parse(content); + + WiremockMapper wiremockMapper = new WiremockMapper(); + + List definitions = wiremockMapper.map(mock); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(definitions).hasSize(2); + + softly.assertThat(definitions).anySatisfy(wiremockDefinition -> { + // request + softly.assertThat(wiremockDefinition.request().url()).isEqualTo("/users/1"); + softly.assertThat(wiremockDefinition.request().method()).isEqualTo("GET"); + // response + softly.assertThat(wiremockDefinition.response().status()).isEqualTo(200); + softly.assertThat(wiremockDefinition.response().body()).isEqualTo("{\"name\": \"John Doe\"}"); + }); + + softly.assertThat(definitions).anySatisfy(wiremockDefinition -> { + // request + softly.assertThat(wiremockDefinition.request().url()).isEqualTo("/frameworks/quarkus"); + softly.assertThat(wiremockDefinition.request().method()).isEqualTo("GET"); + // response + softly.assertThat(wiremockDefinition.response().status()).isEqualTo(200); + softly.assertThat(wiremockDefinition.response().body()) + .isEqualTo("{\"description\": \"Quarkus, build time augmentation toolkit\"}"); + }); + }); + } + + @Test + @DisplayName("Should convert with a combination of path param") + void shouldConvertWithACombinationOfPathParam() { + + String content = TestUtils.readContentFromFile("wiremock/path_param_two_path_params_combination.yml"); + if (content == null) { + Assertions.fail("Was not possible to read the file!"); + } + + OpenAPIMoquImporter importer = new OpenAPIMoquImporter(); + + Moqu mock = importer.parse(content); + + WiremockMapper wiremockMapper = new WiremockMapper(); + + List definitions = wiremockMapper.map(mock); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(definitions).hasSize(2); + + softly.assertThat(definitions).anySatisfy(wiremockDefinition -> { + // request + softly.assertThat(wiremockDefinition.request().url()).isEqualTo("/users/1/books/80"); + softly.assertThat(wiremockDefinition.request().method()).isEqualTo("GET"); + // response + softly.assertThat(wiremockDefinition.response().status()).isEqualTo(200); + softly.assertThat(wiremockDefinition.response().body()) + .isEqualTo("{\"name\": \"Book for John\", \"chapters\": 8}"); + }); + + softly.assertThat(definitions).anySatisfy(wiremockDefinition -> { + // request + softly.assertThat(wiremockDefinition.request().url()).isEqualTo("/users/2/books/70"); + softly.assertThat(wiremockDefinition.request().method()).isEqualTo("GET"); + // response + softly.assertThat(wiremockDefinition.response().status()).isEqualTo(200); + softly.assertThat(wiremockDefinition.response().body()) + .isEqualTo("{\"name\": \"Book for Mary\", \"chapters\": 10}"); + }); + }); + } + + @Test + @DisplayName("Should convert with a combination but only one with example") + void shouldConvertPathParamCombinationOnlyOneWithExample() { + + String content = TestUtils.readContentFromFile("wiremock/path_param_two_path_params_only_one_with_example.yml"); + if (content == null) { + Assertions.fail("Was not possible to read the file!"); + } + + OpenAPIMoquImporter importer = new OpenAPIMoquImporter(); + + Moqu mock = importer.parse(content); + + WiremockMapper wiremockMapper = new WiremockMapper(); + + List definitions = wiremockMapper.map(mock); + + SoftAssertions.assertSoftly(softly -> { + softly.assertThat(definitions).hasSize(1); + + softly.assertThat(definitions).anySatisfy(wiremockDefinition -> { + // request + softly.assertThat(wiremockDefinition.request().url()).isEqualTo("/users/1/books/{bookId}"); + softly.assertThat(wiremockDefinition.request().method()).isEqualTo("GET"); + // response + softly.assertThat(wiremockDefinition.response().status()).isEqualTo(200); + softly.assertThat(wiremockDefinition.response().body()) + .isEqualTo("{\"name\": \"Book for John\", \"chapters\": 8}"); + }); + }); + } + +} diff --git a/moqu/core/src/test/resources/wiremock/full.yml b/moqu/core/src/test/resources/wiremock/full.yml new file mode 100644 index 000000000..d91f48923 --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/full.yml @@ -0,0 +1,45 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/frameworks/{id}": + get: + parameters: + - name: id + in: path + examples: + quarkus: + value: 1 + responses: + 200: + content: + "application/json": + examples: + quarkus: + $ref: "#/components/schemas/Framework" + description: Ok +components: + schemas: + Framework: + type: object + properties: + name: + type: string + example: "Quarkus" + versions: + type: array + example: ["999-SNAPSHOT", "3.15.1"] + supportsJava: + type: boolean + example: true + contributors: + type: integer + example: 1000 + rules: + type: object + example: + hello: world + diff --git a/moqu/core/src/test/resources/wiremock/mapper/should_map_one_wiremock_definition.yml b/moqu/core/src/test/resources/wiremock/mapper/should_map_one_wiremock_definition.yml new file mode 100644 index 000000000..92ea0200c --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/mapper/should_map_one_wiremock_definition.yml @@ -0,0 +1,28 @@ +openapi: 3.0.3 +info: + title: "Users API" + version: 1.0.0-alpha +servers: + - url: http://localhost:8888 +paths: + /users/{userId}: + get: + description: Get user by ID + parameters: + - name: userId + in: path + required: true + schema: + type: number + examples: + john: + value: 1 + responses: + "200": + description: Ok + content: + "application/json": + examples: + john: + value: + '{"id": 1, "name": "John Doe"}' \ No newline at end of file diff --git a/moqu/core/src/test/resources/wiremock/mapper/should_map_two_wiremock_definition.yml b/moqu/core/src/test/resources/wiremock/mapper/should_map_two_wiremock_definition.yml new file mode 100644 index 000000000..2dabf118f --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/mapper/should_map_two_wiremock_definition.yml @@ -0,0 +1,33 @@ +openapi: 3.0.3 +info: + title: "Users API" + version: 1.0.0-alpha +servers: + - url: http://localhost:8888 +paths: + /users/{userId}: + get: + description: Get user by ID + parameters: + - name: userId + in: path + required: true + schema: + type: number + examples: + john: + value: 1 + mary: + value: 2 + responses: + "200": + description: Ok + content: + "application/json": + examples: + john: + value: + '{"id": 1, "name": "John Doe"}' + mary: + value: + '{"id": 2, "name": "Mary Doe"}' \ No newline at end of file diff --git a/moqu/core/src/test/resources/wiremock/one_example_in_the_same_path.yml b/moqu/core/src/test/resources/wiremock/one_example_in_the_same_path.yml new file mode 100644 index 000000000..eac9598b5 --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/one_example_in_the_same_path.yml @@ -0,0 +1,28 @@ +openapi: 3.0.3 +info: + title: "Users API" + version: 1.0.0-alpha +servers: + - url: http://localhost:8888 +paths: + /users/{id}: + get: + description: Get user by ID + parameters: + - name: id + in: path + required: true + schema: + type: number + examples: + john: + value: 1 + responses: + "200": + description: Ok + content: + "application/json": + examples: + john: + value: + '{"id": 1, "name": "John Doe"}' \ No newline at end of file diff --git a/moqu/core/src/test/resources/wiremock/path_param_one_path_param.yml b/moqu/core/src/test/resources/wiremock/path_param_one_path_param.yml new file mode 100644 index 000000000..a87227d57 --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/path_param_one_path_param.yml @@ -0,0 +1,23 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/users/{userId}": + get: + parameters: + - name: userId + in: path + examples: + quarkus: + value: 1 + responses: + 200: + content: + "application/json": + examples: + quarkus: + value: '{"name": "Quarkus"}' + description: Ok diff --git a/moqu/core/src/test/resources/wiremock/path_param_two_params_but_different_path.yml b/moqu/core/src/test/resources/wiremock/path_param_two_params_but_different_path.yml new file mode 100644 index 000000000..26eb2dba8 --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/path_param_two_params_but_different_path.yml @@ -0,0 +1,39 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/users/{userId}": + get: + parameters: + - name: userId + in: path + examples: + john: + value: 1 + responses: + 200: + content: + "application/json": + examples: + john: + value: '{"name": "John Doe"}' + description: Ok + "/frameworks/{name}": + get: + parameters: + - name: name + in: path + examples: + quarkus: + value: quarkus + responses: + 200: + content: + "application/json": + examples: + quarkus: + value: '{"description": "Quarkus, build time augmentation toolkit"}' + description: Ok diff --git a/moqu/core/src/test/resources/wiremock/path_param_two_path_params_combination.yml b/moqu/core/src/test/resources/wiremock/path_param_two_path_params_combination.yml new file mode 100644 index 000000000..140eec479 --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/path_param_two_path_params_combination.yml @@ -0,0 +1,35 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/users/{userId}/books/{bookId}": + get: + parameters: + - name: userId + in: path + examples: + john: + value: 1 + mary: + value: 2 + - name: bookId + in: path + examples: + john: + value: 80 + mary: + value: 70 + + responses: + 200: + content: + "application/json": + examples: + john: + value: '{"name": "Book for John", "chapters": 8}' + mary: + value: '{"name": "Book for Mary", "chapters": 10}' + description: Ok diff --git a/moqu/core/src/test/resources/wiremock/path_param_two_path_params_only_one_with_example.yml b/moqu/core/src/test/resources/wiremock/path_param_two_path_params_only_one_with_example.yml new file mode 100644 index 000000000..3b1f159a0 --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/path_param_two_path_params_only_one_with_example.yml @@ -0,0 +1,25 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/users/{userId}/books/{bookId}": + get: + parameters: + - name: userId + in: path + examples: + john: + value: 1 + - name: bookId + in: path + responses: + 200: + content: + "application/json": + examples: + john: + value: '{"name": "Book for John", "chapters": 8}' + description: Ok \ No newline at end of file diff --git a/moqu/core/src/test/resources/wiremock/response_from_ref.yml b/moqu/core/src/test/resources/wiremock/response_from_ref.yml new file mode 100644 index 000000000..4e62b37fb --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/response_from_ref.yml @@ -0,0 +1,31 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/frameworks/{id}": + get: + parameters: + - name: id + in: path + examples: + quarkus: + value: 1 + responses: + 200: + content: + "application/json": + examples: + quarkus: + $ref: "#/components/schemas/Framework" + description: Ok +components: + schemas: + Framework: + type: object + properties: + name: + type: string + example: "Quarkus" \ No newline at end of file diff --git a/moqu/core/src/test/resources/wiremock/response_from_ref_and_noref.yml b/moqu/core/src/test/resources/wiremock/response_from_ref_and_noref.yml new file mode 100644 index 000000000..b9537febc --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/response_from_ref_and_noref.yml @@ -0,0 +1,38 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/frameworks/{id}": + get: + parameters: + - name: id + in: path + examples: + quarkus: + value: 1 + vertx: + value: 2 + responses: + 200: + content: + "application/json": + examples: + quarkus: + $ref: "#/components/schemas/Framework" + vertx: + value: '{ "name": "Vert.x", "versions": ["999-SNAPSHOT"]}' + description: Ok +components: + schemas: + Framework: + type: object + properties: + name: + type: string + example: "Quarkus" + versions: + type: array + example: [ "999-SNAPSHOT", "3.15.1" ] diff --git a/moqu/core/src/test/resources/wiremock/response_from_ref_array.yml b/moqu/core/src/test/resources/wiremock/response_from_ref_array.yml new file mode 100644 index 000000000..8707012bd --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/response_from_ref_array.yml @@ -0,0 +1,37 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/frameworks/{id}": + get: + parameters: + - name: id + in: path + examples: + quarkus: + value: 1 + responses: + 200: + content: + "application/json": + examples: + quarkus: + $ref: "#/components/schemas/Framework" + description: Ok +components: + schemas: + Framework: + type: object + properties: + name: + type: string + example: "Quarkus" + versions: + type: array + example: ["999-SNAPSHOT", "3.15.1"] + supportsJava: + type: boolean + example: true diff --git a/moqu/core/src/test/resources/wiremock/two_examples_in_the_same_path.yml b/moqu/core/src/test/resources/wiremock/two_examples_in_the_same_path.yml new file mode 100644 index 000000000..2dabf118f --- /dev/null +++ b/moqu/core/src/test/resources/wiremock/two_examples_in_the_same_path.yml @@ -0,0 +1,33 @@ +openapi: 3.0.3 +info: + title: "Users API" + version: 1.0.0-alpha +servers: + - url: http://localhost:8888 +paths: + /users/{userId}: + get: + description: Get user by ID + parameters: + - name: userId + in: path + required: true + schema: + type: number + examples: + john: + value: 1 + mary: + value: 2 + responses: + "200": + description: Ok + content: + "application/json": + examples: + john: + value: + '{"id": 1, "name": "John Doe"}' + mary: + value: + '{"id": 2, "name": "Mary Doe"}' \ No newline at end of file diff --git a/moqu/deployment/pom.xml b/moqu/deployment/pom.xml new file mode 100644 index 000000000..0eb16fca6 --- /dev/null +++ b/moqu/deployment/pom.xml @@ -0,0 +1,66 @@ + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu-parent + 3.0.0-SNAPSHOT + + 4.0.0 + + quarkus-openapi-generator-moqu-wiremock-deployment + Quarkus - Openapi Generator - Moqu - Wiremock - Deployment + + + + io.quarkus + quarkus-core-deployment + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu-wiremock + ${project.version} + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu-core + ${project.version} + + + + io.quarkus + quarkus-junit5-internal + + + + io.quarkus + quarkus-vertx-http-deployment + + + + io.rest-assured + rest-assured + test + + + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquProjectProcessor.java b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquProjectProcessor.java new file mode 100644 index 000000000..94ed8ff79 --- /dev/null +++ b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquProjectProcessor.java @@ -0,0 +1,102 @@ +package io.quarkiverse.openapi.generator; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.Map; +import java.util.Objects; +import java.util.Optional; +import java.util.Set; +import java.util.stream.Stream; + +import org.jboss.logging.Logger; + +import io.quarkiverse.openapi.generator.items.MoquBuildItem; +import io.quarkiverse.openapi.generator.items.MoquProjectBuildItem; +import io.quarkiverse.openapi.generator.moqu.MoquConfig; +import io.quarkiverse.openapi.moqu.Moqu; +import io.quarkiverse.openapi.moqu.OpenAPIMoquImporter; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.runtime.util.ClassPathUtils; + +public class MoquProjectProcessor { + + private static final Logger LOGGER = Logger.getLogger(MoquProjectProcessor.class); + + private static final Set SUPPORTED_EXTENSIONS = Set.of("yaml", "yml", "json"); + + @BuildStep + MoquProjectBuildItem generate(MoquConfig config) { + try { + + HashMap filesMap = new HashMap<>(); + ClassPathUtils.consumeAsPaths(config.resourceDir(), path -> { + try { + boolean directory = Files.isDirectory(path); + if (directory) { + try (Stream pathStream = Files.find(path, Integer.MAX_VALUE, + (p, a) -> Files.isRegularFile(p) && SUPPORTED_EXTENSIONS.contains( + getExtension(p.getFileName().toString())))) { + + pathStream.forEach(p -> { + try { + String filename = p.getFileName().toString(); + + MoquProjectBuildItem.File moquFile = new MoquProjectBuildItem.File( + removeExtension(filename), getExtension(filename), Files.readString(p)); + + filesMap.put(filename, moquFile); + + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + return new MoquProjectBuildItem(filesMap); + + } catch (IOException e) { + LOGGER.error("Was not possible to scan Moqu project files.", e); + throw new RuntimeException(e); + } + } + + @BuildStep(onlyIf = { IsDevelopment.class }) + void consume(Optional moquProject, + BuildProducer moquMocks) { + + OpenAPIMoquImporter importer = new OpenAPIMoquImporter(); + moquProject.ifPresent(project -> { + for (Map.Entry spec : project.specs().entrySet()) { + + MoquProjectBuildItem.File moquFile = spec.getValue(); + + Moqu moqu = importer.parse(moquFile.content()); + + moquMocks.produce(new MoquBuildItem( + moquFile.filename(), + moquFile.extension(), + moqu)); + } + }); + } + + public static String getExtension(String path) { + Objects.requireNonNull(path, "path is required"); + final int i = path.lastIndexOf("."); + return i > 0 ? path.substring(i + 1) : null; + } + + public static String removeExtension(String path) { + Objects.requireNonNull(path, "path is required"); + final int i = path.lastIndexOf("."); + return i > 0 ? path.substring(0, i) : path; + } +} diff --git a/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquWiremockProcessor.java b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquWiremockProcessor.java new file mode 100644 index 000000000..086f65f88 --- /dev/null +++ b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/MoquWiremockProcessor.java @@ -0,0 +1,12 @@ +package io.quarkiverse.openapi.generator; + +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.builditem.FeatureBuildItem; + +public class MoquWiremockProcessor { + + @BuildStep + FeatureBuildItem feature() { + return new FeatureBuildItem("moqu-wiremock"); + } +} diff --git a/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquModel.java b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquModel.java new file mode 100644 index 000000000..38cc5c795 --- /dev/null +++ b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquModel.java @@ -0,0 +1,5 @@ +package io.quarkiverse.openapi.generator.devui; + +public record MoquModel(String name, String link) { + +} diff --git a/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquWiremockDevUIProcessor.java b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquWiremockDevUIProcessor.java new file mode 100644 index 000000000..0514701af --- /dev/null +++ b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/devui/MoquWiremockDevUIProcessor.java @@ -0,0 +1,81 @@ +package io.quarkiverse.openapi.generator.devui; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import io.quarkiverse.openapi.generator.items.MoquBuildItem; +import io.quarkiverse.openapi.generator.moqu.recorder.MoquRoutesRecorder; +import io.quarkiverse.openapi.moqu.marshall.ObjectMapperFactory; +import io.quarkiverse.openapi.moqu.wiremock.mapper.WiremockMapper; +import io.quarkiverse.openapi.moqu.wiremock.model.WiremockMapping; +import io.quarkus.deployment.IsDevelopment; +import io.quarkus.deployment.annotations.BuildProducer; +import io.quarkus.deployment.annotations.BuildStep; +import io.quarkus.deployment.annotations.ExecutionTime; +import io.quarkus.deployment.annotations.Record; +import io.quarkus.devui.spi.page.CardPageBuildItem; +import io.quarkus.devui.spi.page.Page; +import io.quarkus.vertx.http.deployment.NonApplicationRootPathBuildItem; +import io.quarkus.vertx.http.deployment.RouteBuildItem; + +public class MoquWiremockDevUIProcessor { + + private static final String MAPPINGS_KEY = "mappings"; + private static final String WIREMOCK_MAPPINGS_JSON = "/wiremock-mappings.json"; + + @BuildStep(onlyIf = IsDevelopment.class) + @Record(ExecutionTime.RUNTIME_INIT) + void generateWiremock(List mocks, NonApplicationRootPathBuildItem nonApplicationRootPath, + BuildProducer routes, + MoquRoutesRecorder recorder) { + + WiremockMapper wiremockMapper = new WiremockMapper(); + ObjectMapper objMapper = ObjectMapperFactory.getInstance(); + + for (MoquBuildItem mock : mocks) { + List wiremockMappings = wiremockMapper.map(mock.getMoqu()); + try { + String json = objMapper.writeValueAsString(Map.of( + MAPPINGS_KEY, wiremockMappings)); + + String uri = mock.prefixUri(nonApplicationRootPath.resolvePath("moqu")) + .concat(WIREMOCK_MAPPINGS_JSON); + + routes.produce(nonApplicationRootPath.routeBuilder() + .routeFunction(uri, recorder.handleFile(json)) + .displayOnNotFoundPage() + .build()); + + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + } + + @BuildStep(onlyIf = { IsDevelopment.class }) + CardPageBuildItem cardPageBuildItem( + List moquMocks, + NonApplicationRootPathBuildItem nonApplicationRootPath) { + CardPageBuildItem cardPageBuildItem = new CardPageBuildItem(); + + List models = moquMocks.stream() + .map(m -> new MoquModel(m.getFullFilename(), m.prefixUri( + nonApplicationRootPath.resolvePath("moqu")) + .concat("/wiremock-mappings.json"))) + .toList(); + + cardPageBuildItem.addBuildTimeData("mocks", models); + + cardPageBuildItem.addPage( + Page.webComponentPageBuilder() + .title("Moqu Wiremock") + .icon("font-awesome-solid:server") + .componentLink("qwc-moqu.js") + .staticLabel(String.valueOf(moquMocks.size()))); + + return cardPageBuildItem; + } +} diff --git a/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquBuildItem.java b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquBuildItem.java new file mode 100644 index 000000000..a6285f734 --- /dev/null +++ b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquBuildItem.java @@ -0,0 +1,37 @@ +package io.quarkiverse.openapi.generator.items; + +import io.quarkiverse.openapi.moqu.Moqu; +import io.quarkus.builder.item.MultiBuildItem; + +public final class MoquBuildItem extends MultiBuildItem { + + private final String filename; + private final String extension; + private final Moqu moqu; + + public MoquBuildItem(String filename, String extension, Moqu moqu) { + this.filename = filename; + this.extension = extension; + this.moqu = moqu; + } + + public String getFilename() { + return filename; + } + + public String getExtension() { + return extension; + } + + public Moqu getMoqu() { + return moqu; + } + + public String getFullFilename() { + return filename + "." + extension; + } + + public String prefixUri(String basePath) { + return String.format("%s/%s/%s", basePath, extension, filename); + } +} diff --git a/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquProjectBuildItem.java b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquProjectBuildItem.java new file mode 100644 index 000000000..ea8de60a5 --- /dev/null +++ b/moqu/deployment/src/main/java/io/quarkiverse/openapi/generator/items/MoquProjectBuildItem.java @@ -0,0 +1,22 @@ +package io.quarkiverse.openapi.generator.items; + +import java.util.Collections; +import java.util.Map; + +import io.quarkus.builder.item.SimpleBuildItem; + +public final class MoquProjectBuildItem extends SimpleBuildItem { + + private final Map specs; + + public MoquProjectBuildItem(Map specs) { + this.specs = specs; + } + + public Map specs() { + return Collections.unmodifiableMap(specs); + } + + public record File(String filename, String extension, String content) { + } +} diff --git a/moqu/deployment/src/main/resources/dev-ui/qwc-moqu.js b/moqu/deployment/src/main/resources/dev-ui/qwc-moqu.js new file mode 100644 index 000000000..31d42cded --- /dev/null +++ b/moqu/deployment/src/main/resources/dev-ui/qwc-moqu.js @@ -0,0 +1,96 @@ +import {LitElement, html, css} from 'lit'; +import {columnBodyRenderer} from '@vaadin/grid/lit.js'; +import {mocks} from 'build-time-data'; +import '@vaadin/grid'; +import '@vaadin/vertical-layout'; +import '@vaadin/icon'; + +/** + * This component shows the Moqu mocks + */ +export class QwcMoqu extends LitElement { + + static styles = css` + .arctable { + height: 100%; + padding-bottom: 10px; + } + + .moqu-icon { + font-size: small; + color: var(--lumo-contrast-50pct); + cursor: pointer; + } + `; + + static properties = { + _mocks: {state: true} + }; + + constructor() { + super(); + this._mocks = mocks; + } + + render() { + if (this._mocks) { + return this._renderMockList(); + } else { + return html`No mocks found`; + } + } + + _renderMockList() { + return html` + + + + + + + + + `; + } + + _nameRenderer(mock) { + return html` + + ${mock.name} + + `; + } + + _linkDownloadRenderer(mock) { + return html` + + + + + + `; + } + + _linkSeeRenderer(mock) { + return html` + + + + + + `; + } + + +} + +customElements.define('qwc-moqu', QwcMoqu); diff --git a/moqu/deployment/src/test/java/io/quarkiverse/openapi/generator/MoquProjectProcessorTest.java b/moqu/deployment/src/test/java/io/quarkiverse/openapi/generator/MoquProjectProcessorTest.java new file mode 100644 index 000000000..76347c237 --- /dev/null +++ b/moqu/deployment/src/test/java/io/quarkiverse/openapi/generator/MoquProjectProcessorTest.java @@ -0,0 +1,47 @@ +package io.quarkiverse.openapi.generator; + +import org.hamcrest.Matchers; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.RegisterExtension; + +import io.quarkus.test.QuarkusDevModeTest; +import io.restassured.RestAssured; + +public class MoquProjectProcessorTest { + + @RegisterExtension + static final QuarkusDevModeTest unitTest = new QuarkusDevModeTest() + .withApplicationRoot(javaArchive -> javaArchive + .addAsResource("api.yaml", "openapi/openapi.yaml") + .addAsResource("apiv2.json", "openapi/api.json")); + + @Test + void testModeAsSee() { + RestAssured.given() + .when().get("/q/moqu/yaml/openapi/wiremock-mappings.json?mode=see") + .then() + .statusCode(200) + .body(Matchers.containsString("Alice")) + .log().ifError(); + } + + @Test + void testModeAsDownload() { + RestAssured.given() + .when().get("/q/moqu/yaml/openapi/wiremock-mappings.json") + .then() + .statusCode(200) + .body(Matchers.containsString("Alice")) + .log().ifError(); + } + + @Test + void testModeAsDownloadUsingJson() { + RestAssured.given() + .when().get("/q/moqu/json/api/wiremock-mappings.json") + .then() + .statusCode(200) + .body(Matchers.containsString("Alice")) + .log().ifError(); + } +} diff --git a/moqu/deployment/src/test/resources/api.yaml b/moqu/deployment/src/test/resources/api.yaml new file mode 100644 index 000000000..802ebe7e6 --- /dev/null +++ b/moqu/deployment/src/test/resources/api.yaml @@ -0,0 +1,35 @@ +openapi: 3.0.3 +servers: + - url: http://localhost:8888 +info: + version: 999-SNAPSHOT + title: Method GET one path param +paths: + "/users/{id}": + get: + parameters: + - name: id + in: path + examples: + alice: + value: 1 + responses: + 200: + content: + "application/json": + examples: + quarkus: + $ref: "#/components/schemas/User" + description: Ok +components: + schemas: + User: + type: object + properties: + name: + type: string + example: "Alice" + age: + type: number + example: 80 + diff --git a/moqu/deployment/src/test/resources/apiv2.json b/moqu/deployment/src/test/resources/apiv2.json new file mode 100644 index 000000000..4bbe37c02 --- /dev/null +++ b/moqu/deployment/src/test/resources/apiv2.json @@ -0,0 +1,60 @@ +{ + "openapi": "3.0.3", + "servers": [ + { + "url": "http://localhost:8888" + } + ], + "info": { + "version": "999-SNAPSHOT", + "title": "Method GET one path param" + }, + "paths": { + "/users/{id}": { + "get": { + "parameters": [ + { + "name": "id", + "in": "path", + "examples": { + "alice": { + "value": 1 + } + } + } + ], + "responses": { + "200": { + "description": "Ok", + "content": { + "application/json": { + "examples": { + "quarkus": { + "$ref": "#/components/schemas/User" + } + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "User": { + "type": "object", + "properties": { + "name": { + "type": "string", + "example": "Alice" + }, + "age": { + "type": "number", + "example": 80 + } + } + } + } + } +} diff --git a/moqu/pom.xml b/moqu/pom.xml new file mode 100644 index 000000000..79569a72b --- /dev/null +++ b/moqu/pom.xml @@ -0,0 +1,19 @@ + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-parent + 3.0.0-SNAPSHOT + ../pom.xml + + 4.0.0 + pom + quarkus-openapi-generator-moqu-parent + Quarkus - Openapi Generator - Moqu - Parent + + + core + deployment + runtime + + diff --git a/moqu/runtime/pom.xml b/moqu/runtime/pom.xml new file mode 100644 index 000000000..404ab0227 --- /dev/null +++ b/moqu/runtime/pom.xml @@ -0,0 +1,77 @@ + + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu-parent + 3.0.0-SNAPSHOT + + 4.0.0 + + quarkus-openapi-generator-moqu-wiremock + Quarkus - Openapi Generator - Moqu - Wiremock + + + + io.quarkiverse.openapi.generator + quarkus-openapi-generator-moqu-core + ${project.version} + + + + io.quarkus + quarkus-core + + + + io.quarkus + quarkus-vertx-http + + + + io.quarkus + quarkus-junit5 + test + + + + io.rest-assured + rest-assured + test + + + + + + io.quarkus + quarkus-extension-maven-plugin + ${quarkus.version} + + + compile + + extension-descriptor + + + ${project.groupId}:${project.artifactId}-deployment:${project.version} + + true + + + + + + maven-compiler-plugin + + + + io.quarkus + quarkus-extension-processor + ${quarkus.version} + + + + + + + diff --git a/moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/MoquConfig.java b/moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/MoquConfig.java new file mode 100644 index 000000000..ee9912ee8 --- /dev/null +++ b/moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/MoquConfig.java @@ -0,0 +1,19 @@ +package io.quarkiverse.openapi.generator.moqu; + +import io.quarkus.runtime.annotations.ConfigPhase; +import io.quarkus.runtime.annotations.ConfigRoot; +import io.smallrye.config.ConfigMapping; +import io.smallrye.config.WithDefault; + +@ConfigMapping(prefix = "quarkus.openapi-generator.moqu") +@ConfigRoot(phase = ConfigPhase.BUILD_AND_RUN_TIME_FIXED) +public interface MoquConfig { + + String DEFAULT_RESOURCE_DIR = "openapi"; + + /** + * Path to the Moqu (relative to the project). + */ + @WithDefault(DEFAULT_RESOURCE_DIR) + String resourceDir(); +} diff --git a/moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/recorder/MoquRoutesRecorder.java b/moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/recorder/MoquRoutesRecorder.java new file mode 100644 index 000000000..86d211c01 --- /dev/null +++ b/moqu/runtime/src/main/java/io/quarkiverse/openapi/generator/moqu/recorder/MoquRoutesRecorder.java @@ -0,0 +1,47 @@ +package io.quarkiverse.openapi.generator.moqu.recorder; + +import java.util.function.Consumer; + +import io.quarkus.runtime.annotations.Recorder; +import io.vertx.core.Handler; +import io.vertx.core.buffer.Buffer; +import io.vertx.core.http.HttpMethod; +import io.vertx.core.http.HttpServerRequest; +import io.vertx.core.http.HttpServerResponse; +import io.vertx.ext.web.Route; +import io.vertx.ext.web.RoutingContext; + +@Recorder +public class MoquRoutesRecorder { + + public Consumer handleFile(String content) { + return new Consumer() { + @Override + public void accept(Route route) { + route.method(HttpMethod.GET); + route.handler(new Handler() { + @Override + public void handle(RoutingContext routingContext) { + HttpServerResponse response = routingContext.response(); + HttpServerRequest request = routingContext.request(); + + String mode = request.getParam("mode"); + if (mode != null && mode.equalsIgnoreCase("see")) { + response.putHeader("Content-Type", "application/json; charset=utf-8"); + response.end(Buffer.buffer(content)); + } else { + setForDownloading(response, content); + } + } + + }); + } + }; + } + + private void setForDownloading(HttpServerResponse response, String content) { + response.putHeader("Content-Type", "application/octet-stream"); + response.putHeader("Content-Disposition", "attachment; filename=wiremock-mappings.json"); + response.end(Buffer.buffer(content)); + } +} diff --git a/moqu/runtime/src/main/resources/META-INF/quarkus-extension.yaml b/moqu/runtime/src/main/resources/META-INF/quarkus-extension.yaml new file mode 100644 index 000000000..c9383283a --- /dev/null +++ b/moqu/runtime/src/main/resources/META-INF/quarkus-extension.yaml @@ -0,0 +1,11 @@ +name: "OpenAPI Generator - Moqu - Wiremock Generator" +artifact: ${project.groupId}:${project.artifactId}:${project.version} +description: The OpenAPI Generator Moqu Wiremock extension converts an OpenAPI specification into a Wiremock definition. +metadata: + keywords: + - "openapi" + - "openapi-generator" + - "wiremock" + categories: + - "web" + status: "preview" diff --git a/pom.xml b/pom.xml index e03e68289..7e88c2b01 100644 --- a/pom.xml +++ b/pom.xml @@ -14,6 +14,8 @@ client server + docs + moqu :git:git@github.com:quarkiverse/quarkus-openapi-generator.git @@ -32,6 +34,8 @@ 3.27.3 4.1.1 3.12.1 + 2.35.2 + 2.1.22 From b9dc94820fdcf5b7d93a364b079d0c707d0bc0c6 Mon Sep 17 00:00:00 2001 From: Helber Belmiro Date: Thu, 20 Mar 2025 08:04:19 -0300 Subject: [PATCH 2/2] Fixed versions Signed-off-by: Helber Belmiro --- moqu/core/pom.xml | 2 +- moqu/deployment/pom.xml | 2 +- moqu/pom.xml | 2 +- moqu/runtime/pom.xml | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/moqu/core/pom.xml b/moqu/core/pom.xml index 06bd3b4a2..5164dff74 100644 --- a/moqu/core/pom.xml +++ b/moqu/core/pom.xml @@ -6,7 +6,7 @@ io.quarkiverse.openapi.generator quarkus-openapi-generator-moqu-parent - 3.0.0-SNAPSHOT + 3.0.0-lts-SNAPSHOT quarkus-openapi-generator-moqu-core diff --git a/moqu/deployment/pom.xml b/moqu/deployment/pom.xml index 0eb16fca6..c1f53c8eb 100644 --- a/moqu/deployment/pom.xml +++ b/moqu/deployment/pom.xml @@ -4,7 +4,7 @@ io.quarkiverse.openapi.generator quarkus-openapi-generator-moqu-parent - 3.0.0-SNAPSHOT + 3.0.0-lts-SNAPSHOT 4.0.0 diff --git a/moqu/pom.xml b/moqu/pom.xml index 79569a72b..e0fae16e0 100644 --- a/moqu/pom.xml +++ b/moqu/pom.xml @@ -3,7 +3,7 @@ io.quarkiverse.openapi.generator quarkus-openapi-generator-parent - 3.0.0-SNAPSHOT + 3.0.0-lts-SNAPSHOT ../pom.xml 4.0.0 diff --git a/moqu/runtime/pom.xml b/moqu/runtime/pom.xml index 404ab0227..227d687b5 100644 --- a/moqu/runtime/pom.xml +++ b/moqu/runtime/pom.xml @@ -4,7 +4,7 @@ io.quarkiverse.openapi.generator quarkus-openapi-generator-moqu-parent - 3.0.0-SNAPSHOT + 3.0.0-lts-SNAPSHOT 4.0.0