From 7cf8632825ff57fc2163bcc57f96c414446ead6b Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Mon, 17 Aug 2026 13:07:32 -0400 Subject: [PATCH] feat(api): introduce DocumentRequest and ProcessedDocumentResponse sealed base classes Introduce sealed type hierarchies for both requests and responses in the docling-serve-api module, enabling exhaustive pattern matching and polymorphic handling of document processing operations. DocumentRequest (ai.docling.serve.api.request) is an abstract sealed class with common fields (sources, target) that permits ConvertDocumentRequest, BatchConvertDocumentRequest, and ChunkDocumentRequest. ProcessedDocumentResponse (ai.docling.serve.api.response) is an abstract sealed marker class that permits ConvertDocumentResponse (itself sealed) and ChunkDocumentResponse (final). This enables consumers to use a common type bound when working with either conversion or chunking results. Closes #632 Assisted-By: Claude Code Signed-off-by: Eric Deandrea --- .../serve/api/DoclingServeConvertApi.java | 5 +- .../chunk/request/ChunkDocumentRequest.java | 38 +----- .../chunk/response/ChunkDocumentResponse.java | 21 +-- .../request/BatchConvertDocumentRequest.java | 61 +++------ .../request/ConvertDocumentRequest.java | 55 +------- .../response/ConvertDocumentResponse.java | 7 +- .../serve/api/request/DocumentRequest.java | 69 ++++++++++ .../serve/api/request/package-info.java | 4 + .../response/ProcessedDocumentResponse.java | 30 +++++ .../serve/api/response/package-info.java | 4 + .../src/main/java/module-info.java | 3 + .../api/request/DocumentRequestTests.java | 127 ++++++++++++++++++ .../ProcessedDocumentResponseTests.java | 80 +++++++++++ docs/src/doc/docs/whats-new.md | 5 + 14 files changed, 374 insertions(+), 135 deletions(-) create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/DocumentRequest.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/request/package-info.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/ProcessedDocumentResponse.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/response/package-info.java create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/request/DocumentRequestTests.java create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/response/ProcessedDocumentResponseTests.java 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.