From 3229acfbdcfcf65c1e8f597cb9eecaa0dea89e31 Mon Sep 17 00:00:00 2001 From: Daniel Imber Date: Sun, 14 Jun 2026 14:55:25 +0100 Subject: [PATCH] feat(api): add PreSignedUrlConvertResponse for PresignedUrlTarget docling-serve has two distinct response classes for presigned-URL flows (responses.py:236-243): - PresignedUrlConvertDocumentResponse: counts-only response returned when the request targets S3Target or PutTarget, where the document content is delivered out-of-band to the client-supplied target. - PresignedUrlConvertResponse: counts plus a per-source documents list returned when the request targets PresignedUrlTarget, where the server uploads each artifact to its own object storage and returns presigned GET URLs in-band. The existing PreSignedUrlConvertDocumentResponse covers the first case. This commit adds the second as a sibling class under the existing sealed ConvertDocumentResponse hierarchy, plus the supporting types referenced by it: - PreSignedUrlConvertResponse: final, mirrors all counts plus a documents field, returns ResponseType.PRE_SIGNED_URL_RESPONSE. - DocumentArtifactItem: per-source entry on the response. - ArtifactRef: a single presigned URL plus its artifact_type, mime_type, and url_expires_at. - ArtifactType: enum discriminating which output format the artifact represents (JSON, HTML, MARKDOWN, TEXT, DOCTAGS, RESOURCE_BUNDLE). - ConversionStatus: enum for the terminal per-document outcome (PENDING, STARTED, SUCCESS, PARTIAL_SUCCESS, FAILURE, SKIPPED). - ProfilingItem: per-stage timing record, populated when DOCLING_DEBUG_PROFILE_PIPELINE_TIMINGS=true on the worker. - ProfilingScope: enum for ProfilingItem.scope (PAGE, DOCUMENT). ConvertDocumentResponse permits clause and @JsonSubTypes are updated to include the new subtype. Jackson's DEDUCTION dispatch routes responses carrying a documents field to PreSignedUrlConvertResponse and counts-only responses to PreSignedUrlConvertDocumentResponse. The existing PreSignedUrlConvertDocumentResponse, its tests, and the ResponseType.PRE_SIGNED_URL value are unchanged. ResponseType gains a new PRE_SIGNED_URL_RESPONSE entry for the new sibling, and its class- level javadoc is updated to correct the previous description of PRE_SIGNED_URL ("packaged as a ZIP archive and uploaded to the given target URI") which inverted the relationship. Tests follow the established target-test pattern: builder construction plus AssertJ getter assertions for the new types and a both-null and all-fields pair on ConvertDocumentResponseTests for the new sibling. Signed-off-by: Daniel Imber --- .../api/convert/response/ArtifactRef.java | 73 +++++++++++ .../api/convert/response/ArtifactType.java | 25 ++++ .../convert/response/ConversionStatus.java | 25 ++++ .../response/ConvertDocumentResponse.java | 3 +- .../response/DocumentArtifactItem.java | 114 +++++++++++++++++ .../response/PreSignedUrlConvertResponse.java | 116 ++++++++++++++++++ .../api/convert/response/ProfilingItem.java | 81 ++++++++++++ .../api/convert/response/ProfilingScope.java | 17 +++ .../api/convert/response/ResponseType.java | 17 ++- .../convert/response/ArtifactRefTests.java | 55 +++++++++ .../ConvertDocumentResponseTests.java | 48 ++++++++ .../response/DocumentArtifactItemTests.java | 78 ++++++++++++ 12 files changed, 648 insertions(+), 4 deletions(-) create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java new file mode 100644 index 00000000..280d1115 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactRef.java @@ -0,0 +1,73 @@ +package ai.docling.serve.api.convert.response; + +import java.net.URI; +import java.time.Instant; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents a reference to a single output artifact returned as a presigned URL. + * + *

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

