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"); } }