diff --git a/adcp/build.gradle.kts b/adcp/build.gradle.kts index bf40152..6550046 100644 --- a/adcp/build.gradle.kts +++ b/adcp/build.gradle.kts @@ -22,3 +22,64 @@ dependencies { exclude(group = "com.networknt", module = "json-schema-validator") } } + +// -- Build-time SDK version constant ---------------------------------------- +// Reads ADCP_VERSION (e.g. "3.0.11") and generates AdcpSdkVersion.java with +// the major and release-precision (major.minor) constants. This lets callers +// do cross-major validation at config time without hardcoding a version number. +// Output lands in build/generated/ and is NOT checked in. + +val generateSdkVersion = tasks.register("generateSdkVersion") { + val versionFile = rootProject.file("ADCP_VERSION") + inputs.file(versionFile) + val outputDir = layout.buildDirectory.dir("generated/sources/sdk-version/main/java") + outputs.dir(outputDir) + + doLast { + val raw = versionFile.readText().trim() + val parts = raw.split(".") + require(parts.size >= 2) { "ADCP_VERSION must be in major.minor.patch format: $raw" } + val major = parts[0].toInt() + val release = "${parts[0]}.${parts[1]}" // release-precision, e.g. "3.0" + + val pkg = "org.adcontextprotocol.adcp" + val dir = outputDir.get().asFile.resolve(pkg.replace('.', '/')) + dir.mkdirs() + dir.resolve("AdcpSdkVersion.java").writeText( + """ + package $pkg; + + /** + * Build-time AdCP SDK version constants — generated from {@code ADCP_VERSION}. + * Do not edit manually; update {@code ADCP_VERSION} at the repo root instead. + * + *
Used for cross-major validation: if a caller pins + * {@code adcpVersion("X.Y")} and {@code X != SDK_MAJOR_VERSION}, + * a {@link org.adcontextprotocol.adcp.error.ConfigurationError} is thrown + * before any network request is made. + */ + public final class AdcpSdkVersion { + + private AdcpSdkVersion() {} + + /** Major protocol version this SDK was built for (e.g. {@code 3}). */ + public static final int SDK_MAJOR_VERSION = $major; + + /** + * Release-precision protocol version this SDK was built for + * (e.g. {@code "3.0"}). + */ + public static final String SDK_RELEASE_VERSION = "$release"; + } + """.trimIndent() + ) + } +} + +sourceSets.named("main") { + java.srcDir(generateSdkVersion.map { it.outputs.files.singleFile }) +} + +tasks.named("compileJava") { + dependsOn(generateSdkVersion) +} diff --git a/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpClient.java b/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpClient.java index 80ebe88..2407a1f 100644 --- a/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpClient.java +++ b/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpClient.java @@ -157,6 +157,18 @@ public Builder adcpVersion(AdcpVersion adcpVersion) { return this; } + /** + * Pin a specific AdCP protocol version by release-precision string + * (e.g. {@code "3.0"}, {@code "3.1"}). + * + *
Equivalent to {@code adcpVersion(AdcpVersion.of(releaseVersion))}. + * Throws {@link org.adcontextprotocol.adcp.error.ConfigurationError} at + * {@link #build()} time if the major version does not match the SDK. + */ + public Builder adcpVersion(String releaseVersion) { + return adcpVersion(AdcpVersion.of(releaseVersion)); + } + /** Override the Jackson ObjectMapper. */ public Builder objectMapper(ObjectMapper objectMapper) { this.objectMapper = Objects.requireNonNull(objectMapper); @@ -174,7 +186,25 @@ public Builder ssrfPolicy(SsrfPolicy ssrfPolicy) { /** Builds the client. */ public AdcpClient build() { + validateAdcpVersion(adcpVersion); return new AdcpClient(this); } + + /** + * Validates that the pinned version's major matches the SDK's built-in major. + * Cross-major pins (e.g. requesting "2.0" from a major-3 SDK) fail fast before + * any network request. + */ + private static void validateAdcpVersion(@Nullable AdcpVersion version) { + if (version == null) return; + if (version.majorVersion() != AdcpSdkVersion.SDK_MAJOR_VERSION) { + throw new ConfigurationError( + "adcpVersion major " + version.majorVersion() + + " does not match SDK major " + + AdcpSdkVersion.SDK_MAJOR_VERSION + + " (built for AdCP " + AdcpSdkVersion.SDK_RELEASE_VERSION + ")", + "adcpVersion"); + } + } } } diff --git a/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpVersion.java b/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpVersion.java index 38ce4f0..9ab8bf0 100644 --- a/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpVersion.java +++ b/adcp/src/main/java/org/adcontextprotocol/adcp/AdcpVersion.java @@ -43,4 +43,27 @@ public record AdcpVersion(int majorVersion, @Nullable String minorVersion) { } } } + + /** + * Parses a release-precision version string (e.g. {@code "3.0"}, {@code "3.1"}) + * into an {@code AdcpVersion}. + * + *
This is the string-based convenience factory — pass the same value you + * would set in the Python SDK or TS SDK {@code adcpVersion} constructor option. + * + * @param releaseVersion release-precision version (major.minor, e.g. {@code "3.0"}) + * @return parsed {@code AdcpVersion} + * @throws IllegalArgumentException if the string is not in major.minor format + */ + public static AdcpVersion of(String releaseVersion) { + java.util.Objects.requireNonNull(releaseVersion, "releaseVersion"); + if (!MINOR_VERSION_PATTERN.matcher(releaseVersion).matches()) { + throw new IllegalArgumentException( + "releaseVersion must be in major.minor format (e.g. '3.0'): " + + releaseVersion); + } + int dotIndex = releaseVersion.indexOf('.'); + int major = Integer.parseInt(releaseVersion.substring(0, dotIndex)); + return new AdcpVersion(major, releaseVersion); + } } diff --git a/adcp/src/main/java/org/adcontextprotocol/adcp/AgentConfig.java b/adcp/src/main/java/org/adcontextprotocol/adcp/AgentConfig.java index bace3a1..0edd865 100644 --- a/adcp/src/main/java/org/adcontextprotocol/adcp/AgentConfig.java +++ b/adcp/src/main/java/org/adcontextprotocol/adcp/AgentConfig.java @@ -229,6 +229,17 @@ public Builder adcpVersion(@Nullable AdcpVersion adcpVersion) { return this; } + /** + * Pin a specific AdCP protocol version by release-precision string + * (e.g. {@code "3.0"}, {@code "3.1"}). + * + *
Equivalent to {@code adcpVersion(AdcpVersion.of(releaseVersion))}.
+ * Cross-major pins are rejected at {@link AdcpClient} build time.
+ */
+ public Builder adcpVersion(String releaseVersion) {
+ return adcpVersion(AdcpVersion.of(releaseVersion));
+ }
+
/** Extra headers injected into every request to this agent. */
public Builder extraHeaders(Map