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
61 changes: 61 additions & 0 deletions adcp/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
*
* <p>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)
}
30 changes: 30 additions & 0 deletions adcp/src/main/java/org/adcontextprotocol/adcp/AdcpClient.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"}).
*
* <p>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);
Expand All @@ -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");
}
}
}
}
23 changes: 23 additions & 0 deletions adcp/src/main/java/org/adcontextprotocol/adcp/AdcpVersion.java
Original file line number Diff line number Diff line change
Expand Up @@ -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}.
*
* <p>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);
}
}
11 changes: 11 additions & 0 deletions adcp/src/main/java/org/adcontextprotocol/adcp/AgentConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -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"}).
*
* <p>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<String, String> extraHeaders) {
this.extraHeaders = Map.copyOf(extraHeaders);
Expand Down
21 changes: 21 additions & 0 deletions adcp/src/test/java/org/adcontextprotocol/adcp/AdcpClientTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -110,4 +110,25 @@ void callTool_accepts_null_args_without_npe() {
() -> client.callTool("get_products", null, java.util.Map.class));
}
}

@Test
void builder_accepts_string_version() {
try (AdcpClient client = AdcpClient.builder()
.agent(AgentConfig.mcp("test", AGENT_URI))
.adcpVersion("3.0")
.build()) {
assertNotNull(client.adcpVersion());
assertEquals(3, client.adcpVersion().majorVersion());
assertEquals("3.0", client.adcpVersion().minorVersion());
}
}

@Test
void builder_rejects_cross_major_version() {
assertThrows(org.adcontextprotocol.adcp.error.ConfigurationError.class,
() -> AdcpClient.builder()
.agent(AgentConfig.mcp("test", AGENT_URI))
.adcpVersion(new AdcpVersion(2, null))
.build());
}
}
47 changes: 47 additions & 0 deletions adcp/src/test/java/org/adcontextprotocol/adcp/AdcpVersionTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -73,4 +73,51 @@ void accepts_three_part_minor_version() {
var v = new AdcpVersion(3, "3.1.2");
assertEquals("3.1.2", v.minorVersion());
}

// -- AdcpVersion.of(String) --

@Test
void of_parses_release_precision_version() {
AdcpVersion v = AdcpVersion.of("3.0");
assertEquals(3, v.majorVersion());
assertEquals("3.0", v.minorVersion());
}

@Test
void of_parses_minor_version() {
AdcpVersion v = AdcpVersion.of("3.1");
assertEquals(3, v.majorVersion());
assertEquals("3.1", v.minorVersion());
}

@Test
void of_rejects_major_only_string() {
assertThrows(IllegalArgumentException.class, () -> AdcpVersion.of("3"));
}

@Test
void of_rejects_non_numeric() {
assertThrows(IllegalArgumentException.class, () -> AdcpVersion.of("abc.def"));
}

@Test
void of_rejects_null() {
assertThrows(NullPointerException.class, () -> AdcpVersion.of(null));
}

// -- AdcpSdkVersion constants (build-time generated) --

@Test
void sdk_major_version_is_positive() {
assertTrue(AdcpSdkVersion.SDK_MAJOR_VERSION > 0,
"SDK_MAJOR_VERSION must be a positive integer");
}

@Test
void sdk_release_version_matches_major() {
String release = AdcpSdkVersion.SDK_RELEASE_VERSION;
assertTrue(release.startsWith(AdcpSdkVersion.SDK_MAJOR_VERSION + "."),
"SDK_RELEASE_VERSION must start with SDK_MAJOR_VERSION: " + release);
}
}

Loading