From 6cfde8967d8054950cf7bfb62c3b5e9022f79107 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:01:32 +0000 Subject: [PATCH 01/20] feat(client): add `HttpRequest#url()` method --- .../com/branddev/api/core/http/HttpRequest.kt | 30 +++++ .../branddev/api/core/http/HttpRequestTest.kt | 110 ++++++++++++++++++ 2 files changed, 140 insertions(+) create mode 100644 brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/HttpRequestTest.kt diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/HttpRequest.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/HttpRequest.kt index fea19e7..b29f9ee 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/HttpRequest.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/HttpRequest.kt @@ -2,6 +2,7 @@ package com.branddev.api.core.http import com.branddev.api.core.checkRequired import com.branddev.api.core.toImmutable +import java.net.URLEncoder class HttpRequest private constructor( @@ -13,6 +14,35 @@ private constructor( @get:JvmName("body") val body: HttpRequestBody?, ) { + fun url(): String = buildString { + append(baseUrl) + + pathSegments.forEach { segment -> + if (!endsWith("/")) { + append("/") + } + append(URLEncoder.encode(segment, "UTF-8")) + } + + if (queryParams.isEmpty()) { + return@buildString + } + + append("?") + var isFirst = true + queryParams.keys().forEach { key -> + queryParams.values(key).forEach { value -> + if (!isFirst) { + append("&") + } + append(URLEncoder.encode(key, "UTF-8")) + append("=") + append(URLEncoder.encode(value, "UTF-8")) + isFirst = false + } + } + } + fun toBuilder(): Builder = Builder().from(this) override fun toString(): String = diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/HttpRequestTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/HttpRequestTest.kt new file mode 100644 index 0000000..4efb71c --- /dev/null +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/http/HttpRequestTest.kt @@ -0,0 +1,110 @@ +package com.branddev.api.core.http + +import org.assertj.core.api.Assertions.assertThat +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.EnumSource + +internal class HttpRequestTest { + + enum class UrlTestCase(val request: HttpRequest, val expectedUrl: String) { + BASE_URL_ONLY( + HttpRequest.builder().method(HttpMethod.GET).baseUrl("https://api.example.com").build(), + expectedUrl = "https://api.example.com", + ), + BASE_URL_WITH_TRAILING_SLASH( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com/") + .build(), + expectedUrl = "https://api.example.com/", + ), + SINGLE_PATH_SEGMENT( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .build(), + expectedUrl = "https://api.example.com/users", + ), + MULTIPLE_PATH_SEGMENTS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegments("users", "123", "profile") + .build(), + expectedUrl = "https://api.example.com/users/123/profile", + ), + PATH_SEGMENT_WITH_SPECIAL_CHARS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("user name") + .build(), + expectedUrl = "https://api.example.com/user+name", + ), + SINGLE_QUERY_PARAM( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParam("limit", "10") + .build(), + expectedUrl = "https://api.example.com/users?limit=10", + ), + MULTIPLE_QUERY_PARAMS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParam("limit", "10") + .putQueryParam("offset", "20") + .build(), + expectedUrl = "https://api.example.com/users?limit=10&offset=20", + ), + QUERY_PARAM_WITH_SPECIAL_CHARS( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("search") + .putQueryParam("q", "hello world") + .build(), + expectedUrl = "https://api.example.com/search?q=hello+world", + ), + MULTIPLE_VALUES_SAME_PARAM( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com") + .addPathSegment("users") + .putQueryParams("tags", listOf("admin", "user")) + .build(), + expectedUrl = "https://api.example.com/users?tags=admin&tags=user", + ), + BASE_URL_WITH_TRAILING_SLASH_AND_PATH( + HttpRequest.builder() + .method(HttpMethod.GET) + .baseUrl("https://api.example.com/") + .addPathSegment("users") + .build(), + expectedUrl = "https://api.example.com/users", + ), + COMPLEX_URL( + HttpRequest.builder() + .method(HttpMethod.POST) + .baseUrl("https://api.example.com") + .addPathSegments("v1", "users", "123") + .putQueryParams("include", listOf("profile", "settings")) + .putQueryParam("format", "json") + .build(), + expectedUrl = + "https://api.example.com/v1/users/123?include=profile&include=settings&format=json", + ), + } + + @ParameterizedTest + @EnumSource + fun url(testCase: UrlTestCase) { + val actualUrl = testCase.request.url() + + assertThat(actualUrl).isEqualTo(testCase.expectedUrl) + } +} From 18a03480949d4afc6d8ed4a45a55c84094aadae6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 6 Jan 2026 03:12:09 +0000 Subject: [PATCH 02/20] docs: prominently feature MCP server setup in root SDK readmes --- README.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/README.md b/README.md index ee3a67c..4a91fd4 100644 --- a/README.md +++ b/README.md @@ -11,6 +11,15 @@ The Brand Dev Java SDK provides convenient access to the [Brand Dev REST API](ht It is generated with [Stainless](https://www.stainless.com/). +## MCP Server + +Use the Brand Dev MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. + +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=brand.dev-mcp&config=eyJuYW1lIjoiYnJhbmQuZGV2LW1jcCIsInRyYW5zcG9ydCI6InNzZSIsInVybCI6Imh0dHBzOi8vYnJhbmQtZGV2LnN0bG1jcC5jb20vc3NlIn0) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22brand.dev-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fbrand-dev.stlmcp.com%2Fsse%22%7D) + +> Note: You may need to set environment variables in your MCP client. + The REST API documentation can be found on [docs.brand.dev](https://docs.brand.dev/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.26). From 7eca57352b72de96b78b7ee16be87ea844a10f89 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 9 Jan 2026 03:23:25 +0000 Subject: [PATCH 03/20] feat(client): allow configuring dispatcher executor service --- .../api/client/okhttp/BrandDevOkHttpClient.kt | 22 +++++++++++++++++++ .../okhttp/BrandDevOkHttpClientAsync.kt | 22 +++++++++++++++++++ .../api/client/okhttp/OkHttpClient.kt | 9 ++++++++ 3 files changed, 53 insertions(+) diff --git a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClient.kt b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClient.kt index 1332c82..bd414fd 100644 --- a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClient.kt +++ b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClient.kt @@ -16,6 +16,7 @@ import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -44,11 +45,31 @@ class BrandDevOkHttpClient private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + /** + * The executor service to use for running HTTP requests. + * + * Defaults to OkHttp's + * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104). + * + * This class takes ownership of the executor service and shuts it down when closed. + */ + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + /** + * Alias for calling [Builder.dispatcherExecutorService] with + * `dispatcherExecutorService.orElse(null)`. + */ + fun dispatcherExecutorService(dispatcherExecutorService: Optional) = + dispatcherExecutorService(dispatcherExecutorService.getOrNull()) + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ @@ -296,6 +317,7 @@ class BrandDevOkHttpClient private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClientAsync.kt b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClientAsync.kt index c7f3ecc..398393f 100644 --- a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClientAsync.kt +++ b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/BrandDevOkHttpClientAsync.kt @@ -16,6 +16,7 @@ import java.net.Proxy import java.time.Clock import java.time.Duration import java.util.Optional +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager @@ -44,11 +45,31 @@ class BrandDevOkHttpClientAsync private constructor() { class Builder internal constructor() { private var clientOptions: ClientOptions.Builder = ClientOptions.builder() + private var dispatcherExecutorService: ExecutorService? = null private var proxy: Proxy? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null + /** + * The executor service to use for running HTTP requests. + * + * Defaults to OkHttp's + * [default executor service](https://github.com/square/okhttp/blob/ace792f443b2ffb17974f5c0d1cecdf589309f26/okhttp/src/commonJvmAndroid/kotlin/okhttp3/Dispatcher.kt#L98-L104). + * + * This class takes ownership of the executor service and shuts it down when closed. + */ + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + + /** + * Alias for calling [Builder.dispatcherExecutorService] with + * `dispatcherExecutorService.orElse(null)`. + */ + fun dispatcherExecutorService(dispatcherExecutorService: Optional) = + dispatcherExecutorService(dispatcherExecutorService.getOrNull()) + fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } /** Alias for calling [Builder.proxy] with `proxy.orElse(null)`. */ @@ -296,6 +317,7 @@ class BrandDevOkHttpClientAsync private constructor() { OkHttpClient.builder() .timeout(clientOptions.timeout()) .proxy(proxy) + .dispatcherExecutorService(dispatcherExecutorService) .sslSocketFactory(sslSocketFactory) .trustManager(trustManager) .hostnameVerifier(hostnameVerifier) diff --git a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt index 40153ed..39daac2 100644 --- a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt +++ b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt @@ -15,11 +15,13 @@ import java.net.Proxy import java.time.Duration import java.util.concurrent.CancellationException import java.util.concurrent.CompletableFuture +import java.util.concurrent.ExecutorService import javax.net.ssl.HostnameVerifier import javax.net.ssl.SSLSocketFactory import javax.net.ssl.X509TrustManager import okhttp3.Call import okhttp3.Callback +import okhttp3.Dispatcher import okhttp3.HttpUrl.Companion.toHttpUrl import okhttp3.MediaType import okhttp3.MediaType.Companion.toMediaType @@ -198,6 +200,7 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien private var timeout: Timeout = Timeout.default() private var proxy: Proxy? = null + private var dispatcherExecutorService: ExecutorService? = null private var sslSocketFactory: SSLSocketFactory? = null private var trustManager: X509TrustManager? = null private var hostnameVerifier: HostnameVerifier? = null @@ -208,6 +211,10 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien fun proxy(proxy: Proxy?) = apply { this.proxy = proxy } + fun dispatcherExecutorService(dispatcherExecutorService: ExecutorService?) = apply { + this.dispatcherExecutorService = dispatcherExecutorService + } + fun sslSocketFactory(sslSocketFactory: SSLSocketFactory?) = apply { this.sslSocketFactory = sslSocketFactory } @@ -229,6 +236,8 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien .callTimeout(timeout.request()) .proxy(proxy) .apply { + dispatcherExecutorService?.let { dispatcher(Dispatcher(it)) } + val sslSocketFactory = sslSocketFactory val trustManager = trustManager if (sslSocketFactory != null && trustManager != null) { From 95ab800f7fd4ce873d638764f1c86ab6d1d427c1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Tue, 13 Jan 2026 04:47:52 +0000 Subject: [PATCH 04/20] chore(internal): support uploading Maven repo artifacts to stainless package server --- .github/workflows/ci.yml | 18 ++++ .../main/kotlin/brand-dev.publish.gradle.kts | 8 ++ scripts/upload-artifacts | 96 +++++++++++++++++++ 3 files changed, 122 insertions(+) create mode 100755 scripts/upload-artifacts diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d4408a4..e0cb504 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -40,6 +40,9 @@ jobs: build: timeout-minutes: 15 name: build + permissions: + contents: read + id-token: write runs-on: ${{ github.repository == 'stainless-sdks/brand.dev-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork @@ -61,6 +64,21 @@ jobs: - name: Build SDK run: ./scripts/build + - name: Get GitHub OIDC Token + if: github.repository == 'stainless-sdks/brand.dev-java' + id: github-oidc + uses: actions/github-script@v6 + with: + script: core.setOutput('github_token', await core.getIDToken()); + + - name: Build and upload Maven artifacts + if: github.repository == 'stainless-sdks/brand.dev-java' + env: + URL: https://pkg.stainless.com/s + AUTH: ${{ steps.github-oidc.outputs.github_token }} + SHA: ${{ github.sha }} + PROJECT: brand.dev-java + run: ./scripts/upload-artifacts test: timeout-minutes: 15 name: test diff --git a/buildSrc/src/main/kotlin/brand-dev.publish.gradle.kts b/buildSrc/src/main/kotlin/brand-dev.publish.gradle.kts index 61fc55a..1b8d2d3 100644 --- a/buildSrc/src/main/kotlin/brand-dev.publish.gradle.kts +++ b/buildSrc/src/main/kotlin/brand-dev.publish.gradle.kts @@ -40,6 +40,14 @@ configure { } } } + repositories { + if (project.hasProperty("publishLocal")) { + maven { + name = "LocalFileSystem" + url = uri("${rootProject.layout.buildDirectory.get()}/local-maven-repo") + } + } + } } signing { diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts new file mode 100755 index 0000000..729e6f2 --- /dev/null +++ b/scripts/upload-artifacts @@ -0,0 +1,96 @@ +#!/usr/bin/env bash + +set -euo pipefail + +# ANSI Color Codes +GREEN='\033[32m' +RED='\033[31m' +NC='\033[0m' # No Color + +log_error() { + local msg="$1" + local headers="$2" + local body="$3" + echo -e "${RED}${msg}${NC}" + [[ -f "$headers" ]] && echo -e "${RED}Headers:$(cat "$headers")${NC}" + echo -e "${RED}Body: ${body}${NC}" + exit 1 +} + +upload_file() { + local file_name="$1" + local tmp_headers + tmp_headers=$(mktemp) + + if [ -f "$file_name" ]; then + echo -e "${GREEN}Processing file: $file_name${NC}" + pkg_file_name="mvn${file_name#./build/local-maven-repo}" + + # Get signed URL for uploading artifact file + signed_url_response=$(curl -X POST -G "$URL" \ + -sS --retry 5 \ + -D "$tmp_headers" \ + --data-urlencode "filename=$pkg_file_name" \ + -H "Authorization: Bearer $AUTH" \ + -H "Content-Type: application/json") + + # Validate JSON and extract URL + if ! signed_url=$(echo "$signed_url_response" | jq -e -r '.url' 2>/dev/null) || [[ "$signed_url" == "null" ]]; then + log_error "Failed to get valid signed URL" "$tmp_headers" "$signed_url_response" + fi + + # Set content-type based on file extension + local extension="${file_name##*.}" + local content_type + case "$extension" in + jar) content_type="application/java-archive" ;; + md5|sha1|sha256|sha512) content_type="text/plain" ;; + module) content_type="application/json" ;; + pom|xml) content_type="application/xml" ;; + *) content_type="application/octet-stream" ;; + esac + + # Upload file + upload_response=$(curl -v -X PUT \ + --retry 5 \ + -D "$tmp_headers" \ + -H "Content-Type: $content_type" \ + --data-binary "@${file_name}" "$signed_url" 2>&1) + + if ! echo "$upload_response" | grep -q "HTTP/[0-9.]* 200"; then + log_error "Failed upload artifact file" "$tmp_headers" "$upload_response" + fi + + # Insert small throttle to reduce rate limiting risk + sleep 0.1 + fi +} + +walk_tree() { + local current_dir="$1" + + for entry in "$current_dir"/*; do + # Check that entry is valid + [ -e "$entry" ] || [ -h "$entry" ] || continue + + if [ -d "$entry" ]; then + walk_tree "$entry" + else + upload_file "$entry" + fi + done +} + +cd "$(dirname "$0")/.." + +echo "::group::Creating local Maven content" +./gradlew publishMavenPublicationToLocalFileSystemRepository -PpublishLocal +echo "::endgroup::" + +echo "::group::Uploading to pkg.stainless.com" +walk_tree "./build/local-maven-repo" +echo "::endgroup::" + +echo "::group::Generating instructions" +echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" +echo "::endgroup::" From 47186793b72b219c12faa54c3e3b54197880ff5a Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 16 Jan 2026 03:14:48 +0000 Subject: [PATCH 05/20] chore(internal): clean up maven repo artifact script and add html documentation to repo root --- scripts/upload-artifacts | 44 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 41 insertions(+), 3 deletions(-) diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts index 729e6f2..df0c8d9 100755 --- a/scripts/upload-artifacts +++ b/scripts/upload-artifacts @@ -7,6 +7,8 @@ GREEN='\033[32m' RED='\033[31m' NC='\033[0m' # No Color +MAVEN_REPO_PATH="./build/local-maven-repo" + log_error() { local msg="$1" local headers="$2" @@ -24,7 +26,7 @@ upload_file() { if [ -f "$file_name" ]; then echo -e "${GREEN}Processing file: $file_name${NC}" - pkg_file_name="mvn${file_name#./build/local-maven-repo}" + pkg_file_name="mvn${file_name#"${MAVEN_REPO_PATH}"}" # Get signed URL for uploading artifact file signed_url_response=$(curl -X POST -G "$URL" \ @@ -47,6 +49,7 @@ upload_file() { md5|sha1|sha256|sha512) content_type="text/plain" ;; module) content_type="application/json" ;; pom|xml) content_type="application/xml" ;; + html) content_type="text/html" ;; *) content_type="application/octet-stream" ;; esac @@ -81,6 +84,41 @@ walk_tree() { done } +generate_instructions() { + cat << EOF > "$MAVEN_REPO_PATH/index.html" + + + + Maven Repo + + +

Stainless SDK Maven Repository

+

This is the Maven repository for your Stainless Java SDK build.

+ +

Directions

+

To use the uploaded Maven repository, add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

If you're using Gradle, add the following to your build.gradle file:

+
repositories {
+    maven {
+        url 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'
+    }
+}
+ + +EOF + upload_file "${MAVEN_REPO_PATH}/index.html" + + echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" + echo "For more details, see the directions in https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn/index.html" +} + cd "$(dirname "$0")/.." echo "::group::Creating local Maven content" @@ -88,9 +126,9 @@ echo "::group::Creating local Maven content" echo "::endgroup::" echo "::group::Uploading to pkg.stainless.com" -walk_tree "./build/local-maven-repo" +walk_tree "$MAVEN_REPO_PATH" echo "::endgroup::" echo "::group::Generating instructions" -echo "Configure maven or gradle to use the repo located at 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'" +generate_instructions echo "::endgroup::" From 0c299fefeefcbec581ad9c341d0a587eae6d2977 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:19:45 +0000 Subject: [PATCH 06/20] chore: test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind#3240 in tests fix: date time deserialization leniency --- README.md | 2 ++ brand-dev-java-core/build.gradle.kts | 18 +++++----- .../com/branddev/api/core/ObjectMappers.kt | 33 ++++++++++++------- .../branddev/api/core/ObjectMappersTest.kt | 16 +++------ brand-dev-java-proguard-test/build.gradle.kts | 2 +- 5 files changed, 38 insertions(+), 33 deletions(-) diff --git a/README.md b/README.md index 4a91fd4..40a18e6 100644 --- a/README.md +++ b/README.md @@ -279,6 +279,8 @@ If the SDK threw an exception, but you're _certain_ the version is compatible, t > [!CAUTION] > We make no guarantee that the SDK works correctly when the Jackson version check is disabled. +Also note that there are bugs in older Jackson versions that can affect the SDK. We don't work around all Jackson bugs ([example](https://github.com/FasterXML/jackson-databind/issues/3240)) and expect users to upgrade Jackson for those instead. + ## Network options ### Retries diff --git a/brand-dev-java-core/build.gradle.kts b/brand-dev-java-core/build.gradle.kts index 63ae8af..6098014 100644 --- a/brand-dev-java-core/build.gradle.kts +++ b/brand-dev-java-core/build.gradle.kts @@ -5,14 +5,16 @@ plugins { configurations.all { resolutionStrategy { - // Compile and test against a lower Jackson version to ensure we're compatible with it. - // We publish with a higher version (see below) to ensure users depend on a secure version by default. - force("com.fasterxml.jackson.core:jackson-core:2.13.4") - force("com.fasterxml.jackson.core:jackson-databind:2.13.4") - force("com.fasterxml.jackson.core:jackson-annotations:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.13.4") - force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.13.4") - force("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + // Compile and test against a lower Jackson version to ensure we're compatible with it. Note that + // we generally support 2.13.4, but test against 2.14.0 because 2.13.4 has some annoying (but + // niche) bugs (users should upgrade if they encounter them). We publish with a higher version + // (see below) to ensure users depend on a secure version by default. + force("com.fasterxml.jackson.core:jackson-core:2.14.0") + force("com.fasterxml.jackson.core:jackson-databind:2.14.0") + force("com.fasterxml.jackson.core:jackson-annotations:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.14.0") + force("com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.14.0") + force("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } } diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt index 2bfe68d..a860d04 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt @@ -24,6 +24,7 @@ import java.io.InputStream import java.time.DateTimeException import java.time.LocalDate import java.time.LocalDateTime +import java.time.OffsetDateTime import java.time.ZonedDateTime import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField @@ -36,7 +37,7 @@ fun jsonMapper(): JsonMapper = .addModule( SimpleModule() .addSerializer(InputStreamSerializer) - .addDeserializer(LocalDateTime::class.java, LenientLocalDateTimeDeserializer()) + .addDeserializer(OffsetDateTime::class.java, LenientOffsetDateTimeDeserializer()) ) .withCoercionConfig(LogicalType.Boolean) { it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -64,6 +65,12 @@ fun jsonMapper(): JsonMapper = .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) } + .withCoercionConfig(LogicalType.DateTime) { + it.setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) + } .withCoercionConfig(LogicalType.Array) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) .setCoercion(CoercionInputShape.Integer, CoercionAction.Fail) @@ -124,10 +131,10 @@ private object InputStreamSerializer : BaseSerializer(InputStream:: } /** - * A deserializer that can deserialize [LocalDateTime] from datetimes, dates, and zoned datetimes. + * A deserializer that can deserialize [OffsetDateTime] from datetimes, dates, and zoned datetimes. */ -private class LenientLocalDateTimeDeserializer : - StdDeserializer(LocalDateTime::class.java) { +private class LenientOffsetDateTimeDeserializer : + StdDeserializer(OffsetDateTime::class.java) { companion object { @@ -141,7 +148,7 @@ private class LenientLocalDateTimeDeserializer : override fun logicalType(): LogicalType = LogicalType.DateTime - override fun deserialize(p: JsonParser, context: DeserializationContext?): LocalDateTime { + override fun deserialize(p: JsonParser, context: DeserializationContext): OffsetDateTime { val exceptions = mutableListOf() for (formatter in DATE_TIME_FORMATTERS) { @@ -149,18 +156,20 @@ private class LenientLocalDateTimeDeserializer : val temporal = formatter.parse(p.text) return when { - !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> - LocalDate.from(temporal).atStartOfDay() - !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> - LocalDateTime.from(temporal) - else -> ZonedDateTime.from(temporal).toLocalDateTime() - } + !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> + LocalDate.from(temporal).atStartOfDay() + !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> + LocalDateTime.from(temporal) + else -> ZonedDateTime.from(temporal).toLocalDateTime() + } + .atZone(context.timeZone.toZoneId()) + .toOffsetDateTime() } catch (e: DateTimeException) { exceptions.add(e) } } - throw JsonParseException(p, "Cannot parse `LocalDateTime` from value: ${p.text}").apply { + throw JsonParseException(p, "Cannot parse `OffsetDateTime` from value: ${p.text}").apply { exceptions.forEach { addSuppressed(it) } } } diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt index 5d134d3..54a52dc 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt @@ -3,7 +3,7 @@ package com.branddev.api.core import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.readValue -import java.time.LocalDateTime +import java.time.OffsetDateTime import kotlin.reflect.KClass import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable @@ -58,14 +58,6 @@ internal class ObjectMappersTest { LONG to DOUBLE, LONG to INTEGER, CLASS to MAP, - // These aren't actually valid, but coercion configs don't work for String until - // v2.14.0: https://github.com/FasterXML/jackson-databind/issues/3240 - // We currently test on v2.13.4. - BOOLEAN to STRING, - FLOAT to STRING, - DOUBLE to STRING, - INTEGER to STRING, - LONG to STRING, ) } } @@ -84,7 +76,7 @@ internal class ObjectMappersTest { } } - enum class LenientLocalDateTimeTestCase(val string: String) { + enum class LenientOffsetDateTimeTestCase(val string: String) { DATE("1998-04-21"), DATE_TIME("1998-04-21T04:00:00"), ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"), @@ -93,10 +85,10 @@ internal class ObjectMappersTest { @ParameterizedTest @EnumSource - fun readLocalDateTime_lenient(testCase: LenientLocalDateTimeTestCase) { + fun readOffsetDateTime_lenient(testCase: LenientOffsetDateTimeTestCase) { val jsonMapper = jsonMapper() val json = jsonMapper.writeValueAsString(testCase.string) - assertDoesNotThrow { jsonMapper().readValue(json) } + assertDoesNotThrow { jsonMapper().readValue(json) } } } diff --git a/brand-dev-java-proguard-test/build.gradle.kts b/brand-dev-java-proguard-test/build.gradle.kts index a4f5b59..1be525d 100644 --- a/brand-dev-java-proguard-test/build.gradle.kts +++ b/brand-dev-java-proguard-test/build.gradle.kts @@ -19,7 +19,7 @@ dependencies { testImplementation(kotlin("test")) testImplementation("org.junit.jupiter:junit-jupiter-api:5.9.3") testImplementation("org.assertj:assertj-core:3.25.3") - testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.13.4") + testImplementation("com.fasterxml.jackson.module:jackson-module-kotlin:2.14.0") } tasks.shadowJar { From 0e378d5bf0f3016be36b8ca0189d1ee9614bbd1b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:25:20 +0000 Subject: [PATCH 07/20] chore(internal): improve maven repo docs --- scripts/upload-artifacts | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts index df0c8d9..548d152 100755 --- a/scripts/upload-artifacts +++ b/scripts/upload-artifacts @@ -56,12 +56,13 @@ upload_file() { # Upload file upload_response=$(curl -v -X PUT \ --retry 5 \ + --retry-all-errors \ -D "$tmp_headers" \ -H "Content-Type: $content_type" \ --data-binary "@${file_name}" "$signed_url" 2>&1) if ! echo "$upload_response" | grep -q "HTTP/[0-9.]* 200"; then - log_error "Failed upload artifact file" "$tmp_headers" "$upload_response" + log_error "Failed to upload artifact file" "$tmp_headers" "$upload_response" fi # Insert small throttle to reduce rate limiting risk @@ -110,6 +111,10 @@ generate_instructions() { url 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn' } } + +

Once you've added the repository, you can include dependencies from it as usual. See your + project README + for more details.

EOF From f59913f9452f71603dac23c42101f5f32696fe06 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:32:26 +0000 Subject: [PATCH 08/20] fix(client): disallow coercion from float to int --- .../src/main/kotlin/com/branddev/api/core/ObjectMappers.kt | 1 + .../test/kotlin/com/branddev/api/core/ObjectMappersTest.kt | 4 ---- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt index a860d04..5e7d07d 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt @@ -48,6 +48,7 @@ fun jsonMapper(): JsonMapper = } .withCoercionConfig(LogicalType.Integer) { it.setCoercion(CoercionInputShape.Boolean, CoercionAction.Fail) + .setCoercion(CoercionInputShape.Float, CoercionAction.Fail) .setCoercion(CoercionInputShape.String, CoercionAction.Fail) .setCoercion(CoercionInputShape.Array, CoercionAction.Fail) .setCoercion(CoercionInputShape.Object, CoercionAction.Fail) diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt index 54a52dc..eaee6cf 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt @@ -46,11 +46,7 @@ internal class ObjectMappersTest { val VALID_CONVERSIONS = listOf( FLOAT to DOUBLE, - FLOAT to INTEGER, - FLOAT to LONG, DOUBLE to FLOAT, - DOUBLE to INTEGER, - DOUBLE to LONG, INTEGER to FLOAT, INTEGER to DOUBLE, INTEGER to LONG, From fec5d3a47682efc4f92c4b9890de7fbf6588b906 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:33:51 +0000 Subject: [PATCH 09/20] chore(internal): update `actions/checkout` version --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-sonatype.yml | 2 +- .github/workflows/release-doctor.yml | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e0cb504..7e538e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,7 +20,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java uses: actions/setup-java@v4 @@ -47,7 +47,7 @@ jobs: if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java uses: actions/setup-java@v4 @@ -85,7 +85,7 @@ jobs: runs-on: ${{ github.repository == 'stainless-sdks/brand.dev-java' && 'depot-ubuntu-24.04' || 'ubuntu-latest' }} if: github.event_name == 'push' || github.event.pull_request.head.repo.fork steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java uses: actions/setup-java@v4 diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml index 9a2b820..91ae7c7 100644 --- a/.github/workflows/publish-sonatype.yml +++ b/.github/workflows/publish-sonatype.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Set up Java uses: actions/setup-java@v4 diff --git a/.github/workflows/release-doctor.yml b/.github/workflows/release-doctor.yml index d0fbe28..266abc4 100644 --- a/.github/workflows/release-doctor.yml +++ b/.github/workflows/release-doctor.yml @@ -12,7 +12,7 @@ jobs: if: github.repository == 'brand-dot-dev/java-sdk' && (github.event_name == 'push' || github.event_name == 'workflow_dispatch' || startsWith(github.head_ref, 'release-please') || github.head_ref == 'next') steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@v6 - name: Check release environment run: | From a0aa7903dfc548c4dd25ef4985e80937646539de Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:36:43 +0000 Subject: [PATCH 10/20] fix(client): fully respect max retries fix(client): send retry count header for max retries 0 chore(internal): depend on packages directly in example --- .../api/client/okhttp/OkHttpClient.kt | 2 ++ .../api/core/http/RetryingHttpClient.kt | 20 +++++++++---------- 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt index 39daac2..445709f 100644 --- a/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt +++ b/brand-dev-java-client-okhttp/src/main/kotlin/com/branddev/api/client/okhttp/OkHttpClient.kt @@ -230,6 +230,8 @@ private constructor(@JvmSynthetic internal val okHttpClient: okhttp3.OkHttpClien fun build(): OkHttpClient = OkHttpClient( okhttp3.OkHttpClient.Builder() + // `RetryingHttpClient` handles retries if the user enabled them. + .retryOnConnectionFailure(false) .connectTimeout(timeout.connect()) .readTimeout(timeout.read()) .writeTimeout(timeout.write()) diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt index 105f3c9..27b4a7e 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/http/RetryingHttpClient.kt @@ -31,10 +31,6 @@ private constructor( ) : HttpClient { override fun execute(request: HttpRequest, requestOptions: RequestOptions): HttpResponse { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.execute(request, requestOptions) - } - var modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -48,6 +44,10 @@ private constructor( modifiedRequest = setRetryCountHeader(modifiedRequest, retries) } + if (!isRetryable(modifiedRequest)) { + return httpClient.execute(modifiedRequest, requestOptions) + } + val response = try { val response = httpClient.execute(modifiedRequest, requestOptions) @@ -75,10 +75,6 @@ private constructor( request: HttpRequest, requestOptions: RequestOptions, ): CompletableFuture { - if (!isRetryable(request) || maxRetries <= 0) { - return httpClient.executeAsync(request, requestOptions) - } - val modifiedRequest = maybeAddIdempotencyHeader(request) // Don't send the current retry count in the headers if the caller set their own value. @@ -94,8 +90,12 @@ private constructor( val requestWithRetryCount = if (shouldSendRetryCount) setRetryCountHeader(request, retries) else request - return httpClient - .executeAsync(requestWithRetryCount, requestOptions) + val responseFuture = httpClient.executeAsync(requestWithRetryCount, requestOptions) + if (!isRetryable(requestWithRetryCount)) { + return responseFuture + } + + return responseFuture .handleAsync( fun( response: HttpResponse?, From 376fbbd3410d63bd7306bba1ccde2d264fa0cd3b Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 17 Jan 2026 03:37:52 +0000 Subject: [PATCH 11/20] chore(ci): upgrade `actions/setup-java` --- .github/workflows/ci.yml | 6 +++--- .github/workflows/publish-sonatype.yml | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7e538e6..5d2c29b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -50,7 +50,7 @@ jobs: - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | @@ -88,7 +88,7 @@ jobs: - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | diff --git a/.github/workflows/publish-sonatype.yml b/.github/workflows/publish-sonatype.yml index 91ae7c7..3c8a62b 100644 --- a/.github/workflows/publish-sonatype.yml +++ b/.github/workflows/publish-sonatype.yml @@ -17,7 +17,7 @@ jobs: - uses: actions/checkout@v6 - name: Set up Java - uses: actions/setup-java@v4 + uses: actions/setup-java@v5 with: distribution: temurin java-version: | From b1ed94ce0d3afefc3597f5978bafa9adecc0ff20 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 03:11:12 +0000 Subject: [PATCH 12/20] chore(internal): update maven repo doc to include authentication --- scripts/upload-artifacts | 64 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 59 insertions(+), 5 deletions(-) diff --git a/scripts/upload-artifacts b/scripts/upload-artifacts index 548d152..10f3c70 100755 --- a/scripts/upload-artifacts +++ b/scripts/upload-artifacts @@ -96,8 +96,52 @@ generate_instructions() {

Stainless SDK Maven Repository

This is the Maven repository for your Stainless Java SDK build.

-

Directions

-

To use the uploaded Maven repository, add the following to your project's pom.xml:

+

Project configuration

+ +

The details depend on whether you're using Maven or Gradle as your build tool.

+ +

Maven

+ +

Add the following to your project's pom.xml:

+
<repositories>
+    <repository>
+        <id>stainless-sdk-repo</id>
+        <url>https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn</url>
+    </repository>
+</repositories>
+ +

Gradle

+

Add the following to your build.gradle file:

+
repositories {
+    maven {
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+    }
+}
+ +
+

Configuring authentication (if required)

+ +

Some accounts may require authentication to access the repository. If so, use the + following instructions, replacing YOUR_STAINLESS_API_TOKEN with your actual token.

+ +

Maven with authentication

+ +

First, ensure you have the following in your Maven settings.xml for repo authentication:

+
<servers>
+    <server>
+        <id>stainless-sdk-repo</id>
+        <configuration>
+            <httpHeaders>
+                <property>
+                    <name>Authorization</name>
+                    <value>Bearer YOUR_STAINLESS_API_TOKEN</value>
+                </property>
+            </httpHeaders>
+        </configuration>
+    </server>
+</servers>
+ +

Then, add the following to your project's pom.xml:

<repositories>
     <repository>
         <id>stainless-sdk-repo</id>
@@ -105,14 +149,24 @@ generate_instructions() {
     </repository>
 </repositories>
-

If you're using Gradle, add the following to your build.gradle file:

+

Gradle with authentication

+

Add the following to your build.gradle file:

repositories {
     maven {
-        url 'https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn'
+        url "https://pkg.stainless.com/s/${PROJECT}/${SHA}/mvn"
+        credentials(HttpHeaderCredentials) {
+            name = "Authorization"
+            value = "Bearer YOUR_STAINLESS_API_TOKEN"
+        }
+        authentication {
+            header(HttpHeaderAuthentication)
+        }
     }
 }
+
-

Once you've added the repository, you can include dependencies from it as usual. See your +

Using the repository

+

Once you've configured the repository, you can include dependencies from it as usual. See your project README for more details.

From a67a40257d88d3d34379b2f704d50958292b04e3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 03:11:37 +0000 Subject: [PATCH 13/20] feat(client): send `X-Stainless-Kotlin-Version` header --- .../src/main/kotlin/com/branddev/api/core/ClientOptions.kt | 1 + 1 file changed, 1 insertion(+) diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ClientOptions.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ClientOptions.kt index 4d81751..999fc6d 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ClientOptions.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ClientOptions.kt @@ -402,6 +402,7 @@ private constructor( headers.put("X-Stainless-Package-Version", getPackageVersion()) headers.put("X-Stainless-Runtime", "JRE") headers.put("X-Stainless-Runtime-Version", getJavaVersion()) + headers.put("X-Stainless-Kotlin-Version", KotlinVersion.CURRENT.toString()) apiKey.let { if (!it.isEmpty()) { headers.put("Authorization", "Bearer $it") From 0b13154ffa5b630ca445371d68b676bc5eadcea3 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:29:55 +0000 Subject: [PATCH 14/20] chore(internal): correct cache invalidation for `SKIP_MOCK_TESTS` --- buildSrc/src/main/kotlin/brand-dev.kotlin.gradle.kts | 3 +++ 1 file changed, 3 insertions(+) diff --git a/buildSrc/src/main/kotlin/brand-dev.kotlin.gradle.kts b/buildSrc/src/main/kotlin/brand-dev.kotlin.gradle.kts index c927e18..bc2fd3f 100644 --- a/buildSrc/src/main/kotlin/brand-dev.kotlin.gradle.kts +++ b/buildSrc/src/main/kotlin/brand-dev.kotlin.gradle.kts @@ -33,6 +33,9 @@ kotlin { tasks.withType().configureEach { systemProperty("junit.jupiter.execution.parallel.enabled", true) systemProperty("junit.jupiter.execution.parallel.mode.default", "concurrent") + + // `SKIP_MOCK_TESTS` affects which tests run so it must be added as input for proper cache invalidation. + inputs.property("skipMockTests", System.getenv("SKIP_MOCK_TESTS")).optional(true) } val ktfmt by configurations.creating From 615ebc293f66bbdd2ce451b0620a5cd37728212f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Fri, 23 Jan 2026 03:34:32 +0000 Subject: [PATCH 15/20] fix(client): preserve time zone in lenient date-time parsing --- .../com/branddev/api/core/ObjectMappers.kt | 19 +++++---- .../branddev/api/core/ObjectMappersTest.kt | 41 +++++++++++++++---- 2 files changed, 44 insertions(+), 16 deletions(-) diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt index 5e7d07d..889705d 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/core/ObjectMappers.kt @@ -25,7 +25,7 @@ import java.time.DateTimeException import java.time.LocalDate import java.time.LocalDateTime import java.time.OffsetDateTime -import java.time.ZonedDateTime +import java.time.ZoneId import java.time.format.DateTimeFormatter import java.time.temporal.ChronoField @@ -157,14 +157,15 @@ private class LenientOffsetDateTimeDeserializer : val temporal = formatter.parse(p.text) return when { - !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> - LocalDate.from(temporal).atStartOfDay() - !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> - LocalDateTime.from(temporal) - else -> ZonedDateTime.from(temporal).toLocalDateTime() - } - .atZone(context.timeZone.toZoneId()) - .toOffsetDateTime() + !temporal.isSupported(ChronoField.HOUR_OF_DAY) -> + LocalDate.from(temporal) + .atStartOfDay() + .atZone(ZoneId.of("UTC")) + .toOffsetDateTime() + !temporal.isSupported(ChronoField.OFFSET_SECONDS) -> + LocalDateTime.from(temporal).atZone(ZoneId.of("UTC")).toOffsetDateTime() + else -> OffsetDateTime.from(temporal) + } } catch (e: DateTimeException) { exceptions.add(e) } diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt index eaee6cf..7270533 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/core/ObjectMappersTest.kt @@ -3,12 +3,14 @@ package com.branddev.api.core import com.fasterxml.jackson.annotation.JsonProperty import com.fasterxml.jackson.databind.exc.MismatchedInputException import com.fasterxml.jackson.module.kotlin.readValue +import java.time.LocalDate +import java.time.LocalTime import java.time.OffsetDateTime +import java.time.ZoneOffset import kotlin.reflect.KClass import org.assertj.core.api.Assertions.assertThat import org.assertj.core.api.Assertions.catchThrowable import org.junit.jupiter.api.Test -import org.junit.jupiter.api.assertDoesNotThrow import org.junit.jupiter.params.ParameterizedTest import org.junit.jupiter.params.provider.EnumSource import org.junitpioneer.jupiter.cartesian.CartesianTest @@ -72,11 +74,34 @@ internal class ObjectMappersTest { } } - enum class LenientOffsetDateTimeTestCase(val string: String) { - DATE("1998-04-21"), - DATE_TIME("1998-04-21T04:00:00"), - ZONED_DATE_TIME_1("1998-04-21T04:00:00+03:00"), - ZONED_DATE_TIME_2("1998-04-21T04:00:00Z"), + enum class LenientOffsetDateTimeTestCase( + val string: String, + val expectedOffsetDateTime: OffsetDateTime, + ) { + DATE( + "1998-04-21", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(0, 0), ZoneOffset.UTC), + ), + DATE_TIME( + "1998-04-21T04:00:00", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), + ZONED_DATE_TIME_1( + "1998-04-21T04:00:00+03:00", + expectedOffsetDateTime = + OffsetDateTime.of( + LocalDate.of(1998, 4, 21), + LocalTime.of(4, 0), + ZoneOffset.ofHours(3), + ), + ), + ZONED_DATE_TIME_2( + "1998-04-21T04:00:00Z", + expectedOffsetDateTime = + OffsetDateTime.of(LocalDate.of(1998, 4, 21), LocalTime.of(4, 0), ZoneOffset.UTC), + ), } @ParameterizedTest @@ -85,6 +110,8 @@ internal class ObjectMappersTest { val jsonMapper = jsonMapper() val json = jsonMapper.writeValueAsString(testCase.string) - assertDoesNotThrow { jsonMapper().readValue(json) } + val offsetDateTime = jsonMapper().readValue(json) + + assertThat(offsetDateTime).isEqualTo(testCase.expectedOffsetDateTime) } } From b05460be067f2336ff4c1aebbd5a29c2b9cdd8db Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 24 Jan 2026 03:23:54 +0000 Subject: [PATCH 16/20] chore(ci): upgrade `actions/github-script` --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5d2c29b..dc3c101 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,7 +67,7 @@ jobs: - name: Get GitHub OIDC Token if: github.repository == 'stainless-sdks/brand.dev-java' id: github-oidc - uses: actions/github-script@v6 + uses: actions/github-script@v8 with: script: core.setOutput('github_token', await core.getIDToken()); From 02417434d1b53a5458346be5e14512bf5dcf092f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 29 Jan 2026 03:28:23 +0000 Subject: [PATCH 17/20] fix(docs): fix mcp installation instructions for remote servers --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 40a18e6..df9128f 100644 --- a/README.md +++ b/README.md @@ -15,8 +15,8 @@ It is generated with [Stainless](https://www.stainless.com/). Use the Brand Dev MCP Server to enable AI assistants to interact with this API, allowing them to explore endpoints, make test requests, and use documentation to help integrate this SDK into your application. -[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=brand.dev-mcp&config=eyJuYW1lIjoiYnJhbmQuZGV2LW1jcCIsInRyYW5zcG9ydCI6InNzZSIsInVybCI6Imh0dHBzOi8vYnJhbmQtZGV2LnN0bG1jcC5jb20vc3NlIn0) -[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22brand.dev-mcp%22%2C%22type%22%3A%22sse%22%2C%22url%22%3A%22https%3A%2F%2Fbrand-dev.stlmcp.com%2Fsse%22%7D) +[![Add to Cursor](https://cursor.com/deeplink/mcp-install-dark.svg)](https://cursor.com/en-US/install-mcp?name=brand.dev-mcp&config=eyJuYW1lIjoiYnJhbmQuZGV2LW1jcCIsInRyYW5zcG9ydCI6Imh0dHAiLCJ1cmwiOiJodHRwczovL2JyYW5kLWRldi5zdGxtY3AuY29tIiwiaGVhZGVycyI6eyJ4LWJyYW5kLWRldi1hcGkta2V5IjoiTXkgQVBJIEtleSJ9fQ) +[![Install in VS Code](https://img.shields.io/badge/_-Add_to_VS_Code-blue?style=for-the-badge&logo=data:image/svg%2bxml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIGZpbGw9Im5vbmUiIHZpZXdCb3g9IjAgMCA0MCA0MCI+PHBhdGggZmlsbD0iI0VFRSIgZmlsbC1ydWxlPSJldmVub2RkIiBkPSJNMzAuMjM1IDM5Ljg4NGEyLjQ5MSAyLjQ5MSAwIDAgMS0xLjc4MS0uNzNMMTIuNyAyNC43OGwtMy40NiAyLjYyNC0zLjQwNiAyLjU4MmExLjY2NSAxLjY2NSAwIDAgMS0xLjA4Mi4zMzggMS42NjQgMS42NjQgMCAwIDEtMS4wNDYtLjQzMWwtMi4yLTJhMS42NjYgMS42NjYgMCAwIDEgMC0yLjQ2M0w3LjQ1OCAyMCA0LjY3IDE3LjQ1MyAxLjUwNyAxNC41N2ExLjY2NSAxLjY2NSAwIDAgMSAwLTIuNDYzbDIuMi0yYTEuNjY1IDEuNjY1IDAgMCAxIDIuMTMtLjA5N2w2Ljg2MyA1LjIwOUwyOC40NTIuODQ0YTIuNDg4IDIuNDg4IDAgMCAxIDEuODQxLS43MjljLjM1MS4wMDkuNjk5LjA5MSAxLjAxOS4yNDVsOC4yMzYgMy45NjFhMi41IDIuNSAwIDAgMSAxLjQxNSAyLjI1M3YuMDk5LS4wNDVWMzMuMzd2LS4wNDUuMDk1YTIuNTAxIDIuNTAxIDAgMCAxLTEuNDE2IDIuMjU3bC04LjIzNSAzLjk2MWEyLjQ5MiAyLjQ5MiAwIDAgMS0xLjA3Ny4yNDZabS43MTYtMjguOTQ3LTExLjk0OCA5LjA2MiAxMS45NTIgOS4wNjUtLjAwNC0xOC4xMjdaIi8+PC9zdmc+)](https://vscode.stainless.com/mcp/%7B%22name%22%3A%22brand.dev-mcp%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fbrand-dev.stlmcp.com%22%2C%22headers%22%3A%7B%22x-brand-dev-api-key%22%3A%22My%20API%20Key%22%7D%7D) > Note: You may need to set environment variables in your MCP client. From e27c90dd180ba3a7cea0e0082f484138f07175a1 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Sat, 31 Jan 2026 03:46:43 +0000 Subject: [PATCH 18/20] chore(internal): allow passing args to `./scripts/test` --- scripts/build | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/build b/scripts/build index f406348..16a2b00 100755 --- a/scripts/build +++ b/scripts/build @@ -5,4 +5,4 @@ set -e cd "$(dirname "$0")/.." echo "==> Building classes" -./gradlew build testClasses -x test +./gradlew build testClasses "$@" -x test From fed044d09175f814f8e0bad4df5f9555f6410e15 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:58:28 +0000 Subject: [PATCH 19/20] feat(api): api update --- .stats.yml | 4 +-- README.md | 11 ++++--- .../api/models/brand/BrandRetrieveParams.kt | 32 ++++++++++++------- .../api/services/async/BrandServiceAsync.kt | 32 ++++--------------- .../api/services/blocking/BrandService.kt | 30 ++++------------- .../models/brand/BrandRetrieveParamsTest.kt | 4 +-- 6 files changed, 45 insertions(+), 68 deletions(-) diff --git a/.stats.yml b/.stats.yml index ccc6aee..69f3e77 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 15 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-863ddc13e032497459a639cf02a16349831dda7e39557cbd5ce33da34d086b02.yml -openapi_spec_hash: f972aac9618fe8df340d96344b3d0578 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/brand-dev%2Fbrand.dev-737dbedd830e2c989387e90a9bb5baa3915306ecfef2e46b09d02cb1879f043c.yml +openapi_spec_hash: 7bc21f4c6d5fd39c1a3b22626846ca87 config_hash: 6f10592c7d0c3bafefc1271472283217 diff --git a/README.md b/README.md index df9128f..cf40af1 100644 --- a/README.md +++ b/README.md @@ -318,7 +318,9 @@ To set a custom timeout, configure the method call using the `timeout` method: ```java import com.branddev.api.models.brand.BrandRetrieveResponse; -BrandRetrieveResponse brand = client.brand().retrieve(RequestOptions.builder().timeout(Duration.ofSeconds(30)).build()); +BrandRetrieveResponse brand = client.brand().retrieve( + params, RequestOptions.builder().timeout(Duration.ofSeconds(30)).build() +); ``` Or configure the default for all method calls at the client level: @@ -502,10 +504,9 @@ To forcibly omit a required parameter or property, pass [`JsonMissing`](brand-de ```java import com.branddev.api.core.JsonMissing; -import com.branddev.api.models.brand.BrandAiProductsParams; import com.branddev.api.models.brand.BrandRetrieveParams; -BrandRetrieveParams params = BrandAiProductsParams.builder() +BrandRetrieveParams params = BrandRetrieveParams.builder() .domain(JsonMissing.of()) .build(); ``` @@ -583,7 +584,9 @@ Or configure the method call to validate the response using the `responseValidat ```java import com.branddev.api.models.brand.BrandRetrieveResponse; -BrandRetrieveResponse brand = client.brand().retrieve(RequestOptions.builder().responseValidation(true).build()); +BrandRetrieveResponse brand = client.brand().retrieve( + params, RequestOptions.builder().responseValidation(true).build() +); ``` Or configure the default for all method calls at the client level: diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveParams.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveParams.kt index 372177d..8ba80d7 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveParams.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/models/brand/BrandRetrieveParams.kt @@ -5,6 +5,7 @@ package com.branddev.api.models.brand import com.branddev.api.core.Enum import com.branddev.api.core.JsonField import com.branddev.api.core.Params +import com.branddev.api.core.checkRequired import com.branddev.api.core.http.Headers import com.branddev.api.core.http.QueryParams import com.branddev.api.errors.BrandDevInvalidDataException @@ -16,7 +17,7 @@ import kotlin.jvm.optionals.getOrNull /** Retrieve logos, backdrops, colors, industry, description, and more from any domain */ class BrandRetrieveParams private constructor( - private val domain: String?, + private val domain: String, private val forceLanguage: ForceLanguage?, private val maxSpeed: Boolean?, private val timeoutMs: Long?, @@ -28,7 +29,7 @@ private constructor( * Domain name to retrieve brand data for (e.g., 'example.com', 'google.com'). Cannot be used * with name or ticker parameters. */ - fun domain(): Optional = Optional.ofNullable(domain) + fun domain(): String = domain /** * Optional parameter to force the language of the retrieved brand data. Works with all three @@ -60,9 +61,14 @@ private constructor( companion object { - @JvmStatic fun none(): BrandRetrieveParams = builder().build() - - /** Returns a mutable builder for constructing an instance of [BrandRetrieveParams]. */ + /** + * Returns a mutable builder for constructing an instance of [BrandRetrieveParams]. + * + * The following fields are required: + * ```java + * .domain() + * ``` + */ @JvmStatic fun builder() = Builder() } @@ -90,10 +96,7 @@ private constructor( * Domain name to retrieve brand data for (e.g., 'example.com', 'google.com'). Cannot be * used with name or ticker parameters. */ - fun domain(domain: String?) = apply { this.domain = domain } - - /** Alias for calling [Builder.domain] with `domain.orElse(null)`. */ - fun domain(domain: Optional) = domain(domain.getOrNull()) + fun domain(domain: String) = apply { this.domain = domain } /** * Optional parameter to force the language of the retrieved brand data. Works with all @@ -243,10 +246,17 @@ private constructor( * Returns an immutable instance of [BrandRetrieveParams]. * * Further updates to this [Builder] will not mutate the returned instance. + * + * The following fields are required: + * ```java + * .domain() + * ``` + * + * @throws IllegalStateException if any required field is unset. */ fun build(): BrandRetrieveParams = BrandRetrieveParams( - domain, + checkRequired("domain", domain), forceLanguage, maxSpeed, timeoutMs, @@ -260,7 +270,7 @@ private constructor( override fun _queryParams(): QueryParams = QueryParams.builder() .apply { - domain?.let { put("domain", it) } + put("domain", domain) forceLanguage?.let { put("force_language", it.toString()) } maxSpeed?.let { put("maxSpeed", it.toString()) } timeoutMs?.let { put("timeoutMS", it.toString()) } diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/async/BrandServiceAsync.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/async/BrandServiceAsync.kt index 37db106..4500458 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/async/BrandServiceAsync.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/async/BrandServiceAsync.kt @@ -53,23 +53,15 @@ interface BrandServiceAsync { fun withOptions(modifier: Consumer): BrandServiceAsync /** Retrieve logos, backdrops, colors, industry, description, and more from any domain */ - fun retrieve(): CompletableFuture = retrieve(BrandRetrieveParams.none()) + fun retrieve(params: BrandRetrieveParams): CompletableFuture = + retrieve(params, RequestOptions.none()) /** @see retrieve */ fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none(), + params: BrandRetrieveParams, requestOptions: RequestOptions = RequestOptions.none(), ): CompletableFuture - /** @see retrieve */ - fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none() - ): CompletableFuture = retrieve(params, RequestOptions.none()) - - /** @see retrieve */ - fun retrieve(requestOptions: RequestOptions): CompletableFuture = - retrieve(BrandRetrieveParams.none(), requestOptions) - /** * Beta feature: Extract product information from a brand's website. Brand.dev will analyze the * website and return a list of products with details such as name, description, image, pricing, @@ -290,26 +282,16 @@ interface BrandServiceAsync { * Returns a raw HTTP response for `get /brand/retrieve`, but is otherwise the same as * [BrandServiceAsync.retrieve]. */ - fun retrieve(): CompletableFuture> = - retrieve(BrandRetrieveParams.none()) - - /** @see retrieve */ fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none(), - requestOptions: RequestOptions = RequestOptions.none(), - ): CompletableFuture> - - /** @see retrieve */ - fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none() + params: BrandRetrieveParams ): CompletableFuture> = retrieve(params, RequestOptions.none()) /** @see retrieve */ fun retrieve( - requestOptions: RequestOptions - ): CompletableFuture> = - retrieve(BrandRetrieveParams.none(), requestOptions) + params: BrandRetrieveParams, + requestOptions: RequestOptions = RequestOptions.none(), + ): CompletableFuture> /** * Returns a raw HTTP response for `post /brand/ai/products`, but is otherwise the same as diff --git a/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/blocking/BrandService.kt b/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/blocking/BrandService.kt index a4ec1a5..b467fc0 100644 --- a/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/blocking/BrandService.kt +++ b/brand-dev-java-core/src/main/kotlin/com/branddev/api/services/blocking/BrandService.kt @@ -53,22 +53,15 @@ interface BrandService { fun withOptions(modifier: Consumer): BrandService /** Retrieve logos, backdrops, colors, industry, description, and more from any domain */ - fun retrieve(): BrandRetrieveResponse = retrieve(BrandRetrieveParams.none()) + fun retrieve(params: BrandRetrieveParams): BrandRetrieveResponse = + retrieve(params, RequestOptions.none()) /** @see retrieve */ fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none(), + params: BrandRetrieveParams, requestOptions: RequestOptions = RequestOptions.none(), ): BrandRetrieveResponse - /** @see retrieve */ - fun retrieve(params: BrandRetrieveParams = BrandRetrieveParams.none()): BrandRetrieveResponse = - retrieve(params, RequestOptions.none()) - - /** @see retrieve */ - fun retrieve(requestOptions: RequestOptions): BrandRetrieveResponse = - retrieve(BrandRetrieveParams.none(), requestOptions) - /** * Beta feature: Extract product information from a brand's website. Brand.dev will analyze the * website and return a list of products with details such as name, description, image, pricing, @@ -273,27 +266,16 @@ interface BrandService { * [BrandService.retrieve]. */ @MustBeClosed - fun retrieve(): HttpResponseFor = - retrieve(BrandRetrieveParams.none()) + fun retrieve(params: BrandRetrieveParams): HttpResponseFor = + retrieve(params, RequestOptions.none()) /** @see retrieve */ @MustBeClosed fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none(), + params: BrandRetrieveParams, requestOptions: RequestOptions = RequestOptions.none(), ): HttpResponseFor - /** @see retrieve */ - @MustBeClosed - fun retrieve( - params: BrandRetrieveParams = BrandRetrieveParams.none() - ): HttpResponseFor = retrieve(params, RequestOptions.none()) - - /** @see retrieve */ - @MustBeClosed - fun retrieve(requestOptions: RequestOptions): HttpResponseFor = - retrieve(BrandRetrieveParams.none(), requestOptions) - /** * Returns a raw HTTP response for `post /brand/ai/products`, but is otherwise the same as * [BrandService.aiProducts]. diff --git a/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveParamsTest.kt b/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveParamsTest.kt index 794e470..5f9b0b2 100644 --- a/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveParamsTest.kt +++ b/brand-dev-java-core/src/test/kotlin/com/branddev/api/models/brand/BrandRetrieveParamsTest.kt @@ -43,10 +43,10 @@ internal class BrandRetrieveParamsTest { @Test fun queryParamsWithoutOptionalFields() { - val params = BrandRetrieveParams.builder().build() + val params = BrandRetrieveParams.builder().domain("domain").build() val queryParams = params._queryParams() - assertThat(queryParams).isEqualTo(QueryParams.builder().build()) + assertThat(queryParams).isEqualTo(QueryParams.builder().put("domain", "domain").build()) } } From b90c89e30838e49f4dbf382456cde1205840d29e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Mon, 2 Feb 2026 22:58:48 +0000 Subject: [PATCH 20/20] release: 0.1.0-alpha.27 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 41 +++++++++++++++++++++++++++++++++++ README.md | 10 ++++----- build.gradle.kts | 2 +- 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 315f7d3..7657c56 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.1.0-alpha.26" + ".": "0.1.0-alpha.27" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md index b3a2053..ba339d0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,46 @@ # Changelog +## 0.1.0-alpha.27 (2026-02-02) + +Full Changelog: [v0.1.0-alpha.26...v0.1.0-alpha.27](https://github.com/brand-dot-dev/java-sdk/compare/v0.1.0-alpha.26...v0.1.0-alpha.27) + +### Features + +* **api:** api update ([fed044d](https://github.com/brand-dot-dev/java-sdk/commit/fed044d09175f814f8e0bad4df5f9555f6410e15)) +* **client:** add `HttpRequest#url()` method ([6cfde89](https://github.com/brand-dot-dev/java-sdk/commit/6cfde8967d8054950cf7bfb62c3b5e9022f79107)) +* **client:** allow configuring dispatcher executor service ([7eca573](https://github.com/brand-dot-dev/java-sdk/commit/7eca57352b72de96b78b7ee16be87ea844a10f89)) +* **client:** send `X-Stainless-Kotlin-Version` header ([a67a402](https://github.com/brand-dot-dev/java-sdk/commit/a67a40257d88d3d34379b2f704d50958292b04e3)) + + +### Bug Fixes + +* **client:** disallow coercion from float to int ([f59913f](https://github.com/brand-dot-dev/java-sdk/commit/f59913f9452f71603dac23c42101f5f32696fe06)) +* **client:** fully respect max retries ([a0aa790](https://github.com/brand-dot-dev/java-sdk/commit/a0aa7903dfc548c4dd25ef4985e80937646539de)) +* **client:** preserve time zone in lenient date-time parsing ([615ebc2](https://github.com/brand-dot-dev/java-sdk/commit/615ebc293f66bbdd2ce451b0620a5cd37728212f)) +* **client:** send retry count header for max retries 0 ([a0aa790](https://github.com/brand-dot-dev/java-sdk/commit/a0aa7903dfc548c4dd25ef4985e80937646539de)) +* date time deserialization leniency ([0c299fe](https://github.com/brand-dot-dev/java-sdk/commit/0c299fefeefcbec581ad9c341d0a587eae6d2977)) +* **docs:** fix mcp installation instructions for remote servers ([0241743](https://github.com/brand-dot-dev/java-sdk/commit/02417434d1b53a5458346be5e14512bf5dcf092f)) + + +### Chores + +* **ci:** upgrade `actions/github-script` ([b05460b](https://github.com/brand-dot-dev/java-sdk/commit/b05460be067f2336ff4c1aebbd5a29c2b9cdd8db)) +* **ci:** upgrade `actions/setup-java` ([376fbbd](https://github.com/brand-dot-dev/java-sdk/commit/376fbbd3410d63bd7306bba1ccde2d264fa0cd3b)) +* **internal:** allow passing args to `./scripts/test` ([e27c90d](https://github.com/brand-dot-dev/java-sdk/commit/e27c90dd180ba3a7cea0e0082f484138f07175a1)) +* **internal:** clean up maven repo artifact script and add html documentation to repo root ([4718679](https://github.com/brand-dot-dev/java-sdk/commit/47186793b72b219c12faa54c3e3b54197880ff5a)) +* **internal:** correct cache invalidation for `SKIP_MOCK_TESTS` ([0b13154](https://github.com/brand-dot-dev/java-sdk/commit/0b13154ffa5b630ca445371d68b676bc5eadcea3)) +* **internal:** depend on packages directly in example ([a0aa790](https://github.com/brand-dot-dev/java-sdk/commit/a0aa7903dfc548c4dd25ef4985e80937646539de)) +* **internal:** improve maven repo docs ([0e378d5](https://github.com/brand-dot-dev/java-sdk/commit/0e378d5bf0f3016be36b8ca0189d1ee9614bbd1b)) +* **internal:** support uploading Maven repo artifacts to stainless package server ([95ab800](https://github.com/brand-dot-dev/java-sdk/commit/95ab800f7fd4ce873d638764f1c86ab6d1d427c1)) +* **internal:** update `actions/checkout` version ([fec5d3a](https://github.com/brand-dot-dev/java-sdk/commit/fec5d3a47682efc4f92c4b9890de7fbf6588b906)) +* **internal:** update maven repo doc to include authentication ([b1ed94c](https://github.com/brand-dot-dev/java-sdk/commit/b1ed94ce0d3afefc3597f5978bafa9adecc0ff20)) +* test on Jackson 2.14.0 to avoid encountering FasterXML/jackson-databind[#3240](https://github.com/brand-dot-dev/java-sdk/issues/3240) in tests ([0c299fe](https://github.com/brand-dot-dev/java-sdk/commit/0c299fefeefcbec581ad9c341d0a587eae6d2977)) + + +### Documentation + +* prominently feature MCP server setup in root SDK readmes ([18a0348](https://github.com/brand-dot-dev/java-sdk/commit/18a03480949d4afc6d8ed4a45a55c84094aadae6)) + ## 0.1.0-alpha.26 (2026-01-05) Full Changelog: [v0.1.0-alpha.25...v0.1.0-alpha.26](https://github.com/brand-dot-dev/java-sdk/compare/v0.1.0-alpha.25...v0.1.0-alpha.26) diff --git a/README.md b/README.md index cf40af1..bbdfde7 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.branddev.api/brand-dev-java)](https://central.sonatype.com/artifact/com.branddev.api/brand-dev-java/0.1.0-alpha.26) -[![javadoc](https://javadoc.io/badge2/com.branddev.api/brand-dev-java/0.1.0-alpha.26/javadoc.svg)](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.26) +[![Maven Central](https://img.shields.io/maven-central/v/com.branddev.api/brand-dev-java)](https://central.sonatype.com/artifact/com.branddev.api/brand-dev-java/0.1.0-alpha.27) +[![javadoc](https://javadoc.io/badge2/com.branddev.api/brand-dev-java/0.1.0-alpha.27/javadoc.svg)](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.27) @@ -22,7 +22,7 @@ Use the Brand Dev MCP Server to enable AI assistants to interact with this API, -The REST API documentation can be found on [docs.brand.dev](https://docs.brand.dev/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.26). +The REST API documentation can be found on [docs.brand.dev](https://docs.brand.dev/). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.branddev.api/brand-dev-java/0.1.0-alpha.27). @@ -33,7 +33,7 @@ The REST API documentation can be found on [docs.brand.dev](https://docs.brand.d ### Gradle ```kotlin -implementation("com.branddev.api:brand-dev-java:0.1.0-alpha.26") +implementation("com.branddev.api:brand-dev-java:0.1.0-alpha.27") ``` ### Maven @@ -42,7 +42,7 @@ implementation("com.branddev.api:brand-dev-java:0.1.0-alpha.26") com.branddev.api brand-dev-java - 0.1.0-alpha.26 + 0.1.0-alpha.27 ``` diff --git a/build.gradle.kts b/build.gradle.kts index 7086b78..5b39dcd 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ repositories { allprojects { group = "com.branddev.api" - version = "0.1.0-alpha.26" // x-release-please-version + version = "0.1.0-alpha.27" // x-release-please-version } subprojects {