+ */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@tools.jackson.databind.annotation.JsonDeserialize(builder = ArtifactRef.Builder.class) +@lombok.extern.jackson.Jacksonized +@lombok.Builder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public class ArtifactRef { + + /** + * Export format of the artifact. + * + * @param artifactType the artifact type + * @return the artifact type + */ + @JsonProperty("artifact_type") + private ArtifactType artifactType; + + /** + * MIME type of the artifact content. + * + * @param mimeType the MIME type + * @return the MIME type + */ + @JsonProperty("mime_type") + private String mimeType; + + /** + * Presigned URL used to download the artifact. + * + * @param uri the presigned URL + * @return the presigned URL + */ + @JsonProperty("uri") + private URI uri; + + /** + * Instant at which the presigned URL signature stops being valid. + * + * @param urlExpiresAt the expiry timestamp + * @return the expiry timestamp + */ + @JsonProperty("url_expires_at") + private Instant urlExpiresAt; + + /** + * Builder for creating {@link ArtifactRef} instances. + * Generated by Lombok's {@code @Builder} annotation. + * + *

Builder methods: + *

+ */ + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + public static class Builder { } +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java new file mode 100644 index 00000000..29a4a540 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ArtifactType.java @@ -0,0 +1,25 @@ +package ai.docling.serve.api.convert.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the output format of a converted-document artifact. + * Each value is mapped to a specific JSON property for serialization and deserialization. + * + * + */ +public enum ArtifactType { + @JsonProperty("json") JSON, + @JsonProperty("html") HTML, + @JsonProperty("markdown") MARKDOWN, + @JsonProperty("text") TEXT, + @JsonProperty("doctags") DOCTAGS, + @JsonProperty("resource_bundle") RESOURCE_BUNDLE; +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java new file mode 100644 index 00000000..f35f8366 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ConversionStatus.java @@ -0,0 +1,25 @@ +package ai.docling.serve.api.convert.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the possible conversion outcomes for a document. + * Each status is mapped to a specific JSON property for serialization and deserialization. + * + * + */ +public enum ConversionStatus { + @JsonProperty("pending") PENDING, + @JsonProperty("started") STARTED, + @JsonProperty("success") SUCCESS, + @JsonProperty("partial_success") PARTIAL_SUCCESS, + @JsonProperty("failure") FAILURE, + @JsonProperty("skipped") SKIPPED; +} 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 8b7dc49a..a36104b4 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 @@ -18,10 +18,11 @@ @JsonSubTypes({ @JsonSubTypes.Type(InBodyConvertDocumentResponse.class), @JsonSubTypes.Type(PreSignedUrlConvertDocumentResponse.class), + @JsonSubTypes.Type(PreSignedUrlConvertResponse.class), @JsonSubTypes.Type(ZipArchiveConvertDocumentResponse.class) }) public abstract sealed class ConvertDocumentResponse permits InBodyConvertDocumentResponse, PreSignedUrlConvertDocumentResponse, - ZipArchiveConvertDocumentResponse { + PreSignedUrlConvertResponse, ZipArchiveConvertDocumentResponse { /** * Type of response * diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java new file mode 100644 index 00000000..9952297d --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/DocumentArtifactItem.java @@ -0,0 +1,114 @@ +package ai.docling.serve.api.convert.response; + +import java.util.List; +import java.util.Map; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +/** + * Represents the conversion outcome and artifact references for a single source document. + * + *

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

+ */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@tools.jackson.databind.annotation.JsonDeserialize(builder = DocumentArtifactItem.Builder.class) +@lombok.extern.jackson.Jacksonized +@lombok.Builder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public class DocumentArtifactItem { + + /** + * Zero-based index of the source document within the originating task. + * + * @param sourceIndex the source index + * @return the source index + */ + @JsonProperty("source_index") + private Integer sourceIndex; + + /** + * Canonical identifier of the source document. + * + * @param sourceUri the source URI + * @return the source URI + */ + @JsonProperty("source_uri") + private String sourceUri; + + /** + * Filename used as the stem of each output artifact. + * + * @param filename the source filename + * @return the source filename + */ + @JsonProperty("filename") + private String filename; + + /** + * Terminal conversion outcome for this document. + * + * @param status the conversion status + * @return the conversion status + */ + @JsonProperty("status") + private ConversionStatus status; + + /** + * Errors encountered while converting this document. + * + * @param errors the list of errors + * @return the list of errors + */ + @JsonProperty("errors") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private List errors; + + /** + * Per-stage timing breakdown keyed by stage name. + * + * @param timings the timings map + * @return the timings map + */ + @JsonProperty("timings") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private Map timings; + + /** + * Presigned URLs for each requested output format. + * + * @param artifacts the list of artifact references + * @return the list of artifact references + */ + @JsonProperty("artifacts") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private List artifacts; + + /** + * Builder for creating {@link DocumentArtifactItem} instances. + * Generated by Lombok's {@code @Builder} annotation. + * + *

Builder methods: + *

    + *
  • {@code sourceIndex(Integer)} - Set the source index
  • + *
  • {@code sourceUri(String)} - Set the source URI
  • + *
  • {@code filename(String)} - Set the source filename
  • + *
  • {@code status(ConversionStatus)} - Set the conversion status
  • + *
  • {@code errors(List)} - Set the list of errors
  • + *
  • {@code error(ErrorItem)} - Add a single error (use with @Singular)
  • + *
  • {@code timings(Map)} - Set the timings map
  • + *
  • {@code timing(String, ProfilingItem)} - Add a single timing entry (use with @Singular)
  • + *
  • {@code artifacts(List)} - Set the list of artifact references
  • + *
  • {@code artifact(ArtifactRef)} - Add a single artifact reference (use with @Singular)
  • + *
+ */ + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + public static class Builder { } +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java new file mode 100644 index 00000000..586e50be --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/PreSignedUrlConvertResponse.java @@ -0,0 +1,116 @@ +package ai.docling.serve.api.convert.response; + +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +/** + * Response for document conversions that target server-managed presigned URLs. + * + *

This response type is returned when the conversion request specifies a + * {@link ai.docling.serve.api.convert.request.target.PresignedUrlTarget}. Each + * source document is represented by a {@link DocumentArtifactItem} in + * {@link #getDocuments() documents} which carries its conversion outcome and + * the list of presigned URLs the server produced for the requested output + * formats.

+ * + *

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

+ * + * @see ConvertDocumentResponse + * @see ResponseType#PRE_SIGNED_URL_RESPONSE + * @see ai.docling.serve.api.convert.request.target.PresignedUrlTarget + * @see DocumentArtifactItem + */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@tools.jackson.databind.annotation.JsonDeserialize(builder = PreSignedUrlConvertResponse.Builder.class) +@lombok.extern.jackson.Jacksonized +@lombok.Builder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public final class PreSignedUrlConvertResponse extends ConvertDocumentResponse { + + /** + * Total processing time in seconds. + * + * @param processingTime the processing time in seconds + * @return the processing time in seconds + */ + @JsonProperty("processing_time") + private Double processingTime; + + /** + * Number of attempted conversions. + * + * @param numConverted the number of attempted conversions + * @return the number of attempted conversions + */ + @JsonProperty("num_converted") + private Integer numConverted; + + /** + * Number of successful conversions. + * + * @param numSucceeded the number of successful conversions + * @return the number of successful conversions + */ + @JsonProperty("num_succeeded") + private Integer numSucceeded; + + /** + * Number of partial successes. + * + * @param numPartiallySucceeded the number of partial successes + * @return the number of partial successes + */ + @JsonProperty("num_partially_succeeded") + private Integer numPartiallySucceeded; + + /** + * Number of failed conversions. + * + * @param numFailed the number of failed conversions + * @return the number of failed conversions + */ + @JsonProperty("num_failed") + private Integer numFailed; + + /** + * Per-source conversion outcomes and presigned artifact URLs. + * + * @param documents the list of per-source artifact items + * @return the list of per-source artifact items + */ + @JsonProperty("documents") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private List documents; + + @Override + @lombok.ToString.Include + public ResponseType getResponseType() { + return ResponseType.PRE_SIGNED_URL_RESPONSE; + } + + /** + * Builder for creating {@link PreSignedUrlConvertResponse} instances. + * Generated by Lombok's {@code @Builder} annotation. + * + *

Builder methods: + *

    + *
  • {@code processingTime(Double)} - Set the total processing time in seconds
  • + *
  • {@code numConverted(Integer)} - Set the number of attempted conversions
  • + *
  • {@code numSucceeded(Integer)} - Set the number of successful conversions
  • + *
  • {@code numPartiallySucceeded(Integer)} - Set the number of partial successes
  • + *
  • {@code numFailed(Integer)} - Set the number of failed conversions
  • + *
  • {@code documents(List)} - Set the list of per-source artifact items
  • + *
  • {@code document(DocumentArtifactItem)} - Add a single per-source artifact item (use with @Singular)
  • + *
+ */ + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + public static class Builder { } + +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java new file mode 100644 index 00000000..383000e4 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingItem.java @@ -0,0 +1,81 @@ +package ai.docling.serve.api.convert.response; + +import java.time.Instant; +import java.util.List; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonSetter; +import com.fasterxml.jackson.annotation.Nulls; + +/** + * Represents per-stage timing measurements produced during a document conversion. + * + *

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

+ */ +@JsonInclude(JsonInclude.Include.NON_EMPTY) +@tools.jackson.databind.annotation.JsonDeserialize(builder = ProfilingItem.Builder.class) +@lombok.extern.jackson.Jacksonized +@lombok.Builder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public class ProfilingItem { + + /** + * Scope of the stage being measured. + * + * @param scope the profiling scope + * @return the profiling scope + */ + @JsonProperty("scope") + private ProfilingScope scope; + + /** + * Number of measurements recorded. + * + * @param count the measurement count + * @return the measurement count + */ + @JsonProperty("count") + private Integer count; + + /** + * Per-measurement durations in seconds. + * + * @param times the list of durations + * @return the list of durations + */ + @JsonProperty("times") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private List times; + + /** + * Start timestamps for each measurement. + * + * @param startTimestamps the list of start timestamps + * @return the list of start timestamps + */ + @JsonProperty("start_timestamps") + @JsonSetter(nulls = Nulls.AS_EMPTY) + @lombok.Singular + private List startTimestamps; + + /** + * Builder for creating {@link ProfilingItem} instances. + * Generated by Lombok's {@code @Builder} annotation. + * + *

Builder methods: + *

    + *
  • {@code scope(ProfilingScope)} - Set the profiling scope
  • + *
  • {@code count(Integer)} - Set the measurement count
  • + *
  • {@code times(List)} - Set the list of durations
  • + *
  • {@code time(Double)} - Add a single duration (use with @Singular)
  • + *
  • {@code startTimestamps(List)} - Set the list of start timestamps
  • + *
  • {@code startTimestamp(Instant)} - Add a single start timestamp (use with @Singular)
  • + *
+ */ + @tools.jackson.databind.annotation.JsonPOJOBuilder(withPrefix = "") + public static class Builder { } +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java new file mode 100644 index 00000000..ac33b0af --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ProfilingScope.java @@ -0,0 +1,17 @@ +package ai.docling.serve.api.convert.response; + +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * Represents the scope of a profiling measurement. + * Each value is mapped to a specific JSON property for serialization and deserialization. + * + *
    + *
  • {@code PAGE}: Indicates that the measurement is recorded per page.
  • + *
  • {@code DOCUMENT}: Indicates that the measurement is recorded per document.
  • + *
+ */ +public enum ProfilingScope { + @JsonProperty("page") PAGE, + @JsonProperty("document") DOCUMENT; +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java index 608c2355..4e7ad254 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/convert/response/ResponseType.java @@ -13,15 +13,19 @@ * ({@link InBodyConvertDocumentResponse}) *
  • {@link #ZIP_ARCHIVE} - Content is packaged and returned as a ZIP archive * ({@link ZipArchiveConvertDocumentResponse})
  • - *
  • {@link #PRE_SIGNED_URL} - Content is packaged as a ZIP archive and uploaded to the given target URI - * and statistical information is returned. + *
  • {@link #PRE_SIGNED_URL} - Content is uploaded to a client-supplied remote target and + * only aggregate processing statistics are returned. * ({@link PreSignedUrlConvertDocumentResponse})
  • + *
  • {@link #PRE_SIGNED_URL_RESPONSE} - Each output artifact is uploaded to the server's + * configured object storage and returned as a time-limited presigned URL grouped by source document. + * ({@link PreSignedUrlConvertResponse})
  • * * * @see ConvertDocumentResponse * @see InBodyConvertDocumentResponse * @see ZipArchiveConvertDocumentResponse * @see PreSignedUrlConvertDocumentResponse + * @see PreSignedUrlConvertResponse */ public enum ResponseType { @@ -44,5 +48,12 @@ public enum ResponseType { * */ @JsonProperty("presigned_url") - PRE_SIGNED_URL + PRE_SIGNED_URL, + + /** + * Represents response type - {@link PreSignedUrlConvertResponse} + * + */ + @JsonProperty("presigned_url_response") + PRE_SIGNED_URL_RESPONSE } diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java new file mode 100644 index 00000000..35ce6f76 --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ArtifactRefTests.java @@ -0,0 +1,55 @@ +package ai.docling.serve.api.convert.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.time.Instant; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link ArtifactRef}. + */ +class ArtifactRefTests { + + @Test + void whenBuiltWithAllFieldsThenGettersReturnSetValues() { + URI uri = URI.create("https://example.com/doc.json"); + Instant expiresAt = Instant.parse("2026-06-15T11:22:41Z"); + + ArtifactRef ref = ArtifactRef.builder() + .artifactType(ArtifactType.JSON) + .mimeType("application/json") + .uri(uri) + .urlExpiresAt(expiresAt) + .build(); + + assertThat(ref.getArtifactType()).isEqualTo(ArtifactType.JSON); + assertThat(ref.getMimeType()).isEqualTo("application/json"); + assertThat(ref.getUri()).isEqualTo(uri); + assertThat(ref.getUrlExpiresAt()).isEqualTo(expiresAt); + } + + @Test + void whenBuiltWithNullFieldsThenGettersReturnNull() { + ArtifactRef ref = ArtifactRef.builder().build(); + + assertThat(ref.getArtifactType()).isNull(); + assertThat(ref.getMimeType()).isNull(); + assertThat(ref.getUri()).isNull(); + assertThat(ref.getUrlExpiresAt()).isNull(); + } + + @Test + void toBuilderRoundtripsToEqualInstance() { + ArtifactRef original = ArtifactRef.builder() + .artifactType(ArtifactType.MARKDOWN) + .mimeType("text/markdown") + .uri(URI.create("https://example.com/doc.md")) + .build(); + + ArtifactRef roundtripped = original.toBuilder().build(); + + assertThat(roundtripped).usingRecursiveComparison().isEqualTo(original); + } +} diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java index f6f95a73..b34eace1 100644 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/ConvertDocumentResponseTests.java @@ -87,6 +87,54 @@ void createPreSignedUrlConvertDocumentResponseWithNullFields() { assertThat(response.getNumSucceeded()).isNull(); } + @Test + void createPreSignedUrlConvertResponseWithNullFields() { + PreSignedUrlConvertResponse response = PreSignedUrlConvertResponse.builder().build(); + + assertThat(response.getNumConverted()).isNull(); + assertThat(response.getNumFailed()).isNull(); + assertThat(response.getNumPartiallySucceeded()).isNull(); + assertThat(response.getNumSucceeded()).isNull(); + assertThat(response.getProcessingTime()).isNull(); + assertThat(response.getDocuments()).isNotNull().isEmpty(); + } + + @Test + void createPreSignedUrlConvertResponseWithAllFields() { + ArtifactRef jsonArtifact = ArtifactRef.builder() + .artifactType(ArtifactType.JSON) + .mimeType("application/json") + .uri(java.net.URI.create("https://example.com/doc.json")) + .urlExpiresAt(java.time.Instant.parse("2026-06-15T11:22:41Z")) + .build(); + DocumentArtifactItem document = DocumentArtifactItem.builder() + .sourceIndex(0) + .sourceUri("https://example.com/example.pdf") + .filename("example.pdf") + .status(ConversionStatus.SUCCESS) + .artifact(jsonArtifact) + .build(); + + PreSignedUrlConvertResponse response = PreSignedUrlConvertResponse.builder() + .processingTime(2.41) + .numConverted(1) + .numSucceeded(1) + .numPartiallySucceeded(0) + .numFailed(0) + .document(document) + .build(); + + assertThat(response.getProcessingTime()).isEqualTo(2.41); + assertThat(response.getNumConverted()).isEqualTo(1); + assertThat(response.getNumSucceeded()).isEqualTo(1); + assertThat(response.getNumPartiallySucceeded()).isZero(); + assertThat(response.getNumFailed()).isZero(); + assertThat(response.getDocuments()).hasSize(1); + assertThat(response.getDocuments().get(0).getStatus()).isEqualTo(ConversionStatus.SUCCESS); + assertThat(response.getDocuments().get(0).getArtifacts()).hasSize(1); + assertThat(response.getDocuments().get(0).getArtifacts().get(0).getArtifactType()).isEqualTo(ArtifactType.JSON); + } + @Test void createResponseWithEmptyCollections() { DocumentResponse document = DocumentResponse.builder() diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java new file mode 100644 index 00000000..21093b70 --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/convert/response/DocumentArtifactItemTests.java @@ -0,0 +1,78 @@ +package ai.docling.serve.api.convert.response; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.net.URI; +import java.util.List; + +import org.junit.jupiter.api.Test; + +/** + * Unit tests for {@link DocumentArtifactItem}. + */ +class DocumentArtifactItemTests { + + @Test + void whenBuiltWithAllFieldsThenGettersReturnSetValues() { + ArtifactRef jsonRef = ArtifactRef.builder() + .artifactType(ArtifactType.JSON) + .mimeType("application/json") + .uri(URI.create("https://example.com/doc.json")) + .build(); + ArtifactRef mdRef = ArtifactRef.builder() + .artifactType(ArtifactType.MARKDOWN) + .mimeType("text/markdown") + .uri(URI.create("https://example.com/doc.md")) + .build(); + ErrorItem error = ErrorItem.builder() + .componentType("pipeline") + .errorMessage("page 3 malformed") + .moduleName("StandardPdfPipeline") + .build(); + + DocumentArtifactItem item = DocumentArtifactItem.builder() + .sourceIndex(0) + .sourceUri("https://example.com/example.pdf") + .filename("example.pdf") + .status(ConversionStatus.PARTIAL_SUCCESS) + .error(error) + .artifact(jsonRef) + .artifact(mdRef) + .build(); + + assertThat(item.getSourceIndex()).isZero(); + assertThat(item.getSourceUri()).isEqualTo("https://example.com/example.pdf"); + assertThat(item.getFilename()).isEqualTo("example.pdf"); + assertThat(item.getStatus()).isEqualTo(ConversionStatus.PARTIAL_SUCCESS); + assertThat(item.getErrors()).containsExactly(error); + assertThat(item.getArtifacts()).containsExactly(jsonRef, mdRef); + assertThat(item.getTimings()).isNotNull().isEmpty(); + } + + @Test + void whenBuiltWithNullCollectionsThenGettersReturnEmpty() { + DocumentArtifactItem item = DocumentArtifactItem.builder() + .sourceIndex(0) + .filename("example.pdf") + .status(ConversionStatus.SUCCESS) + .build(); + + assertThat(item.getErrors()).isNotNull().isEmpty(); + assertThat(item.getTimings()).isNotNull().isEmpty(); + assertThat(item.getArtifacts()).isNotNull().isEmpty(); + } + + @Test + void errorsListIsImmutableAfterBuild() { + ErrorItem first = ErrorItem.builder().errorMessage("first").build(); + + DocumentArtifactItem item = DocumentArtifactItem.builder() + .filename("example.pdf") + .status(ConversionStatus.FAILURE) + .errors(List.of(first)) + .build(); + + assertThat(item.getErrors()).containsExactly(first); + assertThat(item.getErrors()).isUnmodifiable(); + } +}