From dc74c29e4a751c218d66b33325154d8d04cc654d Mon Sep 17 00:00:00 2001 From: Eric Deandrea Date: Thu, 11 Dec 2025 15:29:25 -0500 Subject: [PATCH] feat!: Add `apiKey` support to API requests - Introduced `apiKey` field for secure authentication in configuration and API requests. - Updated clear and task APIs to accept enhanced request objects. - Added required tests and assertions for `apiKey` handling across all request types. - Refactored HTTP operations to include `apiKey` in request headers. - Introduced new classes for managing authentication details. Fixes #187 Signed-off-by: Eric Deandrea --- .../serve/api/DoclingServeClearApi.java | 23 ++-- .../serve/api/auth/AuthenticatedRequest.java | 28 +++++ .../serve/api/auth/Authentication.java | 31 +++++ .../docling/serve/api/auth/package-info.java | 4 + .../HierarchicalChunkDocumentRequest.java | 10 +- .../request/HybridChunkDocumentRequest.java | 10 +- .../clear/request/ClearConvertersRequest.java | 30 +++++ ...rRequest.java => ClearResultsRequest.java} | 12 +- .../request/ConvertDocumentRequest.java | 10 +- .../api/task/request/TaskResultRequest.java | 12 +- .../task/request/TaskStatusPollRequest.java | 12 +- .../src/main/java/module-info.java | 3 + .../serve/api/auth/AuthenticationTests.java | 23 ++++ ...HierarchicalChunkDocumentRequestTests.java | 54 +++++++++ .../HybridChunkDocumentRequestTests.java | 54 +++++++++ .../request/ClearConvertersRequestTests.java | 29 +++++ .../api/clear/request/ClearRequestTests.java | 25 ---- .../request/ClearResultsRequestTests.java | 46 ++++++++ .../task/request/TaskResultRequestTests.java | 27 +++++ .../request/TaskStatusPollRequestTests.java | 34 +++++- .../docling/serve/client/ClearOperations.java | 40 ------- .../serve/client/DoclingServeClient.java | 64 +++++++++-- .../{ => operations}/ChunkOperations.java | 6 +- .../client/operations/ClearOperations.java | 50 ++++++++ .../{ => operations}/ConvertOperations.java | 6 +- .../{ => operations}/HealthOperations.java | 6 +- .../{ => operations}/HttpOperations.java | 32 +++++- .../{ => operations}/TaskOperations.java | 10 +- .../serve/client/operations/package-info.java | 4 + .../AbstractDoclingServeClientTests.java | 7 +- docling-testcontainers/build.gradle.kts | 1 + .../serve/DoclingServeContainer.java | 4 + .../config/DefaultDoclingContainerConfig.java | 14 ++- .../config/DoclingServeContainerConfig.java | 33 ++++++ .../DoclingServeContainerAvailableTests.java | 107 +++++++++++++----- docs/src/doc/docs/docling-serve/serve-api.md | 6 +- .../doc/docs/docling-serve/serve-client.md | 1 + docs/src/doc/docs/testcontainers.md | 2 + docs/src/doc/docs/whats-new.md | 2 + 39 files changed, 725 insertions(+), 147 deletions(-) create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/AuthenticatedRequest.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/Authentication.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/package-info.java create mode 100644 docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearConvertersRequest.java rename docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/{ClearRequest.java => ClearResultsRequest.java} (76%) create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/auth/AuthenticationTests.java create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearConvertersRequestTests.java delete mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearRequestTests.java create mode 100644 docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearResultsRequestTests.java delete mode 100644 docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ClearOperations.java rename docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/{ => operations}/ChunkOperations.java (90%) create mode 100644 docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ClearOperations.java rename docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/{ => operations}/ConvertOperations.java (85%) rename docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/{ => operations}/HealthOperations.java (78%) rename docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/{ => operations}/HttpOperations.java (52%) rename docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/{ => operations}/TaskOperations.java (92%) create mode 100644 docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/package-info.java diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeClearApi.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeClearApi.java index 62990f2..c230650 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeClearApi.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeClearApi.java @@ -1,6 +1,7 @@ package ai.docling.serve.api; -import ai.docling.serve.api.clear.request.ClearRequest; +import ai.docling.serve.api.clear.request.ClearConvertersRequest; +import ai.docling.serve.api.clear.request.ClearResultsRequest; import ai.docling.serve.api.clear.response.ClearResponse; /** @@ -11,23 +12,27 @@ */ public interface DoclingServeClearApi { /** - * Clears all registered converters associated with the API. - * This method removes any previously configured or cached converters, - * effectively resetting the converter state to an uninitialized state. - * After invoking this method, no converters will be available until new ones are added or configured. + * Clears all currently configured converters within the Docling Serve API. + * This operation removes any registered converters, effectively resetting + * the system to a state without active converter configurations. + * + * @param request an instance of {@link ClearConvertersRequest} containing + * the authentication details required to authorize this operation. + * @return a {@link ClearResponse} object indicating the status of the clear + * operation, such as success or failure. */ - ClearResponse clearConverters(); + ClearResponse clearConverters(ClearConvertersRequest request); /** - * Clears stored results based on the specified {@link ClearRequest}. + * Clears stored results based on the specified {@link ClearResultsRequest}. * This method removes results that match the criteria provided in the * request, such as results older than a specified duration. * - * @param request an instance of {@link ClearRequest} containing the criteria + * @param request an instance of {@link ClearResultsRequest} containing the criteria * for clearing stored results, including the duration threshold * or other parameters. * @return a {@link ClearResponse} object indicating the status of the clear * operation, such as success or failure. */ - ClearResponse clearResults(ClearRequest request); + ClearResponse clearResults(ClearResultsRequest request); } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/AuthenticatedRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/AuthenticatedRequest.java new file mode 100644 index 0000000..6928d29 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/AuthenticatedRequest.java @@ -0,0 +1,28 @@ +package ai.docling.serve.api.auth; + +/** + * Represents a request that requires authentication for secure access to resources. + * + * Implementations of this interface define a contract for providing authentication + * details associated with the request. This is used to ensure authorized interaction + * with APIs or protected services. + * + * Methods: + * - {@code getAuthentication()}: Retrieves the {@link Authentication} details + * required to authorize the request. + */ +public interface AuthenticatedRequest { + /** + * Retrieves the authentication details associated with the request. + * + * This method provides the {@link Authentication} object required to authorize + * interaction with secure resources or APIs. Implementations should ensure + * the returned object contains the necessary credentials for successful + * authentication. + * + * @return an {@link Authentication} instance containing the authentication + * details required for the request, or {@code null} if authentication + * is not applicable. + */ + Authentication getAuthentication(); +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/Authentication.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/Authentication.java new file mode 100644 index 0000000..4dc4c17 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/Authentication.java @@ -0,0 +1,31 @@ +package ai.docling.serve.api.auth; + +import org.jspecify.annotations.Nullable; + +/** + * Represents authentication details required for secure API usage. + * + * The {@code Authentication} class encapsulates the necessary credentials + * for accessing restricted resources, such as an API key. It is designed + * to be immutable and thread-safe. + * + * This class uses Lombok annotations to provide a builder pattern, generate + * a getter for its fields, and produce a string representation. + * + * Features: + * - {@code apiKey}: Optional API key to authorize requests. + */ +@lombok.Builder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public class Authentication { + /** + * Represents an optional API key used to authenticate requests to a secure service. + * + * This field is nullable, indicating that the API key may not always be provided + * or required, depending on the specific use case or configuration of the client. + * When present, the API key is used to authorize access to restricted resources. + */ + @Nullable + private String apiKey; +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/package-info.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/package-info.java new file mode 100644 index 0000000..a4c33c0 --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/auth/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package ai.docling.serve.api.auth; + +import org.jspecify.annotations.NullMarked; diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequest.java index 159be6f..a3f99f0 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequest.java @@ -4,11 +4,14 @@ import org.jspecify.annotations.Nullable; +import com.fasterxml.jackson.annotation.JsonIgnore; 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.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; import ai.docling.serve.api.chunk.request.options.HierarchicalChunkerOptions; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; import ai.docling.serve.api.convert.request.source.Source; @@ -24,7 +27,7 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class HierarchicalChunkDocumentRequest { +public class HierarchicalChunkDocumentRequest implements AuthenticatedRequest { @JsonProperty("sources") @JsonSetter(nulls = Nulls.AS_EMPTY) @@ -48,6 +51,11 @@ public class HierarchicalChunkDocumentRequest { @lombok.Builder.Default private HierarchicalChunkerOptions chunkingOptions = HierarchicalChunkerOptions.builder().build(); + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); + @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/chunk/request/HybridChunkDocumentRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequest.java index 6e76b8d..d55b626 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequest.java @@ -4,11 +4,14 @@ import org.jspecify.annotations.Nullable; +import com.fasterxml.jackson.annotation.JsonIgnore; 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.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; import ai.docling.serve.api.chunk.request.options.HybridChunkerOptions; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; import ai.docling.serve.api.convert.request.source.Source; @@ -24,7 +27,7 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class HybridChunkDocumentRequest { +public class HybridChunkDocumentRequest implements AuthenticatedRequest { @JsonProperty("sources") @JsonSetter(nulls = Nulls.AS_EMPTY) @@ -48,6 +51,11 @@ public class HybridChunkDocumentRequest { @lombok.Builder.Default private HybridChunkerOptions chunkingOptions = HybridChunkerOptions.builder().build(); + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); + @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/clear/request/ClearConvertersRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearConvertersRequest.java new file mode 100644 index 0000000..a9e9a6e --- /dev/null +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearConvertersRequest.java @@ -0,0 +1,30 @@ +package ai.docling.serve.api.clear.request; + +import com.fasterxml.jackson.annotation.JsonIgnore; + +import ai.docling.serve.api.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; + +/** + * Represents a request to clear or reset configured converters using the Docling Serve API. + * + * This class provides a mechanism to manage converter configurations through explicit + * reset operations. It includes authentication information required to authorize the request. + * + * Features: + * - {@code authentication}: Provides the authentication details needed to validate + * and authorize the reset operation. A default authentication instance is used + * when none is explicitly provided. + * + * This class is immutable and can be constructed or modified using a builder + * that is generated via Lombok annotations. + */ +@lombok.Builder(toBuilder = true) +@lombok.Getter +@lombok.ToString +public class ClearConvertersRequest implements AuthenticatedRequest { + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); +} diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearResultsRequest.java similarity index 76% rename from docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearRequest.java rename to docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearResultsRequest.java index f7667f2..881dfd9 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/clear/request/ClearResultsRequest.java @@ -2,6 +2,11 @@ import java.time.Duration; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import ai.docling.serve.api.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; + /** * Represents a request to clear stale data via the Docling Serve Clear API. * This class provides a mechanism to specify a threshold duration, after which data @@ -17,7 +22,7 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class ClearRequest { +public class ClearResultsRequest implements AuthenticatedRequest { /** * Represents the default duration used as a threshold for clearing stale results * or data in the Docling Serve Clear API. Results older than this duration @@ -30,4 +35,9 @@ public class ClearRequest { @lombok.NonNull @lombok.Builder.Default private Duration olderThen = DEFAULT_OLDER_THAN; + + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); } 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 bcfea23..e41ab91 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 @@ -4,11 +4,14 @@ import org.jspecify.annotations.Nullable; +import com.fasterxml.jackson.annotation.JsonIgnore; 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.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; 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; @@ -27,7 +30,7 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class ConvertDocumentRequest { +public class ConvertDocumentRequest implements AuthenticatedRequest { @JsonProperty("sources") @JsonSetter(nulls = Nulls.AS_EMPTY) @lombok.Singular @@ -42,6 +45,11 @@ public class ConvertDocumentRequest { @Nullable private Target target; + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); + @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/task/request/TaskResultRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskResultRequest.java index f413c3e..414fa21 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskResultRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskResultRequest.java @@ -1,5 +1,10 @@ package ai.docling.serve.api.task.request; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import ai.docling.serve.api.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; + /** * Represents a request to retrieve the result of a task. * @@ -15,7 +20,12 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class TaskResultRequest { +public class TaskResultRequest implements AuthenticatedRequest { @lombok.NonNull private String taskId; + + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); } diff --git a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskStatusPollRequest.java b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskStatusPollRequest.java index c6d7156..134ecf7 100644 --- a/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskStatusPollRequest.java +++ b/docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/task/request/TaskStatusPollRequest.java @@ -2,6 +2,11 @@ import java.time.Duration; +import com.fasterxml.jackson.annotation.JsonIgnore; + +import ai.docling.serve.api.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; + /** * Represents a request for polling the status of a task. * @@ -22,7 +27,7 @@ @lombok.Builder(toBuilder = true) @lombok.Getter @lombok.ToString -public class TaskStatusPollRequest { +public class TaskStatusPollRequest implements AuthenticatedRequest { /** * The default wait time between status polling attempts for a task. *

