diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java index 5686d0c40..733422d73 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/ResourceParameters.java @@ -24,10 +24,29 @@ */ public class ResourceParameters { - public static final Comparator PARAMETER_COMPARATOR = Comparator - .comparing(Parameter::getRef, Comparator.nullsFirst(Comparator.naturalOrder())) - .thenComparing(Parameter::getIn, Comparator.nullsLast(Comparator.naturalOrder())) - .thenComparing(Parameter::getName, Comparator.nullsLast(Comparator.naturalOrder())); + private static int position(List preferredOrder, Parameter p) { + int pos = preferredOrder.indexOf(p); + return pos >= 0 ? pos : Integer.MAX_VALUE; + } + + public static Comparator parameterComparator(List preferredOrder) { + Comparator primaryComparator; + + if (preferredOrder != null) { + Comparator preferredComparator = (p1, p2) -> Integer.compare(position(preferredOrder, p1), + position(preferredOrder, p2)); + + primaryComparator = preferredComparator + .thenComparing(Parameter::getRef, Comparator.nullsFirst(Comparator.naturalOrder())); + } else { + primaryComparator = Comparator + .comparing(Parameter::getRef, Comparator.nullsFirst(Comparator.naturalOrder())); + } + + return primaryComparator + .thenComparing(Parameter::getIn, Comparator.nullsLast(Comparator.naturalOrder())) + .thenComparing(Parameter::getName, Comparator.nullsLast(Comparator.naturalOrder())); + } static final Pattern TEMPLATE_PARAM_PATTERN = Pattern.compile("\\{(\\w[\\w\\.-]*)\\}"); @@ -108,12 +127,14 @@ public void setFormBodyContent(Content formBodyContent) { this.formBodyContent = formBodyContent; } - public void sort() { + public void sort(List preferredOrder) { + Comparator comparator = parameterComparator(preferredOrder); + if (pathItemParameters != null) { - pathItemParameters.sort(PARAMETER_COMPARATOR); + pathItemParameters.sort(comparator); } if (operationParameters != null) { - operationParameters.sort(PARAMETER_COMPARATOR); + operationParameters.sort(comparator); } } diff --git a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java index 061d955e6..11cc58c89 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/scanner/spi/AbstractParameterProcessor.java @@ -113,6 +113,7 @@ public abstract class AbstractParameterProcessor { protected Map> matrixParams = new LinkedHashMap<>(); private Set processedMatrixSegments = new HashSet<>(); + private List preferredOrder; /** * Used for collecting and merging any scanned {@link Parameter} annotations @@ -276,7 +277,7 @@ protected void processOperationParameters(MethodInfo resourceMethod, ResourcePar .forEach(this::readAnnotatedType); /* - * Phase III - Read @Parameter(s) annotations directly on the recourd method + * Phase III - Read @Parameter(s) annotations directly on the record method * * Read the resource method and any method super classes/interfaces that it may override */ @@ -309,7 +310,7 @@ protected void processFinalize(ClassInfo resourceClass, MethodInfo resourceMetho .forEach(parameters::addOperationParameter); // Re-sort (names of matrix parameters may have changed) - parameters.sort(); + parameters.sort(preferredOrder); parameters.setFormBodyContent(getFormBodyContent()); } @@ -424,7 +425,7 @@ protected List getParameters(MethodInfo resourceMethod) { .stream() .map(context -> this.mapParameter(resourceMethod, context)) .filter(Objects::nonNull) - .sorted(ResourceParameters.PARAMETER_COMPARATOR) + .sorted(ResourceParameters.parameterComparator(preferredOrder)) .collect(Collectors.toList()); return parameters.isEmpty() ? null : parameters; @@ -936,11 +937,14 @@ protected void readParameterAnnotation(AnnotationInstance annotation) { AnnotationValue annotationValue = annotation.value(); if (annotationValue != null) { + AnnotationInstance[] parameters = annotationValue.asNestedArray(); + preferredOrder = new ArrayList<>(parameters.length); + /* * Unwrap annotations wrapped by @Parameters and * identify the target as the target of the @Parameters annotation */ - for (AnnotationInstance nested : annotationValue.asNestedArray()) { + for (AnnotationInstance nested : parameters) { readAnnotatedType(AnnotationInstance.create(nested.name(), annotation.target(), nested.values()), @@ -1264,6 +1268,13 @@ protected boolean isReadableParameterAnnotation(DotName name) { protected void readParameterAnnotation(AnnotationInstance annotation, boolean overriddenParametersOnly) { Parameter oaiParam = readerFunction.apply(annotation); + if (oaiParam.getRef() != null) { + Parameter commonParam = ModelUtil.dereference(scannerContext.getOpenApi(), oaiParam); + oaiParam.setName(commonParam.getName()); + oaiParam.setIn(commonParam.getIn()); + oaiParam.setStyle(commonParam.getStyle()); + } + readParameter(new ParameterContextKey(oaiParam), oaiParam, null, @@ -1363,6 +1374,10 @@ protected void readParameter(ParameterContextKey key, if (addParam) { params.put(new ParameterContextKey(context), context); } + + if (preferredOrder != null) { + preferredOrder.add(context.oaiParam); + } } /** diff --git a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java index c1f2b41f9..337467fd5 100644 --- a/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java +++ b/extension-jaxrs/src/test/java/io/smallrye/openapi/runtime/scanner/ParameterScanTests.java @@ -442,4 +442,10 @@ void testJakartaGenericTypeVariableResource() throws IOException, JSONException test.io.smallrye.openapi.runtime.scanner.jakarta.BaseGenericResource.GenericBean.class, test.io.smallrye.openapi.runtime.scanner.jakarta.IntegerStringUUIDResource.class); } + + @Test + void testPreferredParameterOrderWithAnnotation() throws IOException, JSONException { + test("params.annotation-preferred-order.json", + test.io.smallrye.openapi.runtime.scanner.jakarta.ParameterOrderResource.CLASSES); + } } diff --git a/extension-jaxrs/src/test/java/test/io/smallrye/openapi/runtime/scanner/jakarta/ParameterOrderResource.java b/extension-jaxrs/src/test/java/test/io/smallrye/openapi/runtime/scanner/jakarta/ParameterOrderResource.java new file mode 100644 index 000000000..2b7db26f6 --- /dev/null +++ b/extension-jaxrs/src/test/java/test/io/smallrye/openapi/runtime/scanner/jakarta/ParameterOrderResource.java @@ -0,0 +1,65 @@ +package test.io.smallrye.openapi.runtime.scanner.jakarta; + +import java.util.List; + +import jakarta.json.JsonObject; +import jakarta.ws.rs.GET; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.QueryParam; +import jakarta.ws.rs.core.Application; + +import org.eclipse.microprofile.openapi.annotations.Components; +import org.eclipse.microprofile.openapi.annotations.OpenAPIDefinition; +import org.eclipse.microprofile.openapi.annotations.enums.ParameterIn; +import org.eclipse.microprofile.openapi.annotations.enums.SchemaType; +import org.eclipse.microprofile.openapi.annotations.info.Info; +import org.eclipse.microprofile.openapi.annotations.media.Schema; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameter; +import org.eclipse.microprofile.openapi.annotations.parameters.Parameters; + +public class ParameterOrderResource { + + public static final Class[] CLASSES = { + Application1.class, + Resource1.class + }; + + @OpenAPIDefinition(info = @Info(title = "Parameter Order", version = "1.0.0"), components = @Components(parameters = { + @Parameter(name = "namespace", schema = @Schema(type = SchemaType.STRING)), + @Parameter(name = "collection", schema = @Schema(type = SchemaType.STRING)), + @Parameter(name = "where", schema = @Schema(type = SchemaType.OBJECT)), + @Parameter(name = "fields", schema = @Schema(implementation = String[].class)), + @Parameter(name = "page-state", schema = @Schema(type = SchemaType.BOOLEAN)), + @Parameter(name = "profile", schema = @Schema(type = SchemaType.BOOLEAN)), + @Parameter(name = "raw", schema = @Schema(type = SchemaType.BOOLEAN)) + })) + static class Application1 extends Application { + } + + @Path("/1") + static class Resource1 { + @Parameters(value = { + @Parameter(ref = "namespace"), + @Parameter(name = "collection", ref = "collection"), + @Parameter(name = "where", ref = "where"), + @Parameter(ref = "fields"), + @Parameter(name = "page-size", in = ParameterIn.QUERY, description = "The max number of results to return.", schema = @Schema(implementation = Integer.class, defaultValue = "3", minimum = "1", maximum = "20")), + @Parameter(name = "page-state", ref = "page-state"), + @Parameter(name = "profile", ref = "profile"), + @Parameter(name = "raw", ref = "raw"), + }) + @GET + public String get( + // Order purposefully different than parameters listed in `@Parameters` on method + @QueryParam("raw") boolean raw, + @QueryParam("collection") String collection, + @QueryParam("fields") List fields, + @QueryParam("namespace") String namespace, + @QueryParam("page-size") int pageSize, + @QueryParam("page-state") boolean pageState, + @QueryParam("profile") boolean profile, + @QueryParam("where") JsonObject where) { + return null; + } + } +} diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.annotation-preferred-order.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.annotation-preferred-order.json new file mode 100644 index 000000000..2f666257b --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.annotation-preferred-order.json @@ -0,0 +1,109 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Parameter Order", + "version": "1.0.0" + }, + "paths": { + "/1": { + "get": { + "parameters": [ + { + "$ref": "#/components/parameters/namespace" + }, + { + "$ref": "#/components/parameters/collection" + }, + { + "$ref": "#/components/parameters/where" + }, + { + "$ref": "#/components/parameters/fields" + }, + { + "name": "page-size", + "in": "query", + "description": "The max number of results to return.", + "schema": { + "format": "int32", + "default": 3, + "maximum": 20, + "minimum": 1, + "type": "integer" + } + }, + { + "$ref": "#/components/parameters/page-state" + }, + { + "$ref": "#/components/parameters/profile" + }, + { + "$ref": "#/components/parameters/raw" + } + ], + "responses": { + "200": { + "description": "OK", + "content": { + "*/*": { + "schema": { + "type": "string" + } + } + } + } + } + } + } + }, + "components": { + "parameters": { + "collection": { + "name": "collection", + "schema": { + "type": "string" + } + }, + "fields": { + "name": "fields", + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + }, + "namespace": { + "name": "namespace", + "schema": { + "type": "string" + } + }, + "page-state": { + "name": "page-state", + "schema": { + "type": "boolean" + } + }, + "profile": { + "name": "profile", + "schema": { + "type": "boolean" + } + }, + "raw": { + "name": "raw", + "schema": { + "type": "boolean" + } + }, + "where": { + "name": "where", + "schema": { + "type": "object" + } + } + } + } +} diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.method-target-nojaxrs.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.method-target-nojaxrs.json index 5fcfcc4ee..58ddeb7ae 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.method-target-nojaxrs.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.method-target-nojaxrs.json @@ -6,54 +6,45 @@ "summary": "Return all policies for a given account", "parameters": [ { - "name": "filter:op[description]", + "name": "offset", "in": "query", - "description": "Operations used with the filter", + "description": "Page number, starts 0, if not specified uses 0.", "schema": { - "default": "equal", - "enum": [ - "equal", - "like", - "ilike", - "not_equal" - ], - "type": "string" + "type": "integer" } }, { - "name": "filter:op[name]", + "name": "limit", "in": "query", - "description": "Operations used with the filter", + "description": "Number of items per page, if not specified uses 10. NO_LIMIT can be used to specify an unlimited page, when specified it ignores the offset", "schema": { - "default": "equal", - "enum": [ - "equal", - "like", - "ilike", - "not_equal" - ], - "type": "string" + "type": "integer" } }, { - "name": "filter[description]", + "name": "sortColumn", "in": "query", - "description": "Filtering policies by the description depending on the Filter operator used.", + "description": "Column to sort the results by", "schema": { + "enum": [ + "name", + "description", + "is_enabled", + "mtime" + ], "type": "string" } }, { - "name": "filter[is_enabled]", + "name": "sortDirection", "in": "query", - "description": "Filtering policies by the is_enabled field.Defaults to true if no operand is given.", + "description": "Sort direction used", "schema": { - "default": true, "enum": [ - true, - false + "asc", + "desc" ], - "type": "boolean" + "type": "string" } }, { @@ -65,45 +56,54 @@ } }, { - "name": "limit", + "name": "filter:op[name]", "in": "query", - "description": "Number of items per page, if not specified uses 10. NO_LIMIT can be used to specify an unlimited page, when specified it ignores the offset", + "description": "Operations used with the filter", "schema": { - "type": "integer" + "default": "equal", + "enum": [ + "equal", + "like", + "ilike", + "not_equal" + ], + "type": "string" } }, { - "name": "offset", + "name": "filter[description]", "in": "query", - "description": "Page number, starts 0, if not specified uses 0.", + "description": "Filtering policies by the description depending on the Filter operator used.", "schema": { - "type": "integer" + "type": "string" } }, { - "name": "sortColumn", + "name": "filter:op[description]", "in": "query", - "description": "Column to sort the results by", + "description": "Operations used with the filter", "schema": { + "default": "equal", "enum": [ - "name", - "description", - "is_enabled", - "mtime" + "equal", + "like", + "ilike", + "not_equal" ], "type": "string" } }, { - "name": "sortDirection", + "name": "filter[is_enabled]", "in": "query", - "description": "Sort direction used", + "description": "Filtering policies by the is_enabled field.Defaults to true if no operand is given.", "schema": { + "default": true, "enum": [ - "asc", - "desc" + true, + false ], - "type": "string" + "type": "boolean" } }, { diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-on-method.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-on-method.json index 7f4921a00..1ca86d4d7 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-on-method.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-on-method.json @@ -5,20 +5,20 @@ "get": { "parameters": [ { - "name": "id", - "in": "path", + "name": "X-Custom-Header", + "in": "header", "required": true, "schema": { - "type": "string", - "default": "000" + "type": "string" } }, { - "name": "X-Custom-Header", - "in": "header", + "name": "id", + "in": "path", "required": true, "schema": { - "type": "string" + "type": "string", + "default": "000" } } ], diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json index d06be7169..5142fc0c2 100644 --- a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.parameter-ref-property.json @@ -54,9 +54,6 @@ "type": "string" } }, - { - "$ref": "#/components/parameters/pathParam2" - }, { "$ref": "#/components/parameters/queryParam1" }