From 458906e8e3d7f367628790991cced35d37032566 Mon Sep 17 00:00:00 2001 From: Michael Edgar Date: Sat, 19 Oct 2024 07:58:31 -0400 Subject: [PATCH] Update SmallRye OpenAPI and MicroProfile OpenAPI to 4.0 - OAS 3.1, normalize `type` to arrays, do not test order of health keys - Fix health schema type tests - Update Spring Web test for OAS 3.1 Signed-off-by: Michael Edgar --- bom/application/pom.xml | 4 +- .../deployment/HealthOpenAPIFilter.java | 66 ++++---- .../test/DeprecatedHealthOpenAPITest.java | 4 +- .../health/test/HealthOpenAPITest.java | 5 +- .../deployment/SmallRyeOpenApiProcessor.java | 105 ++++++++----- .../deployment/filter/AutoServerFilter.java | 13 +- .../deployment/filter/AutoTagFilter.java | 90 ----------- ...llowedFilter.java => OperationFilter.java} | 147 ++++++++++-------- .../jaxrs/OpenApiDefaultPathTestCase.java | 2 +- .../OpenApiEmptyStaticModelsTestCase.java | 2 +- .../OpenApiHttpRootDefaultPathTestCase.java | 2 +- ...steasyPathHttpRootDefaultPathTestCase.java | 2 +- .../OpenApiWithResteasyPathTestCase.java | 2 +- .../vertx/OpenApiDefaultPathTestCase.java | 2 +- .../OpenApiHttpRootDefaultPathTestCase.java | 2 +- .../io/quarkus/it/main/OpenApiTestCase.java | 65 ++++---- .../web/openapi/OpenApiDefaultPathPMT.java | 2 +- tcks/microprofile-openapi/pom.xml | 2 +- 18 files changed, 226 insertions(+), 291 deletions(-) delete mode 100644 extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java rename extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/{AutoRolesAllowedFilter.java => OperationFilter.java} (50%) diff --git a/bom/application/pom.xml b/bom/application/pom.xml index 50447c314ed78..f9f8c07333e3a 100644 --- a/bom/application/pom.xml +++ b/bom/application/pom.xml @@ -46,12 +46,12 @@ 4.0 2.1 2.0 - 3.1.2 + 4.0.2 2.8.0 3.10.0 4.1.0 4.0.0 - 3.13.0 + 4.0.0 2.11.0 6.6.0 4.6.0 diff --git a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java index 62d7fd5232743..2730fe38d1510 100644 --- a/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java +++ b/extensions/smallrye-health/deployment/src/main/java/io/quarkus/smallrye/health/deployment/HealthOpenAPIFilter.java @@ -4,6 +4,7 @@ import java.util.List; import java.util.Map; +import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.models.Operation; @@ -13,16 +14,6 @@ import org.eclipse.microprofile.openapi.models.media.Schema; import org.eclipse.microprofile.openapi.models.responses.APIResponses; -import io.smallrye.openapi.api.models.ComponentsImpl; -import io.smallrye.openapi.api.models.OperationImpl; -import io.smallrye.openapi.api.models.PathItemImpl; -import io.smallrye.openapi.api.models.PathsImpl; -import io.smallrye.openapi.api.models.media.ContentImpl; -import io.smallrye.openapi.api.models.media.MediaTypeImpl; -import io.smallrye.openapi.api.models.media.SchemaImpl; -import io.smallrye.openapi.api.models.responses.APIResponseImpl; -import io.smallrye.openapi.api.models.responses.APIResponsesImpl; - /** * Create OpenAPI entries (if configured) */ @@ -32,37 +23,37 @@ public class HealthOpenAPIFilter implements OASFilter { private static final String HEALTH_RESPONSE_SCHEMA_NAME = "HealthResponse"; private static final String HEALTH_CHECK_SCHEMA_NAME = "HealthCheck"; - private static final Schema healthResponseSchemaDefinition = new SchemaImpl(HEALTH_RESPONSE_SCHEMA_NAME) - .type(Schema.SchemaType.OBJECT) + private static final Schema healthResponseSchemaDefinition = OASFactory.createSchema() + .type(Collections.singletonList(Schema.SchemaType.OBJECT)) .properties(Map.ofEntries( Map.entry("status", - new SchemaImpl() - .type(Schema.SchemaType.STRING) + OASFactory.createSchema() + .type(Collections.singletonList(Schema.SchemaType.STRING)) .enumeration(List.of("UP", "DOWN"))), Map.entry("checks", - new SchemaImpl() - .type(Schema.SchemaType.ARRAY) - .items(new SchemaImpl().ref("#/components/schemas/" + HEALTH_CHECK_SCHEMA_NAME))))); + OASFactory.createSchema() + .type(Collections.singletonList(Schema.SchemaType.ARRAY)) + .items(OASFactory.createSchema() + .ref("#/components/schemas/" + HEALTH_CHECK_SCHEMA_NAME))))); - private static final Schema healthCheckSchemaDefinition = new SchemaImpl(HEALTH_CHECK_SCHEMA_NAME) - .type(Schema.SchemaType.OBJECT) + private static final Schema healthCheckSchemaDefinition = OASFactory.createSchema() + .type(Collections.singletonList(Schema.SchemaType.OBJECT)) .properties(Map.ofEntries( Map.entry("name", - new SchemaImpl() - .type(Schema.SchemaType.STRING)), + OASFactory.createSchema() + .type(Collections.singletonList(Schema.SchemaType.STRING))), Map.entry("status", - new SchemaImpl() - .type(Schema.SchemaType.STRING) + OASFactory.createSchema() + .type(Collections.singletonList(Schema.SchemaType.STRING)) .enumeration(List.of("UP", "DOWN"))), Map.entry("data", - new SchemaImpl() - .type(Schema.SchemaType.OBJECT) - .nullable(Boolean.TRUE)))); + OASFactory.createSchema() + .type(List.of(Schema.SchemaType.OBJECT, Schema.SchemaType.NULL))))); private final String rootPath; private final String livenessPath; @@ -79,13 +70,14 @@ public HealthOpenAPIFilter(String rootPath, String livenessPath, String readines @Override public void filterOpenAPI(OpenAPI openAPI) { if (openAPI.getComponents() == null) { - openAPI.setComponents(new ComponentsImpl()); + openAPI.setComponents(OASFactory.createComponents()); } + openAPI.getComponents().addSchema(HEALTH_RESPONSE_SCHEMA_NAME, healthResponseSchemaDefinition); openAPI.getComponents().addSchema(HEALTH_CHECK_SCHEMA_NAME, healthCheckSchemaDefinition); if (openAPI.getPaths() == null) { - openAPI.setPaths(new PathsImpl()); + openAPI.setPaths(OASFactory.createPaths()); } final Paths paths = openAPI.getPaths(); @@ -150,31 +142,31 @@ private PathItem createHealthEndpoint( String operationDescription, String operationId, String operationSummary) { - final Content content = new ContentImpl() + final Content content = OASFactory.createContent() .addMediaType( "application/json", - new MediaTypeImpl() - .schema(new SchemaImpl().ref("#/components/schemas/" + HEALTH_RESPONSE_SCHEMA_NAME))); + OASFactory.createMediaType() + .schema(OASFactory.createSchema().ref("#/components/schemas/" + HEALTH_RESPONSE_SCHEMA_NAME))); - final APIResponses responses = new APIResponsesImpl() + final APIResponses responses = OASFactory.createAPIResponses() .addAPIResponse( "200", - new APIResponseImpl().description("OK").content(content)) + OASFactory.createAPIResponse().description("OK").content(content)) .addAPIResponse( "503", - new APIResponseImpl().description("Service Unavailable").content(content)) + OASFactory.createAPIResponse().description("Service Unavailable").content(content)) .addAPIResponse( "500", - new APIResponseImpl().description("Internal Server Error").content(content)); + OASFactory.createAPIResponse().description("Internal Server Error").content(content)); - final Operation getOperation = new OperationImpl() + final Operation getOperation = OASFactory.createOperation() .operationId(operationId) .description(operationDescription) .tags(MICROPROFILE_HEALTH_TAG) .summary(operationSummary) .responses(responses); - return new PathItemImpl() + return OASFactory.createPathItem() .description(endpointDescription) .summary(endpointSummary) .GET(getOperation); diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java index e9e26dc46e743..6c96b912c21d0 100644 --- a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/DeprecatedHealthOpenAPITest.java @@ -42,9 +42,7 @@ void testOpenApiPathAccessResource() { .body("components.schemas.HealthCheck.type", Matchers.equalTo("object")) .body("components.schemas.HealthCheck.properties.status.type", Matchers.equalTo("string")) .body("components.schemas.HealthCheck.properties.name.type", Matchers.equalTo("string")) - .body("components.schemas.HealthCheck.properties.data.type", Matchers.equalTo("object")) - .body("components.schemas.HealthCheck.properties.data.nullable", Matchers.is(true)); - + .body("components.schemas.HealthCheck.properties.data.type", Matchers.contains("object", "null")); } } diff --git a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java index fe4e1f28f9ee9..d99fb26c43221 100644 --- a/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java +++ b/extensions/smallrye-health/deployment/src/test/java/io/quarkus/smallrye/health/test/HealthOpenAPITest.java @@ -28,6 +28,7 @@ void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") + .body("paths", Matchers.hasKey("/q/health/ready")) .body("paths", Matchers.hasKey("/q/health/live")) .body("paths", Matchers.hasKey("/q/health/started")) @@ -40,9 +41,7 @@ void testOpenApiPathAccessResource() { .body("components.schemas.HealthCheck.type", Matchers.equalTo("object")) .body("components.schemas.HealthCheck.properties.status.type", Matchers.equalTo("string")) .body("components.schemas.HealthCheck.properties.name.type", Matchers.equalTo("string")) - .body("components.schemas.HealthCheck.properties.data.type", Matchers.equalTo("object")) - .body("components.schemas.HealthCheck.properties.data.nullable", Matchers.is(true)); - + .body("components.schemas.HealthCheck.properties.data.type", Matchers.contains("object", "null")); } } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java index 4913faee60e20..c8f0178cb4218 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/SmallRyeOpenApiProcessor.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; +import java.util.Collections; import java.util.Comparator; import java.util.EnumSet; import java.util.HashMap; @@ -45,6 +46,7 @@ import org.eclipse.microprofile.openapi.annotations.responses.APIResponse; import org.eclipse.microprofile.openapi.annotations.responses.APIResponses; import org.eclipse.microprofile.openapi.annotations.security.SecurityRequirement; +import org.eclipse.microprofile.openapi.models.Operation; import org.eclipse.microprofile.openapi.spi.OASFactoryResolver; import org.jboss.jandex.AnnotationInstance; import org.jboss.jandex.AnnotationTarget; @@ -92,11 +94,10 @@ import io.quarkus.security.PermissionsAllowed; import io.quarkus.smallrye.openapi.OpenApiFilter; import io.quarkus.smallrye.openapi.common.deployment.SmallRyeOpenApiConfig; -import io.quarkus.smallrye.openapi.deployment.filter.AutoRolesAllowedFilter; import io.quarkus.smallrye.openapi.deployment.filter.AutoServerFilter; -import io.quarkus.smallrye.openapi.deployment.filter.AutoTagFilter; import io.quarkus.smallrye.openapi.deployment.filter.ClassAndMethod; import io.quarkus.smallrye.openapi.deployment.filter.DefaultInfoFilter; +import io.quarkus.smallrye.openapi.deployment.filter.OperationFilter; import io.quarkus.smallrye.openapi.deployment.filter.SecurityConfigFilter; import io.quarkus.smallrye.openapi.deployment.spi.AddToOpenAPIDefinitionBuildItem; import io.quarkus.smallrye.openapi.deployment.spi.IgnoreStaticDocumentBuildItem; @@ -124,7 +125,6 @@ import io.smallrye.openapi.api.util.MergeUtil; import io.smallrye.openapi.jaxrs.JaxRsConstants; import io.smallrye.openapi.runtime.scanner.FilteredIndexView; -import io.smallrye.openapi.runtime.util.JandexUtil; import io.smallrye.openapi.spring.SpringConstants; import io.smallrye.openapi.vertx.VertxConstants; import io.vertx.core.Handler; @@ -135,7 +135,6 @@ * The main OpenAPI Processor. This will scan for JAX-RS, Spring and Vert.x Annotations, and, if any, add supplied schemas. * The result is added to the deployable unit to be loaded at runtime. */ -@SuppressWarnings("deprecation") public class SmallRyeOpenApiProcessor { private static final Logger log = Logger.getLogger("io.quarkus.smallrye.openapi"); @@ -213,7 +212,7 @@ void registerAutoSecurityFilter(BuildProducer syntheticB if (openApiConfig.autoAddSecurity) { autoSecurityFilter = getAutoSecurityFilter(securityInformationBuildItems, openApiConfig) .filter(securityFilter -> autoSecurityRuntimeEnabled(securityFilter, - () -> getAutoRolesAllowedFilter(apiFilteredIndexViewBuildItem, openApiConfig))) + () -> hasAutoEndpointSecurity(apiFilteredIndexViewBuildItem, openApiConfig))) .orElse(null); } @@ -222,9 +221,9 @@ void registerAutoSecurityFilter(BuildProducer syntheticB } static boolean autoSecurityRuntimeEnabled(AutoSecurityFilter autoSecurityFilter, - Supplier autoRolesAllowedFilterSource) { + Supplier autoRolesAllowedFilterSource) { // When the filter is not runtime required, add the security only if there are secured endpoints - return autoSecurityFilter.runtimeRequired() || autoRolesAllowedFilterSource.get() != null; + return autoSecurityFilter.runtimeRequired() || autoRolesAllowedFilterSource.get(); } @BuildStep @@ -406,17 +405,11 @@ void addAutoFilters(BuildProducer addToOpenAPID .ifPresent(addToOpenAPIDefinitionProducer::produce); } - OASFilter autoRolesAllowedFilter = getAutoRolesAllowedFilter(apiFilteredIndexViewBuildItem, config); - // Add Auto roles allowed - if (autoRolesAllowedFilter != null) { - addToOpenAPIDefinitionProducer.produce(new AddToOpenAPIDefinitionBuildItem(autoRolesAllowedFilter)); - } + // Add operation filter to add tags/descriptions/security requirements + OASFilter operationFilter = getOperationFilter(apiFilteredIndexViewBuildItem, config); - // Add Auto Tag based on the class name - OASFilter autoTagFilter = getAutoTagFilter(apiFilteredIndexViewBuildItem, - config); - if (autoTagFilter != null) { - addToOpenAPIDefinitionProducer.produce(new AddToOpenAPIDefinitionBuildItem(autoTagFilter)); + if (operationFilter != null) { + addToOpenAPIDefinitionProducer.produce(new AddToOpenAPIDefinitionBuildItem(operationFilter)); } // Add Auto Server based on the current server details @@ -522,40 +515,53 @@ private Optional getAutoSecurityFilter(List> rolesAllowedMethodReferences = getRolesAllowedMethodReferences( - apiFilteredIndexViewBuildItem); - getPermissionsAllowedMethodReferences(apiFilteredIndexViewBuildItem) - .forEach(k -> rolesAllowedMethodReferences.putIfAbsent(k, List.of())); + if (config.autoAddSecurityRequirement) { + Map> rolesAllowedMethods = Collections.emptyMap(); + List authenticatedMethods = Collections.emptyList(); - List authenticatedMethodReferences = getAuthenticatedMethodReferences( - apiFilteredIndexViewBuildItem); + rolesAllowedMethods = getRolesAllowedMethodReferences(indexViewBuildItem); - if (!rolesAllowedMethodReferences.isEmpty() || !authenticatedMethodReferences.isEmpty()) { - return new AutoRolesAllowedFilter( - config.securitySchemeName, - rolesAllowedMethodReferences, - authenticatedMethodReferences); + for (String methodRef : getPermissionsAllowedMethodReferences(indexViewBuildItem)) { + rolesAllowedMethods.putIfAbsent(methodRef, List.of()); } + + authenticatedMethods = getAuthenticatedMethodReferences(indexViewBuildItem); + + return !rolesAllowedMethods.isEmpty() || !authenticatedMethods.isEmpty(); } - return null; + + return false; } - private OASFilter getAutoTagFilter(OpenApiFilteredIndexViewBuildItem apiFilteredIndexViewBuildItem, + private OASFilter getOperationFilter(OpenApiFilteredIndexViewBuildItem indexViewBuildItem, SmallRyeOpenApiConfig config) { + Map classNamesMethods = Collections.emptyMap(); + Map> rolesAllowedMethods = Collections.emptyMap(); + List authenticatedMethods = Collections.emptyList(); + if (config.autoAddTags) { - Map classNamesMethodReferences = getClassNamesMethodReferences( - apiFilteredIndexViewBuildItem); + classNamesMethods = getClassNamesMethodReferences(indexViewBuildItem); + } - if (!classNamesMethodReferences.isEmpty()) { - return new AutoTagFilter(classNamesMethodReferences); + if (config.autoAddSecurityRequirement) { + rolesAllowedMethods = getRolesAllowedMethodReferences(indexViewBuildItem); + + for (String methodRef : getPermissionsAllowedMethodReferences(indexViewBuildItem)) { + rolesAllowedMethods.putIfAbsent(methodRef, List.of()); } + + authenticatedMethods = getAuthenticatedMethodReferences(indexViewBuildItem); + } + + if (!classNamesMethods.isEmpty() || !rolesAllowedMethods.isEmpty() || !authenticatedMethods.isEmpty()) { + return new OperationFilter(classNamesMethods, rolesAllowedMethods, authenticatedMethods, config.securitySchemeName); } + return null; } @@ -588,7 +594,7 @@ private Map> getRolesAllowedMethodReferences(OpenApiFiltere .flatMap(Collection::stream) .flatMap(t -> getMethods(t, index)) .collect(Collectors.toMap( - e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey()), + e -> createUniqueMethodReference(e.getKey().declaringClass(), e.getKey()), e -> List.of(e.getValue().value().asStringArray()), (v1, v2) -> { if (!Objects.equals(v1, v2)) { @@ -608,7 +614,7 @@ private List getPermissionsAllowedMethodReferences( .getAnnotations(DotName.createSimple(PermissionsAllowed.class)) .stream() .flatMap(t -> getMethods(t, index)) - .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) + .map(e -> createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) .distinct() .toList(); } @@ -619,7 +625,7 @@ private List getAuthenticatedMethodReferences(OpenApiFilteredIndexViewBu .getAnnotations(DotName.createSimple(Authenticated.class.getName())) .stream() .flatMap(t -> getMethods(t, index)) - .map(e -> JandexUtil.createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) + .map(e -> createUniqueMethodReference(e.getKey().declaringClass(), e.getKey())) .distinct() .toList(); } @@ -670,7 +676,7 @@ private Map getClassNamesMethodReferences( addMethodImplementationClassNames(method, params, filteredIndex .getAllKnownSubclasses(declaringClass.name()), classNames); } else { - String ref = JandexUtil.createUniqueMethodReference(declaringClass, method); + String ref = createUniqueMethodReference(declaringClass, method); classNames.put(ref, new ClassAndMethod(declaringClass.simpleName(), method.name())); } } @@ -685,15 +691,19 @@ void addMethodImplementationClassNames(MethodInfo method, Type[] params, Collect MethodInfo implMethod = impl.method(method.name(), params); if (implMethod != null) { - classNames.put(JandexUtil.createUniqueMethodReference(impl, implMethod), + classNames.put(createUniqueMethodReference(impl, implMethod), new ClassAndMethod(simpleClassName, implMethod.name())); } - classNames.put(JandexUtil.createUniqueMethodReference(impl, method), + classNames.put(createUniqueMethodReference(impl, method), new ClassAndMethod(simpleClassName, method.name())); } } + public static String createUniqueMethodReference(ClassInfo classInfo, MethodInfo methodInfo) { + return "m" + classInfo.hashCode() + "_" + methodInfo.hashCode(); + } + private static boolean isValidOpenAPIMethodForAutoAdd(MethodInfo method) { return isOpenAPIEndpoint(method) && !method.hasAnnotation(OPENAPI_SECURITY_REQUIREMENT) && method.declaringClass().declaredAnnotation(OPENAPI_SECURITY_REQUIREMENT) == null; @@ -819,6 +829,11 @@ private void registerReflectionForApiResponseSchemaSerialization(BuildProducer feature, BuildProducer resourceBuildItemBuildProducer, @@ -862,6 +877,7 @@ public void build(BuildProducer feature, .withScannerFilter(getScannerFilter(capabilities, index)) .withContextRootResolver(getContextRootResolver(config, capabilities, httpRootPathBuildItem)) .withTypeConverter(getTypeConverter(index, capabilities)) + .withOperationHandler(this::handleOperation) .enableUnannotatedPathParameters(capabilities.isPresent(Capability.RESTEASY_REACTIVE)) .enableStandardFilter(false) .withFilters(openAPIBuildItems.stream().map(AddToOpenAPIDefinitionBuildItem::getOASFilter).toList()); @@ -892,8 +908,10 @@ Map.> entry("YAML", openAPI::toYAML)) .enableAnnotationScan(false) .enableStandardFilter(false) .withInitialModel(openAPI.model()); + Optional.ofNullable(getAutoServerFilter(smallRyeOpenApiConfig, true, "Auto generated value")) .ifPresent(runtimeFilterBuilder::addFilter); + return runtimeFilterBuilder; }; @@ -917,6 +935,7 @@ Map.> entry("YAML", openAPI::toYAML)) } }); + @SuppressWarnings("deprecation") OpenApiDocument output = OpenApiDocument.newInstance(); output.set(finalOpenAPI.model()); openApiDocumentProducer.produce(new OpenApiDocumentBuildItem(output)); diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoServerFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoServerFilter.java index 0519a91303139..a36fde454bb0f 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoServerFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoServerFilter.java @@ -3,12 +3,11 @@ import java.util.ArrayList; import java.util.List; +import org.eclipse.microprofile.openapi.OASFactory; import org.eclipse.microprofile.openapi.OASFilter; import org.eclipse.microprofile.openapi.models.OpenAPI; import org.eclipse.microprofile.openapi.models.servers.Server; -import io.smallrye.openapi.api.models.servers.ServerImpl; - /** * Automatically add default server if none is provided */ @@ -46,16 +45,16 @@ public void filterOpenAPI(OpenAPI openAPI) { // In case of 0.0.0.0, also add localhost if (this.defaultHost.equals(ZEROS)) { - ServerImpl localhost = new ServerImpl(); + Server localhost = OASFactory.createServer(); localhost.setUrl(getUrl(this.defaultScheme, LOCALHOST, this.defaultPort)); localhost.setDescription(this.description); servers.add(localhost); } - ServerImpl serverImpl = new ServerImpl(); - serverImpl.setUrl(getUrl(this.defaultScheme, this.defaultHost, this.defaultPort)); - serverImpl.setDescription(this.description); - servers.add(serverImpl); + Server server = OASFactory.createServer(); + server.setUrl(getUrl(this.defaultScheme, this.defaultHost, this.defaultPort)); + server.setDescription(this.description); + servers.add(server); openAPI.setServers(servers); } diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java deleted file mode 100644 index 22a119e1e6fe8..0000000000000 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoTagFilter.java +++ /dev/null @@ -1,90 +0,0 @@ -package io.quarkus.smallrye.openapi.deployment.filter; - -import java.util.Map; -import java.util.Set; - -import org.eclipse.microprofile.openapi.OASFilter; -import org.eclipse.microprofile.openapi.models.OpenAPI; -import org.eclipse.microprofile.openapi.models.Operation; -import org.eclipse.microprofile.openapi.models.PathItem; -import org.eclipse.microprofile.openapi.models.Paths; - -import io.smallrye.openapi.api.models.OperationImpl; - -/** - * Automatically tag operations based on the class name. - */ -public class AutoTagFilter implements OASFilter { - private Map classNameMap; - - public AutoTagFilter() { - - } - - public AutoTagFilter(Map classNameMap) { - this.classNameMap = classNameMap; - } - - public Map getClassNameMap() { - return classNameMap; - } - - public void setClassNameMap(Map classNameMap) { - this.classNameMap = classNameMap; - } - - @Override - public void filterOpenAPI(OpenAPI openAPI) { - if (!classNameMap.isEmpty()) { - Paths paths = openAPI.getPaths(); - if (paths != null) { - Map pathItems = paths.getPathItems(); - if (pathItems != null && !pathItems.isEmpty()) { - Set> pathItemsEntries = pathItems.entrySet(); - for (Map.Entry pathItem : pathItemsEntries) { - Map operations = pathItem.getValue().getOperations(); - if (operations != null && !operations.isEmpty()) { - for (Operation operation : operations.values()) { - - if (operation.getDescription() == null || operation.getDescription().isBlank()) { - // Auto add a description - OperationImpl operationImpl = (OperationImpl) operation; - String methodRef = operationImpl.getMethodRef(); - if (classNameMap.containsKey(methodRef)) { - operation.setDescription(capitalizeFirstLetter( - splitCamelCase(classNameMap.get(methodRef).methodName()))); - } - } - - if (operation.getTags() == null || operation.getTags().isEmpty()) { - // Auto add a tag - OperationImpl operationImpl = (OperationImpl) operation; - String methodRef = operationImpl.getMethodRef(); - if (classNameMap.containsKey(methodRef)) { - operation.addTag(splitCamelCase(classNameMap.get(methodRef).className())); - } - } - } - } - } - } - } - } - } - - private String splitCamelCase(String s) { - return s.replaceAll( - String.format("%s|%s|%s", - "(?<=[A-Z])(?=[A-Z][a-z])", - "(?<=[^A-Z])(?=[A-Z])", - "(?<=[A-Za-z])(?=[^A-Za-z])"), - " "); - } - - private String capitalizeFirstLetter(String str) { - if (str == null || str.isEmpty()) { - return str; - } - return str.substring(0, 1).toUpperCase() + str.substring(1); - } -} \ No newline at end of file diff --git a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java similarity index 50% rename from extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java rename to extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java index 2873b039cbabb..82c7483621c6d 100644 --- a/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/AutoRolesAllowedFilter.java +++ b/extensions/smallrye-openapi/deployment/src/main/java/io/quarkus/smallrye/openapi/deployment/filter/OperationFilter.java @@ -22,73 +22,38 @@ import org.eclipse.microprofile.openapi.models.security.SecurityRequirement; import org.eclipse.microprofile.openapi.models.security.SecurityScheme; -import io.smallrye.openapi.api.models.OperationImpl; - /** - * Automatically add security requirement to RolesAllowed methods + * This filter replaces the former AutoTagFilter and AutoRolesAllowedFilter and has three functions: + *
    + *
  • Add operation descriptions based on the associated Java method name handling the operation + *
  • Add operation tags based on the associated Java class of the operation + *
  • Add security requirements based on discovered {@link jakarta.annotation.security.RolesAllowed}, + * {@link io.quarkus.security.PermissionsAllowed}, and {@link io.quarkus.security.Authenticated} + * annotations. + *
