Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions .github/project.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
release:
previous-version: 0.6.6
current-version: 0.6.7
next-version: 0.6.8
current-version: 0.7.0
next-version: 0.7.1
4 changes: 2 additions & 2 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,14 +28,14 @@ This is a multi-module Gradle project (Kotlin DSL) with group `ai.docling`. Vers
### Modules

- **`docling-core`**: Java types mirroring the [docling-core](https://github.com/docling-project/docling-core) Python library's document representation model. Uses Lombok and JSpecify. Jackson is a `compileOnly` dependency — consumers must bring their own.
- **`docling-serve-api`** (`docling-serve/docling-serve-api`): Framework-agnostic API interfaces for interacting with a [Docling Serve](https://github.com/docling-project/docling-serve) backend. Defines `DoclingServeApi` (extends `DoclingServeHealthApi`, `DoclingServeConvertApi`, `DoclingServeChunkApi`, `DoclingServeClearApi`, `DoclingServeTaskApi`) and the SPI interface `DoclingServeApiBuilderFactory` used for discovery via `java.util.ServiceLoader`.
- **`docling-serve-api`** (`docling-serve/docling-serve-api`): Framework-agnostic API interfaces for interacting with a [Docling Serve](https://github.com/docling-project/docling-serve) backend. Defines `DoclingServeApi` (extends `DoclingServeHealthApi`, `DoclingServeConvertApi`, `DoclingServeChunkApi`, `DoclingServeClearApi`, `DoclingServeTaskApi`) and the SPI interface `DoclingServeApiProvider` used for discovery via `java.util.ServiceLoader` (the older `DoclingServeApiBuilderFactory` is deprecated and only used as a fallback).
- **`docling-serve-client`** (`docling-serve/docling-serve-client`): Reference implementation using Java's `HttpClient`. Provides `DoclingServeJackson2Client` and `DoclingServeJackson3Client` — concrete implementations for Jackson 2.x and 3.x respectively. `DoclingServeClient` (abstract) contains all HTTP logic.
- **`docling-testcontainers`**: Testcontainers module exposing `DoclingServeContainer` for spinning up a Docling Serve Docker container in tests.
- **`docling-testing/docling-version-tests`**: Internal tooling for running compatibility tests across Docling Serve container versions.

### Key Design Patterns

**SPI for client discovery**: `DoclingServeApi.builder()` uses `ServiceLoader` to discover a `DoclingServeApiBuilderFactory`. Exactly one implementation must be on the classpath — having zero or more than one throws `IllegalStateException`. The `docling-serve-client` module registers itself as the factory.
**SPI for client discovery**: `DoclingServeApi.builder()` returns a `DoclingServeApiBuilder` that collects an immutable `DoclingServeApiConfig`; `build()` uses `ServiceLoader` to discover a `DoclingServeApiProvider` and calls `create(config)`. Exactly one provider must be on the classpath — zero or more than one throws `IllegalStateException`. If no provider is found, deprecated `DoclingServeApiBuilderFactory` implementations are adapted as a fallback. Providers declare options they cannot honor via `unsupportedOptions()`; new options are added to `DoclingServeApiConfig` (a constant in `ALL_OPTIONS`, an accessor and a builder setter — guarded by `DoclingServeApiConfigTests`), never to the deprecated `DoclingApiBuilder`. Every `DoclingServeApi` implements the abstract `config()`, which must report the effective value of every option; `DoclingServeApi.toBuilder()` is deprecated in favor of `config().toBuilder()`. The `docling-serve-client` module registers `DoclingServeClientProvider`.

**Dual Jackson support**: All Jackson dependencies are `compileOnly` in production code. Jackson 2.x and 3.x are both supported through parallel implementations. Consumers must include one Jackson version on their classpath.

Expand Down
2 changes: 2 additions & 0 deletions docling-serve/docling-serve-api/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,11 @@ nativeImageMetadata {

dependencies {
api(project(":docling-core"))
api(libs.slf4j.api)
compileOnly(platform(libs.jackson.bom))
compileOnly(libs.jackson.annotations)
compileOnly(libs.jackson.databind)
compileOnly(libs.jackson2.databind)
testImplementation(project(":docling-testcontainers"))
testImplementation(libs.slf4j.simple)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
package ai.docling.serve.api;

import static ai.docling.serve.api.util.ValidationUtils.ensureNotBlank;
import static ai.docling.serve.api.util.ValidationUtils.ensureNotNull;

/**
* A typed key identifying a single configuration option of a {@link DoclingServeApiConfig}.
*
* <p>The set of options is closed: instances can only be created by the {@code ai.docling.serve.api}
* package, and every available option is exposed as a {@code public static final} constant on
* {@link DoclingServeApiConfig}. Options are compared by identity.
*
* <p>Options are primarily useful to implementors of {@link ai.docling.serve.api.spi.DoclingServeApiProvider}, to declare
* which options they do not honor via {@link ai.docling.serve.api.spi.DoclingServeApiProvider#unsupportedOptions()}, and to
* check whether a caller explicitly set an option via {@link DoclingServeApiConfig#isExplicitlySet(ConfigOption)}.
*
* @param <T> the type of the option's value
*/
public final class ConfigOption<T> {
private final String name;
private final Class<T> type;

private ConfigOption(String name, Class<T> type) {
this.name = ensureNotBlank(name, "name");
this.type = ensureNotNull(type, "type");
}

/**
* Creates a new option. Package-private so that the set of options stays closed.
*
* @param name the name of the option, matching the accessor on {@link DoclingServeApiConfig}
* @param type the type of the option's value
* @param <T> the type of the option's value
* @return a new option
*/
static <T> ConfigOption<T> of(String name, Class<T> type) {
return new ConfigOption<>(name, type);
}

/**
* The name of this option. It matches the name of the corresponding accessor on
* {@link DoclingServeApiConfig} and of the corresponding setter on {@link DoclingServeApiBuilder}.
*
* @return the name of this option
*/
public String name() {
return this.name;
}

/**
* The type of this option's value.
*
* @return the type of this option's value
*/
public Class<T> type() {
return this.type;
}

@Override
public String toString() {
return this.name;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,59 +4,59 @@

import java.net.URI;
import java.time.Duration;
import java.util.concurrent.Executor;
import java.util.stream.Collectors;

import org.jspecify.annotations.Nullable;

Check failure on line 8 in docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeApi.java

View workflow job for this annotation

GitHub Actions / jvm-build-test-docling-serve-api-java17

package org.jspecify.annotations is not visible

Check failure on line 8 in docling-serve/docling-serve-api/src/main/java/ai/docling/serve/api/DoclingServeApi.java

View workflow job for this annotation

GitHub Actions / jvm-build-test-docling-serve-api-java26

package org.jspecify.annotations is not visible

import ai.docling.serve.api.convert.request.ConvertDocumentRequest;
import ai.docling.serve.api.spi.DoclingServeApiBuilderFactory;
import ai.docling.serve.api.spi.ServiceLoaderHelper;

/**
* Docling Serve API interface.
*/
public interface DoclingServeApi extends DoclingServeHealthApi, DoclingServeConvertApi, DoclingServeChunkApi, DoclingServeClearApi, DoclingServeTaskApi {

/**
* Creates and returns a builder instance capable of constructing implementations of {@link DoclingServeApi}.
* The method ensures that exactly one factory capable of building a builder instance is available
* via the {@link DoclingServeApiBuilderFactory} interface.
* Creates a new builder for a {@link DoclingServeApi}.
*
* If no factories are found, or if multiple factories are found, an {@link IllegalStateException} is thrown.
* <p>Calling {@link DoclingServeApiBuilder#build()} creates the API using the single
* {@link ai.docling.serve.api.spi.DoclingServeApiProvider} available through {@link java.util.ServiceLoader}. If none is
* available, it falls back to the deprecated {@link ai.docling.serve.api.spi.DoclingServeApiBuilderFactory} SPI.
*
* @param <T> the type of the {@link DoclingServeApi} implementation being built
* @param <B> the type of the builder implementation for the {@link DoclingServeApi}
* @return a builder instance of type {@code B} constructed using the available factory
* @throws IllegalStateException if no factories or more than one factory are found
* @return a new builder with no option set
*/
static <T extends DoclingServeApi, B extends DoclingApiBuilder<T, B>> B builder() {
var factories = ServiceLoaderHelper.loadFactories(DoclingServeApiBuilderFactory.class);

if (factories.isEmpty()) {
// No factory found
throw new IllegalStateException("No instance of %s found to build a %s instance. You are probably missing a library on your classpath."
.formatted(DoclingServeApiBuilderFactory.class.getName(), DoclingApiBuilder.class.getName()));
}

if (factories.size() > 1) {
// Multiple factories found
throw new IllegalStateException("Multiple instances of %s found to build a %s instance: [%s]".formatted(DoclingServeApiBuilderFactory.class.getName(), DoclingApiBuilder.class
.getName(), factories.stream().map(f -> f.getClass().getName()).collect(Collectors.joining(", "))));
}

// Only 1 factory (what we want)
return factories.iterator().next().getBuilder();
static DoclingServeApiBuilder builder() {
return new DoclingServeApiBuilder();
}

/**
* The configuration this API runs with.
*
* <p>Use {@code config().toBuilder()} to create a modified copy of this API through the available
* {@link ai.docling.serve.api.spi.DoclingServeApiProvider}, for example
* {@code api.config().toBuilder().logRequests().build()}.
*
* <p>Implementations must report the effective value of every option, so that an API built from the
* returned configuration behaves like this one. An implementation created from a
* {@link DoclingServeApiConfig} may simply return it.
*
* @return the configuration of this API
*/
DoclingServeApiConfig config();

/**
* Creates and returns a builder instance capable of constructing a duplicate or modified
* version of the current API instance. The builder provides a customizable way to adjust
* configuration or properties before constructing a new API instance.
*
* @param <T> the type of the {@link DoclingServeApi} implementation being built
* @param <B> the type of the builder implementation
* @return a {@link DoclingApiBuilder} initialized with the state of the current API instance.
* @deprecated Use {@code config().toBuilder()} instead, which does not depend on the implementation.
* Implementation-specific builders remain available from the {@code toBuilder()} method
* of the concrete implementation.
*/
@SuppressWarnings("unchecked")
@Deprecated(since = "0.7.0", forRemoval = true)
@SuppressWarnings({
"unchecked",
"removal"
})
<T extends DoclingServeApi, B extends DoclingApiBuilder<T, B>> DoclingApiBuilder<T, B> toBuilder();

/**
Expand All @@ -65,7 +65,11 @@
*
* @param <T> the type of the {@link DoclingServeApi} implementation being built.
* @param <B> the type of the concrete builder implementation.
* @deprecated This interface only exists to support the deprecated {@link ai.docling.serve.api.spi.DoclingServeApiBuilderFactory} SPI.
* Use {@link DoclingServeApi#builder()} to configure an API, and implement {@link ai.docling.serve.api.spi.DoclingServeApiProvider}
* to provide one. This interface will not gain new configuration options.
*/
@Deprecated(since = "0.7.0", forRemoval = true)
interface DoclingApiBuilder<T extends DoclingServeApi, B extends DoclingApiBuilder<T, B>> {
/**
* Sets the base URL for the client.
Expand Down Expand Up @@ -184,7 +188,7 @@
* Sets the polling interval for async operations.
*
* <p>This configures how frequently the client will check the status of async
* conversion tasks when using {@link DoclingServeApi#convertSourceAsync(ConvertDocumentRequest)} (ConvertDocumentRequest)}.
* conversion tasks when using {@link DoclingServeApi#convertSourceAsync(ai.docling.serve.api.convert.request.ConvertDocumentRequest)} (ConvertDocumentRequest)}.
*
* @param asyncPollInterval the polling interval (must not be null or negative)
* @return this builder instance for method chaining
Expand All @@ -196,39 +200,14 @@
* Sets the timeout for async operations.
*
* <p>This configures the maximum time to wait for an async conversion task to complete
* when using {@link DoclingServeApi#convertSourceAsync(ConvertDocumentRequest)} (ConvertDocumentRequest)}.
* when using {@link DoclingServeApi#convertSourceAsync(ai.docling.serve.api.convert.request.ConvertDocumentRequest)} (ConvertDocumentRequest)}.
*
* @param asyncTimeout the timeout duration (must not be null or negative)
* @return this builder instance for method chaining
* @throws IllegalArgumentException if asyncTimeout is null or negative
*/
B asyncTimeout(Duration asyncTimeout);

/**
* Sets the {@link Executor} used to run async operations.
*
* <p>This configures where the work of the async methods (such as
* {@link DoclingServeApi#convertSourceAsync(ConvertDocumentRequest)}) is executed: submitting
* the task, polling for its status and retrieving its result. If not set, async operations run
* on the default async executor of {@link java.util.concurrent.CompletableFuture}.
*
* <p>The lifecycle of the executor is owned by the caller: the client never shuts it down.
* Avoid direct executors such as {@code Runnable::run}: the blocking HTTP requests would then run
* on the calling thread, making the async methods partially blocking, and on the shared scheduler
* thread of {@link java.util.concurrent.CompletableFuture#delayedExecutor(long, java.util.concurrent.TimeUnit, Executor)}.
*
* <p>The default implementation throws {@link UnsupportedOperationException}, so that existing
* builder implementations keep compiling; builders supporting a custom executor override it.
*
* @param asyncExecutor the executor to use for async operations (must not be null)
* @return this builder instance for method chaining
* @throws IllegalArgumentException if asyncExecutor is null
* @throws UnsupportedOperationException if this builder does not support a custom executor
*/
default B asyncExecutor(Executor asyncExecutor) {
throw new UnsupportedOperationException("A custom async executor is not supported by " + getClass().getName());
}

/**
* Builds and returns an instance of the specified type, representing the completed configuration
* of the builder. The returned instance is typically an implementation of the Docling API.
Expand Down
Loading
Loading