diff --git a/core/src/main/java/io/apicurio/hub/api/codegen/OpenApi2JaxRs.java b/core/src/main/java/io/apicurio/hub/api/codegen/OpenApi2JaxRs.java index fd32c230..09209439 100755 --- a/core/src/main/java/io/apicurio/hub/api/codegen/OpenApi2JaxRs.java +++ b/core/src/main/java/io/apicurio/hub/api/codegen/OpenApi2JaxRs.java @@ -111,6 +111,7 @@ public class OpenApi2JaxRs { static final Map> TYPE_CACHE = new HashMap<>(); static final String OPENAPI_OPERATION_ANNOTATION = "org.eclipse.microprofile.openapi.annotations.Operation"; + private static final String MEDIA_TYPE_CONSTANT_PREFIX = "MediaType."; protected static ObjectMapper mapper = new ObjectMapper(); protected static Charset utf8 = StandardCharsets.UTF_8; @@ -603,9 +604,14 @@ protected String generateJavaInterface(CodegenInfo info, CodegenJavaInterface in Optional.ofNullable(methodInfo.getConsumes()) .filter(Predicate.not(Collection::isEmpty)) - .map(OpenApi2JaxRs::toStringArrayLiteral) - .ifPresent(consumes -> - operationMethod.addAnnotation(String.format("%s.ws.rs.Consumes", topLevelPackage)).setLiteralValue(consumes)); + .ifPresent(consumesSet -> { + // Add MediaType import if any consumes value uses MediaType constants + if (consumesSet.stream().anyMatch(consume -> consume.contains(MEDIA_TYPE_CONSTANT_PREFIX))) { + resourceInterface.addImport(String.format("%s.ws.rs.core.MediaType", topLevelPackage)); + } + String consumesLiteral = toStringArrayLiteral(consumesSet); + operationMethod.addAnnotation(String.format("%s.ws.rs.Consumes", topLevelPackage)).setLiteralValue(consumesLiteral); + }); final boolean reactive; @@ -638,6 +644,10 @@ protected String generateJavaInterface(CodegenInfo info, CodegenJavaInterface in if (arg.getIn().equals("body")) { // Swagger 2.0? defaultParamType = InputStream.class.getName(); + } else if (arg.getIn().equals("form") + && arg.getType() != null + && !arg.getType().isEmpty()) { + defaultParamType = arg.getType().get(0); } Type paramType = generateTypeName(arg, arg.getRequired(), defaultParamType); @@ -667,6 +677,11 @@ protected String generateJavaInterface(CodegenInfo info, CodegenJavaInterface in param.addAnnotation(String.format("%s.ws.rs.CookieParam", topLevelPackage)) .setStringValue(arg.getName()); break; + case "form": + param.addAnnotation("org.jboss.resteasy.annotations.providers.multipart.RestForm") + .setStringValue(arg.getName()); + resourceInterface.addImport("org.jboss.resteasy.annotations.providers.multipart.RestForm"); + break; default: break; } @@ -876,9 +891,15 @@ protected static String toStringArrayLiteral(Set values) { StringBuilder builder = new StringBuilder(); if (values.size() == 1) { - builder.append("\""); - builder.append(values.iterator().next().replace("\"", "\\\"")); - builder.append("\""); + String value = values.iterator().next(); + if (value.startsWith(MEDIA_TYPE_CONSTANT_PREFIX)) { + // Don't quote MediaType constants + builder.append(value); + } else { + builder.append("\""); + builder.append(value.replace("\"", "\\\"")); + builder.append("\""); + } } else { builder.append("{"); boolean first = true; @@ -886,9 +907,14 @@ protected static String toStringArrayLiteral(Set values) { if (!first) { builder.append(", "); } - builder.append("\""); - builder.append(value.replace("\"", "\\\"")); - builder.append("\""); + if (value.startsWith(MEDIA_TYPE_CONSTANT_PREFIX)) { + // Don't quote MediaType constants + builder.append(value); + } else { + builder.append("\""); + builder.append(value.replace("\"", "\\\"")); + builder.append("\""); + } first = false; } builder.append("}"); diff --git a/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/MultipartFormDataRequestBodyProcessor.java b/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/MultipartFormDataRequestBodyProcessor.java new file mode 100644 index 00000000..c34fef5d --- /dev/null +++ b/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/MultipartFormDataRequestBodyProcessor.java @@ -0,0 +1,286 @@ +package io.apicurio.hub.api.codegen.jaxrs; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +import io.apicurio.datamodels.models.Document; +import io.apicurio.datamodels.models.Schema; +import io.apicurio.datamodels.models.openapi.OpenApiMediaType; +import io.apicurio.datamodels.models.openapi.v31.OpenApi31Schema; +import io.apicurio.hub.api.codegen.JaxRsProjectSettings; +import io.apicurio.hub.api.codegen.beans.CodegenJavaArgument; +import io.apicurio.hub.api.codegen.beans.CodegenJavaMethod; +import io.apicurio.hub.api.codegen.util.CodegenUtil; + +import static io.apicurio.hub.api.codegen.util.CodegenUtil.containsValue; + +/** + * Utility class for processing multipart/form-data request bodies. + * Extracts form-field parameters from OpenAPI schema properties. + * + * @author lpieprzyk + */ +public class MultipartFormDataRequestBodyProcessor { + + private static final String JAVA_LANG_STRING = "java.lang.String"; + private static final String JBOSS_FILE_UPLOAD = "org.jboss.resteasy.plugins.providers.multipart.FileUpload"; + private static final String JAVA_TIME_LOCAL_DATE = "java.time.LocalDate"; + private static final String JAVA_TIME_LOCAL_DATE_TIME = "java.time.LocalDateTime"; + private static final String JAVA_LANG_LONG = "java.lang.Long"; + private static final String JAVA_LANG_INTEGER = "java.lang.Integer"; + private static final String JAVA_LANG_DOUBLE = "java.lang.Double"; + private static final String JAVA_LANG_FLOAT = "java.lang.Float"; + private static final String JAVA_MATH_BIG_DECIMAL = "java.math.BigDecimal"; + private static final String JAVA_UTIL_LIST = "java.util.List"; + private static final String JAVA_UTIL_MAP = "java.util.Map"; + private static final String JAVA_LANG_OBJECT = "java.lang.Object"; + private static final String JAVA_LANG_BOOLEAN = "java.lang.Boolean"; + + private MultipartFormDataRequestBodyProcessor() { + // Functional class + } + + /** + * Processes a multipart/form-data media type by creating individual form field parameters + * instead of a generic body parameter. + * + * @param mediaType the OpenAPI media type to process + * @param methodTemplate the method template to modify with form field parameters + * @param settings the project settings for type mapping + * @param document the OpenAPI document for reference resolution + */ + public static void processMultipartFormData(OpenApiMediaType mediaType, + CodegenJavaMethod methodTemplate, + JaxRsProjectSettings settings, + Document document) { + if (mediaType.getSchema() != null) { + OpenApi31Schema schema = (OpenApi31Schema) mediaType.getSchema(); + if (containsValue(schema.getType(), "object")) { + Map properties = schema.getProperties(); + if (properties != null) { + methodTemplate.getConsumes().add("MediaType.MULTIPART_FORM_DATA"); + createIndividualFormFieldParams(methodTemplate, settings, document, properties, schema); + } + } + } + } + + /** + * Adds individual form field parameters to the method template based on the provided schema properties. + * + * @param methodTemplate the method template to modify with form field parameters + * @param settings the project settings used for type mapping and configuration + * @param document the OpenAPI document used for reference resolution + * @param properties a map of property names to their corresponding schema definitions + * @param schema the parent schema containing details about required fields + */ + private static void createIndividualFormFieldParams(CodegenJavaMethod methodTemplate, + JaxRsProjectSettings settings, + Document document, + Map properties, + OpenApi31Schema schema) { + for (Map.Entry property : properties.entrySet()) { + String fieldName = property.getKey(); + Schema fieldSchema = property.getValue(); + + CodegenJavaArgument cgArgument = createFormFieldArgument(fieldName, fieldSchema, schema, settings, document); + methodTemplate.getArguments().add(cgArgument); + } + } + + /** + * Creates a CodegenJavaArgument for a form field based on the schema property. + * + * @param fieldName the name of the form field + * @param fieldSchema the schema definition for the field + * @param parentSchema the parent schema containing the required field list + * @param settings the project settings for type mapping + * @param document the OpenAPI document for reference resolution + * @return the configured CodegenJavaArgument + */ + private static CodegenJavaArgument createFormFieldArgument(String fieldName, Schema fieldSchema, OpenApi31Schema parentSchema, JaxRsProjectSettings settings, Document document) { + CodegenJavaArgument cgArgument = new CodegenJavaArgument(); + cgArgument.setName(fieldName); + cgArgument.setIn("form"); + + boolean isRequired = checkIfFieldIsRequired(fieldName, parentSchema); + cgArgument.setRequired(isRequired); + + defineArgumentTypeForSchema(cgArgument, fieldSchema, settings, document); + return cgArgument; + } + + /** + * Checks whether a specified field is marked as required in the given parent schema. + * + * @param fieldName the name of the field to check + * @param parentSchema the parent schema containing the list of required fields + * @return true if the field is marked as required, false otherwise + */ + private static boolean checkIfFieldIsRequired(String fieldName, OpenApi31Schema parentSchema) { + boolean isRequired = false; + if (parentSchema != null && parentSchema.getRequired() != null) { + isRequired = parentSchema.getRequired().contains(fieldName); + } + return isRequired; + } + + /** + * Defines the appropriate type and format for the argument based on the schema definition. + * + * @param argument the argument to configure + * @param fieldSchema the schema to analyze + * @param settings the project settings for type mapping + * @param document the OpenAPI document for reference resolution + */ + private static void defineArgumentTypeForSchema(CodegenJavaArgument argument, + Schema fieldSchema, + JaxRsProjectSettings settings, + Document document) { + if (!validateSchemaAndSetDefaults(argument, fieldSchema)) { + return; + } + OpenApi31Schema oas31Schema = (OpenApi31Schema) fieldSchema; + if (resolveSchemaReference(argument, oas31Schema, settings, document)) { + return; + } + if (handleArrayType(argument, oas31Schema, settings, document)) { + return; + } + mapSchemaTypeToJavaType(argument, oas31Schema); + } + + private static boolean validateSchemaAndSetDefaults(CodegenJavaArgument argument, Schema fieldSchema) { + if (fieldSchema == null) { + argument.setType(Collections.singletonList(JAVA_LANG_STRING)); + return false; + } + if (!(fieldSchema instanceof OpenApi31Schema)) { + argument.setType(Collections.singletonList(JAVA_LANG_STRING)); + return false; + } + return true; + } + + private static boolean resolveSchemaReference(CodegenJavaArgument argument, OpenApi31Schema oas31Schema, JaxRsProjectSettings settings, Document document) { + String ref = oas31Schema.get$ref(); + if (ref != null) { + String className = CodegenUtil.schemaRefToFQCN(settings, document, ref, settings.getJavaPackage() + ".beans"); + argument.setType(Collections.singletonList(className)); + return true; + } + return false; + } + + private static boolean mapStringType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema) { + if (!containsValue(oas31Schema.getType(), "string")) { + return false; + } + String format = oas31Schema.getFormat(); + if ("binary".equals(format)) { + argument.setType(Collections.singletonList(JBOSS_FILE_UPLOAD)); + argument.setFormat("binary"); + } else if ("date".equals(format)) { + argument.setType(Collections.singletonList(JAVA_TIME_LOCAL_DATE)); + argument.setFormat("date"); + } else if ("date-time".equals(format)) { + argument.setType(Collections.singletonList(JAVA_TIME_LOCAL_DATE_TIME)); + argument.setFormat("date-time"); + } else { + argument.setType(Collections.singletonList(JAVA_LANG_STRING)); + } + return true; + } + + private static boolean mapIntegerType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema) { + if (!containsValue(oas31Schema.getType(), "integer")) { + return false; + } + String format = oas31Schema.getFormat(); + if ("int64".equals(format) || "long".equals(format)) { + argument.setType(Collections.singletonList(JAVA_LANG_LONG)); + } else { + argument.setType(Collections.singletonList(JAVA_LANG_INTEGER)); + } + if (format != null) { + argument.setFormat(format); + } + return true; + } + + private static boolean mapNumberType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema) { + if (!containsValue(oas31Schema.getType(), "number")) { + return false; + } + + String format = oas31Schema.getFormat(); + if ("double".equals(format)) { + argument.setType(Collections.singletonList(JAVA_LANG_DOUBLE)); + } else if ("float".equals(format)) { + argument.setType(Collections.singletonList(JAVA_LANG_FLOAT)); + } else { + argument.setType(Collections.singletonList(JAVA_MATH_BIG_DECIMAL)); + } + if (format != null) { + argument.setFormat(format); + } + return true; + } + + + private static boolean mapBooleanType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema) { + if (!containsValue(oas31Schema.getType(), "boolean")) { + return false; + } + argument.setType(Collections.singletonList(JAVA_LANG_BOOLEAN)); + return true; + } + + private static void mapObjectOrFallbackType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema) { + if (containsValue(oas31Schema.getType(), "object") || oas31Schema.getType() == null) { + argument.setType(Collections.singletonList(JAVA_UTIL_MAP + "<" + JAVA_LANG_STRING + ", " + JAVA_LANG_OBJECT + ">")); + } else { + // Fallback to String for unknown types + argument.setType(Collections.singletonList(JAVA_LANG_STRING)); + } + } + + private static boolean handleArrayType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema, JaxRsProjectSettings settings, Document document) { + if (!containsValue(oas31Schema.getType(), "array")) { + return false; + } + if (oas31Schema.getItems() != null) { + CodegenJavaArgument itemArgument = new CodegenJavaArgument(); + defineArgumentTypeForSchema(itemArgument, oas31Schema.getItems(), settings, document); + + List itemTypes = itemArgument.getType(); + if (itemTypes != null && !itemTypes.isEmpty()) { + String itemType = itemTypes.get(0); + argument.setType(Collections.singletonList(JAVA_UTIL_LIST + "<" + itemType + ">")); + } else { + argument.setType(Collections.singletonList(JAVA_UTIL_LIST + "<" + JAVA_LANG_STRING + ">")); + } + } else { + argument.setType(Collections.singletonList(JAVA_UTIL_LIST + "<" + JAVA_LANG_STRING + ">")); + } + return true; + } + + private static void mapSchemaTypeToJavaType(CodegenJavaArgument argument, OpenApi31Schema oas31Schema) { + if (mapStringType(argument, oas31Schema)) { + return; + } + if (mapIntegerType(argument, oas31Schema)) { + return; + } + if (mapNumberType(argument, oas31Schema)) { + return; + } + if (mapBooleanType(argument, oas31Schema)) { + return; + } + mapObjectOrFallbackType(argument, oas31Schema); + } + +} \ No newline at end of file diff --git a/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/OpenApi2CodegenVisitor.java b/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/OpenApi2CodegenVisitor.java index ab878583..e0fd032d 100755 --- a/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/OpenApi2CodegenVisitor.java +++ b/core/src/main/java/io/apicurio/hub/api/codegen/jaxrs/OpenApi2CodegenVisitor.java @@ -16,6 +16,7 @@ package io.apicurio.hub.api.codegen.jaxrs; + import static io.apicurio.hub.api.codegen.util.CodegenUtil.containsValue; import static io.apicurio.hub.api.codegen.util.CodegenUtil.toStringList; @@ -35,10 +36,8 @@ import java.util.function.Supplier; import java.util.stream.Collectors; import java.util.stream.Stream; - import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; - import io.apicurio.datamodels.Library; import io.apicurio.datamodels.models.Document; import io.apicurio.datamodels.models.Extensible; @@ -202,6 +201,8 @@ public void visitOperation(Operation node) { } method.setAsync(async); + checkForDuplicateMethodNames(method); + this._currentMethods = new ArrayList<>(); this._currentMethods.add(method); this._currentInterface.getMethods().add(method); @@ -217,6 +218,30 @@ public void visitOperation(Operation node) { this._processPathItemParams = false; } + /** + * Checks for duplicate method names within the current interface and throws a + * {@link IllegalStateException} if a conflict is detected. This can occur when multiple + * operations resolve to the same method name, typically due to missing or identical + * {@code operationId} values in the OpenAPI specification. + * + * @param method the new method to check against existing methods in the current interface + * @throws IllegalStateException if a method with the same name already exists in the interface + */ + private void checkForDuplicateMethodNames(CodegenJavaMethod method) { + String newMethodName = method.getName(); + for (CodegenJavaMethod existingMethod : this._currentInterface.getMethods()) { + if (existingMethod.getName().equals(newMethodName)) { + throw new IllegalStateException( + "Duplicate method name '" + newMethodName + "' detected in interface '" + + this._currentInterface.getName() + "'. " + + "Operation at path '" + currentPath + "' (HTTP " + method.getMethod() + + ") resolves to the same method name as an existing operation. " + + "Consider adding a unique 'operationId' to one of the conflicting operations." + ); + } + } + } + /** * @see io.apicurio.datamodels.models.openapi.v31.visitors.OpenApi31VisitorAdapter#visitParameter(io.apicurio.datamodels.models.Parameter) */ @@ -277,11 +302,24 @@ public void visitRequestBody(OpenApiRequestBody node) { content = new HashMap<>(); } + // Check for multipart/form-data first and handle it specially + for (Map.Entry entry : content.entrySet()) { + String name = entry.getKey(); + OpenApiMediaType mediaType = entry.getValue(); + + if ("multipart/form-data".equals(name)) { + // Handle multipart form data specially + MultipartFormDataRequestBodyProcessor.processMultipartFormData(mediaType, this._currentMethods.get(0), this.settings, (Document) mediaType.getSchema().root()); + return; // Skip generic body parameter creation + } + } + Map> allReturnTypes = new LinkedHashMap<>(); if (!content.isEmpty()) { content.entrySet().forEach(entry -> { String name = entry.getKey(); OpenApiMediaType mediaType = entry.getValue(); + CodegenJavaReturn cgReturn = new CodegenJavaReturn(); if (mediaType.getSchema() != null) { setSchemaProperties(cgReturn, (OpenApi31Schema) mediaType.getSchema()); @@ -506,6 +544,20 @@ private String methodName(OpenApiOperation operation) { return decapitalize(builder.toString()); } } + // Try to extract the method name from the path segment + if (currentPath != null && currentPath.length() > 0) { + String[] pathSegments = currentPath.split("/"); + if (pathSegments.length > 0) { + String lastSegment = pathSegments[pathSegments.length - 1]; + if (lastSegment != null && lastSegment.trim().length() > 0) { + // Remove path parameters (e.g., {id}) and sanitize + String sanitized = lastSegment.replaceAll("\\{[^}]*\\}", "").replaceAll("\\W", ""); + if (sanitized.length() > 0) { + return sanitized; + } + } + } + } return "generatedMethod" + this._methodCounter++; } diff --git a/core/src/test/java/io/apicurio/hub/api/codegen/OpenApi2JaxRsTest.java b/core/src/test/java/io/apicurio/hub/api/codegen/OpenApi2JaxRsTest.java index d1437ad2..4f7c32e3 100755 --- a/core/src/test/java/io/apicurio/hub/api/codegen/OpenApi2JaxRsTest.java +++ b/core/src/test/java/io/apicurio/hub/api/codegen/OpenApi2JaxRsTest.java @@ -16,8 +16,15 @@ package io.apicurio.hub.api.codegen; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; +import org.apache.commons.io.IOUtils; +import org.junit.Assert; import org.junit.Test; /** @@ -223,6 +230,81 @@ public void testEnumWithSpecialChars() throws IOException { doFullTest("OpenApi2JaxRsTest/enum-with-special-chars.json", UpdateOnly.no, Reactive.no, "_expected-enum-with-special-chars/generated-api", false); } + /** + * Test method for {@link io.apicurio.hub.api.codegen.OpenApi2JaxRs#generate()}. + */ + @Test + public void testGenerateMultipartFormContent() throws IOException { + doFullTest("OpenApi2JaxRsTest/multipart-form-content.json", UpdateOnly.no, Reactive.no, + "_expected-multipart-form-content.generated-api", false); + } + + /** + * Test method for comprehensive multipart form data type support. + */ + @Test + public void testGenerateMultipartComprehensive() throws IOException { + doFullTest("OpenApi2JaxRsTest/multipart-comprehensive.json", UpdateOnly.no, Reactive.no, + "_expected-multipart-comprehensive.generated-api", false); + } + + @Test + public void testMultipartArrayOnlyRef() throws IOException { + doFullTest("OpenApi2JaxRsTest/multipart-array-only-ref.json", UpdateOnly.no, Reactive.no, + "_expected-multipart-array-only-ref/generated-api", false); + } + + @Test + public void testMultipartOpenapiV300() throws IOException { + doFullTest("OpenApi2JaxRsTest/multipart-openapi-v-3-0-0.json", UpdateOnly.no, Reactive.no, + "_expected-multipart-openapi-v-3-0-0/generated-api", false); + } + + @Test + public void testMultipartEdgeCases() throws IOException { + doFullTest("OpenApi2JaxRsTest/multipart-edge-cases.json", UpdateOnly.no, Reactive.no, + "_expected-multipart-edge-cases/generated-api", false); + } + + /** + * Test that the generator detects and reports duplicate method names with a error. + * Two operations on the same interface resolve to method name "items": + * - GET /products (operationId: "items") + * - GET /products/items (no operationId, falls back to last path segment "items") + */ + @Test + public void testDuplicateMethodNameDetection() throws IOException { + JaxRsProjectSettings settings = new JaxRsProjectSettings(); + settings.codeOnly = false; + settings.reactive = false; + settings.artifactId = "generated-api"; + settings.groupId = "org.example.api"; + settings.javaPackage = "org.example.api"; + + OpenApi2JaxRs generator = new OpenApi2JaxRs(); + generator.setSettings(settings); + generator.setUpdateOnly(false); + generator.setOpenApiDocument(getClass().getClassLoader().getResource( + "OpenApi2JaxRsTest/duplicate-method-name.json")); + + ByteArrayOutputStream outputStream = generator.generate(); + + String errorContent = null; + try (ZipInputStream zis = new ZipInputStream(new ByteArrayInputStream(outputStream.toByteArray()))) { + ZipEntry entry; + while ((entry = zis.getNextEntry()) != null) { + if ("PROJECT_GENERATION_FAILED.txt".equals(entry.getName())) { + errorContent = IOUtils.toString(zis, StandardCharsets.UTF_8); + break; + } + } + } + Assert.assertNotNull( errorContent); + Assert.assertTrue(errorContent.contains("Duplicate method name")); + Assert.assertTrue(errorContent.contains("items")); + Assert.assertTrue( errorContent.contains("ProductsResource")); + } + /** * Shared test method. * @param apiDef diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-issue885Api-full/generated-api/src/main/java/org/example/api/TestResource.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-issue885Api-full/generated-api/src/main/java/org/example/api/TestResource.java index 5da50ce3..854d1be2 100755 --- a/core/src/test/resources/OpenApi2JaxRsTest/_expected-issue885Api-full/generated-api/src/main/java/org/example/api/TestResource.java +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-issue885Api-full/generated-api/src/main/java/org/example/api/TestResource.java @@ -18,5 +18,5 @@ public interface TestResource { @Operation(description = "\u6D4B\u8BD5\u4E2D\u6587\u5B57\u7B26", summary = "\u6D4B\u8BD5\u4E2D\u6587\u5B57\u7B26") @Path("/chinese/character") @GET - void generatedMethod1(); + void character(); } diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/pom.xml b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/pom.xml new file mode 100644 index 00000000..7439091b --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + org.example.api + generated-api + 1.0.0 + jar + Array-Only Ref Multipart API + A generated project with JAX-RS and Microprofile OpenAPI features enabled. + + + 11 + 11 + false + UTF-8 + + 3.1.0 + 3.0.2 + 4.0.1 + 2.15.1 + 4.0.2 + + + + + + com.fasterxml.jackson.core + jackson-annotations + ${version.com.fasterxml.jackson} + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs-jakarta.ws.rs-api} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.jakarta.enterprise-jakarta.enterprise.cdi-api} + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation-jakarta.validation-api} + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${version.org.eclipse.microprofile.openapi-microprofile-openapi-api} + + + diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/JaxRsApplication.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/JaxRsApplication.java new file mode 100644 index 00000000..6ade2169 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/JaxRsApplication.java @@ -0,0 +1,13 @@ +package org.example.api; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * The JAX-RS application. + */ +@ApplicationScoped +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/UploadResource.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/UploadResource.java new file mode 100644 index 00000000..93f1d2a1 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/UploadResource.java @@ -0,0 +1,24 @@ +package org.example.api; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.util.List; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.example.api.beans.TestObject; +import org.jboss.resteasy.annotations.providers.multipart.RestForm; + +/** + * A JAX-RS interface. An implementation of this interface must be provided. + */ +@Path("/upload") +public interface UploadResource { + @Operation(summary = "Upload with array-only ref field") + @Path("/items") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + void uploadWithArrayonlyRefField(@RestForm("items") List items, + @RestForm("amounts") List amounts); +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/beans/TestObject.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/beans/TestObject.java new file mode 100644 index 00000000..ea0a5171 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/java/org/example/api/beans/TestObject.java @@ -0,0 +1,42 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "value" +}) +@Generated("jsonschema2pojo") +public class TestObject { + + @JsonProperty("name") + private String name; + @JsonProperty("value") + private Integer value; + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + @JsonProperty("value") + public Integer getValue() { + return value; + } + + @JsonProperty("value") + public void setValue(Integer value) { + this.value = value; + } + +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/resources/META-INF/openapi.json b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/resources/META-INF/openapi.json new file mode 100644 index 00000000..4e55d69f --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-array-only-ref/generated-api/src/main/resources/META-INF/openapi.json @@ -0,0 +1,61 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Array-Only Ref Multipart API", + "version": "1.0.0" + }, + "paths": { + "/upload/items": { + "post": { + "summary": "Upload with array-only ref field", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestObject" + }, + "description": "Array of object references (no direct ref)" + }, + "amounts": { + "type": "array", + "items": { + "type": "number", + "format": "decimal" + }, + "description": "Array of BigDecimal values" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "TestObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/pom.xml b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/pom.xml new file mode 100644 index 00000000..fcc657e8 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + org.example.api + generated-api + 1.0.0 + jar + Comprehensive Multipart API + A generated project with JAX-RS and Microprofile OpenAPI features enabled. + + + 11 + 11 + false + UTF-8 + + 3.1.0 + 3.0.2 + 4.0.1 + 2.15.1 + 4.0.2 + + + + + + com.fasterxml.jackson.core + jackson-annotations + ${version.com.fasterxml.jackson} + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs-jakarta.ws.rs-api} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.jakarta.enterprise-jakarta.enterprise.cdi-api} + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation-jakarta.validation-api} + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${version.org.eclipse.microprofile.openapi-microprofile-openapi-api} + + + diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/JaxRsApplication.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/JaxRsApplication.java new file mode 100644 index 00000000..6ade2169 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/JaxRsApplication.java @@ -0,0 +1,13 @@ +package org.example.api; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * The JAX-RS application. + */ +@ApplicationScoped +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/UploadResource.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/UploadResource.java new file mode 100644 index 00000000..621ed04b --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/UploadResource.java @@ -0,0 +1,49 @@ +package org.example.api; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.List; +import java.util.Map; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.example.api.beans.TestObject; +import org.example.api.beans.UploadResponse; +import org.jboss.resteasy.annotations.providers.multipart.RestForm; +import org.jboss.resteasy.plugins.providers.multipart.FileUpload; + +/** + * A JAX-RS interface. An implementation of this interface must be provided. + */ +@Path("/upload") +public interface UploadResource { + @Operation(summary = "Upload with comprehensive field types") + @Path("/comprehensive") + @POST + @Produces("application/json") + @Consumes(MediaType.MULTIPART_FORM_DATA) + UploadResponse uploadWithComprehensiveFieldTypes(@RestForm("requiredFile") @NotNull FileUpload requiredFile, + @RestForm("requiredText") @NotNull String requiredText, + @RestForm("requiredNumber") @NotNull Integer requiredNumber, @RestForm("optionalFile") FileUpload optionalFile, + @RestForm("optionalText") String optionalText, @RestForm("longNumber") Integer longNumber, + @RestForm("floatNumber") Float floatNumber, @RestForm("doubleNumber") Double doubleNumber, + @RestForm("bigDecimalNumber") BigDecimal bigDecimalNumber, @RestForm("booleanFlag") Boolean booleanFlag, + @RestForm("dateField") LocalDate dateField, @RestForm("dateTimeField") LocalDateTime dateTimeField, + @RestForm("stringArray") List stringArray, @RestForm("integerArray") List integerArray, + @RestForm("objectReference") TestObject objectReference, + @RestForm("objectArrayReference") List objectArrayReference, + @RestForm("genericObject") Map genericObject); + + @Operation(summary = "Edge cases for multipart processing") + @Path("/edge-cases") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + void edgeCasesForMultipartProcessing(@RestForm("unknownType") String unknownType, + @RestForm("noTypeField") Map noTypeField, + @RestForm("emptyArrayItems") List emptyArrayItems); +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/beans/TestObject.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/beans/TestObject.java new file mode 100644 index 00000000..ea0a5171 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/beans/TestObject.java @@ -0,0 +1,42 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "value" +}) +@Generated("jsonschema2pojo") +public class TestObject { + + @JsonProperty("name") + private String name; + @JsonProperty("value") + private Integer value; + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + @JsonProperty("value") + public Integer getValue() { + return value; + } + + @JsonProperty("value") + public void setValue(Integer value) { + this.value = value; + } + +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/beans/UploadResponse.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/beans/UploadResponse.java new file mode 100644 index 00000000..eb69657e --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/java/org/example/api/beans/UploadResponse.java @@ -0,0 +1,42 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "message", + "success" +}) +@Generated("jsonschema2pojo") +public class UploadResponse { + + @JsonProperty("message") + private String message; + @JsonProperty("success") + private Boolean success; + + @JsonProperty("message") + public String getMessage() { + return message; + } + + @JsonProperty("message") + public void setMessage(String message) { + this.message = message; + } + + @JsonProperty("success") + public Boolean getSuccess() { + return success; + } + + @JsonProperty("success") + public void setSuccess(Boolean success) { + this.success = success; + } + +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/resources/META-INF/openapi.json b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/resources/META-INF/openapi.json new file mode 100644 index 00000000..f226fb8b --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-comprehensive.generated-api/src/main/resources/META-INF/openapi.json @@ -0,0 +1,182 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Comprehensive Multipart API", + "version": "1.0.0" + }, + "paths": { + "/upload/comprehensive": { + "post": { + "summary": "Upload with comprehensive field types", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["requiredFile", "requiredText", "requiredNumber"], + "properties": { + "requiredFile": { + "type": "string", + "format": "binary", + "description": "Required binary file" + }, + "requiredText": { + "type": "string", + "description": "Required text field" + }, + "requiredNumber": { + "type": "integer", + "description": "Required integer field" + }, + "optionalFile": { + "type": "string", + "format": "binary", + "description": "Optional binary file" + }, + "optionalText": { + "type": "string", + "description": "Optional text field" + }, + "longNumber": { + "type": "integer", + "format": "int64", + "description": "Long integer" + }, + "floatNumber": { + "type": "number", + "format": "float", + "description": "Float number" + }, + "doubleNumber": { + "type": "number", + "format": "double", + "description": "Double number" + }, + "bigDecimalNumber": { + "type": "number", + "description": "BigDecimal number (no format)" + }, + "booleanFlag": { + "type": "boolean", + "description": "Boolean field" + }, + "dateField": { + "type": "string", + "format": "date", + "description": "Date field" + }, + "dateTimeField": { + "type": "string", + "format": "date-time", + "description": "DateTime field" + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of strings" + }, + "integerArray": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Array of integers" + }, + "objectReference": { + "$ref": "#/components/schemas/TestObject", + "description": "Object reference" + }, + "objectArrayReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestObject" + }, + "description": "Array of object references" + }, + "genericObject": { + "type": "object", + "description": "Generic object without properties" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadResponse" + } + } + } + } + } + } + }, + "/upload/edge-cases": { + "post": { + "summary": "Edge cases for multipart processing", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "unknownType": { + "type": "unknown", + "description": "Unknown type should fallback to String" + }, + "noTypeField": { + "description": "Field without type specification" + }, + "emptyArrayItems": { + "type": "array", + "description": "Array without items specification" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "TestObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "UploadResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + } + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/pom.xml b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/pom.xml new file mode 100644 index 00000000..17e19726 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + org.example.api + generated-api + 1.0.0 + jar + Multipart Edge Cases API + A generated project with JAX-RS and Microprofile OpenAPI features enabled. + + + 11 + 11 + false + UTF-8 + + 3.1.0 + 3.0.2 + 4.0.1 + 2.15.1 + 4.0.2 + + + + + + com.fasterxml.jackson.core + jackson-annotations + ${version.com.fasterxml.jackson} + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs-jakarta.ws.rs-api} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.jakarta.enterprise-jakarta.enterprise.cdi-api} + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation-jakarta.validation-api} + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${version.org.eclipse.microprofile.openapi-microprofile-openapi-api} + + + diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/EdgeResource.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/EdgeResource.java new file mode 100644 index 00000000..f2fbe853 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/EdgeResource.java @@ -0,0 +1,54 @@ +package org.example.api; + +import jakarta.validation.constraints.NotNull; +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import java.util.Map; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.example.api.beans.JsonPayload; +import org.jboss.resteasy.annotations.providers.multipart.RestForm; +import org.jboss.resteasy.plugins.providers.multipart.FileUpload; + +/** + * A JAX-RS interface. An implementation of this interface must be provided. + */ +@Path("/edge") +public interface EdgeResource { + @Operation(summary = "Multipart with top-level string schema (not object) - should generate no params", operationId = "invalidStringSchema") + @Path("/invalid-string-schema") + @POST + void invalidStringSchema(); + + @Operation(summary = "Multipart with top-level array schema (not object) - should generate no params", operationId = "invalidArraySchema") + @Path("/invalid-array-schema") + @POST + void invalidArraySchema(); + + @Operation(summary = "Multipart with a property having an empty schema {} - maps to Map", operationId = "nullFieldSchemas") + @Path("/null-field-schemas") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + void nullFieldSchemas(@RestForm("emptySchema") Map emptySchema, + @RestForm("normalField") String normalField); + + @Operation(summary = "Multipart with required list containing a ghost field not in properties", operationId = "requiredFields") + @Path("/required-fields") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + void requiredFields(@RestForm("existingRequired") @NotNull String existingRequired, + @RestForm("optionalField") Integer optionalField); + + @Operation(summary = "Non-multipart JSON content - should generate a normal body param", operationId = "jsonOnly") + @Path("/json-only") + @POST + @Consumes("application/json") + void jsonOnly(@NotNull JsonPayload data); + + @Operation(summary = "Mixed content types - multipart takes priority, JSON body is skipped", operationId = "mixedMultipartJson") + @Path("/mixed-multipart-json") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + void mixedMultipartJson(@RestForm("file") FileUpload file, @RestForm("description") String description); +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/JaxRsApplication.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/JaxRsApplication.java new file mode 100644 index 00000000..6ade2169 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/JaxRsApplication.java @@ -0,0 +1,13 @@ +package org.example.api; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * The JAX-RS application. + */ +@ApplicationScoped +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/beans/JsonPayload.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/beans/JsonPayload.java new file mode 100644 index 00000000..6d9de0bb --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/java/org/example/api/beans/JsonPayload.java @@ -0,0 +1,42 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "value" +}) +@Generated("jsonschema2pojo") +public class JsonPayload { + + @JsonProperty("name") + private String name; + @JsonProperty("value") + private Integer value; + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + @JsonProperty("value") + public Integer getValue() { + return value; + } + + @JsonProperty("value") + public void setValue(Integer value) { + this.value = value; + } + +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/resources/META-INF/openapi.json b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/resources/META-INF/openapi.json new file mode 100644 index 00000000..9f88d441 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-edge-cases/generated-api/src/main/resources/META-INF/openapi.json @@ -0,0 +1,182 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Multipart Edge Cases API", + "version": "1.0.0" + }, + "paths": { + "/edge/invalid-string-schema": { + "post": { + "operationId": "invalidStringSchema", + "summary": "Multipart with top-level string schema (not object) - should generate no params", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/invalid-array-schema": { + "post": { + "operationId": "invalidArraySchema", + "summary": "Multipart with top-level array schema (not object) - should generate no params", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/null-field-schemas": { + "post": { + "operationId": "nullFieldSchemas", + "summary": "Multipart with a property having an empty schema {} - maps to Map", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "emptySchema": {}, + "normalField": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/required-fields": { + "post": { + "operationId": "requiredFields", + "summary": "Multipart with required list containing a ghost field not in properties", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["existingRequired", "ghostField"], + "properties": { + "existingRequired": { + "type": "string" + }, + "optionalField": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/json-only": { + "post": { + "operationId": "jsonOnly", + "summary": "Non-multipart JSON content - should generate a normal body param", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/mixed-multipart-json": { + "post": { + "operationId": "mixedMultipartJson", + "summary": "Mixed content types - multipart takes priority, JSON body is skipped", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "description": { + "type": "string" + } + } + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "JsonPayload": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/pom.xml b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/pom.xml new file mode 100644 index 00000000..d74e3d9f --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + org.example.api + generated-api + 1.0.0 + jar + Example API + A generated project with JAX-RS and Microprofile OpenAPI features enabled. + + + 11 + 11 + false + UTF-8 + + 3.1.0 + 3.0.2 + 4.0.1 + 2.15.1 + 4.0.2 + + + + + + com.fasterxml.jackson.core + jackson-annotations + ${version.com.fasterxml.jackson} + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs-jakarta.ws.rs-api} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.jakarta.enterprise-jakarta.enterprise.cdi-api} + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation-jakarta.validation-api} + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${version.org.eclipse.microprofile.openapi-microprofile-openapi-api} + + + \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/FilesResource.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/FilesResource.java new file mode 100644 index 00000000..f0580d04 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/FilesResource.java @@ -0,0 +1,22 @@ +package org.example.api; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.Produces; +import jakarta.ws.rs.core.MediaType; +import org.example.api.beans.FileUploadProcessResponse; +import org.jboss.resteasy.annotations.providers.multipart.RestForm; +import org.jboss.resteasy.plugins.providers.multipart.FileUpload; + +/** + * A JAX-RS interface. An implementation of this interface must be provided. + */ +@Path("/files") +public interface FilesResource { + @Path("/postFile") + @POST + @Produces("application/json") + @Consumes(MediaType.MULTIPART_FORM_DATA) + FileUploadProcessResponse postFile(@RestForm("file") FileUpload file); +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/JaxRsApplication.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/JaxRsApplication.java new file mode 100644 index 00000000..3aaee03a --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/JaxRsApplication.java @@ -0,0 +1,13 @@ +package org.example.api; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * The JAX-RS application. + */ +@ApplicationScoped +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/FileUploadProcessResponse.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/FileUploadProcessResponse.java new file mode 100644 index 00000000..51a1820f --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/FileUploadProcessResponse.java @@ -0,0 +1,42 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "message", + "success" +}) +@Generated("jsonschema2pojo") +public class FileUploadProcessResponse { + + @JsonProperty("message") + private String message; + @JsonProperty("success") + private Boolean success; + + @JsonProperty("message") + public String getMessage() { + return message; + } + + @JsonProperty("message") + public void setMessage(String message) { + this.message = message; + } + + @JsonProperty("success") + public Boolean getSuccess() { + return success; + } + + @JsonProperty("success") + public void setSuccess(Boolean success) { + this.success = success; + } + +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/SomeObject.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/SomeObject.java new file mode 100644 index 00000000..1ce80504 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/SomeObject.java @@ -0,0 +1,31 @@ + +package org.example.api.beans; + +import java.util.ArrayList; +import java.util.List; +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "someList" +}) +@Generated("jsonschema2pojo") +public class SomeObject { + + @JsonProperty("someList") + private List someList = new ArrayList(); + + @JsonProperty("someList") + public List getSomeList() { + return someList; + } + + @JsonProperty("someList") + public void setSomeList(List someList) { + this.someList = someList; + } + +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/SomeOtherObject.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/SomeOtherObject.java new file mode 100644 index 00000000..04395d24 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/java/org/example/api/beans/SomeOtherObject.java @@ -0,0 +1,29 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "exampleProperty" +}) +@Generated("jsonschema2pojo") +public class SomeOtherObject { + + @JsonProperty("exampleProperty") + private String exampleProperty; + + @JsonProperty("exampleProperty") + public String getExampleProperty() { + return exampleProperty; + } + + @JsonProperty("exampleProperty") + public void setExampleProperty(String exampleProperty) { + this.exampleProperty = exampleProperty; + } + +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/resources/META-INF/openapi.json b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/resources/META-INF/openapi.json new file mode 100644 index 00000000..866a3fff --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-form-content.generated-api/src/main/resources/META-INF/openapi.json @@ -0,0 +1,75 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Example API", + "version": "1.0.0" + }, + "paths": { + "/files/postFile": { + "post": { + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadProcessResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SomeObject": { + "type": "object", + "properties": { + "someList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Some-Other-Object" + } + } + } + }, + "Some-Other-Object": { + "type": "object", + "properties": { + "exampleProperty": { + "type": "string" + } + } + }, + "FileUploadProcessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/pom.xml b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/pom.xml new file mode 100644 index 00000000..7439091b --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/pom.xml @@ -0,0 +1,55 @@ + + + 4.0.0 + org.example.api + generated-api + 1.0.0 + jar + Array-Only Ref Multipart API + A generated project with JAX-RS and Microprofile OpenAPI features enabled. + + + 11 + 11 + false + UTF-8 + + 3.1.0 + 3.0.2 + 4.0.1 + 2.15.1 + 4.0.2 + + + + + + com.fasterxml.jackson.core + jackson-annotations + ${version.com.fasterxml.jackson} + + + + jakarta.ws.rs + jakarta.ws.rs-api + ${version.jakarta.ws.rs-jakarta.ws.rs-api} + + + jakarta.enterprise + jakarta.enterprise.cdi-api + ${version.jakarta.enterprise-jakarta.enterprise.cdi-api} + + + jakarta.validation + jakarta.validation-api + ${version.jakarta.validation-jakarta.validation-api} + + + org.eclipse.microprofile.openapi + microprofile-openapi-api + ${version.org.eclipse.microprofile.openapi-microprofile-openapi-api} + + + diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/JaxRsApplication.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/JaxRsApplication.java new file mode 100644 index 00000000..6ade2169 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/JaxRsApplication.java @@ -0,0 +1,13 @@ +package org.example.api; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.ws.rs.ApplicationPath; +import jakarta.ws.rs.core.Application; + +/** + * The JAX-RS application. + */ +@ApplicationScoped +@ApplicationPath("/") +public class JaxRsApplication extends Application { +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/UploadResource.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/UploadResource.java new file mode 100644 index 00000000..93f1d2a1 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/UploadResource.java @@ -0,0 +1,24 @@ +package org.example.api; + +import jakarta.ws.rs.Consumes; +import jakarta.ws.rs.POST; +import jakarta.ws.rs.Path; +import jakarta.ws.rs.core.MediaType; +import java.math.BigDecimal; +import java.util.List; +import org.eclipse.microprofile.openapi.annotations.Operation; +import org.example.api.beans.TestObject; +import org.jboss.resteasy.annotations.providers.multipart.RestForm; + +/** + * A JAX-RS interface. An implementation of this interface must be provided. + */ +@Path("/upload") +public interface UploadResource { + @Operation(summary = "Upload with array-only ref field") + @Path("/items") + @POST + @Consumes(MediaType.MULTIPART_FORM_DATA) + void uploadWithArrayonlyRefField(@RestForm("items") List items, + @RestForm("amounts") List amounts); +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/beans/TestObject.java b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/beans/TestObject.java new file mode 100644 index 00000000..ea0a5171 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/java/org/example/api/beans/TestObject.java @@ -0,0 +1,42 @@ + +package org.example.api.beans; + +import javax.annotation.processing.Generated; +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyOrder; + +@JsonInclude(JsonInclude.Include.NON_NULL) +@JsonPropertyOrder({ + "name", + "value" +}) +@Generated("jsonschema2pojo") +public class TestObject { + + @JsonProperty("name") + private String name; + @JsonProperty("value") + private Integer value; + + @JsonProperty("name") + public String getName() { + return name; + } + + @JsonProperty("name") + public void setName(String name) { + this.name = name; + } + + @JsonProperty("value") + public Integer getValue() { + return value; + } + + @JsonProperty("value") + public void setValue(Integer value) { + this.value = value; + } + +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/resources/META-INF/openapi.json b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/resources/META-INF/openapi.json new file mode 100644 index 00000000..065151cf --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/_expected-multipart-openapi-v-3-0-0/generated-api/src/main/resources/META-INF/openapi.json @@ -0,0 +1,67 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Array-Only Ref Multipart API", + "version": "1.0.0" + }, + "paths": { + "/upload/items": { + "post": { + "summary": "Upload with array-only ref field", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestObject" + }, + "description": "Array of object references (no direct ref)" + }, + "amounts": { + "type": "array", + "items": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "description": "Array of BigDecimal values" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "TestObject": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "example": "Widget A" + }, + "value": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 100 + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/duplicate-method-name.json b/core/src/test/resources/OpenApi2JaxRsTest/duplicate-method-name.json new file mode 100644 index 00000000..a9b2d8b0 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/duplicate-method-name.json @@ -0,0 +1,31 @@ +{ + "openapi": "3.0.2", + "info": { + "title": "Duplicate Method Name Test API", + "description": "API spec that triggers a duplicate method name collision.", + "version": "1.0.0" + }, + "paths": { + "/products": { + "get": { + "operationId": "items", + "summary": "List products", + "responses": { + "200": { + "description": "OK" + } + } + } + }, + "/products/items": { + "get": { + "summary": "", + "responses": { + "200": { + "description": "OK" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/multipart-array-only-ref.json b/core/src/test/resources/OpenApi2JaxRsTest/multipart-array-only-ref.json new file mode 100644 index 00000000..4e55d69f --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/multipart-array-only-ref.json @@ -0,0 +1,61 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Array-Only Ref Multipart API", + "version": "1.0.0" + }, + "paths": { + "/upload/items": { + "post": { + "summary": "Upload with array-only ref field", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestObject" + }, + "description": "Array of object references (no direct ref)" + }, + "amounts": { + "type": "array", + "items": { + "type": "number", + "format": "decimal" + }, + "description": "Array of BigDecimal values" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "TestObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/multipart-comprehensive.json b/core/src/test/resources/OpenApi2JaxRsTest/multipart-comprehensive.json new file mode 100644 index 00000000..f226fb8b --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/multipart-comprehensive.json @@ -0,0 +1,182 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Comprehensive Multipart API", + "version": "1.0.0" + }, + "paths": { + "/upload/comprehensive": { + "post": { + "summary": "Upload with comprehensive field types", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["requiredFile", "requiredText", "requiredNumber"], + "properties": { + "requiredFile": { + "type": "string", + "format": "binary", + "description": "Required binary file" + }, + "requiredText": { + "type": "string", + "description": "Required text field" + }, + "requiredNumber": { + "type": "integer", + "description": "Required integer field" + }, + "optionalFile": { + "type": "string", + "format": "binary", + "description": "Optional binary file" + }, + "optionalText": { + "type": "string", + "description": "Optional text field" + }, + "longNumber": { + "type": "integer", + "format": "int64", + "description": "Long integer" + }, + "floatNumber": { + "type": "number", + "format": "float", + "description": "Float number" + }, + "doubleNumber": { + "type": "number", + "format": "double", + "description": "Double number" + }, + "bigDecimalNumber": { + "type": "number", + "description": "BigDecimal number (no format)" + }, + "booleanFlag": { + "type": "boolean", + "description": "Boolean field" + }, + "dateField": { + "type": "string", + "format": "date", + "description": "Date field" + }, + "dateTimeField": { + "type": "string", + "format": "date-time", + "description": "DateTime field" + }, + "stringArray": { + "type": "array", + "items": { + "type": "string" + }, + "description": "Array of strings" + }, + "integerArray": { + "type": "array", + "items": { + "type": "integer" + }, + "description": "Array of integers" + }, + "objectReference": { + "$ref": "#/components/schemas/TestObject", + "description": "Object reference" + }, + "objectArrayReference": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestObject" + }, + "description": "Array of object references" + }, + "genericObject": { + "type": "object", + "description": "Generic object without properties" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/UploadResponse" + } + } + } + } + } + } + }, + "/upload/edge-cases": { + "post": { + "summary": "Edge cases for multipart processing", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "unknownType": { + "type": "unknown", + "description": "Unknown type should fallback to String" + }, + "noTypeField": { + "description": "Field without type specification" + }, + "emptyArrayItems": { + "type": "array", + "description": "Array without items specification" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "TestObject": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + }, + "UploadResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + } + } + } +} \ No newline at end of file diff --git a/core/src/test/resources/OpenApi2JaxRsTest/multipart-edge-cases.json b/core/src/test/resources/OpenApi2JaxRsTest/multipart-edge-cases.json new file mode 100644 index 00000000..9f88d441 --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/multipart-edge-cases.json @@ -0,0 +1,182 @@ +{ + "openapi": "3.1.0", + "info": { + "title": "Multipart Edge Cases API", + "version": "1.0.0" + }, + "paths": { + "/edge/invalid-string-schema": { + "post": { + "operationId": "invalidStringSchema", + "summary": "Multipart with top-level string schema (not object) - should generate no params", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "string" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/invalid-array-schema": { + "post": { + "operationId": "invalidArraySchema", + "summary": "Multipart with top-level array schema (not object) - should generate no params", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "array", + "items": { + "type": "string" + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/null-field-schemas": { + "post": { + "operationId": "nullFieldSchemas", + "summary": "Multipart with a property having an empty schema {} - maps to Map", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "emptySchema": {}, + "normalField": { + "type": "string" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/required-fields": { + "post": { + "operationId": "requiredFields", + "summary": "Multipart with required list containing a ghost field not in properties", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "required": ["existingRequired", "ghostField"], + "properties": { + "existingRequired": { + "type": "string" + }, + "optionalField": { + "type": "integer" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/json-only": { + "post": { + "operationId": "jsonOnly", + "summary": "Non-multipart JSON content - should generate a normal body param", + "requestBody": { + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + }, + "/edge/mixed-multipart-json": { + "post": { + "operationId": "mixedMultipartJson", + "summary": "Mixed content types - multipart takes priority, JSON body is skipped", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + }, + "description": { + "type": "string" + } + } + } + }, + "application/json": { + "schema": { + "$ref": "#/components/schemas/JsonPayload" + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "JsonPayload": { + "type": "object", + "properties": { + "name": { + "type": "string" + }, + "value": { + "type": "integer" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/multipart-form-content.json b/core/src/test/resources/OpenApi2JaxRsTest/multipart-form-content.json new file mode 100644 index 00000000..866a3fff --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/multipart-form-content.json @@ -0,0 +1,75 @@ +{ + "openapi": "3.0.3", + "info": { + "title": "Example API", + "version": "1.0.0" + }, + "paths": { + "/files/postFile": { + "post": { + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "file": { + "type": "string", + "format": "binary" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/FileUploadProcessResponse" + } + } + } + } + } + } + } + }, + "components": { + "schemas": { + "SomeObject": { + "type": "object", + "properties": { + "someList": { + "type": "array", + "items": { + "$ref": "#/components/schemas/Some-Other-Object" + } + } + } + }, + "Some-Other-Object": { + "type": "object", + "properties": { + "exampleProperty": { + "type": "string" + } + } + }, + "FileUploadProcessResponse": { + "type": "object", + "properties": { + "message": { + "type": "string" + }, + "success": { + "type": "boolean" + } + } + } + } + } +} diff --git a/core/src/test/resources/OpenApi2JaxRsTest/multipart-openapi-v-3-0-0.json b/core/src/test/resources/OpenApi2JaxRsTest/multipart-openapi-v-3-0-0.json new file mode 100644 index 00000000..065151cf --- /dev/null +++ b/core/src/test/resources/OpenApi2JaxRsTest/multipart-openapi-v-3-0-0.json @@ -0,0 +1,67 @@ +{ + "openapi": "3.0.0", + "info": { + "title": "Array-Only Ref Multipart API", + "version": "1.0.0" + }, + "paths": { + "/upload/items": { + "post": { + "summary": "Upload with array-only ref field", + "requestBody": { + "required": true, + "content": { + "multipart/form-data": { + "schema": { + "type": "object", + "properties": { + "items": { + "type": "array", + "items": { + "$ref": "#/components/schemas/TestObject" + }, + "description": "Array of object references (no direct ref)" + }, + "amounts": { + "type": "array", + "items": { + "type": "number", + "format": "decimal", + "nullable": true + }, + "description": "Array of BigDecimal values" + } + } + } + } + } + }, + "responses": { + "200": { + "description": "Success" + } + } + } + } + }, + "components": { + "schemas": { + "TestObject": { + "type": "object", + "properties": { + "name": { + "type": "string", + "nullable": true, + "example": "Widget A" + }, + "value": { + "type": "integer", + "minimum": 0, + "exclusiveMinimum": true, + "maximum": 100 + } + } + } + } + } +}