@@ -39,4 +44,9 @@ public class TaskStatusPollRequest { @lombok.NonNull @lombok.Builder.Default private Duration waitTime = DEFAULT_STATUS_POLL_WAIT_TIME; + + @JsonIgnore + @lombok.NonNull + @lombok.Builder.Default + private Authentication authentication = Authentication.builder().build(); } 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 eb58960..455e531 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 @@ -14,6 +14,9 @@ exports ai.docling.serve.api.health; exports ai.docling.serve.api.util; + // Auth + exports ai.docling.serve.api.auth; + // Chunking API exports ai.docling.serve.api.chunk.request; exports ai.docling.serve.api.chunk.request.options; diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/auth/AuthenticationTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/auth/AuthenticationTests.java new file mode 100644 index 0000000..98e6b00 --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/auth/AuthenticationTests.java @@ -0,0 +1,23 @@ +package ai.docling.serve.api.auth; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +class AuthenticationTests { + @Test + void nothingSpecified() { + assertThat(Authentication.builder().build()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void keySpecified() { + assertThat(Authentication.builder().apiKey("key").build()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); + } +} diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequestTests.java index bdadb09..af45343 100644 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequestTests.java +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HierarchicalChunkDocumentRequestTests.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; +import ai.docling.serve.api.auth.Authentication; import ai.docling.serve.api.chunk.request.options.HierarchicalChunkerOptions; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; import ai.docling.serve.api.convert.request.source.FileSource; @@ -40,6 +41,10 @@ void buildWithHttpSourcesAsList() { .build(); assertThat(request.getSources()).hasSize(1); assertThat(request.getSources().get(0)).isInstanceOf(HttpSource.class); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -54,6 +59,11 @@ void buildWithHttpSourcesAsVarargs() { assertThat(request.getSources()) .hasSize(2) .allSatisfy(source -> assertThat(source).isInstanceOf(HttpSource.class)); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -65,6 +75,11 @@ void buildWithFileSourcesAsList() { assertThat(request.getSources()) .hasSize(1) .allSatisfy(source -> assertThat(source).isInstanceOf(FileSource.class)); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -80,6 +95,11 @@ void buildWithFileSourcesAsVarargs() { assertThat(request.getSources()) .hasSize(2) .allSatisfy(source -> assertThat(source).isInstanceOf(FileSource.class)); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -105,6 +125,11 @@ void hierarchicalChunkDocumentRequestIsImmutable() { .asInstanceOf(type(FileSource.class)) .extracting(FileSource::getFilename, FileSource::getBase64String) .containsExactly("test.txt", "dGVzdCBjb250ZW50"); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -115,6 +140,10 @@ void buildWithIncludeConvertedDoc() { .build(); assertThat(request.isIncludeConvertedDoc()).isTrue(); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -135,6 +164,11 @@ void buildWithChunkingOptions() { assertThat(options.isUseMarkdownTables()).isTrue(); assertThat(options.isIncludeRawText()).isFalse(); }); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -157,6 +191,10 @@ void buildWithAllFields() { assertThat(request.getTarget()).isNotNull(); assertThat(request.isIncludeConvertedDoc()).isTrue(); assertThat(request.getChunkingOptions()).isNotNull(); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -169,5 +207,21 @@ void buildWithDefaultOptions() { assertThat(request.isIncludeConvertedDoc()).isFalse(); assertThat(request.getChunkingOptions()).isNotNull(); assertThat(request.getTarget()).isNull(); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void buildWithAuth() { + var request = HierarchicalChunkDocumentRequest.builder() + .authentication(Authentication.builder().apiKey("key").build()) + .build(); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); } } diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequestTests.java index c39e739..392af4c 100644 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequestTests.java +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/chunk/request/HybridChunkDocumentRequestTests.java @@ -10,6 +10,7 @@ import org.junit.jupiter.api.Test; +import ai.docling.serve.api.auth.Authentication; import ai.docling.serve.api.chunk.request.options.HybridChunkerOptions; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; import ai.docling.serve.api.convert.request.source.FileSource; @@ -40,6 +41,10 @@ void buildWithHttpSourcesAsList() { .build(); assertThat(request.getSources()).hasSize(1); assertThat(request.getSources().get(0)).isInstanceOf(HttpSource.class); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -54,6 +59,11 @@ void buildWithHttpSourcesAsVarargs() { assertThat(request.getSources()) .hasSize(2) .allSatisfy(source -> assertThat(source).isInstanceOf(HttpSource.class)); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -65,6 +75,11 @@ void buildWithFileSourcesAsList() { assertThat(request.getSources()) .hasSize(1) .allSatisfy(source -> assertThat(source).isInstanceOf(FileSource.class)); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -80,6 +95,11 @@ void buildWithFileSourcesAsVarargs() { assertThat(request.getSources()) .hasSize(2) .allSatisfy(source -> assertThat(source).isInstanceOf(FileSource.class)); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -105,6 +125,11 @@ void hybridChunkDocumentRequestIsImmutable() { .asInstanceOf(type(FileSource.class)) .extracting(FileSource::getFilename, FileSource::getBase64String) .containsExactly("test.txt", "dGVzdCBjb250ZW50"); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -115,6 +140,10 @@ void buildWithIncludeConvertedDoc() { .build(); assertThat(request.isIncludeConvertedDoc()).isTrue(); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -141,6 +170,11 @@ void buildWithChunkingOptions() { assertThat(options.isIncludeRawText()).isFalse(); assertThat(options.getMergePeers()).isTrue(); }); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -162,6 +196,10 @@ void buildWithAllFields() { assertThat(request.getTarget()).isNotNull(); assertThat(request.isIncludeConvertedDoc()).isTrue(); assertThat(request.getChunkingOptions()).isNotNull(); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); } @Test @@ -174,5 +212,21 @@ void buildWithDefaultOptions() { assertThat(request.isIncludeConvertedDoc()).isFalse(); assertThat(request.getChunkingOptions()).isNotNull(); assertThat(request.getTarget()).isNull(); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void buildWithAuth() { + var request = HybridChunkDocumentRequest.builder() + .authentication(Authentication.builder().apiKey("key").build()) + .build(); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); } } diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearConvertersRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearConvertersRequestTests.java new file mode 100644 index 0000000..9d0039e --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearConvertersRequestTests.java @@ -0,0 +1,29 @@ +package ai.docling.serve.api.clear.request; + +import static org.assertj.core.api.Assertions.assertThat; + +import org.junit.jupiter.api.Test; + +import ai.docling.serve.api.auth.Authentication; + +class ClearConvertersRequestTests { + @Test + void noAuthSpecified() { + assertThat(ClearConvertersRequest.builder().build()) + .isNotNull() + .extracting(ClearConvertersRequest::getAuthentication) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void authSpecified() { + assertThat(ClearConvertersRequest.builder().authentication(Authentication.builder().apiKey("key").build()).build()) + .isNotNull() + .extracting(ClearConvertersRequest::getAuthentication) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); + } +} diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearRequestTests.java deleted file mode 100644 index 91c0025..0000000 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearRequestTests.java +++ /dev/null @@ -1,25 +0,0 @@ -package ai.docling.serve.api.clear.request; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatExceptionOfType; - -import org.assertj.core.api.InstanceOfAssertFactories; -import org.junit.jupiter.api.Test; - -class ClearRequestTests { - @Test - void nullOlderThen() { - assertThatExceptionOfType(IllegalArgumentException.class) - .isThrownBy(() -> ClearRequest.builder().olderThen(null).build()) - .withMessage("olderThen is marked non-null but is null"); - } - - @Test - void defaultOlderThen() { - assertThat(ClearRequest.builder().build()) - .isNotNull() - .extracting(ClearRequest::getOlderThen) - .asInstanceOf(InstanceOfAssertFactories.DURATION) - .isEqualByComparingTo(ClearRequest.DEFAULT_OLDER_THAN); - } -} diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearResultsRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearResultsRequestTests.java new file mode 100644 index 0000000..bef64d7 --- /dev/null +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/clear/request/ClearResultsRequestTests.java @@ -0,0 +1,46 @@ +package ai.docling.serve.api.clear.request; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import org.assertj.core.api.InstanceOfAssertFactories; +import org.junit.jupiter.api.Test; + +import ai.docling.serve.api.auth.Authentication; + +class ClearResultsRequestTests { + @Test + void nullOlderThen() { + assertThatExceptionOfType(IllegalArgumentException.class) + .isThrownBy(() -> ClearResultsRequest.builder().olderThen(null).build()) + .withMessage("olderThen is marked non-null but is null"); + } + + @Test + void defaultOlderThen() { + var request = ClearResultsRequest.builder().build(); + + assertThat(request) + .isNotNull() + .extracting(ClearResultsRequest::getOlderThen) + .asInstanceOf(InstanceOfAssertFactories.DURATION) + .isEqualByComparingTo(ClearResultsRequest.DEFAULT_OLDER_THAN); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void buildWithAuth() { + var request = ClearResultsRequest.builder() + .authentication(Authentication.builder().apiKey("key").build()) + .build(); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); + } +} diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskResultRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskResultRequestTests.java index c5fdf74..bbb6799 100644 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskResultRequestTests.java +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskResultRequestTests.java @@ -1,9 +1,12 @@ package ai.docling.serve.api.task.request; +import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatExceptionOfType; import org.junit.jupiter.api.Test; +import ai.docling.serve.api.auth.Authentication; + class TaskResultRequestTests { @Test void nullTaskId() { @@ -18,4 +21,28 @@ void noTaskId() { .isThrownBy(() -> TaskResultRequest.builder().build()) .withMessage("taskId is marked non-null but is null"); } + + @Test + void buildWithoutAuth() { + var request = TaskResultRequest.builder().taskId("1").build(); + + assertThat(request.getTaskId()).isEqualTo("1"); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void buildWithAuth() { + var request = TaskResultRequest.builder() + .taskId("1") + .authentication(Authentication.builder().apiKey("key").build()) + .build(); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); + } } diff --git a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskStatusPollRequestTests.java b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskStatusPollRequestTests.java index ecc85e6..a8e10b7 100644 --- a/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskStatusPollRequestTests.java +++ b/docling-serve/docling-serve-api/src/test/java/ai/docling/serve/api/task/request/TaskStatusPollRequestTests.java @@ -6,6 +6,8 @@ import org.assertj.core.api.InstanceOfAssertFactories; import org.junit.jupiter.api.Test; +import ai.docling.serve.api.auth.Authentication; + class TaskStatusPollRequestTests { @Test void nullTaskId() { @@ -30,10 +32,40 @@ void nullWaitTime() { @Test void defaultWaitTime() { - assertThat(TaskStatusPollRequest.builder().taskId("1").build()) + var request = TaskStatusPollRequest.builder().taskId("1").build(); + assertThat(request) .isNotNull() .extracting(TaskStatusPollRequest::getWaitTime) .asInstanceOf(InstanceOfAssertFactories.DURATION) .isEqualByComparingTo(TaskStatusPollRequest.DEFAULT_STATUS_POLL_WAIT_TIME); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void buildWithoutAuth() { + var request = TaskStatusPollRequest.builder().taskId("1").build(); + + assertThat(request.getTaskId()).isEqualTo("1"); + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isNull(); + } + + @Test + void buildWithAuth() { + var request = TaskStatusPollRequest.builder() + .taskId("1") + .authentication(Authentication.builder().apiKey("key").build()) + .build(); + + assertThat(request.getAuthentication()) + .isNotNull() + .extracting(Authentication::getApiKey) + .isEqualTo("key"); } } diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ClearOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ClearOperations.java deleted file mode 100644 index 87bd109..0000000 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ClearOperations.java +++ /dev/null @@ -1,40 +0,0 @@ -package ai.docling.serve.client; - -import ai.docling.serve.api.DoclingServeClearApi; -import ai.docling.serve.api.clear.request.ClearRequest; -import ai.docling.serve.api.clear.response.ClearResponse; -import ai.docling.serve.api.util.ValidationUtils; - -/** - * Base class for clear API operations. Provides functionality for managing and cleaning up - * converters and stale data retained by the service. - */ -final class ClearOperations implements DoclingServeClearApi { - private final HttpOperations httpOperations; - - ClearOperations(HttpOperations httpOperations) { - this.httpOperations = httpOperations; - } - - /** - * Clears all registered converters associated with the API. - */ - public ClearResponse clearConverters() { - return this.httpOperations.executeGet("/v1/clear/converters", ClearResponse.class); - } - - /** - * Clears stale results retained by the service, based on the specified threshold duration. - * Results older than the duration specified in the {@link ClearRequest} parameter will be removed. - * - * @param request the {@link ClearRequest} object containing the threshold duration for clearing results; - * must not be null. - * @return a {@link ClearResponse} object representing the result of the clear operation, - * including the status of the operation. - */ - public ClearResponse clearResults(ClearRequest request) { - ValidationUtils.ensureNotNull(request, "request"); - - return this.httpOperations.executeGet("/v1/clear/results?older_then=%d".formatted(request.getOlderThen().toSeconds()), ClearResponse.class); - } -} diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java index e0abb1a..ede14e6 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/DoclingServeClient.java @@ -12,10 +12,13 @@ import java.net.http.HttpResponse; import java.net.http.HttpResponse.BodyHandlers; import java.nio.ByteBuffer; +import java.util.List; +import java.util.Map; import java.util.Objects; import java.util.Optional; import java.util.concurrent.Flow.Subscriber; +import org.jspecify.annotations.Nullable; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -25,10 +28,13 @@ import ai.docling.serve.api.DoclingServeConvertApi; import ai.docling.serve.api.DoclingServeHealthApi; import ai.docling.serve.api.DoclingServeTaskApi; +import ai.docling.serve.api.auth.AuthenticatedRequest; +import ai.docling.serve.api.auth.Authentication; import ai.docling.serve.api.chunk.request.HierarchicalChunkDocumentRequest; import ai.docling.serve.api.chunk.request.HybridChunkDocumentRequest; import ai.docling.serve.api.chunk.response.ChunkDocumentResponse; -import ai.docling.serve.api.clear.request.ClearRequest; +import ai.docling.serve.api.clear.request.ClearConvertersRequest; +import ai.docling.serve.api.clear.request.ClearResultsRequest; import ai.docling.serve.api.clear.response.ClearResponse; import ai.docling.serve.api.convert.request.ConvertDocumentRequest; import ai.docling.serve.api.convert.response.ConvertDocumentResponse; @@ -36,6 +42,12 @@ import ai.docling.serve.api.task.request.TaskResultRequest; import ai.docling.serve.api.task.request.TaskStatusPollRequest; import ai.docling.serve.api.task.response.TaskStatusPollResponse; +import ai.docling.serve.client.operations.ChunkOperations; +import ai.docling.serve.client.operations.ClearOperations; +import ai.docling.serve.client.operations.ConvertOperations; +import ai.docling.serve.client.operations.HealthOperations; +import ai.docling.serve.client.operations.HttpOperations; +import ai.docling.serve.client.operations.TaskOperations; /** * Abstract class representing a client for interacting with the Docling API. @@ -116,14 +128,32 @@ protected void logRequest(HttpRequest request) { stringBuilder.append("\n→ REQUEST: %s %s\n".formatted(request.method(), request.uri())); stringBuilder.append(" HEADERS:\n"); - request.headers().map().forEach((key, values) -> - stringBuilder.append(" %s: %s\n".formatted(key, String.join(", ", values))) + // Need to mask sensitive headers + request.headers() + .map() + .entrySet() + .stream() + .map(this::maskSensitiveHeaderValues) + .forEach(entry -> stringBuilder.append(" %s: %s\n".formatted(entry.getKey(), String.join(", ", entry.getValue()))) ); LOG.info(stringBuilder.toString()); } } + private boolean isSensitiveHeader(String headerName) { + return API_KEY_HEADER_NAME.equalsIgnoreCase(headerName); + } + + private Map.Entry> maskSensitiveHeaderValues(Map.Entry> entry) { + return Map.entry( + entry.getKey(), + entry.getValue().stream() + .map(value -> isSensitiveHeader(entry.getKey()) ? "*".repeat(value.length()) : value) + .toList() + ); + } + protected void logResponse(HttpResponse response, Optional responseBody) { if (LOG.isInfoEnabled()) { var stringBuilder = new StringBuilder(); @@ -159,8 +189,9 @@ protected T execute(HttpRequest request, Class expectedValueType) { } } + @Override protected O executePost(String uri, I request, Class expectedReturnType) { - var httpRequest = createRequestBuilder(uri) + var httpRequest = createRequestBuilder(uri, request) .header("Content-Type", "application/json") .POST(new LoggingBodyPublisher<>(request)) .build(); @@ -169,18 +200,29 @@ protected O executePost(String uri, I request, Class expectedReturnTyp } @Override - protected O executeGet(String uri, Class expectedReturnType) { - var httpRequest = createRequestBuilder(uri) + protected O executeGet(String uri, I request, Class expectedReturnType) { + var httpRequest = createRequestBuilder(uri, request) .GET() .build(); return execute(httpRequest, expectedReturnType); } - protected HttpRequest.Builder createRequestBuilder(String uri) { - return HttpRequest.newBuilder() + protected HttpRequest.Builder createRequestBuilder(String uri, @Nullable I request) { + var requestBuilder = HttpRequest.newBuilder() .uri(baseUrl.resolve(uri)) .header("Accept", "application/json"); + + // Handle the authentication + Optional.ofNullable(request) + .filter(AuthenticatedRequest.class::isInstance) + .map(AuthenticatedRequest.class::cast) + .map(AuthenticatedRequest::getAuthentication) + .map(Authentication::getApiKey) + .map(String::trim) + .ifPresent(apiKey -> requestBuilder.header(API_KEY_HEADER_NAME, apiKey)); + + return requestBuilder; } protected T getResponse(HttpResponse response, Class expectedReturnType) { @@ -237,12 +279,12 @@ public ChunkDocumentResponse chunkTaskResult(TaskResultRequest request) { } @Override - public ClearResponse clearConverters() { - return this.clearOps.clearConverters(); + public ClearResponse clearConverters(ClearConvertersRequest request) { + return this.clearOps.clearConverters(request); } @Override - public ClearResponse clearResults(ClearRequest request) { + public ClearResponse clearResults(ClearResultsRequest request) { return this.clearOps.clearResults(request); } diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ChunkOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ChunkOperations.java similarity index 90% rename from docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ChunkOperations.java rename to docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ChunkOperations.java index 06d6ff7..41f8522 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ChunkOperations.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ChunkOperations.java @@ -1,4 +1,4 @@ -package ai.docling.serve.client; +package ai.docling.serve.client.operations; import ai.docling.serve.api.DoclingServeChunkApi; import ai.docling.serve.api.chunk.request.HierarchicalChunkDocumentRequest; @@ -10,10 +10,10 @@ * Base class for document chunking API operations. Provides access to document chunking * functionality with both hierarchical and hybrid strategies. */ -final class ChunkOperations implements DoclingServeChunkApi { +public final class ChunkOperations implements DoclingServeChunkApi { private final HttpOperations httpOperations; - ChunkOperations(HttpOperations httpOperations) { + public ChunkOperations(HttpOperations httpOperations) { this.httpOperations = httpOperations; } diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ClearOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ClearOperations.java new file mode 100644 index 0000000..b291ea0 --- /dev/null +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ClearOperations.java @@ -0,0 +1,50 @@ +package ai.docling.serve.client.operations; + +import ai.docling.serve.api.DoclingServeClearApi; +import ai.docling.serve.api.clear.request.ClearConvertersRequest; +import ai.docling.serve.api.clear.request.ClearResultsRequest; +import ai.docling.serve.api.clear.response.ClearResponse; +import ai.docling.serve.api.util.ValidationUtils; + +/** + * Base class for clear API operations. Provides functionality for managing and cleaning up + * converters and stale data retained by the service. + */ +public final class ClearOperations implements DoclingServeClearApi { + private final HttpOperations httpOperations; + + public ClearOperations(HttpOperations httpOperations) { + this.httpOperations = httpOperations; + } + + /** + * Clears all configured converters using the Docling Serve API. + * + * This method performs an HTTP GET request to reset or clear any converters that have + * been configured in the current session. The request may include authentication + * details required to authorize the operation. + * + * @param request the {@link ClearConvertersRequest} object containing the parameters + * for the clear operation. This request object must not be null. + * @return a {@link ClearResponse} object representing the result of the clear operation, + * including details such as the status of the operation. + */ + public ClearResponse clearConverters(ClearConvertersRequest request) { + return this.httpOperations.executeGet("/v1/clear/converters", request, ClearResponse.class); + } + + /** + * Clears stale results retained by the service, based on the specified threshold duration. + * Results older than the duration specified in the {@link ClearResultsRequest} parameter will be removed. + * + * @param request the {@link ClearResultsRequest} object containing the threshold duration for clearing results; + * must not be null. + * @return a {@link ClearResponse} object representing the result of the clear operation, + * including the status of the operation. + */ + public ClearResponse clearResults(ClearResultsRequest request) { + ValidationUtils.ensureNotNull(request, "request"); + + return this.httpOperations.executeGet("/v1/clear/results?older_then=%d".formatted(request.getOlderThen().toSeconds()), request, ClearResponse.class); + } +} diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ConvertOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ConvertOperations.java similarity index 85% rename from docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ConvertOperations.java rename to docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ConvertOperations.java index 4693928..54c95dd 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/ConvertOperations.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/ConvertOperations.java @@ -1,4 +1,4 @@ -package ai.docling.serve.client; +package ai.docling.serve.client.operations; import ai.docling.serve.api.DoclingServeConvertApi; import ai.docling.serve.api.convert.request.ConvertDocumentRequest; @@ -9,10 +9,10 @@ * Base class for document conversion API operations. Provides access to document * conversion functionality. */ -final class ConvertOperations implements DoclingServeConvertApi { +public final class ConvertOperations implements DoclingServeConvertApi { private final HttpOperations httpOperations; - ConvertOperations(HttpOperations httpOperations) { + public ConvertOperations(HttpOperations httpOperations) { this.httpOperations = httpOperations; } diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/HealthOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/HealthOperations.java similarity index 78% rename from docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/HealthOperations.java rename to docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/HealthOperations.java index a8bbf91..2c3c528 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/HealthOperations.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/HealthOperations.java @@ -1,4 +1,4 @@ -package ai.docling.serve.client; +package ai.docling.serve.client.operations; import ai.docling.serve.api.DoclingServeHealthApi; import ai.docling.serve.api.health.HealthCheckResponse; @@ -7,10 +7,10 @@ * Base class for health API operations. Provides access to health check functionality * of the Docling service. */ -final class HealthOperations implements DoclingServeHealthApi { +public final class HealthOperations implements DoclingServeHealthApi { private final HttpOperations httpOperations; - HealthOperations(HttpOperations httpOperations) { + public HealthOperations(HttpOperations httpOperations) { this.httpOperations = httpOperations; } diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/HttpOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/HttpOperations.java similarity index 52% rename from docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/HttpOperations.java rename to docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/HttpOperations.java index 880bd20..49bb04c 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/HttpOperations.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/HttpOperations.java @@ -1,4 +1,4 @@ -package ai.docling.serve.client; +package ai.docling.serve.client.operations; /** * Abstract base class for HTTP operations. Provides methods for executing HTTP requests such as GET and POST @@ -6,24 +6,48 @@ * implement these operations for specific use cases. */ public abstract class HttpOperations { + /** + * The header name used to specify the API key in HTTP requests. + * This constant is commonly utilized in authentication mechanisms + * to include the API key in request headers. + */ + public static final String API_KEY_HEADER_NAME = "X-Api-Key"; + /** * Executes an HTTP GET request to the specified URI and deserializes the response into the given type. * + * @param Parameter type for the request * @param the expected return type for the deserialized response. * @param uri the URI to send the GET request to. + * @param request parameters for the request * @param expectedReturnType the class representing the type to which the response should be deserialized. * @return an instance of the specified type containing the deserialized response data. */ - protected abstract O executeGet(String uri, Class expectedReturnType); + protected abstract O executeGet(String uri, I request, Class expectedReturnType); + + /** + * Executes an HTTP GET request to the specified URI and deserializes + * the response into the given type. + * + * @param the expected return type for the deserialized response. + * @param uri the URI to send the GET request to. Must not be null. + * @param expectedReturnType the class representing the type to which + * the response should be deserialized. Must not be null. + * @return an instance of the specified type containing the deserialized + * response data. + */ + protected O executeGet(String uri, Class expectedReturnType) { + return executeGet(uri, null, expectedReturnType); + } /** * Executes an HTTP POST request to the specified URI with the given request payload and deserializes * the response into the specified return type. * - * @param the type of the request payload sent in the POST request. + * @param Parameter type for the request * @param the expected return type for the deserialized response. * @param uri the URI to send the POST request to. - * @param request the payload to be sent in the POST request. + * @param request parameters for the request * @param expectedReturnType the class representing the type to which the response should be deserialized. * @return an instance of the specified type containing the deserialized response data. */ diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/TaskOperations.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/TaskOperations.java similarity index 92% rename from docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/TaskOperations.java rename to docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/TaskOperations.java index d34d099..a60c1bc 100644 --- a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/TaskOperations.java +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/TaskOperations.java @@ -1,4 +1,4 @@ -package ai.docling.serve.client; +package ai.docling.serve.client.operations; import ai.docling.serve.api.DoclingServeTaskApi; import ai.docling.serve.api.chunk.response.ChunkDocumentResponse; @@ -12,10 +12,10 @@ * Base class for task API operations. Provides operations for managing and querying * the status of asynchronous tasks. */ -final class TaskOperations implements DoclingServeTaskApi { +public final class TaskOperations implements DoclingServeTaskApi { private final HttpOperations httpOperations; - TaskOperations(HttpOperations httpOperations) { + public TaskOperations(HttpOperations httpOperations) { this.httpOperations = httpOperations; } @@ -61,7 +61,7 @@ public TaskStatusPollResponse pollTaskStatus(TaskStatusPollRequest request) { */ public ConvertDocumentResponse convertTaskResult(TaskResultRequest request) { ValidationUtils.ensureNotNull(request, "request"); - return this.httpOperations.executeGet("/v1/result/%s".formatted(request.getTaskId()), ConvertDocumentResponse.class); + return this.httpOperations.executeGet("/v1/result/%s".formatted(request.getTaskId()), request, ConvertDocumentResponse.class); } /** @@ -79,6 +79,6 @@ public ConvertDocumentResponse convertTaskResult(TaskResultRequest request) { */ public ChunkDocumentResponse chunkTaskResult(TaskResultRequest request) { ValidationUtils.ensureNotNull(request, "request"); - return this.httpOperations.executeGet("/v1/result/%s".formatted(request.getTaskId()), ChunkDocumentResponse.class); + return this.httpOperations.executeGet("/v1/result/%s".formatted(request.getTaskId()), request, ChunkDocumentResponse.class); } } diff --git a/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/package-info.java b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/package-info.java new file mode 100644 index 0000000..ffffad3 --- /dev/null +++ b/docling-serve/docling-serve-client/src/main/java/ai/docling/serve/client/operations/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package ai.docling.serve.client.operations; + +import org.jspecify.annotations.NullMarked; diff --git a/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java b/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java index badeab4..1f63b36 100644 --- a/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java +++ b/docling-serve/docling-serve-client/src/test/java/ai/docling/serve/client/AbstractDoclingServeClientTests.java @@ -43,7 +43,8 @@ import ai.docling.serve.api.chunk.request.options.HybridChunkerOptions; import ai.docling.serve.api.chunk.response.Chunk; import ai.docling.serve.api.chunk.response.ChunkDocumentResponse; -import ai.docling.serve.api.clear.request.ClearRequest; +import ai.docling.serve.api.clear.request.ClearConvertersRequest; +import ai.docling.serve.api.clear.request.ClearResultsRequest; import ai.docling.serve.api.clear.response.ClearResponse; import ai.docling.serve.api.convert.request.ConvertDocumentRequest; import ai.docling.serve.api.convert.request.options.ConvertDocumentOptions; @@ -108,7 +109,7 @@ private String writeValueAsString(T value) { class ClearTests { @Test void shouldClearConvertersSuccessfully() { - var response = getDoclingClient().clearConverters(); + var response = getDoclingClient().clearConverters(ClearConvertersRequest.builder().build()); assertThat(response) .isNotNull() @@ -118,7 +119,7 @@ void shouldClearConvertersSuccessfully() { @Test void shouldClearResultsSuccessfully() { - var response = getDoclingClient().clearResults(ClearRequest.builder().build()); + var response = getDoclingClient().clearResults(ClearResultsRequest.builder().build()); assertThat(response) .isNotNull() diff --git a/docling-testcontainers/build.gradle.kts b/docling-testcontainers/build.gradle.kts index 3e83884..1241b19 100644 --- a/docling-testcontainers/build.gradle.kts +++ b/docling-testcontainers/build.gradle.kts @@ -21,4 +21,5 @@ dependencies { testImplementation(libs.jackson.databind) testImplementation(libs.testcontainers.junit.jupiter) testImplementation(libs.slf4j.simple) + testImplementation(project(":docling-serve-client")) } diff --git a/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/DoclingServeContainer.java b/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/DoclingServeContainer.java index 7fa3b3e..10909c2 100644 --- a/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/DoclingServeContainer.java +++ b/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/DoclingServeContainer.java @@ -53,6 +53,10 @@ public DoclingServeContainer(DoclingServeContainerConfig config) { } withStartupTimeout(config.startupTimeout()); + + Optional.ofNullable(config.apiKey()) + .map(String::strip) + .ifPresent(apiKey -> withEnv("DOCLING_SERVE_API_KEY", apiKey)); } /** diff --git a/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DefaultDoclingContainerConfig.java b/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DefaultDoclingContainerConfig.java index 53a9606..f9503fa 100644 --- a/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DefaultDoclingContainerConfig.java +++ b/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DefaultDoclingContainerConfig.java @@ -5,19 +5,25 @@ import java.util.Map; import java.util.Optional; +import org.jspecify.annotations.Nullable; + final class DefaultDoclingContainerConfig implements DoclingServeContainerConfig { private final String image; private final boolean enableUi; private final Map containerEnv; private final Duration startupTimeout; + @Nullable + private final String apiKey; + DefaultDoclingContainerConfig(Builder builder) { - if ((builder.image== null) || builder.image.strip().isEmpty()) { + if ((builder.image == null) || builder.image.strip().isEmpty()) { throw new IllegalArgumentException("image must be specified"); } this.enableUi = builder.enableUi; this.image = builder.image; + this.apiKey = builder.apiKey; this.startupTimeout = Optional.ofNullable(builder.startupTimeout) .orElse(Duration.ofMinutes(1)); this.containerEnv = Optional.ofNullable(builder.containerEnv) @@ -44,4 +50,10 @@ public Map containerEnv() { public Duration startupTimeout() { return this.startupTimeout; } + + @Nullable + @Override + public String apiKey() { + return this.apiKey; + } } diff --git a/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DoclingServeContainerConfig.java b/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DoclingServeContainerConfig.java index b08edf7..04bfe8b 100644 --- a/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DoclingServeContainerConfig.java +++ b/docling-testcontainers/src/main/java/ai/docling/testcontainers/serve/config/DoclingServeContainerConfig.java @@ -3,6 +3,8 @@ import java.time.Duration; import java.util.Map; +import org.jspecify.annotations.Nullable; + /** * A configuration interface for defining properties and settings related * to the Docling container. This interface provides methods for retrieving @@ -68,6 +70,14 @@ public interface DoclingServeContainerConfig { */ Duration startupTimeout(); + /** + * Retrieves the API key associated with the configuration. + * + * @return a {@code String} representing the API key. + */ + @Nullable + String apiKey(); + /** * Creates a new instance of {@link Builder} initialized with the current configuration values. * @@ -132,6 +142,16 @@ class Builder { */ protected Duration startupTimeout; + /** + * The API key used for authenticating with external services or systems. + * This key is typically required to enable secure communication and interaction + * with third-party APIs or services. + * + * This field is protected and may be configured through the {@link Builder} class. + */ + @Nullable + protected String apiKey; + /** * Initializes a new instance of the {@link Builder} class. * This constructor is protected to restrict direct instantiation outside the package @@ -151,6 +171,7 @@ protected Builder(DoclingServeContainerConfig config) { this.enableUi = config.enableUi(); this.containerEnv = config.containerEnv(); this.startupTimeout = config.startupTimeout(); + this.apiKey = config.apiKey(); } /** @@ -214,6 +235,18 @@ public Builder startupTimeout(Duration startupTimeout) { return this; } + /** + * Sets the API key to be used for the container configuration. + * + * @param apiKey the API key as a string; this value is used to authenticate + * or authorize the container's operations + * @return the builder instance for method chaining + */ + public Builder apiKey(String apiKey) { + this.apiKey = apiKey; + return this; + } + /** * Builds and returns a new instance of {@link DoclingServeContainerConfig} based on the current builder state. * diff --git a/docling-testcontainers/src/test/java/ai/docling/testcontainers/serve/DoclingServeContainerAvailableTests.java b/docling-testcontainers/src/test/java/ai/docling/testcontainers/serve/DoclingServeContainerAvailableTests.java index 4a75977..2d8ae2b 100644 --- a/docling-testcontainers/src/test/java/ai/docling/testcontainers/serve/DoclingServeContainerAvailableTests.java +++ b/docling-testcontainers/src/test/java/ai/docling/testcontainers/serve/DoclingServeContainerAvailableTests.java @@ -1,18 +1,21 @@ package ai.docling.testcontainers.serve; import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.io.IOException; -import java.net.URI; -import java.net.http.HttpClient; -import java.net.http.HttpRequest; import java.net.http.HttpResponse; -import java.time.Duration; import org.junit.jupiter.api.Test; import org.testcontainers.junit.jupiter.Container; import org.testcontainers.junit.jupiter.Testcontainers; +import ai.docling.serve.api.auth.Authentication; +import ai.docling.serve.api.clear.request.ClearResultsRequest; +import ai.docling.serve.api.clear.response.ClearResponse; +import ai.docling.serve.api.health.HealthCheckResponse; +import ai.docling.serve.client.DoclingServeClientBuilderFactory; +import ai.docling.serve.client.DoclingServeClientException; import ai.docling.testcontainers.serve.config.DoclingServeContainerConfig; import tools.jackson.databind.json.JsonMapper; @@ -20,7 +23,7 @@ @Testcontainers class DoclingServeContainerAvailableTests { private static final JsonMapper JSON_MAPPER = JsonMapper.builder().build(); - private record HealthResponse(String status) {} + private static final String DEFAULT_API_KEY = "default-api-key"; @Container private final DoclingServeContainer doclingContainer = new DoclingServeContainer( @@ -38,22 +41,25 @@ private record HealthResponse(String status) {} .build() ); + @Container + private final DoclingServeContainer withApiKeyDoclingContainer = new DoclingServeContainer( + DoclingServeContainerConfig.builder() + .image(DoclingServeContainerConfig.DOCLING_IMAGE) + .enableUi(true) + .apiKey(DEFAULT_API_KEY) + .build() + ); + @Test - void containerNoUI() throws IOException, InterruptedException { - var healthRequest = HttpRequest.newBuilder(URI.create("%s/health".formatted(this.noUiDoclingContainer.getApiUrl()))) - .header("Accept", "application/json") - .timeout(Duration.ofSeconds(10)) - .GET() + void containerNoUI() { + var client = DoclingServeClientBuilderFactory.newBuilder() + .baseUrl(this.noUiDoclingContainer.getApiUrl()) .build(); - var response = HttpClient.newHttpClient() - .send(healthRequest, jsonBodyHandler(HealthResponse.class)) - .body(); - - assertThat(response) + assertThat(client.health()) .isNotNull() - .usingRecursiveComparison() - .isEqualTo(new HealthResponse("ok")); + .extracting(HealthCheckResponse::getStatus) + .isEqualTo("ok"); assertThat(this.noUiDoclingContainer.getUiUrl()) .isNotNull() @@ -62,26 +68,71 @@ void containerNoUI() throws IOException, InterruptedException { @Test void containerAvailable() throws IOException, InterruptedException { - var healthRequest = HttpRequest.newBuilder(URI.create("%s/health".formatted(this.doclingContainer.getApiUrl()))) - .header("Accept", "application/json") - .timeout(Duration.ofSeconds(10)) - .GET() + var client = DoclingServeClientBuilderFactory.newBuilder() + .baseUrl(this.noUiDoclingContainer.getApiUrl()) .build(); - var response = HttpClient.newHttpClient() - .send(healthRequest, jsonBodyHandler(DoclingServeContainerAvailableTests.HealthResponse.class)) - .body(); - - assertThat(response) + assertThat(client.health()) .isNotNull() - .usingRecursiveComparison() - .isEqualTo(new HealthResponse("ok")); + .extracting(HealthCheckResponse::getStatus) + .isEqualTo("ok"); assertThat(this.doclingContainer.getUiUrl()) .get() .isEqualTo("%s/ui".formatted(this.doclingContainer.getApiUrl())); } + @Test + void containerWithApiKeyClientWithout() { + var client = DoclingServeClientBuilderFactory.newBuilder() + .baseUrl(this.withApiKeyDoclingContainer.getApiUrl()) + .logRequests() + .logResponses() + .prettyPrint() + .build(); + + assertThatThrownBy(() -> client.clearResults(ClearResultsRequest.builder().build())) + .hasRootCauseInstanceOf(DoclingServeClientException.class) + .rootCause() + .hasMessageContaining("Unauthorized") + .extracting(t -> ((DoclingServeClientException) t).getStatusCode()) + .isEqualTo(401); + } + + @Test + void containerWithoutApiKeyClientWith() { + var client = DoclingServeClientBuilderFactory.newBuilder() + .baseUrl(this.doclingContainer.getApiUrl()) + .logRequests() + .logResponses() + .prettyPrint() + .build(); + + var auth = Authentication.builder().apiKey(DEFAULT_API_KEY).build(); + + assertThat(client.clearResults(ClearResultsRequest.builder().authentication(auth).build())) + .isNotNull() + .extracting(ClearResponse::getStatus) + .isEqualTo("ok"); + } + + @Test + void containerWithApiKeyClientWith() { + var client = DoclingServeClientBuilderFactory.newBuilder() + .baseUrl(this.withApiKeyDoclingContainer.getApiUrl()) + .logRequests() + .logResponses() + .prettyPrint() + .build(); + + var auth = Authentication.builder().apiKey(DEFAULT_API_KEY).build(); + + assertThat(client.clearResults(ClearResultsRequest.builder().authentication(auth).build())) + .isNotNull() + .extracting(ClearResponse::getStatus) + .isEqualTo("ok"); + } + private static HttpResponse.BodyHandler jsonBodyHandler(Class type) { return responseInfo -> HttpResponse.BodySubscribers.mapping( HttpResponse.BodySubscribers.ofByteArray(), diff --git a/docs/src/doc/docs/docling-serve/serve-api.md b/docs/src/doc/docs/docling-serve/serve-api.md index 9e2a65f..709ba25 100644 --- a/docs/src/doc/docs/docling-serve/serve-api.md +++ b/docs/src/doc/docs/docling-serve/serve-api.md @@ -85,11 +85,7 @@ System.out.println(response.getDocument().getMarkdownContent()); ### The `DoclingServeApi` interface -Defined in `ai.docling.serve.api.DoclingServeApi`, this interface exposes two primary operations: - -- `health()` → returns a `HealthCheckResponse` with service status. -- `convertSource(request)` → submits one or more sources plus options and an optional target, - returning `ConvertDocumentResponse`. +Defined in `ai.docling.serve.api.DoclingServeApi`, this interface exposes many operations: Any HTTP or non-HTTP implementation can implement this interface. The reference implementation is provided by the `docling-serve-client` module. diff --git a/docs/src/doc/docs/docling-serve/serve-client.md b/docs/src/doc/docs/docling-serve/serve-client.md index bf21455..af49351 100644 --- a/docs/src/doc/docs/docling-serve/serve-client.md +++ b/docs/src/doc/docs/docling-serve/serve-client.md @@ -134,6 +134,7 @@ DoclingServeApi noisy = DoclingServeClientBuilderFactory.newBuilder() .baseUrl("http://localhost:8000") .logRequests() .logResponses() + .prettyPrint() .build(); ``` diff --git a/docs/src/doc/docs/testcontainers.md b/docs/src/doc/docs/testcontainers.md index f9532dc..e11068b 100644 --- a/docs/src/doc/docs/testcontainers.md +++ b/docs/src/doc/docs/testcontainers.md @@ -143,6 +143,8 @@ DoclingServeContainerConfig config = DoclingServeContainerConfig.builder() )) // Increase startup timeout if your CI is slow to pull images .startupTimeout(Duration.ofMinutes(2)) + // Set an optional api key for that consumers must specify + .apiKey("my-secret-api-key") .build(); DoclingServeContainer container = new DoclingServeContainer(config); diff --git a/docs/src/doc/docs/whats-new.md b/docs/src/doc/docs/whats-new.md index 5c51fe6..2a394a8 100644 --- a/docs/src/doc/docs/whats-new.md +++ b/docs/src/doc/docs/whats-new.md @@ -8,6 +8,8 @@ Docling Java {{ gradle.project_version }} provides a number of new features, enh * Implementation of the Docling Serve clear and task APIs in `docling-serve-api` and `docling-serve-client`. * Adding `pretty-print` configuration option to `DoclingServeClient` to enable pretty printing of JSON requests and responses. +* Adding the ability to specify an api key for the Docling Serve Testcontainer. +* Adding the ability to specify the api key for the Docling Serve requests. ### 0.3.0