diff --git a/.release-please-manifest.json b/.release-please-manifest.json index da59f99..2aca35a 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.4.0" + ".": "0.5.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 3a11178..b120b66 100644 --- a/.stats.yml +++ b/.stats.yml @@ -1,4 +1,4 @@ configured_endpoints: 7 -openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-ed52466945f2f8dfd3814a29e948d7bf30af7b76a7a7689079c03b8baf64e26f.yml -openapi_spec_hash: 5d57aaf2362b0d882372dbf76477ba23 -config_hash: 8ec9eaf59304f664cf79f73de1488671 +openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/browserbase%2Fstagehand-39cd9547d16412cf0568f6ce2ad8d43805dffe65bde830beeff630b903ae3b38.yml +openapi_spec_hash: 9cd7c9fefa686f9711392782d948470f +config_hash: 3c21550e2c94cad4339d3093d794beb0 diff --git a/CHANGELOG.md b/CHANGELOG.md index cb9b53c..10cbeca 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,21 @@ # Changelog +## 0.5.0 (2026-01-07) + +Full Changelog: [v0.4.0...v0.5.0](https://github.com/browserbase/stagehand-java/compare/v0.4.0...v0.5.0) + +### Features + +* /end endpoint returns empty object ([058cf38](https://github.com/browserbase/stagehand-java/commit/058cf3853b589356fe0b88d5899c98e4c44c8f19)) +* Added optional param to force empty object ([a93f1bf](https://github.com/browserbase/stagehand-java/commit/a93f1bf0324f4fa0b1bd96122d29534ff03cd9b2)) +* **client:** add `HttpRequest#url()` method ([5a4d99f](https://github.com/browserbase/stagehand-java/commit/5a4d99fd20473dcd6045fb65d8230c3894734364)) + + +### Documentation + +* add full working example script to readme and stagehand-java-example ([#5](https://github.com/browserbase/stagehand-java/issues/5)) ([7135947](https://github.com/browserbase/stagehand-java/commit/713594728cd301df4587bdb0337a5047695704eb)) +* prominently feature MCP server setup in root SDK readmes ([0c7cb02](https://github.com/browserbase/stagehand-java/commit/0c7cb0241f65547071fcd15e7bce41f621cfbfcb)) + ## 0.4.0 (2026-01-05) Full Changelog: [v0.3.0...v0.4.0](https://github.com/browserbase/stagehand-java/compare/v0.3.0...v0.4.0) diff --git a/README.md b/README.md index 35c6e6a..79c6b2b 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,8 @@ -[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-java)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-java/0.4.0) -[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-java/0.4.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-java/0.4.0) +[![Maven Central](https://img.shields.io/maven-central/v/com.browserbase.api/stagehand-java)](https://central.sonatype.com/artifact/com.browserbase.api/stagehand-java/0.5.0) +[![javadoc](https://javadoc.io/badge2/com.browserbase.api/stagehand-java/0.5.0/javadoc.svg)](https://javadoc.io/doc/com.browserbase.api/stagehand-java/0.5.0) @@ -11,9 +11,18 @@ The Stagehand Java SDK provides convenient access to the [Stagehand REST API](ht It is generated with [Stainless](https://www.stainless.com/). +## MCP Server + +Use the Stagehand 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=stagehand-mcp&config=eyJjb21tYW5kIjoibnB4IiwiYXJncyI6WyIteSIsInN0YWdlaGFuZC1tY3AiXX0) +[![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%22stagehand-mcp%22%2C%22command%22%3A%22npx%22%2C%22args%22%3A%5B%22-y%22%2C%22stagehand-mcp%22%5D%7D) + +> Note: You may need to set environment variables in your MCP client. + -The REST API documentation can be found on [docs.stagehand.dev](https://docs.stagehand.dev). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.browserbase.api/stagehand-java/0.4.0). +The REST API documentation can be found on [docs.stagehand.dev](https://docs.stagehand.dev). Javadocs are available on [javadoc.io](https://javadoc.io/doc/com.browserbase.api/stagehand-java/0.5.0). @@ -24,7 +33,7 @@ The REST API documentation can be found on [docs.stagehand.dev](https://docs.sta ### Gradle ```kotlin -implementation("com.browserbase.api:stagehand-java:0.4.0") +implementation("com.browserbase.api:stagehand-java:0.5.0") ``` ### Maven @@ -33,7 +42,7 @@ implementation("com.browserbase.api:stagehand-java:0.4.0") com.browserbase.api stagehand-java - 0.4.0 + 0.5.0 ``` diff --git a/build.gradle.kts b/build.gradle.kts index bd9f794..0db3fe0 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -9,7 +9,7 @@ repositories { allprojects { group = "com.browserbase.api" - version = "0.4.0" // x-release-please-version + version = "0.5.0" // x-release-please-version } subprojects { diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequest.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequest.kt index f42f44a..280bd51 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequest.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/core/http/HttpRequest.kt @@ -2,6 +2,7 @@ package com.browserbase.api.core.http import com.browserbase.api.core.checkRequired import com.browserbase.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/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt index 90a656f..e10e438 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/models/sessions/SessionEndParams.kt @@ -3,16 +3,21 @@ package com.browserbase.api.models.sessions import com.browserbase.api.core.Enum +import com.browserbase.api.core.ExcludeMissing import com.browserbase.api.core.JsonField +import com.browserbase.api.core.JsonMissing import com.browserbase.api.core.JsonValue import com.browserbase.api.core.Params import com.browserbase.api.core.http.Headers import com.browserbase.api.core.http.QueryParams -import com.browserbase.api.core.toImmutable import com.browserbase.api.errors.StagehandInvalidDataException +import com.fasterxml.jackson.annotation.JsonAnyGetter +import com.fasterxml.jackson.annotation.JsonAnySetter import com.fasterxml.jackson.annotation.JsonCreator +import com.fasterxml.jackson.annotation.JsonProperty import java.time.OffsetDateTime import java.time.format.DateTimeFormatter +import java.util.Collections import java.util.Objects import java.util.Optional import kotlin.jvm.optionals.getOrNull @@ -25,9 +30,9 @@ private constructor( private val xSdkVersion: String?, private val xSentAt: OffsetDateTime?, private val xStreamResponse: XStreamResponse?, + private val body: Body, private val additionalHeaders: Headers, private val additionalQueryParams: QueryParams, - private val additionalBodyProperties: Map, ) : Params { /** Unique session identifier */ @@ -45,8 +50,9 @@ private constructor( /** Whether to stream the response via SSE */ fun xStreamResponse(): Optional = Optional.ofNullable(xStreamResponse) - /** Additional body properties to send with the request. */ - fun _additionalBodyProperties(): Map = additionalBodyProperties + fun __forceBody(): JsonValue = body.__forceBody() + + fun _additionalBodyProperties(): Map = body._additionalProperties() /** Additional headers to send with the request. */ fun _additionalHeaders(): Headers = additionalHeaders @@ -72,9 +78,9 @@ private constructor( private var xSdkVersion: String? = null private var xSentAt: OffsetDateTime? = null private var xStreamResponse: XStreamResponse? = null + private var body: Body.Builder = Body.builder() private var additionalHeaders: Headers.Builder = Headers.builder() private var additionalQueryParams: QueryParams.Builder = QueryParams.builder() - private var additionalBodyProperties: MutableMap = mutableMapOf() @JvmSynthetic internal fun from(sessionEndParams: SessionEndParams) = apply { @@ -83,9 +89,9 @@ private constructor( xSdkVersion = sessionEndParams.xSdkVersion xSentAt = sessionEndParams.xSentAt xStreamResponse = sessionEndParams.xStreamResponse + body = sessionEndParams.body.toBuilder() additionalHeaders = sessionEndParams.additionalHeaders.toBuilder() additionalQueryParams = sessionEndParams.additionalQueryParams.toBuilder() - additionalBodyProperties = sessionEndParams.additionalBodyProperties.toMutableMap() } /** Unique session identifier */ @@ -121,6 +127,36 @@ private constructor( fun xStreamResponse(xStreamResponse: Optional) = xStreamResponse(xStreamResponse.getOrNull()) + /** + * Sets the entire request body. + * + * This is generally only useful if you are already constructing the body separately. + * Otherwise, it's more convenient to use the top-level setters instead: + * - [_forceBody] + */ + fun body(body: Body) = apply { this.body = body.toBuilder() } + + fun _forceBody(_forceBody: JsonValue) = apply { body._forceBody(_forceBody) } + + fun additionalBodyProperties(additionalBodyProperties: Map) = apply { + body.additionalProperties(additionalBodyProperties) + } + + fun putAdditionalBodyProperty(key: String, value: JsonValue) = apply { + body.putAdditionalProperty(key, value) + } + + fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) = + apply { + body.putAllAdditionalProperties(additionalBodyProperties) + } + + fun removeAdditionalBodyProperty(key: String) = apply { body.removeAdditionalProperty(key) } + + fun removeAllAdditionalBodyProperties(keys: Set) = apply { + body.removeAllAdditionalProperties(keys) + } + fun additionalHeaders(additionalHeaders: Headers) = apply { this.additionalHeaders.clear() putAllAdditionalHeaders(additionalHeaders) @@ -219,28 +255,6 @@ private constructor( additionalQueryParams.removeAll(keys) } - fun additionalBodyProperties(additionalBodyProperties: Map) = apply { - this.additionalBodyProperties.clear() - putAllAdditionalBodyProperties(additionalBodyProperties) - } - - fun putAdditionalBodyProperty(key: String, value: JsonValue) = apply { - additionalBodyProperties.put(key, value) - } - - fun putAllAdditionalBodyProperties(additionalBodyProperties: Map) = - apply { - this.additionalBodyProperties.putAll(additionalBodyProperties) - } - - fun removeAdditionalBodyProperty(key: String) = apply { - additionalBodyProperties.remove(key) - } - - fun removeAllAdditionalBodyProperties(keys: Set) = apply { - keys.forEach(::removeAdditionalBodyProperty) - } - /** * Returns an immutable instance of [SessionEndParams]. * @@ -253,14 +267,13 @@ private constructor( xSdkVersion, xSentAt, xStreamResponse, + body.build(), additionalHeaders.build(), additionalQueryParams.build(), - additionalBodyProperties.toImmutable(), ) } - fun _body(): Optional> = - Optional.ofNullable(additionalBodyProperties.ifEmpty { null }) + fun _body(): Body = body fun _pathParam(index: Int): String = when (index) { @@ -281,6 +294,123 @@ private constructor( override fun _queryParams(): QueryParams = additionalQueryParams + class Body + @JsonCreator(mode = JsonCreator.Mode.DISABLED) + private constructor( + private val _forceBody: JsonValue, + private val additionalProperties: MutableMap, + ) { + + @JsonCreator + private constructor( + @JsonProperty("_forceBody") @ExcludeMissing _forceBody: JsonValue = JsonMissing.of() + ) : this(_forceBody, mutableMapOf()) + + @JsonProperty("_forceBody") @ExcludeMissing fun __forceBody(): JsonValue = _forceBody + + @JsonAnySetter + private fun putAdditionalProperty(key: String, value: JsonValue) { + additionalProperties.put(key, value) + } + + @JsonAnyGetter + @ExcludeMissing + fun _additionalProperties(): Map = + Collections.unmodifiableMap(additionalProperties) + + fun toBuilder() = Builder().from(this) + + companion object { + + /** Returns a mutable builder for constructing an instance of [Body]. */ + @JvmStatic fun builder() = Builder() + } + + /** A builder for [Body]. */ + class Builder internal constructor() { + + private var _forceBody: JsonValue = JsonMissing.of() + private var additionalProperties: MutableMap = mutableMapOf() + + @JvmSynthetic + internal fun from(body: Body) = apply { + _forceBody = body._forceBody + additionalProperties = body.additionalProperties.toMutableMap() + } + + fun _forceBody(_forceBody: JsonValue) = apply { this._forceBody = _forceBody } + + fun additionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.clear() + putAllAdditionalProperties(additionalProperties) + } + + fun putAdditionalProperty(key: String, value: JsonValue) = apply { + additionalProperties.put(key, value) + } + + fun putAllAdditionalProperties(additionalProperties: Map) = apply { + this.additionalProperties.putAll(additionalProperties) + } + + fun removeAdditionalProperty(key: String) = apply { additionalProperties.remove(key) } + + fun removeAllAdditionalProperties(keys: Set) = apply { + keys.forEach(::removeAdditionalProperty) + } + + /** + * Returns an immutable instance of [Body]. + * + * Further updates to this [Builder] will not mutate the returned instance. + */ + fun build(): Body = Body(_forceBody, additionalProperties.toMutableMap()) + } + + private var validated: Boolean = false + + fun validate(): Body = apply { + if (validated) { + return@apply + } + + validated = true + } + + fun isValid(): Boolean = + try { + validate() + true + } catch (e: StagehandInvalidDataException) { + false + } + + /** + * Returns a score indicating how many valid values are contained in this object + * recursively. + * + * Used for best match union deserialization. + */ + @JvmSynthetic internal fun validity(): Int = 0 + + override fun equals(other: Any?): Boolean { + if (this === other) { + return true + } + + return other is Body && + _forceBody == other._forceBody && + additionalProperties == other.additionalProperties + } + + private val hashCode: Int by lazy { Objects.hash(_forceBody, additionalProperties) } + + override fun hashCode(): Int = hashCode + + override fun toString() = + "Body{_forceBody=$_forceBody, additionalProperties=$additionalProperties}" + } + /** Client SDK language */ class XLanguage @JsonCreator private constructor(private val value: JsonField) : Enum { @@ -560,9 +690,9 @@ private constructor( xSdkVersion == other.xSdkVersion && xSentAt == other.xSentAt && xStreamResponse == other.xStreamResponse && + body == other.body && additionalHeaders == other.additionalHeaders && - additionalQueryParams == other.additionalQueryParams && - additionalBodyProperties == other.additionalBodyProperties + additionalQueryParams == other.additionalQueryParams } override fun hashCode(): Int = @@ -572,11 +702,11 @@ private constructor( xSdkVersion, xSentAt, xStreamResponse, + body, additionalHeaders, additionalQueryParams, - additionalBodyProperties, ) override fun toString() = - "SessionEndParams{id=$id, xLanguage=$xLanguage, xSdkVersion=$xSdkVersion, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams, additionalBodyProperties=$additionalBodyProperties}" + "SessionEndParams{id=$id, xLanguage=$xLanguage, xSdkVersion=$xSdkVersion, xSentAt=$xSentAt, xStreamResponse=$xStreamResponse, body=$body, additionalHeaders=$additionalHeaders, additionalQueryParams=$additionalQueryParams}" } diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt index 82062e4..6b354a0 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/async/SessionServiceAsyncImpl.kt @@ -250,7 +250,7 @@ class SessionServiceAsyncImpl internal constructor(private val clientOptions: Cl .method(HttpMethod.POST) .baseUrl(clientOptions.baseUrl()) .addPathSegments("v1", "sessions", params._pathParam(0), "end") - .apply { params._body().ifPresent { body(json(clientOptions.jsonMapper, it)) } } + .body(json(clientOptions.jsonMapper, params._body())) .build() .prepareAsync(clientOptions, params) val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) diff --git a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt index 4bfce1b..f064761 100644 --- a/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt +++ b/stagehand-java-core/src/main/kotlin/com/browserbase/api/services/blocking/SessionServiceImpl.kt @@ -223,7 +223,7 @@ class SessionServiceImpl internal constructor(private val clientOptions: ClientO .method(HttpMethod.POST) .baseUrl(clientOptions.baseUrl()) .addPathSegments("v1", "sessions", params._pathParam(0), "end") - .apply { params._body().ifPresent { body(json(clientOptions.jsonMapper, it)) } } + .body(json(clientOptions.jsonMapper, params._body())) .build() .prepare(clientOptions, params) val requestOptions = requestOptions.applyDefaults(RequestOptions.from(clientOptions)) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HttpRequestTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HttpRequestTest.kt new file mode 100644 index 0000000..5785890 --- /dev/null +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/core/http/HttpRequestTest.kt @@ -0,0 +1,110 @@ +package com.browserbase.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) + } +} diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt index bc50272..3150dad 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/models/sessions/SessionEndParamsTest.kt @@ -2,6 +2,7 @@ package com.browserbase.api.models.sessions +import com.browserbase.api.core.JsonValue import com.browserbase.api.core.http.Headers import java.time.OffsetDateTime import org.assertj.core.api.Assertions.assertThat @@ -17,6 +18,7 @@ internal class SessionEndParamsTest { .xSdkVersion("3.0.6") .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + ._forceBody(JsonValue.from(mapOf())) .build() } @@ -38,6 +40,7 @@ internal class SessionEndParamsTest { .xSdkVersion("3.0.6") .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + ._forceBody(JsonValue.from(mapOf())) .build() val headers = params._headers() @@ -61,4 +64,28 @@ internal class SessionEndParamsTest { assertThat(headers).isEqualTo(Headers.builder().build()) } + + @Test + fun body() { + val params = + SessionEndParams.builder() + .id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123") + .xLanguage(SessionEndParams.XLanguage.TYPESCRIPT) + .xSdkVersion("3.0.6") + .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) + .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + ._forceBody(JsonValue.from(mapOf())) + .build() + + val body = params._body() + + assertThat(body.__forceBody()).isEqualTo(JsonValue.from(mapOf())) + } + + @Test + fun bodyWithoutOptionalFields() { + val params = SessionEndParams.builder().id("c4dbf3a9-9a58-4b22-8a1c-9f20f9f9e123").build() + + val body = params._body() + } } diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt index f9bc4be..8a517d2 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/async/SessionServiceAsyncTest.kt @@ -121,6 +121,7 @@ internal class SessionServiceAsyncTest { .xSdkVersion("3.0.6") .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + ._forceBody(JsonValue.from(mapOf())) .build() ) diff --git a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt index 1bad240..1a94b1f 100644 --- a/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt +++ b/stagehand-java-core/src/test/kotlin/com/browserbase/api/services/blocking/SessionServiceTest.kt @@ -120,6 +120,7 @@ internal class SessionServiceTest { .xSdkVersion("3.0.6") .xSentAt(OffsetDateTime.parse("2025-01-15T10:30:00Z")) .xStreamResponse(SessionEndParams.XStreamResponse.TRUE) + ._forceBody(JsonValue.from(mapOf())) .build() )