From ddc330801f86293e6a185d2dea0d05ff672a7559 Mon Sep 17 00:00:00 2001 From: Brian Demers Date: Fri, 13 Feb 2026 18:08:56 -0500 Subject: [PATCH] Add Spring Boot starter for Arcade SDK Adds a `ArcadeClient` bean if properties are available --- arcade-java-example/build.gradle.kts | 6 ++- .../dev/arcade/example/SpringBootExample.java | 53 +++++++++++++++++++ arcade-spring-boot-starter/build.gradle.kts | 34 ++++++++++++ .../springboot/ArcadeAutoConfiguration.kt | 41 ++++++++++++++ ...ot.autoconfigure.AutoConfiguration.imports | 1 + settings.gradle.kts | 3 +- 6 files changed, 136 insertions(+), 2 deletions(-) create mode 100644 arcade-java-example/src/main/java/dev/arcade/example/SpringBootExample.java create mode 100644 arcade-spring-boot-starter/build.gradle.kts create mode 100644 arcade-spring-boot-starter/src/main/java/dev/arcade/springboot/ArcadeAutoConfiguration.kt create mode 100644 arcade-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports diff --git a/arcade-java-example/build.gradle.kts b/arcade-java-example/build.gradle.kts index cb30397..4e76953 100644 --- a/arcade-java-example/build.gradle.kts +++ b/arcade-java-example/build.gradle.kts @@ -9,11 +9,15 @@ repositories { dependencies { implementation(project(":arcade-java")) + + // Only needed for SpringBootExample + implementation(project(":arcade-spring-boot-starter")) + implementation("org.springframework.boot:spring-boot-starter:3.5.10") } tasks.withType().configureEach { // Allow using more modern APIs, like `List.of` and `Map.of`, in examples. - options.release.set(9) + options.release.set(17) } application { diff --git a/arcade-java-example/src/main/java/dev/arcade/example/SpringBootExample.java b/arcade-java-example/src/main/java/dev/arcade/example/SpringBootExample.java new file mode 100644 index 0000000..3caa0c5 --- /dev/null +++ b/arcade-java-example/src/main/java/dev/arcade/example/SpringBootExample.java @@ -0,0 +1,53 @@ +package dev.arcade.example; + +import dev.arcade.client.ArcadeClient; +import dev.arcade.models.tools.ExecuteToolRequest; +import dev.arcade.models.tools.ExecuteToolResponse; +import dev.arcade.models.tools.ToolExecuteParams; +import org.springframework.boot.ApplicationRunner; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.context.annotation.Bean; + +/** + * Example of calling a tool using the Arcade Java SDK. + */ +@SpringBootApplication +public class SpringBootExample { + + /** + * Starts the Spring Boot application. + * @param args All args are passed into the SpringApplication + */ + public static void main(String[] args) { + SpringApplication.run(SpringBootExample.class, args); + } + + /** + * Injects an ArcadeClient, and returns an ApplicationRunner that makes a tool call. + * @param client Arcade Client is autoinjected if ARCADE_API_KEY, or equivalent application.properties var is set. + * @return Runs code on application start. + */ + @Bean + ApplicationRunner appRunner(ArcadeClient client) { + return args -> { + String userId = System.getenv("ARCADE_USER_ID"); // the Spotify tool require a userId + if (userId == null) { + throw new IllegalArgumentException("Missing ARCADE_USER_ID environment variable"); + } + + ToolExecuteParams params = ToolExecuteParams.builder() + .executeToolRequest(ExecuteToolRequest.builder() + .toolName("Spotify.ResumePlayback@1.0.2") + .userId(userId) + .build()) + .build(); + ExecuteToolResponse executeToolResponse = client.tools().execute(params); + executeToolResponse + .output() + .ifPresentOrElse( + output -> System.out.println("Tool output: " + output._value()), + () -> System.out.println("No output for this tool")); + }; + } +} diff --git a/arcade-spring-boot-starter/build.gradle.kts b/arcade-spring-boot-starter/build.gradle.kts new file mode 100644 index 0000000..9bf6c56 --- /dev/null +++ b/arcade-spring-boot-starter/build.gradle.kts @@ -0,0 +1,34 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget +import org.jetbrains.kotlin.gradle.tasks.KotlinCompile + +plugins { + id("arcade.kotlin") + id("java") + id("io.spring.dependency-management") version "1.1.7" + application +} + +dependencies { + api(project(":arcade-java")) + implementation("org.springframework.boot:spring-boot-starter:3.5.10") +} + +tasks.withType().configureEach { + // Allow using more modern APIs, like `List.of` and `Map.of`, in examples. + options.release.set(17) +} + +tasks.withType { + compilerOptions { + freeCompilerArgs = listOf( + "-Xjvm-default=all", + "-Xjdk-release=17", + // Suppress deprecation warnings because we may still reference and test deprecated members. + // TODO: Replace with `-Xsuppress-warning=DEPRECATION` once we use Kotlin compiler 2.1.0+. + "-nowarn", + ) + jvmTarget.set(JvmTarget.JVM_17) + } +} + + diff --git a/arcade-spring-boot-starter/src/main/java/dev/arcade/springboot/ArcadeAutoConfiguration.kt b/arcade-spring-boot-starter/src/main/java/dev/arcade/springboot/ArcadeAutoConfiguration.kt new file mode 100644 index 0000000..3522df1 --- /dev/null +++ b/arcade-spring-boot-starter/src/main/java/dev/arcade/springboot/ArcadeAutoConfiguration.kt @@ -0,0 +1,41 @@ +package dev.arcade.springboot + +import dev.arcade.client.ArcadeClient +import dev.arcade.client.okhttp.ArcadeOkHttpClient +import dev.arcade.core.ClientOptions +import org.springframework.boot.autoconfigure.AutoConfiguration +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty +import org.springframework.boot.context.properties.ConfigurationProperties +import org.springframework.boot.context.properties.EnableConfigurationProperties +import org.springframework.context.annotation.Bean +import org.springframework.util.StringUtils + +@AutoConfiguration +@EnableConfigurationProperties(ArcadeAutoConfiguration.Config::class) +open class ArcadeAutoConfiguration { + + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "arcade", name = ["api-key"]) + open fun arcadeClient(config: Config): ArcadeClient { + val clientBuilder = ArcadeOkHttpClient.builder().fromEnv() + + if (config.apiKey != null && StringUtils.hasText(config.apiKey)) { + clientBuilder.apiKey(config.apiKey) + } + + if (config.baseUrl != null && StringUtils.hasText(config.baseUrl)) { + clientBuilder.baseUrl(config.baseUrl) + } + + return clientBuilder.build() + } + + @ConfigurationProperties(prefix = "arcade") + @JvmRecord + data class Config( + val apiKey: String? = System.getenv("ARCADE_API_KEY"), + val baseUrl: String? = ClientOptions.PRODUCTION_URL, + ) {} +} diff --git a/arcade-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports b/arcade-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports new file mode 100644 index 0000000..ce3da62 --- /dev/null +++ b/arcade-spring-boot-starter/src/main/resources/META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports @@ -0,0 +1 @@ +dev.arcade.springboot.ArcadeAutoConfiguration \ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts index b06e706..62cdb99 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -9,6 +9,7 @@ val projectNames = rootDir.listFiles() file.listFiles()?.asSequence().orEmpty().any { it.name == "build.gradle.kts" } } .map { it.name } - .toList() + .toList() + + listOf("arcade-spring-boot-starter") println("projects: $projectNames") projectNames.forEach { include(it) }