diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeConvertApi.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeConvertApi.java index 91ff50b1..7e5bea62 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeConvertApi.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeConvertApi.java @@ -46,7 +46,7 @@ default ConvertDocumentResponse convertFiles(Path... files) { * If the request is null, default conversion options are applied. * * @param request an optional {@link ConvertDocumentRequest} specifying conversion settings and parameters - * @param files an array of {@link Path} objects representing the file paths to be converted + * @param files an array of {@link Path} objects representing the file paths to be converted * @return a {@link ConvertDocumentResponse} containing the processed document data, any errors encountered, * and additional processing metadata * @throws ai.docling.serve.api.validation.ValidationException If request validation fails for any reason. @@ -148,8 +148,7 @@ default CompletionStage convertFilesAsync(@Nullable Con private ConvertDocumentRequest createRequest(@Nullable ConvertDocumentRequest request, Path... files) { ValidationUtils.ensureNotEmpty(files, "files"); - var builder = Optional.ofNullable(request) - .map(ConvertDocumentRequest::toBuilder) + var builder = Optional.ofNullable(request).>map(ConvertDocumentRequest::toBuilder) .orElseGet(ConvertDocumentRequest::builder); FileUtils.createFileSources(files) diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java index d03f4187..e8d08936 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/ChunkDocumentRequest.java @@ -1,35 +1,18 @@ package ai.docling.serve.api.chunk.request; -import java.util.List; - -import org.jspecify.annotations.Nullable; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; -import ai.docling.serve.api.convert.request.source.Source; -import ai.docling.serve.api.convert.request.target.Target; +import ai.docling.serve.api.request.DocumentRequest; @JsonInclude(JsonInclude.Include.NON_EMPTY) @tools.jackson.databind.annotation.JsonDeserialize(builder = ChunkDocumentRequest.ChunkDocumentRequestBuilder.class) @lombok.experimental.SuperBuilder(toBuilder = true) @lombok.Getter -@lombok.ToString -public sealed abstract class ChunkDocumentRequest permits HierarchicalChunkDocumentRequest, HybridChunkDocumentRequest { - /** - * List of input document sources to process. - * - * @param sources the list of document sources - * @return the list of document sources - */ - @JsonProperty("sources") - @JsonSetter(nulls = Nulls.AS_EMPTY) - @lombok.Singular - private List sources; - +@lombok.ToString(callSuper = true) +public sealed abstract class ChunkDocumentRequest extends DocumentRequest + permits HierarchicalChunkDocumentRequest, HybridChunkDocumentRequest { /** * Conversion options. * @@ -41,16 +24,6 @@ public sealed abstract class ChunkDocumentRequest permits HierarchicalChunkDocum @lombok.Builder.Default private ConvertDocumentOptions options = ConvertDocumentOptions.builder().build(); - /** - * Specification for the type of output target. - * - * @param target the output target specification, or null if not specified - * @return the output target specification, or null if not specified - */ - @JsonProperty("target") - @Nullable - private Target target; - /** * If true, the output will include both the chunks and the converted document. * @@ -61,7 +34,6 @@ public sealed abstract class ChunkDocumentRequest permits HierarchicalChunkDocum private boolean includeConvertedDoc; @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static abstract class ChunkDocumentRequestBuilder> { - // Lombok's @SuperBuilder generates the actual implementation + public abstract static class ChunkDocumentRequestBuilder> extends DocumentRequest.DocumentRequestBuilder { } } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/response/ChunkDocumentResponse.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/response/ChunkDocumentResponse.java index 199f1121..9785bb10 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/response/ChunkDocumentResponse.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/response/ChunkDocumentResponse.java @@ -7,11 +7,15 @@ import com.fasterxml.jackson.annotation.JsonSetter; import com.fasterxml.jackson.annotation.Nulls; +import ai.docling.serve.api.response.ProcessedDocumentResponse; + /** * Response returned by the Chunk API for a single conversion request. * *

Serialization uses {@link JsonInclude.Include#NON_EMPTY}, so nulls and empty * collections/strings are omitted from JSON output.

+ * + * @see ProcessedDocumentResponse */ @JsonInclude(JsonInclude.Include.NON_EMPTY) @tools.jackson.databind.annotation.JsonDeserialize(builder = ChunkDocumentResponse.Builder.class) @@ -19,7 +23,7 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class ChunkDocumentResponse { +public final class ChunkDocumentResponse extends ProcessedDocumentResponse { /** * List of document chunks. @@ -55,17 +59,18 @@ public class ChunkDocumentResponse { /** * Builder for creating {@link ChunkDocumentResponse} instances. * Generated by Lombok's {@code @Builder} annotation. - * + * *

Builder methods: *

    - *
  • {@code chunk(Chunk)} - Add a single chunk (use with @Singular)
  • - *
  • {@code chunks(List)} - Set the list of chunks
  • - *
  • {@code document(Document)} - Add a single document (use with @Singular)
  • - *
  • {@code documents(List)} - Set the list of converted documents
  • - *
  • {@code processingTime(Double)} - Set the processing time in seconds
  • + *
  • {@code chunk(Chunk)} - Add a single chunk (use with @Singular)
  • + *
  • {@code chunks(List)} - Set the list of chunks
  • + *
  • {@code document(Document)} - Add a single document (use with @Singular)
  • + *
  • {@code documents(List)} - Set the list of converted documents
  • + *
  • {@code processingTime(Double)} - Set the processing time in seconds
  • *
*/ @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public static class Builder { + } } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/BatchConvertDocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/BatchConvertDocumentRequest.java index ec5d55ba..1d0e1648 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/BatchConvertDocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/BatchConvertDocumentRequest.java @@ -1,6 +1,7 @@ package ai.docling.serve.api.convert.request; import java.util.List; +import java.util.Objects; import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; @@ -8,49 +9,30 @@ import com.fasterxml.jackson.annotation.Nulls; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; -import ai.docling.serve.api.convert.request.source.Source; import ai.docling.serve.api.convert.request.target.Target; +import ai.docling.serve.api.request.DocumentRequest; /** * Represents a request to batch convert document sources. The batch endpoint processes multiple * documents asynchronously and returns a task ID for tracking progress. Sources can be HTTP URLs * or S3 buckets, and results are delivered to a presigned URL or S3 target. * + *

Unlike {@link ConvertDocumentRequest}, the {@linkplain #getTarget() target} is required + * for batch requests — it must be either a + * {@link ai.docling.serve.api.convert.request.target.PresignedUrlTarget} or + * {@link ai.docling.serve.api.convert.request.target.S3Target}. + * *

This class is serialized into JSON to conform to the API specification using * {@link JsonProperty} annotations. Fields with {@code null} values or empty collections * are omitted from the serialized JSON using {@link JsonInclude}. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -@tools.jackson.databind.annotation.JsonDeserialize(builder = BatchConvertDocumentRequest.Builder.class) +@tools.jackson.databind.annotation.JsonDeserialize(builder = BatchConvertDocumentRequest.BuilderImpl.class) @lombok.extern.jackson.Jacksonized -@lombok.Builder(toBuilder = true) +@lombok.experimental.SuperBuilder(toBuilder = true) @lombok.Getter -@lombok.ToString -public class BatchConvertDocumentRequest { - /** - * List of document sources to be converted. - * Each source can be an HTTP URL or S3 reference. - * - * @param sources the list of document sources - * @return the list of document sources - */ - @JsonProperty("sources") - @JsonSetter(nulls = Nulls.AS_EMPTY) - @lombok.Singular - private List sources; - - /** - * Target specification for where the converted documents should be delivered. - * Must be either a {@link ai.docling.serve.api.convert.request.target.PresignedUrlTarget} - * or {@link ai.docling.serve.api.convert.request.target.S3Target}. - * - * @param target the output target - * @return the output target - */ - @JsonProperty("target") - @lombok.NonNull - private Target target; - +@lombok.ToString(callSuper = true) +public final class BatchConvertDocumentRequest extends DocumentRequest { /** * Options controlling the document conversion process. * Includes settings for OCR, output formats, processing pipelines, and more. @@ -75,19 +57,16 @@ public class BatchConvertDocumentRequest { private List callbacks; /** - * Builder for creating {@link BatchConvertDocumentRequest} instances. - * Generated by Lombok's {@code @Builder} annotation. + * Returns the output target, which is required for batch requests. * - *

Builder methods: - *

    - *
  • {@code source(Source)} - Add a single document source
  • - *
  • {@code sources(List)} - Set the list of document sources
  • - *
  • {@code target(Target)} - Set the output target
  • - *
  • {@code options(ConvertDocumentOptions)} - Set the conversion options
  • - *
  • {@code callback(CallbackSpec)} - Add a single callback specification
  • - *
  • {@code callbacks(List)} - Set the list of callback specifications
  • - *
+ * @return the output target, never null */ + @Override + public Target getTarget() { + return Objects.requireNonNull(super.getTarget(), "target is marked non-null but is null"); + } + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public abstract static class BatchConvertDocumentRequestBuilder> extends DocumentRequest.DocumentRequestBuilder { + } } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/ConvertDocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/ConvertDocumentRequest.java index dc83c306..d2a73812 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/ConvertDocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/request/ConvertDocumentRequest.java @@ -1,17 +1,10 @@ package ai.docling.serve.api.convert.request; -import java.util.List; - -import org.jspecify.annotations.Nullable; - import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; -import com.fasterxml.jackson.annotation.JsonSetter; -import com.fasterxml.jackson.annotation.Nulls; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; -import ai.docling.serve.api.convert.request.source.Source; -import ai.docling.serve.api.convert.request.target.Target; +import ai.docling.serve.api.request.DocumentRequest; /** * Represents a request to convert a document. The request includes the source(s) of the document, @@ -22,24 +15,12 @@ * are omitted from the serialized JSON using {@link JsonInclude}. */ @JsonInclude(JsonInclude.Include.NON_EMPTY) -@tools.jackson.databind.annotation.JsonDeserialize(builder = ConvertDocumentRequest.Builder.class) +@tools.jackson.databind.annotation.JsonDeserialize(builder = ConvertDocumentRequest.BuilderImpl.class) @lombok.extern.jackson.Jacksonized -@lombok.Builder(toBuilder = true) +@lombok.experimental.SuperBuilder(toBuilder = true) @lombok.Getter -@lombok.ToString -public class ConvertDocumentRequest { - /** - * List of document sources to be converted. - * Each source can be a file (base64-encoded) or an HTTP URL. - * - * @param sources the list of document sources - * @return the list of document sources - */ - @JsonProperty("sources") - @JsonSetter(nulls = Nulls.AS_EMPTY) - @lombok.Singular - private List sources; - +@lombok.ToString(callSuper = true) +public final class ConvertDocumentRequest extends DocumentRequest { /** * Options controlling the document conversion process. * Includes settings for OCR, output formats, processing pipelines, and more. @@ -52,29 +33,7 @@ public class ConvertDocumentRequest { @lombok.Builder.Default private ConvertDocumentOptions options = ConvertDocumentOptions.builder().build(); - /** - * Target specification for where the converted document should be delivered. - * If not specified, the result is returned in the response body. - * - * @param target the output target, or null if not specified - * @return the output target, or null if not specified - */ - @JsonProperty("target") - @Nullable - private Target target; - - /** - * Builder for creating {@link ConvertDocumentRequest} instances. - * Generated by Lombok's {@code @Builder} annotation. - * - *

Builder methods: - *

    - *
  • {@code source(Source)} - Add a single document source (use with @Singular)
  • - *
  • {@code sources(List)} - Set the list of document sources
  • - *
  • {@code options(ConvertDocumentOptions)} - Set the conversion options
  • - *
  • {@code target(Target)} - Set the output target
  • - *
- */ @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") - public static class Builder { } + public abstract static class ConvertDocumentRequestBuilder> extends DocumentRequest.DocumentRequestBuilder { + } } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java index 58233051..2172bbff 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConvertDocumentResponse.java @@ -3,6 +3,7 @@ import com.fasterxml.jackson.annotation.JsonInclude; import com.fasterxml.jackson.annotation.JsonProperty; +import ai.docling.serve.api.response.ProcessedDocumentResponse; import ai.docling.serve.api.serialization.Jackson2ConvertDocumentResponseDeserializer; import ai.docling.serve.api.serialization.Jackson3ConvertDocumentResponseDeserializer; @@ -14,12 +15,14 @@ * *

Serialization uses {@link JsonInclude.Include#NON_EMPTY}, so nulls and empty * collections/strings are omitted from JSON output.

+ * + * @see ProcessedDocumentResponse */ @JsonInclude(JsonInclude.Include.NON_EMPTY) @com.fasterxml.jackson.databind.annotation.JsonDeserialize(using = Jackson2ConvertDocumentResponseDeserializer.class) @tools.jackson.databind.annotation.JsonDeserialize(using = Jackson3ConvertDocumentResponseDeserializer.class) -public abstract sealed class ConvertDocumentResponse permits InBodyConvertDocumentResponse, PreSignedUrlConvertDocumentResponse, - PreSignedUrlConvertResponse, ZipArchiveConvertDocumentResponse { +public abstract sealed class ConvertDocumentResponse extends ProcessedDocumentResponse permits InBodyConvertDocumentResponse, + PreSignedUrlConvertDocumentResponse, PreSignedUrlConvertResponse, ZipArchiveConvertDocumentResponse { /** * Type of response * diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java new file mode 100644 index 00000000..10e51c6e --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java @@ -0,0 +1,69 @@ +package ai.docling.serve.api.request; + +import java.util.List; + +import org.jspecify.annotations.Nullable; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +import ai.docling.serve.api.chunk.request.ChunkDocumentRequest; +import ai.docling.serve.api.convert.request.BatchConvertDocumentRequest; +import ai.docling.serve.api.convert.request.ConvertDocumentRequest; +import ai.docling.serve.api.convert.request.source.Source; +import ai.docling.serve.api.convert.request.target.Target; + +/** + * Abstract base class for all document processing requests. Provides the common fields shared + * across conversion, chunking, and batch conversion requests: document {@linkplain #getSources() + * sources} and an optional output {@linkplain #getTarget() target}. + * + *

This is a {@code sealed} class — the only permitted subtypes are + * {@link ConvertDocumentRequest}, {@link BatchConvertDocumentRequest}, and + * {@link ChunkDocumentRequest} — enabling exhaustive pattern matching: + * + *

{@code
+ * switch (request) {
+ *   case ConvertDocumentRequest r    -> client.convertSource(r);
+ *   case BatchConvertDocumentRequest r -> client.convertSourceBatch(r);
+ *   case HierarchicalChunkDocumentRequest r -> client.chunkSourceWithHierarchicalChunker(r);
+ *   case HybridChunkDocumentRequest r       -> client.chunkSourceWithHybridChunker(r);
+ * }
+ * }
+ */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@tools.jackson.databind.annotation.JsonDeserialize(builder = DocumentRequest.DocumentRequestBuilder.class) +@lombok.experimental.SuperBuilder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public abstract sealed class DocumentRequest + permits ConvertDocumentRequest, BatchConvertDocumentRequest, ChunkDocumentRequest { + /** + * List of document sources to be processed. + * Each source can be a file (base64-encoded), an HTTP URL, or an S3 reference. + * + * @param sources the list of document sources + * @return the list of document sources + */ + @JsonProperty("sources") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private List sources; + + /** + * Specification for the type of output target. + * If not specified, the result is returned in the response body. + * + * @param target the output target specification, or null if not specified + * @return the output target specification, or null if not specified + */ + @JsonProperty("target") + @Nullable + private Target target; + + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + public abstract static class DocumentRequestBuilder> { + } +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/package-info.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/package-info.java new file mode 100644 index 00000000..43e09b77 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package ai.docling.serve.api.request; + +import org.jspecify.annotations.NullMarked; diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/ProcessedDocumentResponse.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/ProcessedDocumentResponse.java new file mode 100644 index 00000000..7c67a71b --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/ProcessedDocumentResponse.java @@ -0,0 +1,30 @@ +package ai.docling.serve.api.response; + +import ai.docling.serve.api.chunk.response.ChunkDocumentResponse; +import ai.docling.serve.api.convert.response.ConvertDocumentResponse; + +/** + * Abstract base class for all document processing responses. Provides a common type for + * conversion and chunking responses, enabling polymorphism when working with different + * response types. + * + *

This is a {@code sealed} class — the only permitted subtypes are + * {@link ConvertDocumentResponse} (which is itself sealed with four further subtypes) + * and {@link ChunkDocumentResponse} — enabling exhaustive pattern matching: + * + *

{@code
+ * switch (response) {
+ *   case InBodyConvertDocumentResponse r           -> handleInBody(r);
+ *   case PreSignedUrlConvertDocumentResponse r     -> handlePreSignedUrl(r);
+ *   case PreSignedUrlConvertResponse r             -> handlePreSignedUrlResponse(r);
+ *   case ZipArchiveConvertDocumentResponse r       -> handleZipArchive(r);
+ *   case ChunkDocumentResponse r                   -> handleChunk(r);
+ * }
+ * }
+ * + * @see ConvertDocumentResponse + * @see ChunkDocumentResponse + */ +public abstract sealed class ProcessedDocumentResponse + permits ConvertDocumentResponse, ChunkDocumentResponse { +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/package-info.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/package-info.java new file mode 100644 index 00000000..2b683edf --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package ai.docling.serve.api.response; + +import org.jspecify.annotations.NullMarked; diff --git a/docling-serve/docling-serve-api/src/main/java/module-info.java b/docling-serve/docling-serve-api/src/main/java/module-info.java index 4243e573..06678ac3 100644 --- a/docling-serve/docling-serve-api/src/main/java/module-info.java +++ b/docling-serve/docling-serve-api/src/main/java/module-info.java @@ -12,6 +12,8 @@ exports ai.docling.serve.api; exports ai.docling.serve.api.health; + exports ai.docling.serve.api.request; + exports ai.docling.serve.api.response; exports ai.docling.serve.api.util; // Chunking API @@ -42,5 +44,6 @@ // SPI exports ai.docling.serve.api.spi; + uses ai.docling.serve.api.spi.DoclingServeApiBuilderFactory; } diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java new file mode 100644 index 00000000..dc2a180e --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java @@ -0,0 +1,127 @@ +package ai.docling.serve.api.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +import java.net.URI; +import java.util.List; + +import org.junit.jupiter.api.Test; + +import ai.docling.serve.api.chunk.request.HierarchicalChunkDocumentRequest; +import ai.docling.serve.api.chunk.request.HybridChunkDocumentRequest; +import ai.docling.serve.api.convert.request.BatchConvertDocumentRequest; +import ai.docling.serve.api.convert.request.ConvertDocumentRequest; +import ai.docling.serve.api.convert.request.source.HttpSource; +import ai.docling.serve.api.convert.request.target.InBodyTarget; +import ai.docling.serve.api.convert.request.target.ZipTarget; + +/** + * Unit tests for the {@link DocumentRequest} sealed hierarchy. + */ +class DocumentRequestTests { + private static final HttpSource HTTP_SOURCE = HttpSource.builder() + .url(URI.create("http://example.com/doc.pdf")) + .build(); + + @Test + void sourcesAndTargetAccessibleThroughBaseType() { + var target = InBodyTarget.builder().build(); + + DocumentRequest request = ConvertDocumentRequest.builder() + .sources(List.of(HTTP_SOURCE)) + .target(target) + .build(); + + assertThat(request.getSources()) + .singleElement() + .isEqualTo(HTTP_SOURCE); + + assertThat(request.getTarget()).isSameAs(target); + } + + @Test + void targetIsNullByDefault() { + DocumentRequest request = ConvertDocumentRequest.builder() + .source(HTTP_SOURCE) + .build(); + + assertThat(request.getTarget()).isNull(); + } + + @Test + void sourcesDefaultToEmptyList() { + DocumentRequest request = ConvertDocumentRequest.builder().build(); + + assertThat(request.getSources()).isEmpty(); + } + + @Test + void allConcreteSubtypesAreDistinguishableViaInstanceOf() { + List requests = List.of( + ConvertDocumentRequest.builder().source(HTTP_SOURCE).build(), BatchConvertDocumentRequest.builder().source(HTTP_SOURCE).target(ZipTarget.builder().build()) + .build(), HierarchicalChunkDocumentRequest.builder().source(HTTP_SOURCE).build(), HybridChunkDocumentRequest.builder().source(HTTP_SOURCE).build() + ); + + var results = requests.stream() + .map(DocumentRequestTests::classifyRequest) + .toList(); + + assertThat(results).containsExactly("convert", "batch", "hierarchical-chunk", "hybrid-chunk"); + } + + @Test + void toBuilderPreservesSourcesAndTarget() { + var target = InBodyTarget.builder().build(); + + ConvertDocumentRequest original = ConvertDocumentRequest.builder() + .source(HTTP_SOURCE) + .target(target) + .build(); + + ConvertDocumentRequest copy = original.toBuilder().build(); + + assertThat(copy.getSources()) + .isEqualTo(original.getSources()); + + assertThat(copy.getTarget()).isSameAs(original.getTarget()); + } + + @Test + void batchConvertGetTargetThrowsWhenTargetIsNull() { + BatchConvertDocumentRequest request = BatchConvertDocumentRequest.builder() + .source(HTTP_SOURCE) + .build(); + + assertThatThrownBy(request::getTarget) + .isInstanceOf(NullPointerException.class); + } + + @Test + void toStringIncludesSourcesAndTarget() { + ConvertDocumentRequest request = ConvertDocumentRequest.builder() + .source(HTTP_SOURCE) + .target(InBodyTarget.builder().build()) + .build(); + + assertThat(request.toString()) + .contains("sources") + .contains("target"); + } + + private static String classifyRequest(DocumentRequest request) { + if (request instanceof ConvertDocumentRequest) { + return "convert"; + } + else if (request instanceof BatchConvertDocumentRequest) { + return "batch"; + } + else if (request instanceof HierarchicalChunkDocumentRequest) { + return "hierarchical-chunk"; + } + else if (request instanceof HybridChunkDocumentRequest) { + return "hybrid-chunk"; + } + throw new IllegalArgumentException("Unknown request type: " + request.getClass()); + } +} diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/response/ProcessedDocumentResponseTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/response/ProcessedDocumentResponseTests.java new file mode 100644 index 00000000..20224169 --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/response/ProcessedDocumentResponseTests.java @@ -0,0 +1,80 @@ +package ai.docling.serve.api.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.util.List; + +import org.junit.jupiter.api.Test; + +import ai.docling.serve.api.chunk.response.ChunkDocumentResponse; +import ai.docling.serve.api.convert.response.ConvertDocumentResponse; +import ai.docling.serve.api.convert.response.InBodyConvertDocumentResponse; +import ai.docling.serve.api.convert.response.PreSignedUrlConvertDocumentResponse; +import ai.docling.serve.api.convert.response.PreSignedUrlConvertResponse; +import ai.docling.serve.api.convert.response.ZipArchiveConvertDocumentResponse; + +/** + * Unit tests for the {@link ProcessedDocumentResponse} sealed hierarchy. + */ +class ProcessedDocumentResponseTests { + + @Test + void allConcreteSubtypesAreDistinguishableViaInstanceOf() { + List responses = List.of( + InBodyConvertDocumentResponse.builder().build(), PreSignedUrlConvertDocumentResponse.builder().build(), PreSignedUrlConvertResponse.builder() + .build(), ZipArchiveConvertDocumentResponse.builder().build(), ChunkDocumentResponse.builder().build()); + + var results = responses.stream() + .map(ProcessedDocumentResponseTests::classifyResponse) + .toList(); + + assertThat(results).containsExactly( + "in-body", "pre-signed-url", "pre-signed-url-response", "zip-archive", "chunk"); + } + + @Test + void convertDocumentResponseIsAssignableToBase() { + ProcessedDocumentResponse response = InBodyConvertDocumentResponse.builder().build(); + + assertThat(response) + .isInstanceOf(ProcessedDocumentResponse.class) + .isInstanceOf(ConvertDocumentResponse.class); + } + + @Test + void chunkDocumentResponseIsAssignableToBase() { + ProcessedDocumentResponse response = ChunkDocumentResponse.builder().build(); + + assertThat(response) + .isInstanceOf(ProcessedDocumentResponse.class) + .isInstanceOf(ChunkDocumentResponse.class); + } + + @Test + void sealedPermitsOnlyExpectedSubtypes() { + var permitted = ProcessedDocumentResponse.class.getPermittedSubclasses(); + + assertThat(permitted) + .hasSize(2) + .containsExactlyInAnyOrder(ConvertDocumentResponse.class, ChunkDocumentResponse.class); + } + + private static String classifyResponse(ProcessedDocumentResponse response) { + if (response instanceof InBodyConvertDocumentResponse) { + return "in-body"; + } + else if (response instanceof PreSignedUrlConvertDocumentResponse) { + return "pre-signed-url"; + } + else if (response instanceof PreSignedUrlConvertResponse) { + return "pre-signed-url-response"; + } + else if (response instanceof ZipArchiveConvertDocumentResponse) { + return "zip-archive"; + } + else if (response instanceof ChunkDocumentResponse) { + return "chunk"; + } + throw new IllegalArgumentException("Unknown response type: " + response.getClass()); + } +} diff --git a/docs/src/doc/docs/whats-new.md b/docs/src/doc/docs/whats-new.md index 87546dde..fa69c7d6 100644 --- a/docs/src/doc/docs/whats-new.md +++ b/docs/src/doc/docs/whats-new.md @@ -25,6 +25,11 @@ Docling Java {{ gradle.project_version }} includes important breaking changes, a ### {{ gradle.project_version }} +* **New `DocumentRequest` sealed base class** — `ConvertDocumentRequest`, `BatchConvertDocumentRequest`, and `ChunkDocumentRequest` now extend a common `DocumentRequest` abstract class in the `ai.docling.serve.api.request` package. This enables polymorphism when working with different request types — for example, accepting a `DocumentRequest` and dispatching to the correct endpoint based on the concrete type via pattern matching. +* **New `ProcessedDocumentResponse` sealed base class** — `ConvertDocumentResponse` and `ChunkDocumentResponse` now extend a common `ProcessedDocumentResponse` abstract class in the `ai.docling.serve.api.response` package. This enables polymorphic handling of document processing responses — for example, using `ProcessedDocumentResponse` as a type bound in generic APIs that work with both conversion and chunking results. + +### 0.6.1 + * **New batch conversion support** — Added `convertSourceBatch()` and `convertSourceBatchAsync()` methods to `DoclingServeConvertApi` for the new `/v1/convert/source/batch` endpoint. Submit multiple HTTP or S3 sources for batch processing with optional webhook callbacks for progress notifications. Requires docling-serve v1.22.0+. * **New `BatchConvertDocumentRequest`** — Request model for batch conversions, supporting `sources` (HTTP or S3), `target` (PresignedUrlTarget or S3Target), conversion `options`, and optional `callbacks` (webhook specifications). * **New `CallbackSpec`** — Webhook callback specification for receiving progress notifications during batch processing, with `url`, `headers`, and optional `caCert` fields.