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
Original file line number Diff line number Diff line change
Expand Up @@ -44,19 +44,19 @@ public DoclingServeContainer(DoclingServeContainerConfig config) {
this.config = config;

// Configure the container
withExposedPorts(DEFAULT_DOCLING_PORT);
withEnv(config.containerEnv());
waitingFor(Wait.forHttp("/health"));
this.withExposedPorts(DEFAULT_DOCLING_PORT);
this.withEnv(config.containerEnv());
this.waitingFor(Wait.forHttp("/health"));

if (config.enableUi()) {
withEnv("DOCLING_SERVE_ENABLE_UI", "true");
this.withEnv("DOCLING_SERVE_ENABLE_UI", "true");
}

withStartupTimeout(config.startupTimeout());
this.withStartupTimeout(config.startupTimeout());

Optional.ofNullable(config.apiKey())
.map(String::strip)
.ifPresent(apiKey -> withEnv("DOCLING_SERVE_API_KEY", apiKey));
.ifPresent(apiKey -> this.withEnv("DOCLING_SERVE_API_KEY", apiKey));
}

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ final class DefaultDoclingContainerConfig implements DoclingServeContainerConfig
private final String apiKey;

DefaultDoclingContainerConfig(Builder builder) {
if ((builder.image == null) || builder.image.strip().isEmpty()) {
if ((builder.image == null) || builder.image.isBlank()) {
throw new IllegalArgumentException("image must be specified");
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package ai.docling.testcontainers.serve.config;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

import org.jspecify.annotations.Nullable;

Expand Down Expand Up @@ -106,8 +108,13 @@ class Builder {
* The name of the container image to be used in the configuration.
* This variable holds the identifier or tag of the container image
* that will be utilized when creating or configuring container instances.
* It is a required field and must be set to a non-null, non-empty value.
* <p>
* It may be {@code null} while the builder is still being configured, but a
* non-null, non-empty, non-blank value is required by the time {@link #build()} is called;
* otherwise {@link #build()} throws {@link IllegalArgumentException}.
* </p>
*/
@Nullable
Comment thread
edeandrea marked this conversation as resolved.
protected String image;

/**
Expand All @@ -122,13 +129,16 @@ class Builder {
* A map representing the environment variables to be passed to the container.
* The keys in the map represent the names of the environment variables,
* and the corresponding values represent their assigned values.
*
* <p>
* This field is used to configure additional environment-specific settings
* during the container's initialization and execution.
*
* Modifications to this map may be reflected in the container's runtime environment,
* affecting its behavior as defined by the provided environment variables.
* </p>
* <p>
* Mutations to a map passed into {@link #containerEnv(Map)} are not reflected, as the builder makes a defensive copy.
Comment thread
edeandrea marked this conversation as resolved.
* Use {@link #containerEnv(String, String)} to add variables to this builder instance.
* </p>
*/
Comment thread
edeandrea marked this conversation as resolved.
@Nullable
protected Map<String, String> containerEnv;

/**
Expand All @@ -140,6 +150,7 @@ class Builder {
* allows precise specification of time intervals.
* </p>
*/
@Nullable
protected Duration startupTimeout;

/**
Expand Down Expand Up @@ -169,18 +180,18 @@ protected Builder() {
protected Builder(DoclingServeContainerConfig config) {
this.image = config.image();
this.enableUi = config.enableUi();
this.containerEnv = config.containerEnv();
this.containerEnv = new HashMap<>(config.containerEnv());
this.startupTimeout = config.startupTimeout();
Comment thread
edeandrea marked this conversation as resolved.
this.apiKey = config.apiKey();
}

/**
* Sets the name of the container image to use for the configuration.
* This value is required and must not be null or empty.
* This value is required and must not be null, empty, or blank.
*
* @param image the name of the container image
* @return the builder instance for method chaining
* @throws IllegalArgumentException if the provided image is null or empty
* @throws IllegalArgumentException if the provided image is null, empty, or blank
*/
public Builder image(String image) {
this.image = image;
Expand All @@ -200,12 +211,22 @@ public Builder enableUi(boolean enableUi) {

/**
* Sets the environment variables to be passed to the container.
* <p>
* The provided map is defensively copied, so later external mutations do not
* affect this builder. Passing {@code null} clears any previously configured
* environment variables, and the built configuration falls back to an empty map.
* </p>
*
* @param containerEnv a map containing the environment variable names and their corresponding values
* @param containerEnv a map containing the environment variable names and their corresponding values,
* or {@code null} to clear the environment variables
* @return the builder instance for method chaining
*/
public Builder containerEnv(Map<String, String> containerEnv) {
this.containerEnv = containerEnv;
public Builder containerEnv(@Nullable Map<String, String> containerEnv) {
if (containerEnv != null) {
this.containerEnv = new HashMap<>(containerEnv);
} else {
this.containerEnv = null;
}
Comment thread
edeandrea marked this conversation as resolved.
return this;
}

Expand All @@ -214,12 +235,17 @@ public Builder containerEnv(Map<String, String> containerEnv) {
* This method allows setting a single key-value pair representing
* an environment variable and its value to be passed to the container.
*
* @param key the name of the environment variable
* @param value the value of the environment variable
* @param key the name of the environment variable; must not be null
* @param value the value of the environment variable; must not be null
* @return the builder instance for method chaining
* @throws NullPointerException if the key or value is null
*/
public Builder containerEnv(String key, String value) {
Objects.requireNonNull(key, "key must not be null");
Objects.requireNonNull(value, "value must not be null");
if (this.containerEnv == null) {
this.containerEnv = new HashMap<>();
}
this.containerEnv.put(key, value);
return this;
}
Comment thread
edeandrea marked this conversation as resolved.
Expand All @@ -238,11 +264,11 @@ public Builder startupTimeout(Duration startupTimeout) {
/**
* 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
* @param apiKey the API key as a string, used to authenticate or authorize the
* container's operations, or {@code null} to omit the API key
* @return the builder instance for method chaining
*/
public Builder apiKey(String apiKey) {
public Builder apiKey(@Nullable String apiKey) {
this.apiKey = apiKey;
return this;
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
package ai.docling.testcontainers.serve.config;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatCode;
import static org.assertj.core.api.Assertions.assertThatNullPointerException;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.atomic.AtomicReference;

import org.junit.jupiter.api.Test;

class DoclingServeContainerConfigTests {

@Test
void toBuilderPreservesExistingContainerEnv() {
var original = DoclingServeContainerConfig.builder()
.image(DoclingServeContainerConfig.DOCLING_IMAGE)
.containerEnv(Map.of("FIRST", "1"))
.build();

var copy = original.toBuilder().build();

assertThat(copy.containerEnv()).containsExactlyInAnyOrderEntriesOf(Map.of("FIRST", "1"));
}

@Test
void toBuilderAllowsAddingContainerEnvWithoutMutatingOriginal() {
var original = DoclingServeContainerConfig.builder()
.image(DoclingServeContainerConfig.DOCLING_IMAGE)
.containerEnv(Map.of("FIRST", "1"))
.build();

var updated = new AtomicReference<DoclingServeContainerConfig>();
assertThatCode(() -> updated.set(original.toBuilder()
.containerEnv("SECOND", "2")
.build()))
.doesNotThrowAnyException();

assertThat(updated.get().containerEnv())
.containsExactlyInAnyOrderEntriesOf(Map.of("FIRST", "1", "SECOND", "2"));

// The original configuration must remain untouched by the builder copy.
assertThat(original.containerEnv()).containsExactlyInAnyOrderEntriesOf(Map.of("FIRST", "1"));
}

@Test
void containerEnvMakesDefensiveCopyOfCallerMap() {
var source = new HashMap<String, String>();
source.put("FIRST", "1");

var config = DoclingServeContainerConfig.builder()
.image(DoclingServeContainerConfig.DOCLING_IMAGE)
.containerEnv(source)
.build();

// Mutating the caller-provided map after the call must not leak into the config.
source.put("SECOND", "2");
source.remove("FIRST");

assertThat(config.containerEnv()).containsExactlyInAnyOrderEntriesOf(Map.of("FIRST", "1"));
}

@Test
void containerEnvKeyValueRejectsNullKeyFailFast() {
var builder = DoclingServeContainerConfig.builder();

assertThatNullPointerException().isThrownBy(() -> builder.containerEnv(null, "value"));
}

@Test
void containerEnvKeyValueRejectsNullValueFailFast() {
var builder = DoclingServeContainerConfig.builder();

assertThatNullPointerException().isThrownBy(() -> builder.containerEnv("key", null));
}

@Test
void containerEnvNullClearsToEmptyMapWithoutThrowing() {
var config = new AtomicReference<DoclingServeContainerConfig>();
assertThatCode(() -> config.set(DoclingServeContainerConfig.builder()
.image(DoclingServeContainerConfig.DOCLING_IMAGE)
.containerEnv("FIRST", "1")
.containerEnv(null)
.build()))
.doesNotThrowAnyException();

assertThat(config.get().containerEnv()).isEmpty();
}

}
Loading