*/ -public class AutoRolesAllowedFilter implements OASFilter { +public class OperationFilter implements OASFilter { + + public static final String EXT_METHOD_REF = "x-quarkus-openapi-method-ref"; + + private Map classNameMap; private Map> rolesAllowedMethodReferences; private List authenticatedMethodReferences; private String defaultSecuritySchemeName; - public AutoRolesAllowedFilter() { - - } - - public AutoRolesAllowedFilter(String defaultSecuritySchemeName, Map> rolesAllowedMethodReferences, - List authenticatedMethodReferences) { - this.defaultSecuritySchemeName = defaultSecuritySchemeName; - this.rolesAllowedMethodReferences = rolesAllowedMethodReferences; - this.authenticatedMethodReferences = authenticatedMethodReferences; - } - - public Map> getRolesAllowedMethodReferences() { - return rolesAllowedMethodReferences; - } - - public void setRolesAllowedMethodReferences(Map> rolesAllowedMethodReferences) { - this.rolesAllowedMethodReferences = rolesAllowedMethodReferences; - } - - public boolean hasRolesAllowedMethodReferences() { - return this.rolesAllowedMethodReferences != null && !this.rolesAllowedMethodReferences.isEmpty(); - } - - public boolean hasRolesAllowedMethodReference(String methodRef) { - return this.rolesAllowedMethodReferences != null && this.rolesAllowedMethodReferences.containsKey(methodRef); - } - - public List getAuthenticatedMethodReferences() { - return authenticatedMethodReferences; - } - - public void setAuthenticatedMethodReferences(List authenticatedMethodReferences) { - this.authenticatedMethodReferences = authenticatedMethodReferences; - } - - public boolean hasAuthenticatedMethodReferences() { - return this.authenticatedMethodReferences != null && !this.authenticatedMethodReferences.isEmpty(); - } - - public boolean hasAuthenticatedMethodReference(String methodRef) { - return this.authenticatedMethodReferences != null && this.authenticatedMethodReferences.contains(methodRef); - } - - public String getDefaultSecuritySchemeName() { - return defaultSecuritySchemeName; - } + public OperationFilter(Map classNameMap, + Map> rolesAllowedMethodReferences, + List authenticatedMethodReferences, + String defaultSecuritySchemeName) { - public void setDefaultSecuritySchemeName(String defaultSecuritySchemeName) { - this.defaultSecuritySchemeName = defaultSecuritySchemeName; + this.classNameMap = Objects.requireNonNull(classNameMap); + this.rolesAllowedMethodReferences = Objects.requireNonNull(rolesAllowedMethodReferences); + this.authenticatedMethodReferences = Objects.requireNonNull(authenticatedMethodReferences); + this.defaultSecuritySchemeName = Objects.requireNonNull(defaultSecuritySchemeName); } @Override public void filterOpenAPI(OpenAPI openAPI) { - if (!hasRolesAllowedMethodReferences() && !hasAuthenticatedMethodReferences()) { - return; - } - var securityScheme = getSecurityScheme(openAPI); String schemeName = securityScheme.map(Map.Entry::getKey).orElse(defaultSecuritySchemeName); boolean scopesValidForScheme = securityScheme.map(Map.Entry::getValue) @@ -108,19 +73,68 @@ public void filterOpenAPI(OpenAPI openAPI) { .map(Map::values) .flatMap(Collection::stream) .forEach(operation -> { - String methodRef = OperationImpl.getMethodRef(operation); - - if (hasRolesAllowedMethodReference(methodRef)) { - List scopes = rolesAllowedMethodReferences.get(methodRef); - addSecurityRequirement(operation, schemeName, scopesValidForScheme ? scopes : Collections.emptyList()); - addDefaultSecurityResponses(operation, defaultSecurityErrors); - } else if (hasAuthenticatedMethodReference(methodRef)) { - addSecurityRequirement(operation, schemeName, Collections.emptyList()); - addDefaultSecurityResponses(operation, defaultSecurityErrors); + final String methodRef = methodRef(operation); + + if (methodRef != null) { + maybeSetDescriptionAndTag(operation, methodRef); + maybeAddSecurityRequirement(operation, methodRef, schemeName, scopesValidForScheme, + defaultSecurityErrors); } + + operation.removeExtension(EXT_METHOD_REF); }); } + private String methodRef(Operation operation) { + final Map extensions = operation.getExtensions(); + return (String) (extensions != null ? extensions.get(EXT_METHOD_REF) : null); + } + + private void maybeSetDescriptionAndTag(Operation operation, String methodRef) { + if (!classNameMap.containsKey(methodRef)) { + return; + } + + ClassAndMethod classMethod = classNameMap.get(methodRef); + + if (operation.getDescription() == null || operation.getDescription().isBlank()) { + // Auto add a description + operation.setDescription(capitalizeFirstLetter(splitCamelCase(classMethod.methodName()))); + } + + if (operation.getTags() == null || operation.getTags().isEmpty()) { + operation.addTag(splitCamelCase(classMethod.className())); + } + } + + private String splitCamelCase(String s) { + return s.replaceAll( + String.format("%s|%s|%s", + "(?<=[A-Z])(?=[A-Z][a-z])", + "(?<=[^A-Z])(?=[A-Z])", + "(?<=[A-Za-z])(?=[^A-Za-z])"), + " "); + } + + private String capitalizeFirstLetter(String str) { + if (str == null || str.isEmpty()) { + return str; + } + return str.substring(0, 1).toUpperCase() + str.substring(1); + } + + private void maybeAddSecurityRequirement(Operation operation, String methodRef, String schemeName, boolean allowScopes, + Map defaultSecurityErrors) { + if (rolesAllowedMethodReferences.containsKey(methodRef)) { + List scopes = rolesAllowedMethodReferences.get(methodRef); + addSecurityRequirement(operation, schemeName, allowScopes ? scopes : Collections.emptyList()); + addDefaultSecurityResponses(operation, defaultSecurityErrors); + } else if (authenticatedMethodReferences.contains(methodRef)) { + addSecurityRequirement(operation, schemeName, Collections.emptyList()); + addDefaultSecurityResponses(operation, defaultSecurityErrors); + } + } + private Optional> getSecurityScheme(OpenAPI openAPI) { // Might be set in annotations return Optional.ofNullable(openAPI.getComponents()) @@ -152,5 +166,4 @@ private Map getSecurityResponses() { responses.put("403", OASFactory.createAPIResponse().description("Not Allowed")); return responses; } - -} \ No newline at end of file +} diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java index 5954bf61612b5..3802f5478fb2a 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiDefaultPathTestCase.java @@ -33,7 +33,7 @@ public void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("tags.name[0]", Matchers.equalTo("test")) .body("paths.'/resource'.get.servers[0]", Matchers.hasKey("url")) diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiEmptyStaticModelsTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiEmptyStaticModelsTestCase.java index 3ca76e6a9a341..3a87727be5454 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiEmptyStaticModelsTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiEmptyStaticModelsTestCase.java @@ -29,6 +29,6 @@ public void testNonExistingAdditionalDocsDirectory() throws IOException { .when().get("/q/openapi") .then() .log().body().and() - .body("openapi", Matchers.startsWith("3.0.")); + .body("openapi", Matchers.startsWith("3.1.")); } } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java index 1ccb3a7991fb1..f06df9ad3e50f 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiHttpRootDefaultPathTestCase.java @@ -32,7 +32,7 @@ public void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/foo/resource")); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java index 558e67c5e9bc6..fa573d4acc4b7 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathHttpRootDefaultPathTestCase.java @@ -25,7 +25,7 @@ public void testOpenApiResteasyPathHttpRootDefaultPath() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/http-root-path/resteasy-path/resource")); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java index 593f7b9488b71..0040220a3ab45 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/jaxrs/OpenApiWithResteasyPathTestCase.java @@ -24,7 +24,7 @@ public void testOpenApiResteasyPath() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/foo/bar/resource")); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java index 1efed83abe595..0fe5fb83c51b8 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiDefaultPathTestCase.java @@ -30,7 +30,7 @@ public void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/resource")); } diff --git a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java index 7d57df969f1ca..4c2622ee66e42 100644 --- a/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java +++ b/extensions/smallrye-openapi/deployment/src/test/java/io/quarkus/smallrye/openapi/test/vertx/OpenApiHttpRootDefaultPathTestCase.java @@ -34,7 +34,7 @@ public void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-smallrye-openapi-deployment API")) .body("paths", Matchers.hasKey("/foo/resource")); } diff --git a/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java b/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java index f52a42054287f..2d22261477228 100644 --- a/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java +++ b/integration-tests/main/src/test/java/io/quarkus/it/main/OpenApiTestCase.java @@ -5,12 +5,15 @@ import java.io.InputStream; import java.net.URL; import java.net.URLConnection; -import java.util.Iterator; +import java.util.List; import java.util.Set; import jakarta.json.Json; import jakarta.json.JsonObject; import jakarta.json.JsonReader; +import jakarta.json.JsonString; +import jakarta.json.JsonValue; +import jakarta.json.JsonValue.ValueType; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; @@ -43,7 +46,7 @@ public void testOpenAPIJSON() throws Exception { JsonObject obj = parser.readObject(); Assertions.assertNotNull(obj); - Assertions.assertEquals("3.0.3", obj.getString("openapi")); + Assertions.assertEquals("3.1.0", obj.getString("openapi")); Assertions.assertEquals("main-integration-test API", obj.getJsonObject("info").getString("title")); Assertions.assertEquals("1.0", obj.getJsonObject("info").getString("version")); @@ -64,10 +67,10 @@ public void testOpenAPIJSON() throws Exception { // test RESTEasy extensions JsonObject schemasObj = obj.getJsonObject("components").getJsonObject("schemas"); - String testSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE_PRIMITIVE, + List testSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE_PRIMITIVE, testObj.getJsonObject("get").getJsonObject("responses"), schemasObj); - String rxSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE_PRIMITIVE, + List rxSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE_PRIMITIVE, injectionObj.getJsonObject("get").getJsonObject("responses"), schemasObj); // make sure String, CompletionStage and Single are detected the same @@ -91,7 +94,7 @@ public void testOpenAPIJSON() throws Exception { Assertions.assertEquals(1, keys.size()); Assertions.assertEquals("get", keys.iterator().next()); - String uniSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE_PRIMITIVE, + List uniSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE_PRIMITIVE, uniObj.getJsonObject("get").getJsonObject("responses"), schemasObj); // make sure String, CompletionStage and Uni are detected the same @@ -105,12 +108,12 @@ public void testOpenAPIJSON() throws Exception { Assertions.assertEquals(1, keys.size()); Assertions.assertEquals("get", keys.iterator().next()); - String uniTypedSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE, + List uniTypedSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE, uniTypedObj.getJsonObject("get").getJsonObject("responses"), schemasObj); // make sure ComponentType and Uni are detected the same JsonObject ctObj = paths.getJsonObject("/test/compType"); - String ctSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE, ctObj.getJsonObject("get").getJsonObject("responses"), + List ctSchemaType = schemaType("200", DEFAULT_MEDIA_TYPE, ctObj.getJsonObject("get").getJsonObject("responses"), schemasObj); Assertions.assertEquals(ctSchemaType, uniTypedSchemaType, @@ -125,8 +128,8 @@ public void testOpenAPIJSON() throws Exception { // make sure Multi is detected as array JsonObject multiSchema = multiObj.getJsonObject("get").getJsonObject("responses") .getJsonObject("200").getJsonObject("content").getJsonObject(DEFAULT_MEDIA_TYPE).getJsonObject("schema"); - Assertions.assertEquals("array", multiSchema.getString("type")); - Assertions.assertEquals("string", multiSchema.getJsonObject("items").getString("type")); + Assertions.assertEquals(List.of("array"), getTypes(multiSchema)); + Assertions.assertEquals(List.of("string"), getTypes(multiSchema.getJsonObject("items"))); JsonObject multiTypedObj = paths.getJsonObject("/test/multiType"); Assertions.assertNotNull(multiTypedObj); @@ -137,8 +140,8 @@ public void testOpenAPIJSON() throws Exception { JsonObject multiTypedSchema = multiTypedObj.getJsonObject("get").getJsonObject("responses") .getJsonObject("200").getJsonObject("content").getJsonObject(DEFAULT_MEDIA_TYPE).getJsonObject("schema"); // make sure Multi is detected as array - Assertions.assertEquals("array", multiTypedSchema.getString("type")); - String mutliTypedObjectSchema = schemaTypeFromRef(multiTypedSchema.getJsonObject("items"), schemasObj); + Assertions.assertEquals(List.of("array"), getTypes(multiTypedSchema)); + List mutliTypedObjectSchema = schemaTypeFromRef(multiTypedSchema.getJsonObject("items"), schemasObj); Assertions.assertEquals(ctSchemaType, mutliTypedObjectSchema, "Normal and Mutiny Multi have same schema"); @@ -147,32 +150,20 @@ public void testOpenAPIJSON() throws Exception { JsonObject healthPath = paths.getJsonObject("/q/health"); Assertions.assertNotNull(healthPath); Set healthKeys = healthPath.keySet(); - Iterator healthKeysIterator = healthKeys.iterator(); - Assertions.assertEquals(3, healthPath.size()); - Assertions.assertEquals("summary", healthKeysIterator.next()); - Assertions.assertEquals("description", healthKeysIterator.next()); - Assertions.assertEquals("get", healthKeysIterator.next()); + Assertions.assertEquals(Set.of("summary", "description", "get"), healthKeys); JsonObject livenessPath = paths.getJsonObject("/q/health/live"); Assertions.assertNotNull(livenessPath); Set livenessKeys = livenessPath.keySet(); - Iterator livenessKeysIterator = livenessKeys.iterator(); - Assertions.assertEquals(3, livenessKeys.size()); - Assertions.assertEquals("summary", livenessKeysIterator.next()); - Assertions.assertEquals("description", livenessKeysIterator.next()); - Assertions.assertEquals("get", livenessKeysIterator.next()); + Assertions.assertEquals(Set.of("summary", "description", "get"), livenessKeys); JsonObject readinessPath = paths.getJsonObject("/q/health/ready"); Assertions.assertNotNull(readinessPath); Set readinessKeys = readinessPath.keySet(); - Iterator readinessKeysIterator = readinessKeys.iterator(); - Assertions.assertEquals(3, readinessKeys.size()); - Assertions.assertEquals("summary", readinessKeysIterator.next()); - Assertions.assertEquals("description", readinessKeysIterator.next()); - Assertions.assertEquals("get", readinessKeysIterator.next()); + Assertions.assertEquals(Set.of("summary", "description", "get"), readinessKeys); } - protected static String schemaType(String responseCode, String mediaType, JsonObject responses, JsonObject schemas) { + protected static List schemaType(String responseCode, String mediaType, JsonObject responses, JsonObject schemas) { JsonObject responseContent = responses.getJsonObject(responseCode).getJsonObject("content"); if (responseContent == null) { return null; @@ -181,7 +172,7 @@ protected static String schemaType(String responseCode, String mediaType, JsonOb .getJsonObject("schema"); if (schemaObj.containsKey("type")) { - return schemaObj.getString("type"); + return getTypes(schemaObj); } else if (schemaObj.containsKey("$ref")) { return schemaTypeFromRef(schemaObj, schemas); } @@ -190,14 +181,28 @@ protected static String schemaType(String responseCode, String mediaType, JsonOb "Cannot retrieve schema type for response " + responseCode + " and media type " + mediaType); } - protected static String schemaTypeFromRef(JsonObject responseSchema, JsonObject schemas) { + protected static List schemaTypeFromRef(JsonObject responseSchema, JsonObject schemas) { if (responseSchema.containsKey("$ref")) { String schemaReference = responseSchema.getString("$ref"); String schemaRefType = schemaReference.substring(schemaReference.lastIndexOf("/") + 1); - return schemas.getJsonObject(schemaRefType).getString("type"); + return getTypes(schemas.getJsonObject(schemaRefType)); } throw new IllegalArgumentException( "Cannot retrieve schema type for responseSchema " + responseSchema); } + + protected static List getTypes(JsonObject schema) { + JsonValue type = schema.get("type"); + + if (type == null) { + return null; + } else if (type.getValueType() == ValueType.STRING) { + return List.of(((JsonString) type).getString()); + } else if (type.getValueType() == ValueType.ARRAY) { + return type.asJsonArray().stream().map(JsonString.class::cast).map(JsonString::getString).toList(); + } + + return null; + } } diff --git a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java index fa87ce69da8da..e4c1d9e1690cb 100644 --- a/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java +++ b/integration-tests/spring-web/src/test/java/io/quarkus/it/spring/web/openapi/OpenApiDefaultPathPMT.java @@ -33,7 +33,7 @@ public void testOpenApiPathAccessResource() { .when().get(OPEN_API_PATH) .then() .header("Content-Type", "application/json;charset=UTF-8") - .body("openapi", Matchers.startsWith("3.0")) + .body("openapi", Matchers.startsWith("3.1")) .body("info.title", Matchers.equalTo("quarkus-integration-test-spring-web API")) .body("paths", Matchers.hasKey("/resource")); } diff --git a/tcks/microprofile-openapi/pom.xml b/tcks/microprofile-openapi/pom.xml index fdb9e98995ee2..835c48a290666 100644 --- a/tcks/microprofile-openapi/pom.xml +++ b/tcks/microprofile-openapi/pom.xml @@ -14,7 +14,7 @@ Quarkus - TCK - MicroProfile OpenAPI - 3.1.2 + 4.0.2