From 0dc0378738890b6843b7622e5911d00f47292053 Mon Sep 17 00:00:00 2001 From: Andrew Kent Date: Tue, 23 Sep 2025 15:59:06 -0700 Subject: [PATCH] rm builder from Config setup and favor envars Replace Config's Builder with an (optional) envar override map. This favors using envars to configure braintrust and makes unit testing easier If the builder is desired later, we can always bring it back (though I doubt we'll want to do so) --- build.gradle | 207 ++++++++-------- examples/build.gradle | 82 +++---- .../dev/braintrust/config/BaseConfig.java | 75 ++++++ .../braintrust/config/BraintrustConfig.java | 226 ++++-------------- .../dev/braintrust/config/BaseConfigTest.java | 209 ++++++++++++++++ .../config/BraintrustConfigTest.java | 25 +- 6 files changed, 481 insertions(+), 343 deletions(-) create mode 100644 src/main/java/dev/braintrust/config/BaseConfig.java create mode 100644 src/test/java/dev/braintrust/config/BaseConfigTest.java diff --git a/build.gradle b/build.gradle index 3d3be50..4f14205 100644 --- a/build.gradle +++ b/build.gradle @@ -1,75 +1,78 @@ plugins { - id 'java-library' - id 'maven-publish' - id 'com.github.johnrengelman.shadow' version '8.1.1' - id 'com.diffplug.spotless' version '6.25.0' + id 'java-library' + id 'maven-publish' + id 'com.github.johnrengelman.shadow' version '8.1.1' + id 'com.diffplug.spotless' version '6.25.0' + id("io.freefair.lombok") version "9.0.0-rc2" } group = 'dev.braintrust' version = '0.0.1-SNAPSHOT' java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 - withJavadocJar() - withSourcesJar() + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + withJavadocJar() + withSourcesJar() } repositories { - mavenCentral() + mavenCentral() } ext { - otelVersion = '1.36.0' - jacksonVersion = '2.16.1' - junitVersion = '5.10.1' - assertjVersion = '3.25.1' - mockitoVersion = '5.8.0' - slf4jVersion = '2.0.9' + otelVersion = '1.36.0' + jacksonVersion = '2.16.1' + junitVersion = '5.10.1' + assertjVersion = '3.25.1' + mockitoVersion = '5.8.0' + slf4jVersion = '2.0.9' } dependencies { - // OpenTelemetry - api "io.opentelemetry:opentelemetry-api:${otelVersion}" - api "io.opentelemetry:opentelemetry-sdk:${otelVersion}" - api "io.opentelemetry:opentelemetry-sdk-trace:${otelVersion}" - api "io.opentelemetry:opentelemetry-sdk-logs:${otelVersion}" - implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}" - implementation "io.opentelemetry:opentelemetry-exporter-logging:${otelVersion}" - implementation "io.opentelemetry:opentelemetry-semconv:1.25.0-alpha" - - // HTTP Client (Java 11+) - implementation 'com.fasterxml.jackson.core:jackson-databind:' + jacksonVersion - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:' + jacksonVersion - implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:' + jacksonVersion - - // Logging - implementation "org.slf4j:slf4j-api:${slf4jVersion}" - runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" - - // Utilities - implementation 'com.google.code.findbugs:jsr305:3.0.2' // @Nullable annotations - implementation 'org.apache.commons:commons-lang3:3.14.0' - - // AI SDKs (optional dependencies) - compileOnly 'com.openai:openai-java:2.8.1' // Official OpenAI SDK - implementation "io.opentelemetry.instrumentation:opentelemetry-openai-java-1.1:2.19.0-alpha" - // compileOnly 'com.anthropic:anthropic-java:0.1.0' // Not yet available - - // Testing - testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" - testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" - testImplementation "org.assertj:assertj-core:${assertjVersion}" - testImplementation "org.mockito:mockito-junit-jupiter:${mockitoVersion}" - testImplementation "io.opentelemetry:opentelemetry-sdk-testing:${otelVersion}" + // OpenTelemetry + api "io.opentelemetry:opentelemetry-api:${otelVersion}" + api "io.opentelemetry:opentelemetry-sdk:${otelVersion}" + api "io.opentelemetry:opentelemetry-sdk-trace:${otelVersion}" + api "io.opentelemetry:opentelemetry-sdk-logs:${otelVersion}" + implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}" + implementation "io.opentelemetry:opentelemetry-exporter-logging:${otelVersion}" + implementation "io.opentelemetry:opentelemetry-semconv:1.25.0-alpha" + + // HTTP Client (Java 11+) + implementation 'com.fasterxml.jackson.core:jackson-databind:' + jacksonVersion + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:' + jacksonVersion + implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jdk8:' + jacksonVersion + + // Logging + implementation "org.slf4j:slf4j-api:${slf4jVersion}" + runtimeOnly "org.slf4j:slf4j-simple:${slf4jVersion}" + + // Utilities + implementation 'com.google.code.findbugs:jsr305:3.0.2' // @Nullable annotations + implementation 'org.apache.commons:commons-lang3:3.14.0' + + // AI SDKs (optional dependencies) + compileOnly 'com.openai:openai-java:2.8.1' // Official OpenAI SDK + implementation "io.opentelemetry.instrumentation:opentelemetry-openai-java-1.1:2.19.0-alpha" + // compileOnly 'com.anthropic:anthropic-java:0.1.0' // Not yet available + + // Testing + testImplementation "org.junit.jupiter:junit-jupiter:${junitVersion}" + testImplementation "org.junit.jupiter:junit-jupiter-params:${junitVersion}" + testImplementation "org.assertj:assertj-core:${assertjVersion}" + testImplementation "org.mockito:mockito-junit-jupiter:${mockitoVersion}" + testImplementation "io.opentelemetry:opentelemetry-sdk-testing:${otelVersion}" } test { - useJUnitPlatform() - testLogging { - events "passed", "skipped", "failed" - exceptionFormat "full" - } + useJUnitPlatform() + testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + } + environment 'TEST_VAR1', 'fromenv1' + environment 'TEST_VAR2', 'fromenv2' } // Enable preview features for pattern matching, etc. @@ -87,75 +90,75 @@ test { // } jar { - manifest { - attributes( - 'Implementation-Title': 'Braintrust Java SDK', - 'Implementation-Version': version, - 'Implementation-Vendor': 'Braintrust' - ) - } + manifest { + attributes( + 'Implementation-Title': 'Braintrust Java SDK', + 'Implementation-Version': version, + 'Implementation-Vendor': 'Braintrust' + ) + } } publishing { - publications { - maven(MavenPublication) { - from components.java - - pom { - name = 'Braintrust Java SDK' - description = 'OpenTelemetry-based Braintrust SDK for Java' - url = 'https://github.com/braintrustdata/braintrust-x-java' - - licenses { - license { - name = 'MIT License' - url = 'https://opensource.org/licenses/MIT' - } - } - - developers { - developer { - id = 'braintrust' - name = 'Braintrust Team' - email = 'support@braintrust.dev' - } - } - } + publications { + maven(MavenPublication) { + from components.java + + pom { + name = 'Braintrust Java SDK' + description = 'OpenTelemetry-based Braintrust SDK for Java' + url = 'https://github.com/braintrustdata/braintrust-x-java' + + licenses { + license { + name = 'MIT License' + url = 'https://opensource.org/licenses/MIT' + } + } + + developers { + developer { + id = 'braintrust' + name = 'Braintrust Team' + email = 'support@braintrust.dev' + } } + } } + } } // Configure Spotless for code formatting spotless { - java { - target 'src/*/java/**/*.java', 'examples/src/*/java/**/*.java' + java { + target 'src/*/java/**/*.java', 'examples/src/*/java/**/*.java' - // Use Google Java Format - googleJavaFormat('1.19.2').aosp().reflowLongStrings() + // Use Google Java Format + googleJavaFormat('1.19.2').aosp().reflowLongStrings() - // Remove unused imports - removeUnusedImports() + // Remove unused imports + removeUnusedImports() - // Trim trailing whitespace - trimTrailingWhitespace() + // Trim trailing whitespace + trimTrailingWhitespace() - // End with newline - endWithNewline() - } + // End with newline + endWithNewline() + } } // Task to install git hooks task installGitHooks(type: Exec) { - description = 'Install git hooks for code formatting' - group = 'Build Setup' - commandLine 'bash', 'scripts/install-hooks.sh' + description = 'Install git hooks for code formatting' + group = 'Build Setup' + commandLine 'bash', 'scripts/install-hooks.sh' } // Run installGitHooks after project evaluation afterEvaluate { - // Install hooks when building for the first time - tasks.named('build').configure { - dependsOn installGitHooks - } + // Install hooks when building for the first time + tasks.named('build').configure { + dependsOn installGitHooks + } } diff --git a/examples/build.gradle b/examples/build.gradle index 6159077..a0c5982 100644 --- a/examples/build.gradle +++ b/examples/build.gradle @@ -1,69 +1,69 @@ plugins { - id 'java' - id 'application' + id 'java' + id 'application' } java { - sourceCompatibility = JavaVersion.VERSION_17 - targetCompatibility = JavaVersion.VERSION_17 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } repositories { - mavenCentral() + mavenCentral() } dependencies { - implementation project(':') - // TODO: not sure why I have to do this. I would have expected this to come over in the project root transitive deps - implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}" + implementation project(':') + // TODO: not sure why I have to do this. I would have expected this to come over in the project root transitive deps + implementation "io.opentelemetry:opentelemetry-exporter-otlp:${otelVersion}" - // Official OpenAI Java SDK - implementation 'com.openai:openai-java:2.8.1' + // Official OpenAI Java SDK + implementation 'com.openai:openai-java:2.8.1' - // OkHttp for HTTP client (required by OpenAI SDK) - implementation 'com.squareup.okhttp3:okhttp:4.12.0' + // OkHttp for HTTP client (required by OpenAI SDK) + implementation 'com.squareup.okhttp3:okhttp:4.12.0' } application { - mainClass = 'dev.braintrust.examples.SimpleOpenTelemetryExample' + mainClass = 'dev.braintrust.examples.SimpleOpenTelemetryExample' } task runSimpleOpenTelemetry(type: JavaExec) { - group = 'Braintrust SDK Examples' - description = 'Run the simple OpenTelemetry example' - classpath = sourceSets.main.runtimeClasspath - mainClass = 'dev.braintrust.examples.SimpleOpenTelemetryExample' + group = 'Braintrust SDK Examples' + description = 'Run the simple OpenTelemetry example' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'dev.braintrust.examples.SimpleOpenTelemetryExample' } task runCustomOpenTelemetry(type: JavaExec) { - group = 'Braintrust SDK Examples' - description = 'Run the custom OpenTelemetry example' - classpath = sourceSets.main.runtimeClasspath - mainClass = 'dev.braintrust.examples.CustomOpenTelemetryExample' + group = 'Braintrust SDK Examples' + description = 'Run the custom OpenTelemetry example' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'dev.braintrust.examples.CustomOpenTelemetryExample' } task runOpenAIInstrumentation(type: JavaExec) { - group = 'Braintrust SDK Examples' - description = 'Run the OpenAI instrumentation example. NOTE: this requires OPENAI_API_KEY to be exported and will make a small call to openai, using your tokens' - classpath = sourceSets.main.runtimeClasspath - mainClass = 'dev.braintrust.examples.OpenAIInstrumentationExample' - debugOptions { - enabled = true - port = 5566 - server = true - suspend = false - } + group = 'Braintrust SDK Examples' + description = 'Run the OpenAI instrumentation example. NOTE: this requires OPENAI_API_KEY to be exported and will make a small call to openai, using your tokens' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'dev.braintrust.examples.OpenAIInstrumentationExample' + debugOptions { + enabled = true + port = 5566 + server = true + suspend = false + } } task runExperiment(type: JavaExec) { - group = 'Braintrust SDK Examples' - description = 'Run the experiment example' - classpath = sourceSets.main.runtimeClasspath - mainClass = 'dev.braintrust.examples.ExperimentExample' - debugOptions { - enabled = true - port = 5566 - server = true - suspend = false - } + group = 'Braintrust SDK Examples' + description = 'Run the experiment example' + classpath = sourceSets.main.runtimeClasspath + mainClass = 'dev.braintrust.examples.ExperimentExample' + debugOptions { + enabled = true + port = 5566 + server = true + suspend = false + } } diff --git a/src/main/java/dev/braintrust/config/BaseConfig.java b/src/main/java/dev/braintrust/config/BaseConfig.java new file mode 100644 index 0000000..edb9da3 --- /dev/null +++ b/src/main/java/dev/braintrust/config/BaseConfig.java @@ -0,0 +1,75 @@ +package dev.braintrust.config; + +import java.util.List; +import java.util.Map; +import java.util.Objects; +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +class BaseConfig { + /** Sentinal used to set null in the env. Only used for testing. */ + static final String NULL_OVERRIDE = "BRAINTRUST_NULL_SENTINAL_" + System.currentTimeMillis(); + + protected final Map envOverrides; + + BaseConfig(Map envOverrides) { + this.envOverrides = Map.copyOf(envOverrides); + } + + protected @Nonnull T getConfig(@Nonnull String settingName, @Nonnull T defaultValue) { + Objects.requireNonNull(defaultValue); + return getConfig(settingName, defaultValue, (Class) defaultValue.getClass()); + } + + protected @Nullable T getConfig( + @Nonnull String settingName, @Nullable T defaultValue, @Nonnull Class settingClass) { + @Nullable String rawVal = getEnvValue(settingName); + if (rawVal == null) { + return defaultValue; + } else { + return cast(rawVal, settingClass); + } + } + + protected @Nonnull String getRequiredConfig(@Nonnull String settingName) { + return getRequiredConfig(settingName, String.class); + } + + protected @Nonnull T getRequiredConfig(String settingName, Class settingClass) { + T value = getConfig(settingName, null, settingClass); + if (null == value) { + throw new RuntimeException("%s is required".formatted(settingName)); + } + return value; + } + + protected T cast(@Nonnull String value, @Nonnull Class clazz) { + if (clazz.equals(String.class)) { + return (T) value; + } else if (List.of(Boolean.class, boolean.class).contains(clazz)) { + return (T) Boolean.valueOf(value); + } else if (List.of(Integer.class, int.class).contains(clazz)) { + return (T) Integer.valueOf(value); + } else if (List.of(Long.class, long.class).contains(clazz)) { + return (T) Long.valueOf(value); + } else if (List.of(Float.class, float.class).contains(clazz)) { + return (T) Float.valueOf(value); + } else if (List.of(Double.class, double.class).contains(clazz)) { + return (T) Double.valueOf(value); + } else { + throw new RuntimeException( + "Unsupported default class: %s -- please implement or use a different default" + .formatted(clazz)); + } + } + + protected @Nullable String getEnvValue(@Nonnull String settingName) { + // first try the override map + var settingValue = envOverrides.get(settingName); + if (settingValue == null) { + // then get it from the sysenv + settingValue = System.getenv(settingName); + } + return NULL_OVERRIDE.equals(settingValue) ? null : settingValue; + } +} diff --git a/src/main/java/dev/braintrust/config/BraintrustConfig.java b/src/main/java/dev/braintrust/config/BraintrustConfig.java index 34aa299..9c57bba 100644 --- a/src/main/java/dev/braintrust/config/BraintrustConfig.java +++ b/src/main/java/dev/braintrust/config/BraintrustConfig.java @@ -4,88 +4,55 @@ import java.net.URI; import java.net.URISyntaxException; import java.time.Duration; +import java.util.HashMap; +import java.util.Map; import java.util.Optional; -import javax.annotation.Nullable; +import lombok.Getter; +import lombok.experimental.Accessors; /** Configuration for Braintrust SDK */ -public final class BraintrustConfig { - private static final String DEFAULT_PROJECT_NAME = "default-java-project"; - private static final String DEFAULT_API_URL = "https://api.braintrust.dev"; - private static final String DEFAULT_APP_URL = "https://www.braintrust.dev"; +@Getter +@Accessors(fluent = true) +public final class BraintrustConfig extends BaseConfig { + private final String apiKey = getRequiredConfig("BRAINTRUST_API_KEY"); + private final String apiUrl = getConfig("BRAINTRUST_API_URL", "https://api.braintrust.dev"); + private final String appUrl = getConfig("BRAINTRUST_APP_URL", "https://www.braintrust.dev"); + private final String tracesPath = getConfig("BRAINTRUST_TRACES_PATH", "/otel/v1/traces"); + private final String logsPath = getConfig("BRAINTRUST_LOGS_PATH", "/otel/v1/logs"); + private final Optional defaultProjectId = + Optional.ofNullable(getConfig("BRAINTRUST_DEFAULT_PROJECT_ID", null, String.class)); + private final Optional defaultProjectName = + Optional.of(getConfig("BRAINTRUST_DEFAULT_PROJECT_NAME", "default-java-project")); + private final boolean enableTraceConsoleLog = + getConfig("BRAINTRUST_ENABLE_TRACE_CONSOLE_LOG", false); + private final boolean debug = getConfig("BRAINTRUST_DEBUG", false); + private final boolean experimentalOtelLogs = getConfig("BRAINTRUST_X_OTEL_LOGS", false); + private final Duration requestTimeout = + Duration.ofSeconds(getConfig("BRAINTRUST_REQUEST_TIMEOUT", 30)); - private final String apiKey; - private final URI apiUrl; - private final String tracesPath; - private final String logsPath; - private final URI appUrl; - @Nullable private final String defaultProjectId; - @Nullable private final String defaultProjectName; - private final boolean enableTraceConsoleLog; - private final boolean debug; - private final Duration requestTimeout; - private final boolean experimentalOtelLogs; + public static BraintrustConfig fromEnvironment() { + return of(); + } - private BraintrustConfig(Builder builder) { - this.apiKey = builder.apiKey; - this.apiUrl = builder.apiUrl; - this.tracesPath = builder.tracesPath; - this.logsPath = builder.logsPath; - this.appUrl = builder.appUrl; - this.experimentalOtelLogs = builder.experimentalOtelLogs; - this.defaultProjectId = builder.defaultProjectId; - this.defaultProjectName = builder.defaultProjectName; - this.enableTraceConsoleLog = builder.enableTraceConsoleLog; - this.debug = builder.debug; - this.requestTimeout = builder.requestTimeout; - if ((null == defaultProjectId || "".equalsIgnoreCase(defaultProjectId.trim())) - && (null == defaultProjectName || "".equalsIgnoreCase(defaultProjectName.trim()))) { - // NOTE: this should not happen, - // but if someone happens to export their default project to the empty string and - // does not set a default project ID we don't have a valid parent for otel data. + public static BraintrustConfig of(String... envOverrides) { + if (envOverrides.length % 2 != 0) { throw new RuntimeException( - "Missing required envars. Please export BRAINTRUST_DEFAULT_PROJECT_ID or" - + " BRAINTRUST_DEFAULT_PROJECT"); + "config overrides require key-value pairs. Found dangling key: %s" + .formatted(envOverrides[envOverrides.length - 1])); } + var overridesMap = new HashMap(); + for (int i = 0; i < envOverrides.length - 1; i = i + 2) { + overridesMap.put(envOverrides[i], envOverrides[i + 1]); + } + return new BraintrustConfig(overridesMap); } - public String apiKey() { - return apiKey; - } - - public URI apiUrl() { - return apiUrl; - } - - public String tracesPath() { - return tracesPath; - } - - public String logsPath() { - return logsPath; - } - - public URI appUrl() { - return appUrl; - } - - public Optional defaultProjectId() { - return Optional.ofNullable(defaultProjectId); - } - - public Optional defaultProjectName() { - return Optional.ofNullable(defaultProjectName); - } - - public boolean enableTraceConsoleLog() { - return enableTraceConsoleLog; - } - - public boolean debug() { - return debug; - } - - public Duration requestTimeout() { - return requestTimeout; + private BraintrustConfig(Map envOverrides) { + super(envOverrides); + if (defaultProjectId.isEmpty() && defaultProjectName.isEmpty()) { + // should never happen + throw new RuntimeException("A project name or ID is required."); + } } /** @@ -101,25 +68,16 @@ public Duration requestTimeout() { * If neither (a) nor (b) exists, the data is dropped */ public Optional getBraintrustParentValue() { - if (null != defaultProjectId) { - return Optional.of("project_id:" + defaultProjectId); - } else if (null != defaultProjectName) { - return Optional.of("project_name:" + defaultProjectName); + if (defaultProjectId.isPresent()) { + return Optional.of("project_id:" + defaultProjectId.orElseThrow()); + } else if (this.defaultProjectName.isPresent()) { + return Optional.of("project_name:" + defaultProjectName.orElseThrow()); } else { return Optional.empty(); } } - /** Creates a new builder initialized with environment variables. */ - public static Builder builder() { - return new Builder(); - } - - /** Creates a config from environment variables and retrieves organization name from API. */ - public static BraintrustConfig fromEnvironment() { - return builder().build(); - } - + /** fetch all project info and IDs from the braintrust api */ public URI fetchProjectURI() { try { var client = new BraintrustApiClient(this); @@ -134,98 +92,4 @@ public URI fetchProjectURI() { throw new RuntimeException(e); } } - - public static final class Builder { - private String apiKey; - private URI apiUrl; - private String tracesPath; - private String logsPath; - private URI appUrl; - private String defaultProjectId; - private String defaultProjectName; - private boolean enableTraceConsoleLog; - private boolean debug; - private boolean experimentalOtelLogs; - private Duration requestTimeout = Duration.ofSeconds(30); - - private Builder() { - this.apiKey = getEnv("BRAINTRUST_API_KEY", null); - this.apiUrl = URI.create(getEnv("BRAINTRUST_API_URL", DEFAULT_API_URL)); - this.tracesPath = getEnv("BRAINTRUST_TRACES_PATH", "/otel/v1/traces"); - this.logsPath = getEnv("BRAINTRUST_LOGS_PATH", "/otel/v1/logs"); - this.appUrl = URI.create(getEnv("BRAINTRUST_APP_URL", DEFAULT_APP_URL)); - this.defaultProjectId = getEnv("BRAINTRUST_DEFAULT_PROJECT_ID", null); - this.defaultProjectName = - getEnv("BRAINTRUST_DEFAULT_PROJECT", DEFAULT_PROJECT_NAME).trim(); - this.enableTraceConsoleLog = - Boolean.parseBoolean(getEnv("BRAINTRUST_ENABLE_TRACE_CONSOLE_LOG", "false")); - this.debug = Boolean.parseBoolean(getEnv("BRAINTRUST_DEBUG", "false")); - this.experimentalOtelLogs = - Boolean.parseBoolean(getEnv("BRAINTRUST_X_OTEL_LOGS", "false")); - } - - public Builder apiKey(String apiKey) { - this.apiKey = apiKey; - return this; - } - - public Builder apiUrl(String apiUrl) { - return apiUrl(URI.create(apiUrl)); - } - - public Builder apiUrl(URI apiUrl) { - this.apiUrl = apiUrl; - return this; - } - - public Builder appUrl(String appUrl) { - return appUrl(URI.create(appUrl)); - } - - public Builder appUrl(URI appUrl) { - this.appUrl = appUrl; - return this; - } - - public Builder defaultProjectId(String projectId) { - this.defaultProjectId = projectId; - return this; - } - - public Builder defaultProjectName(String projectName) { - this.defaultProjectName = projectName; - return this; - } - - public Builder enableTraceConsoleLog(boolean enable) { - this.enableTraceConsoleLog = enable; - return this; - } - - public Builder debug(boolean debug) { - this.debug = debug; - return this; - } - - public Builder requestTimeout(Duration timeout) { - this.requestTimeout = timeout; - return this; - } - - public BraintrustConfig build() { - if (apiKey == null || apiKey.isBlank()) { - throw new IllegalStateException( - "API key is required. Set BRAINTRUST_API_KEY environment variable or use" - + " apiKey() method."); - } - - return new BraintrustConfig(this); - } - - private static String getEnv(String key, String defaultValue) { - String value = System.getenv(key); - // Trim any whitespace that might have been accidentally included - return value != null ? value.trim() : defaultValue; - } - } } diff --git a/src/test/java/dev/braintrust/config/BaseConfigTest.java b/src/test/java/dev/braintrust/config/BaseConfigTest.java new file mode 100644 index 0000000..7b511fa --- /dev/null +++ b/src/test/java/dev/braintrust/config/BaseConfigTest.java @@ -0,0 +1,209 @@ +package dev.braintrust.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.util.Map; +import org.junit.jupiter.api.Test; + +public class BaseConfigTest { + private static final String TEST_ENV_VAR = "BRAINTRUST_TEST_VAR"; + + @Test + void testGetConfigHierarchy() { + // NOTE: gradle exports TEST_VAR1 and TEST_VAR2 into this test env + TestConfig config = new TestConfig(Map.of("TEST_VAR1", "override")); + + // overrides take precedence + assertEquals("override", config.getConfig("TEST_VAR1", "default")); + // then envars + assertEquals("fromenv2", config.getConfig("TEST_VAR2", "default")); + // finally, defaults + assertEquals("default", config.getConfig("NON_EXISTENT_VAR", "default")); + } + + @Test + void testGetConfigWithNullDefault() { + TestConfig config = new TestConfig(Map.of()); + String result = config.getConfig("NON_EXISTENT_VAR", null, String.class); + assertNull(result); + } + + @Test + void testGetRequiredConfigSuccess() { + TestConfig config = new TestConfig(Map.of("REQUIRED_VAR", "required_value")); + + String result = config.getRequiredConfig("REQUIRED_VAR"); + assertEquals("required_value", result); + } + + @Test + void testGetRequiredConfigFailure() { + TestConfig config = new TestConfig(Map.of()); + + assertThrows( + Exception.class, + () -> { + config.getRequiredConfig("NON_EXISTENT_VAR"); + }); + } + + @Test + void testGetRequiredConfigWithType() { + TestConfig config = new TestConfig(Map.of("INT_VAR", "123")); + + Integer result = config.getRequiredConfig("INT_VAR", Integer.class); + assertEquals(123, result); + } + + @Test + void testCastBoolean() { + TestConfig config = new TestConfig(Map.of()); + + Boolean result1 = config.cast("true", Boolean.class); + assertTrue(result1); + + Boolean result2 = config.cast("false", Boolean.class); + assertFalse(result2); + + boolean result3 = config.cast("true", boolean.class); + assertTrue(result3); + } + + @Test + void testCastInteger() { + TestConfig config = new TestConfig(Map.of()); + + Integer result1 = config.cast("42", Integer.class); + assertEquals(42, result1); + + int result2 = config.cast("123", int.class); + assertEquals(123, result2); + } + + @Test + void testCastLong() { + TestConfig config = new TestConfig(Map.of()); + + Long result1 = config.cast("9223372036854775807", Long.class); + assertEquals(9223372036854775807L, result1); + + long result2 = config.cast("456", long.class); + assertEquals(456L, result2); + } + + @Test + void testCastFloat() { + TestConfig config = new TestConfig(Map.of()); + + Float result1 = config.cast("3.14", Float.class); + assertEquals(3.14f, result1, 0.001f); + + float result2 = config.cast("2.71", float.class); + assertEquals(2.71f, result2, 0.001f); + } + + @Test + void testCastDouble() { + TestConfig config = new TestConfig(Map.of()); + + Double result1 = config.cast("3.14159", Double.class); + assertEquals(3.14159, result1, 0.00001); + + double result2 = config.cast("2.71828", double.class); + assertEquals(2.71828, result2, 0.00001); + } + + @Test + void testCastUnsupportedType() { + TestConfig config = new TestConfig(Map.of()); + + assertThrows( + Exception.class, + () -> { + config.cast("test", Object.class); + }); + } + + @Test + void testGetEnvValueFromOverrides() { + TestConfig config = new TestConfig(Map.of("OVERRIDE_VAR", "override_value")); + + String result = config.getEnvValue("OVERRIDE_VAR"); + assertEquals("override_value", result); + } + + @Test + void testGetEnvValueNonExistent() { + TestConfig config = new TestConfig(Map.of()); + + String result = config.getEnvValue("NON_EXISTENT_VAR_12345"); + assertNull(result); + } + + @Test + void testNullSentinalHandling() { + // Test the NULL_OVERRIDE sentinel behavior + TestConfig config = new TestConfig(Map.of("NULL_VAR", BaseConfig.NULL_OVERRIDE)); + + String result = config.getEnvValue("NULL_VAR"); + assertNull(result); + } + + @Test + void testGetConfigWithNonNullDefaultThrowsOnNull() { + TestConfig config = new TestConfig(Map.of()); + + assertThrows( + NullPointerException.class, + () -> { + config.getConfig("TEST", (String) null); + }); + } + + @Test + void testIntegrationWithAllTypes() { + Map overrides = + Map.of( + "STRING_VAR", "hello", + "BOOL_VAR", "true", + "INT_VAR", "42", + "LONG_VAR", "123456789", + "FLOAT_VAR", "3.14", + "DOUBLE_VAR", "2.71828"); + + TestConfig config = new TestConfig(overrides); + + assertEquals("hello", config.getConfig("STRING_VAR", "default")); + assertEquals(true, config.getConfig("BOOL_VAR", false)); + assertEquals(42, config.getConfig("INT_VAR", 0)); + assertEquals(123456789L, config.getConfig("LONG_VAR", 0L)); + assertEquals(3.14f, config.getConfig("FLOAT_VAR", 0.0f), 0.001f); + assertEquals(2.71828, config.getConfig("DOUBLE_VAR", 0.0), 0.00001); + } + + static class TestConfig extends BaseConfig { + TestConfig(Map envOverrides) { + super(envOverrides); + } + + @Override + public T getConfig(String settingName, T defaultValue) { + return super.getConfig(settingName, defaultValue); + } + + @Override + public T getConfig(String settingName, T defaultValue, Class settingClass) { + return super.getConfig(settingName, defaultValue, settingClass); + } + + @Override + public String getRequiredConfig(String settingName) { + return super.getRequiredConfig(settingName); + } + + @Override + public T getRequiredConfig(String settingName, Class settingClass) { + return super.getRequiredConfig(settingName, settingClass); + } + } +} diff --git a/src/test/java/dev/braintrust/config/BraintrustConfigTest.java b/src/test/java/dev/braintrust/config/BraintrustConfigTest.java index 91487a3..2658832 100644 --- a/src/test/java/dev/braintrust/config/BraintrustConfigTest.java +++ b/src/test/java/dev/braintrust/config/BraintrustConfigTest.java @@ -1,28 +1,15 @@ package dev.braintrust.config; -import static org.junit.jupiter.api.Assertions.*; +import static org.junit.jupiter.api.Assertions.assertTrue; import org.junit.jupiter.api.Test; class BraintrustConfigTest { @Test - void throwsWithoutRequiredFields() { - assertThrows( - Exception.class, // don't care - () -> BraintrustConfig.builder().apiKey(null).build(), - "API key required"); - - assertThrows( - Exception.class, // don't care - () -> - BraintrustConfig.builder() - .apiKey("foo") - .defaultProjectId(null) - .defaultProjectName("") - .build(), - "Project info required"); - assertDoesNotThrow( - () -> BraintrustConfig.builder().apiKey("foobar").build(), - "Only an API key is required"); + void parentLogic() { + var defaultConfig = BraintrustConfig.of("BRAINTRUST_API_KEY", "foobar"); + assertTrue( + defaultConfig.getBraintrustParentValue().isPresent(), + "default config should have a parent"); } }