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 04544dd1b..4fc5d6c7a 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 @@ -49,6 +49,7 @@ import io.smallrye.openapi.api.util.MergeUtil; import io.smallrye.openapi.internal.models.media.SchemaSupport; +import io.smallrye.openapi.model.BaseModel; import io.smallrye.openapi.model.Extensions; import io.smallrye.openapi.runtime.io.Names; import io.smallrye.openapi.runtime.io.schema.SchemaConstant; @@ -688,7 +689,12 @@ void mapParameterStyle(Parameter param, ParameterContext context) { } void mapParameterSchema(Parameter param, ParameterContext context) { - if (ModelUtil.parameterHasSchema(param) || context.targetType == null) { + if (context.targetType == null) { + return; + } + + if (ModelUtil.parameterHasSchema(param)) { + mergeParameterTypeSchema(param, context); return; } @@ -720,6 +726,43 @@ void mapParameterSchema(Parameter param, ParameterContext context) { ModelUtil.setParameterSchema(param, schema); } + private void mergeParameterTypeSchema(Parameter param, ParameterContext context) { + Map paramSchemas = ModelUtil.getParameterMediaTypeSchemas(param); + Map scannableParamSchemas = new HashMap<>(paramSchemas.size()); + + for (var entry : paramSchemas.entrySet()) { + if (entry.getValue().getRef() == null && entry.getValue().getType() == null) { + scannableParamSchemas.put(entry.getKey(), entry.getValue()); + } + } + + if (!scannableParamSchemas.isEmpty()) { + Schema typeSchema = SchemaFactory.typeToSchema(scannerContext, context.targetType, null); + + if (typeSchema == null) { + // Hidden schema + return; + } + + for (var entry : scannableParamSchemas.entrySet()) { + var paramSchema = entry.getValue(); + + if (typeSchema.getRef() != null) { + paramSchema.setRef(typeSchema.getRef()); + } else { + /* + * The type schema must be cloned before using it as the target of an object merge. + * Merging here will overlay the parameter schema's attributes over the type schema's + * attributes before replacing the schema in the parameter model. + */ + typeSchema = BaseModel.deepCopy(typeSchema, Schema.class); + paramSchema = mergeObjects(typeSchema, paramSchema); + ModelUtil.setParameterMediaTypeSchema(param, entry.getKey(), paramSchema); + } + } + } + } + void mapParameterExtensions(Parameter param, ParameterContext context) { if (param.getExtensions() == null) { param.setExtensions(scannerContext.io().extensionIO().readMap(context.target)); diff --git a/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java b/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java index 64bcf6a8a..18b565439 100644 --- a/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java +++ b/core/src/main/java/io/smallrye/openapi/runtime/util/ModelUtil.java @@ -1,8 +1,9 @@ package io.smallrye.openapi.runtime.util; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; +import java.util.HashMap; +import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.function.Supplier; @@ -219,6 +220,59 @@ public static boolean parameterHasSchema(Parameter parameter) { return false; } + /** + * Returns the map of {@link Schema}s defined for the given + * {@link Parameter} with the schema's content type as the key (possibly + * null for the {@linkplain Parameter#getSchema() schema defined directly on + * the parameter}. + * + * A schema can be defined either via the parameter's "schema" property, or + * any "content.*.schema" property. + * + * @param parameter + * Parameter + * @return map of schemas by content type, never null + */ + public static Map getParameterMediaTypeSchemas(Parameter parameter) { + Map mediaTypeSchemas; + + if (parameter.getSchema() != null) { + mediaTypeSchemas = new HashMap<>(1); + mediaTypeSchemas.put(null, parameter.getSchema()); + } else { + Map mediaTypes = getMediaTypesOrEmpty(parameter.getContent()); + mediaTypeSchemas = new LinkedHashMap<>(mediaTypes.size()); + + for (var entry : mediaTypes.entrySet()) { + mediaTypeSchemas.put(entry.getKey(), entry.getValue().getSchema()); + } + } + + return mediaTypeSchemas; + } + + /** + * Set a schema to the parameter for the given content type. When the + * content type is null, the schema is set + * {@linkplain Parameter#setSchema(Schema) directly on the schema}. + * Otherwise, the schema will be set in the parameter's content on a new or + * existing {@link MediaType} using content type as its key. + * + * @param parameter + * The parameter + * @param contentType + * The content type to which the schema applies (null allowed) + * @param schema + * The schema + */ + public static void setParameterMediaTypeSchema(Parameter parameter, String contentType, Schema schema) { + if (contentType == null) { + parameter.setSchema(schema); + } else { + parameter.setContent(putMediaTypeSchema(parameter.getContent(), contentType, schema)); + } + } + /** * Returns the list of {@link Schema}s defined for the given {@link Parameter}. * A schema can be defined either via the parameter's "schema" property, or any @@ -228,20 +282,7 @@ public static boolean parameterHasSchema(Parameter parameter) { * @return list of schemas, never null */ public static List getParameterSchemas(Parameter parameter) { - if (parameter.getSchema() != null) { - return Arrays.asList(parameter.getSchema()); - } - Map mediaTypes = getMediaTypesOrEmpty(parameter.getContent()); - if (!mediaTypes.isEmpty()) { - List schemas = new ArrayList<>(mediaTypes.size()); - - for (MediaType mediaType : mediaTypes.values()) { - if (mediaType.getSchema() != null) { - schemas.add(mediaType.getSchema()); - } - } - } - return Collections.emptyList(); + return new ArrayList<>(getParameterMediaTypeSchemas(parameter).values()); } /** @@ -341,6 +382,23 @@ static Map getMediaTypesOrEmpty(Content content) { return Collections.emptyMap(); } + static Content putMediaTypeSchema(Content content, String contentType, Schema schema) { + if (content == null) { + content = OASFactory.createContent(); + } + + MediaType mediaType = content.getMediaType(contentType); + + if (mediaType == null) { + mediaType = OASFactory.createMediaType(); + content.addMediaType(contentType, mediaType); + } + + mediaType.setSchema(schema); + + return content; + } + /** * Returns the name component of the ref. * 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 5d90c6f65..2290c09fe 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 @@ -163,6 +163,59 @@ void testJakartaParameterInBeanFromField() throws IOException, JSONException { test.io.smallrye.openapi.runtime.scanner.Widget.class); } + @Test + void testBeanParamEnumDefaultValue() throws IOException, JSONException { + class QueryOptions { + @jakarta.ws.rs.QueryParam("sortOrder") + @Parameter(description = "Sort direction", schema = @Schema(defaultValue = "ASC")) + SortOrder sortOrder; + } + + @Schema(name = "filter-option-a", ref = "http://example.com/schemas/filter-schema-a.json") + class FilterOptionA { + } + + class FilterOptionB { + } + + @Schema(hidden = true) + class SecretIdentifier { + } + + @jakarta.ws.rs.Path("/beanparam-enum-default") + class ListResource { + @jakarta.ws.rs.GET + @jakarta.ws.rs.Produces(jakarta.ws.rs.core.MediaType.APPLICATION_JSON) + public String list( + @jakarta.ws.rs.BeanParam QueryOptions options, + @jakarta.ws.rs.QueryParam("altSortOrder") @Parameter(description = "Alternate Sort direction", schema = @Schema(implementation = String.class, enumeration = { + "asc", "desc" }, defaultValue = "desc")) SortOrder sortOrder, + @jakarta.ws.rs.QueryParam("filter1") @Parameter(description = "Filter 1", schema = @Schema(description = "Standard filter schema")) FilterOptionA filter1, + @jakarta.ws.rs.QueryParam("filter2") @Parameter(description = "Filter 2", schema = @Schema(ref = "http://example.com/schemas/filter-schema-b.json", description = "Another filter")) FilterOptionA filter2, + @jakarta.ws.rs.QueryParam("filter3") @Parameter(description = "Filter 3 with multiple variants", content = { + @Content(mediaType = "text/plain", schema = @Schema(description = "Third filter in plain text format")), + @Content(mediaType = "application/json", schema = @Schema(description = "Third filter in JSON format")), + }) FilterOptionA multivariantFilter3, + @jakarta.ws.rs.QueryParam("request-id") @Parameter(description = "Identifier for the request", schema = @Schema(format = "custom-id")) Integer requestId, + @jakarta.ws.rs.QueryParam("secret-id") @Parameter(description = "Secret identifier for the request", schema = @Schema(format = "secret-id", description = "The schema for this parameter is not disclosed")) SecretIdentifier secretId, + @jakarta.ws.rs.QueryParam("variable-id") @Parameter(description = "Identifier with multiple variants", content = { + @Content(mediaType = "text/x-custom-type1", schema = @Schema(minimum = "1", maximum = "50")), + @Content(mediaType = "text/x-custom-type2", schema = @Schema(minimum = "51", maximum = "99")), + }) Long multivariantIdentifier) { + return "ok"; + } + } + + test("params.beanparam-enum-default-value.json", SortOrder.class, FilterOptionA.class, FilterOptionB.class, + SecretIdentifier.class, + QueryOptions.class, ListResource.class); + } + + enum SortOrder { + ASC, + DESC + } + @Test void testJavaxParameterInBeanFromSetter() throws IOException, JSONException { test("params.parameter-in-bean-from-setter.json", diff --git a/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json new file mode 100644 index 000000000..972429039 --- /dev/null +++ b/extension-jaxrs/src/test/resources/io/smallrye/openapi/runtime/scanner/params.beanparam-enum-default-value.json @@ -0,0 +1,122 @@ +{ + "openapi" : "3.1.0", + "components" : { + "schemas" : { + "SortOrder" : { + "type" : "string", + "enum" : [ "ASC", "DESC" ] + }, + "filter-option-a" : { + "$ref" : "http://example.com/schemas/filter-schema-a.json" + } + } + }, + "paths" : { + "/beanparam-enum-default" : { + "get" : { + "parameters" : [ { + "description" : "Alternate Sort direction", + "schema" : { + "type" : "string", + "default" : "desc", + "enum" : [ "asc", "desc" ] + }, + "name" : "altSortOrder", + "in" : "query" + }, { + "description" : "Filter 1", + "schema" : { + "description" : "Standard filter schema", + "$ref" : "#/components/schemas/filter-option-a" + }, + "name" : "filter1", + "in" : "query" + }, { + "description" : "Filter 2", + "schema" : { + "$ref" : "http://example.com/schemas/filter-schema-b.json", + "description" : "Another filter" + }, + "name" : "filter2", + "in" : "query" + }, { + "content" : { + "text/plain" : { + "schema" : { + "description" : "Third filter in plain text format", + "$ref" : "#/components/schemas/filter-option-a" + } + }, + "application/json" : { + "schema" : { + "description" : "Third filter in JSON format", + "$ref" : "#/components/schemas/filter-option-a" + } + } + }, + "description" : "Filter 3 with multiple variants", + "name" : "filter3", + "in" : "query" + }, { + "description" : "Identifier for the request", + "schema" : { + "type" : "integer", + "format" : "custom-id" + }, + "name" : "request-id", + "in" : "query" + }, { + "description" : "Secret identifier for the request", + "schema" : { + "description" : "The schema for this parameter is not disclosed", + "format" : "secret-id" + }, + "name" : "secret-id", + "in" : "query" + }, { + "description" : "Sort direction", + "schema" : { + "default" : "ASC", + "$ref" : "#/components/schemas/SortOrder" + }, + "name" : "sortOrder", + "in" : "query" + }, { + "content" : { + "text/x-custom-type1" : { + "schema" : { + "type" : "integer", + "format" : "int64", + "maximum" : 50, + "minimum" : 1 + } + }, + "text/x-custom-type2" : { + "schema" : { + "type" : "integer", + "format" : "int64", + "maximum" : 99, + "minimum" : 51 + } + } + }, + "description" : "Identifier with multiple variants", + "name" : "variable-id", + "in" : "query" + } ], + "responses" : { + "200" : { + "description" : "OK", + "content" : { + "application/json" : { + "schema" : { + "type" : "string" + } + } + } + } + } + } + } + } +}