From 32e6ec2ea9a3322701e09546e012ba5468ede9c6 Mon Sep 17 00:00:00 2001 From: Anthony Stirling <77850077+Frooodle@users.noreply.github.com> Date: Sun, 21 Sep 2025 13:32:09 +0100 Subject: [PATCH] oneOf and required body --- .../annotations/AutoJobPostMapping.java | 3 ++ .../software/common/model/api/PDFFile.java | 14 ++++-- .../software/SPDF/config/OpenApiConfig.java | 48 +++++++++++++++++++ .../software/SPDF/config/SpringDocConfig.java | 6 ++- 4 files changed, 67 insertions(+), 4 deletions(-) diff --git a/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java b/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java index 8fb729560..48ea489d6 100644 --- a/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java +++ b/app/common/src/main/java/stirling/software/common/annotations/AutoJobPostMapping.java @@ -6,6 +6,8 @@ import org.springframework.core.annotation.AliasFor; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; +import io.swagger.v3.oas.annotations.parameters.RequestBody; + /** * Shortcut for a POST endpoint that is executed through the Stirling "auto‑job" framework. * @@ -29,6 +31,7 @@ import org.springframework.web.bind.annotation.RequestMethod; @Retention(RetentionPolicy.RUNTIME) @Documented @RequestMapping(method = RequestMethod.POST) +@RequestBody(required = true) public @interface AutoJobPostMapping { /** Alias for {@link RequestMapping#value} – the path mapping of the endpoint. */ diff --git a/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java b/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java index 9ccf00aaa..b0ba8f94c 100644 --- a/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java +++ b/app/common/src/main/java/stirling/software/common/model/api/PDFFile.java @@ -4,6 +4,8 @@ import org.springframework.web.multipart.MultipartFile; import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.AssertTrue; + import lombok.Data; import lombok.EqualsAndHashCode; import lombok.NoArgsConstructor; @@ -11,12 +13,18 @@ import lombok.NoArgsConstructor; @Data @NoArgsConstructor @EqualsAndHashCode +@Schema(description = "PDF file input - either upload a file or provide a server-side file ID") public class PDFFile { @Schema(description = "The input PDF file", format = "binary") private MultipartFile fileInput; - @Schema( - description = "File ID for server-side files (can be used instead of fileInput)", - example = "a1b2c3d4-5678-90ab-cdef-ghijklmnopqr") + @Schema(description = "File ID for server-side files (can be used instead of fileInput)") private String fileId; + + @AssertTrue(message = "Either fileInput or fileId must be provided") + @Schema(hidden = true) + private boolean isValid() { + return (fileInput != null && (fileId == null || fileId.trim().isEmpty())) + || (fileId != null && !fileId.trim().isEmpty() && fileInput == null); + } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java index 97e287778..a25287c4d 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/OpenApiConfig.java @@ -1,5 +1,8 @@ package stirling.software.SPDF.config; +import java.util.List; + +import org.springdoc.core.customizers.OpenApiCustomizer; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,7 +11,10 @@ import io.swagger.v3.oas.models.OpenAPI; import io.swagger.v3.oas.models.info.Contact; import io.swagger.v3.oas.models.info.Info; import io.swagger.v3.oas.models.info.License; +import io.swagger.v3.oas.models.media.ComposedSchema; +import io.swagger.v3.oas.models.media.ObjectSchema; import io.swagger.v3.oas.models.media.Schema; +import io.swagger.v3.oas.models.media.StringSchema; import io.swagger.v3.oas.models.security.SecurityRequirement; import io.swagger.v3.oas.models.security.SecurityScheme; import io.swagger.v3.oas.models.servers.Server; @@ -98,4 +104,46 @@ public class OpenApiConfig { .addSecurityItem(new SecurityRequirement().addList("apiKey")); } } + + @Bean + OpenApiCustomizer pdfFileOneOfCustomizer() { + return openApi -> { + var components = openApi.getComponents(); + var schemas = components.getSchemas(); + + // Define the two shapes + var upload = + new ObjectSchema() + .name("PDFFileUpload") + .description("Upload a PDF file") + .addProperty("fileInput", new StringSchema().format("binary")) + .addRequiredItem("fileInput"); + + var ref = + new ObjectSchema() + .name("PDFFileRef") + .description("Reference a server-side file") + .addProperty( + "fileId", + new StringSchema() + .example("a1b2c3d4-5678-90ab-cdef-ghijklmnopqr")) + .addRequiredItem("fileId"); + + schemas.put("PDFFileUpload", upload); + schemas.put("PDFFileRef", ref); + + // Create the oneOf schema + var pdfFileOneOf = + new ComposedSchema() + .oneOf( + List.of( + new Schema<>() + .$ref("#/components/schemas/PDFFileUpload"), + new Schema<>().$ref("#/components/schemas/PDFFileRef"))) + .description("Either upload a file or provide a server-side file ID"); + + // Replace PDFFile schema + schemas.put("PDFFile", pdfFileOneOf); + }; + } } diff --git a/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java b/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java index 20b69e218..cdde38d48 100644 --- a/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java +++ b/app/core/src/main/java/stirling/software/SPDF/config/SpringDocConfig.java @@ -1,6 +1,8 @@ package stirling.software.SPDF.config; +import org.springdoc.core.customizers.OpenApiCustomizer; import org.springdoc.core.models.GroupedOpenApi; +import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; @@ -8,7 +10,8 @@ import org.springframework.context.annotation.Configuration; public class SpringDocConfig { @Bean - public GroupedOpenApi pdfProcessingApi() { + public GroupedOpenApi pdfProcessingApi( + @Qualifier("pdfFileOneOfCustomizer") OpenApiCustomizer pdfFileOneOfCustomizer) { return GroupedOpenApi.builder() .group("file-processing") .displayName("File Processing") @@ -23,6 +26,7 @@ public class SpringDocConfig { "/api/v1/info/**", "/api/v1/general/job/**", "/api/v1/general/files/**") + .addOpenApiCustomizer(pdfFileOneOfCustomizer) .addOpenApiCustomizer( openApi -> { openApi.info(